Files
Codeman/src/utils/cloudflared-resolver.ts
T
Codeman maintainer 0d0b772619 feat: make Antigravity a first-class CLI across docs, installer and UI
Antigravity (agy) was wired into the session layer but never propagated to
the surfaces around it, while Gemini CLI stayed documented as a consumer
product despite being enterprise-only since Google's cutover. Gemini keeps
full support; Antigravity now sits beside it everywhere.

Functional fixes:
- docker/agent.Dockerfile never installed agy, so a docker case with
  mode 'antigravity' died on command-not-found. agy is not on npm, so it
  gets its own installer step. --dir /usr/local/bin is load-bearing: the
  default $HOME/.local/bin resolves to root's home at build time and is
  unreachable by the `agent` user the container runs as. Verified inside
  codeman/agent:base (v1.1.10, reachable as `agent`). Note the binary is
  ~190MB, the largest layer in the image.
- Welcome screen gained a Run Antigravity action, gated on agy being
  present like the other CLI buttons, with a cyan identity matching the
  toolbar run button and run-mode dot.
- install.sh now detects agy (search paths mirroring the resolver), counts
  it as a satisfying AI CLI, and recommends it over Gemini in the install
  hints. Detection only, no new auto-install path.

Docs corrected where they were factually wrong:
- architecture-invariants documented isExternalCliMode() as
  opencode/codex/gemini when the code has included antigravity for a
  while, said "all three modes", and omitted ANTIGRAVITY_ from the env
  prefix allowlist row.
- cron-guide's agentType enum, cron-discovery's SessionMode, and
  remote-sessions' RemoteCommandMode were all stale.

Also: README + README.zh-CN (five CLIs, Gemini marked enterprise-only),
package.json keyword, and comment drift in 8 places.

test/run-mode-ui.test.ts now covers the new welcome button; verified it
fails without the settings-ui wiring.

Antigravity nests its whole state under ~/.gemini/antigravity-cli/, not
~/.antigravity, so the existing .gemini docker credential seed already
covers it. Recorded as a comment so nobody adds dead config later.

isAltScreenStripMode() deliberately still excludes antigravity: whether
its TUI needs the alt-screen strip is a behavioural question that needs a
real agy session, not a guess.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 07:22:29 +02:00

66 lines
2.2 KiB
TypeScript

/**
* @fileoverview Resolve the `cloudflared` binary across common install paths.
*
* Mirrors the CLI resolvers (antigravity-cli-resolver.ts et al), for the same reason
* 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;
}