2026-08-05 01:53:43 +02:00
|
|
|
/**
|
|
|
|
|
* @fileoverview Resolve the `cloudflared` binary across common install paths.
|
|
|
|
|
*
|
2026-08-06 07:21:57 +02:00
|
|
|
* Mirrors the CLI resolvers (antigravity-cli-resolver.ts et al), for the same reason
|
2026-08-05 01:53:43 +02:00
|
|
|
* they exist: the welcome screen should not offer a button whose only possible
|
|
|
|
|
* outcome is an error toast.
|
|
|
|
|
*
|
|
|
|
|
* The search list is deliberately the SAME one `TunnelManager.resolveCloudflared()`
|
|
|
|
|
* has always used, and that method now delegates here so the two can never drift.
|
|
|
|
|
* The difference is the fallback: this module answers "is it installed?" honestly
|
|
|
|
|
* with null, while the tunnel manager keeps falling back to the bare name so a
|
|
|
|
|
* cloudflared that only exists somewhere on the tunnel process's PATH still
|
|
|
|
|
* starts. A stricter answer there would turn a working tunnel into a refusal.
|
|
|
|
|
*
|
|
|
|
|
* @module utils/cloudflared-resolver
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import { execSync } from 'node:child_process';
|
|
|
|
|
import { existsSync } from 'node:fs';
|
|
|
|
|
import { join } from 'node:path';
|
|
|
|
|
import { homedir } from 'node:os';
|
|
|
|
|
import { EXEC_TIMEOUT_MS } from '../config/exec-timeout.js';
|
|
|
|
|
|
|
|
|
|
/** Common directories where the cloudflared binary may be installed */
|
|
|
|
|
const CLOUDFLARED_SEARCH_DIRS = [join(homedir(), '.local', 'bin'), '/usr/local/bin'];
|
|
|
|
|
|
|
|
|
|
/** Cached path to the cloudflared binary (empty string = searched but not found) */
|
|
|
|
|
let _cloudflaredPath: string | null = null;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Finds the `cloudflared` binary.
|
|
|
|
|
*
|
|
|
|
|
* @returns Absolute path, or null if not found
|
|
|
|
|
*/
|
|
|
|
|
export function resolveCloudflaredPath(): string | null {
|
|
|
|
|
if (_cloudflaredPath !== null) return _cloudflaredPath || null;
|
|
|
|
|
|
|
|
|
|
for (const dir of CLOUDFLARED_SEARCH_DIRS) {
|
|
|
|
|
const candidate = join(dir, 'cloudflared');
|
|
|
|
|
if (existsSync(candidate)) {
|
|
|
|
|
_cloudflaredPath = candidate;
|
|
|
|
|
return candidate;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const result = execSync('which cloudflared', { encoding: 'utf-8', timeout: EXEC_TIMEOUT_MS }).trim();
|
|
|
|
|
if (result && existsSync(result)) {
|
|
|
|
|
_cloudflaredPath = result;
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
// Not on PATH either.
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_cloudflaredPath = ''; // mark as searched, not found
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check if cloudflared is available on the system.
|
|
|
|
|
*/
|
|
|
|
|
export function isCloudflaredAvailable(): boolean {
|
|
|
|
|
return resolveCloudflaredPath() !== null;
|
|
|
|
|
}
|