From 52b1a9f41928bbd32cdf487b9370a95db426d8f1 Mon Sep 17 00:00:00 2001 From: Serhii Vecherenko Date: Mon, 15 Jun 2026 12:33:19 -0700 Subject: [PATCH] fix(commandcode): suppress background updater during detection and spawns - Add DetectionSpec.probeEnv and forward it through DetectProbeCtx - Set COMMANDCODE_SKIP_UPDATES on version probes, PTY launch, one-shots, and login --- src/supervisor/agents/base/index.ts | 8 +++--- src/supervisor/agents/base/types.ts | 11 ++++++++ .../agents/commandcode/commandcode.test.ts | 25 ++++++++++++++++++- .../agents/commandcode/detection.ts | 22 ++++++++++++++++ src/supervisor/agents/commandcode/index.ts | 9 ++++++- 5 files changed, 70 insertions(+), 5 deletions(-) diff --git a/src/supervisor/agents/base/index.ts b/src/supervisor/agents/base/index.ts index 8fa8e4e2..ed1d492e 100644 --- a/src/supervisor/agents/base/index.ts +++ b/src/supervisor/agents/base/index.ts @@ -456,6 +456,7 @@ async function readDetectedVersion( location: ProjectLocation, executablePath: string | undefined, versionArgs: string[], + probeEnv?: Record, ): Promise { if (!executablePath) return undefined; if (location.kind === "wsl") { @@ -464,6 +465,7 @@ async function readDetectedVersion( PROBE_WSL_LINUX_PATH, executablePath, versionArgs, + probeEnv ? { env: probeEnv } : undefined, ); return result.ok ? extractSemverFromVersionOutput(result.stdout) : undefined; } @@ -473,7 +475,7 @@ async function readDetectedVersion( // detection — which uses the registry-backed fallback — but its `--version` // would miss and the version would render blank. Matches the WSL branch above // and readAgentCommandOutput. - const spec = buildAgentCommand(location, executablePath, versionArgs); + const spec = buildAgentCommand(location, executablePath, versionArgs, undefined, probeEnv); const result = await readCommandOutputAsync( spec.command, spec.args, @@ -618,7 +620,7 @@ export async function detectAgentInstall( const executablePath = await resolveDetectedBinary(ctx, spec.binary); const versionArgs = spec.versionArgs ?? ["--version"]; - const version = await readDetectedVersion(location, executablePath, versionArgs); + const version = await readDetectedVersion(location, executablePath, versionArgs, spec.probeEnv); let capabilities = spec.capabilities; let statusProbeResult: StatusProbeResult | undefined; @@ -627,7 +629,7 @@ export async function detectAgentInstall( let probedAuthState: AuthState | undefined; let probedProviderMetadata: AgentProviderMetadata | undefined; if (executablePath) { - const probeCtx: DetectProbeCtx = { location, executablePath, version }; + const probeCtx: DetectProbeCtx = { location, executablePath, version, probeEnv: spec.probeEnv }; const [capabilityPartial, nextStatusProbeResult] = await Promise.all([ spec.capabilitiesProbe ? spec.capabilitiesProbe(probeCtx) : Promise.resolve(undefined), spec.statusProbe ? spec.statusProbe(probeCtx) : Promise.resolve(undefined), diff --git a/src/supervisor/agents/base/types.ts b/src/supervisor/agents/base/types.ts index 2a0336e6..90e08855 100644 --- a/src/supervisor/agents/base/types.ts +++ b/src/supervisor/agents/base/types.ts @@ -143,6 +143,8 @@ export interface DetectProbeCtx { location: ProjectLocation; executablePath: string | undefined; version?: string | undefined; + /** {@link DetectionSpec.probeEnv}, so `capabilitiesProbe`/`statusProbe` can forward it. */ + probeEnv?: Record | undefined; } export type AuthProbe = (ctx: DetectProbeCtx) => Promise; @@ -181,6 +183,15 @@ export interface DetectionSpec { capabilities: AgentCapability; update?: AgentUpdateInfo; versionArgs?: string[]; + /** + * Env merged onto the `--version` probe spawn. Used to neutralize a CLI's own + * background self-updater during detection — e.g. `command-code` spawns a + * detached npm install on every invocation unless `COMMANDCODE_SKIP_UPDATES` + * is set, which otherwise surfaces as a stray terminal window on app launch. + * Also exposed to `capabilitiesProbe`/`statusProbe` via `DetectProbeCtx.probeEnv` + * so they can forward it to their own `readAgentCommandOutput` calls. + */ + probeEnv?: Record; statusProbe?: StatusProbe; authProbes?: AuthProbe[]; capabilitiesProbe?: (ctx: DetectProbeCtx) => Promise; diff --git a/src/supervisor/agents/commandcode/commandcode.test.ts b/src/supervisor/agents/commandcode/commandcode.test.ts index 21fbe823..0fb65aff 100644 --- a/src/supervisor/agents/commandcode/commandcode.test.ts +++ b/src/supervisor/agents/commandcode/commandcode.test.ts @@ -191,6 +191,8 @@ describe("createCommandCodeAdapter", () => { command: "command-code", args: ["--trust", "--skip-onboarding", "--model", "gpt-5.4-mini", "-p", "summarize"], stdin: "", + // Suppress the CLI's background self-updater on one-shot utility runs. + env: { COMMANDCODE_SKIP_UPDATES: "1" }, }); }); }); @@ -243,6 +245,22 @@ describe("commandCodeDetectionSpec auth", () => { expect(commandCodeDetectionSpec.loginCommand).toBe("command-code login"); }); + // Suppresses the CLI's background self-updater. The one-shot and terminal-login + // surfaces are asserted by their own tests above; this covers the detection + // probe + PTY-launch surfaces. + it("sets COMMANDCODE_SKIP_UPDATES on the version probe and PTY launch env", () => { + // `--version` probe (also flows to capabilitiesProbe via DetectProbeCtx.probeEnv). + expect(commandCodeDetectionSpec.probeEnv).toEqual({ COMMANDCODE_SKIP_UPDATES: "1" }); + + // Interactive / login PTY launch (spawnEnv); wsl keeps the OAuth BROWSER shim. + const adapter = createCommandCodeAdapter(); + expect(adapter.spawnEnv?.native).toEqual({ COMMANDCODE_SKIP_UPDATES: "1" }); + expect(adapter.spawnEnv?.wsl).toEqual({ + BROWSER: "/bin/true", + COMMANDCODE_SKIP_UPDATES: "1", + }); + }); + it("advertises a terminal login method when installed (drives the Login button)", async () => { const project: ProjectLocation = { kind: "windows", path: "C:\\demo" }; const result = await commandCodeDetectionSpec.capabilitiesProbe?.({ @@ -250,7 +268,12 @@ describe("commandCodeDetectionSpec auth", () => { executablePath: "C:\\bin\\command-code.cmd", }); expect(result?.authMethods).toEqual([ - { id: "commandcode-terminal-login", name: "Login", type: "terminal" }, + { + id: "commandcode-terminal-login", + name: "Login", + type: "terminal", + env: { COMMANDCODE_SKIP_UPDATES: "1" }, + }, ]); }); diff --git a/src/supervisor/agents/commandcode/detection.ts b/src/supervisor/agents/commandcode/detection.ts index 10d337b4..20a02ec4 100644 --- a/src/supervisor/agents/commandcode/detection.ts +++ b/src/supervisor/agents/commandcode/detection.ts @@ -4,6 +4,20 @@ import { type AuthProbe, type DetectionSpec, readAgentCommandOutput } from "../b import { getAgentProbeCwd } from "../probeCwd"; import { commandCodeHasStoredCredentials } from "./session"; +// Command Code's CLI runs a background self-updater on EVERY invocation: when a +// newer npm version exists it spawns a detached `cmd.exe`/`npm i ` (see the +// CLI's `spawnBackgroundUpdate`), which Windows 11 surfaces as a stray terminal +// window — re-triggered by each launch-time detection probe. Lightcode owns +// agent updates (Settings update button → `command-code update`), so we set +// `COMMANDCODE_SKIP_UPDATES` on every command-code spawn we make (detection +// probes, PTY launches, one-shots) to suppress the CLI's own updater. The CLI +// also honors `CI`, but that flips broader non-interactive behavior, so we use +// the dedicated switch. `command-code update` runs without this env (separate +// path), so explicit updates still work. +export const COMMANDCODE_SKIP_UPDATES_ENV: Record = { + COMMANDCODE_SKIP_UPDATES: "1", +}; + // Command Code's CLI default (used with no `-m`). We surface it first so a // fresh thread mirrors what running `command-code` directly would pick. // Source: https://commandcode.ai/docs/reference/cli/models (also `command-code @@ -298,6 +312,10 @@ const COMMANDCODE_TERMINAL_AUTH: AgentTerminalAuthMethod = { id: "commandcode-terminal-login", name: "Login", type: "terminal", + // `runTerminalLogin` forwards `env` into the login command; suppress the + // CLI's background self-updater so `command-code login` doesn't spawn a + // detached `npm i` terminal alongside the login overlay. + env: COMMANDCODE_SKIP_UPDATES_ENV, }; export const commandCodeDetectionSpec: DetectionSpec = { @@ -324,6 +342,8 @@ export const commandCodeDetectionSpec: DetectionSpec = { timeoutMs: 8_000, wslLinuxCwd: "/tmp", posixCwd: getAgentProbeCwd(ctx.location), + // Suppress the CLI's background self-updater (sourced from spec.probeEnv). + ...(ctx.probeEnv ? { env: ctx.probeEnv } : {}), }, ).catch(() => undefined); const parsed = result?.ok ? parseCommandCodeModels(result.stdout) : []; @@ -336,4 +356,6 @@ export const commandCodeDetectionSpec: DetectionSpec = { // and is the automatic fallback if the built-in updater fails, since the CLI // is distributed as the `command-code` npm package on every platform. update: { builtIn: { binary: "command-code", args: ["update"] }, npm: "command-code" }, + // Suppress the CLI's own background self-updater on the `--version` probe. + probeEnv: COMMANDCODE_SKIP_UPDATES_ENV, }; diff --git a/src/supervisor/agents/commandcode/index.ts b/src/supervisor/agents/commandcode/index.ts index 99c0b534..25af0e41 100644 --- a/src/supervisor/agents/commandcode/index.ts +++ b/src/supervisor/agents/commandcode/index.ts @@ -4,6 +4,7 @@ import { resolveInstallNodePath, warnIfPluginManifestMissing } from "../plugin/i import { buildCommandCodeArgs } from "./argv"; import { COMMANDCODE_DEFAULT_MODEL_ID, + COMMANDCODE_SKIP_UPDATES_ENV, commandCodeDetectionSpec, defaultCommandCodeCapabilities, } from "./detection"; @@ -63,8 +64,12 @@ export function createCommandCodeAdapter(): AgentAdapter { // `command-code login` opens a browser for OAuth; BROWSER=/bin/true keeps // the WSL flow from trying to `xdg-open` inside the distro and hanging the // PTY (the user completes auth via the printed URL instead). + // COMMANDCODE_SKIP_UPDATES disables the CLI's background self-updater so an + // interactive/login launch doesn't spawn a detached `npm i` terminal window + // (see COMMANDCODE_SKIP_UPDATES_ENV in detection.ts). spawnEnv: { - wsl: { BROWSER: "/bin/true" }, + native: COMMANDCODE_SKIP_UPDATES_ENV, + wsl: { BROWSER: "/bin/true", ...COMMANDCODE_SKIP_UPDATES_ENV }, }, // ── CLI hook plugin support ────────────────────────────────────────── @@ -172,6 +177,8 @@ export function createCommandCodeAdapter(): AgentAdapter { prompt, ], stdin: "", + // Don't let a one-shot utility run trigger the CLI's background updater. + env: COMMANDCODE_SKIP_UPDATES_ENV, }; }, };