Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions src/supervisor/agents/base/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,7 @@ async function readDetectedVersion(
location: ProjectLocation,
executablePath: string | undefined,
versionArgs: string[],
probeEnv?: Record<string, string>,
): Promise<string | undefined> {
if (!executablePath) return undefined;
if (location.kind === "wsl") {
Expand All @@ -464,6 +465,7 @@ async function readDetectedVersion(
PROBE_WSL_LINUX_PATH,
executablePath,
versionArgs,
probeEnv ? { env: probeEnv } : undefined,
);
return result.ok ? extractSemverFromVersionOutput(result.stdout) : undefined;
}
Expand All @@ -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,
Expand Down Expand Up @@ -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;
Expand All @@ -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),
Expand Down
11 changes: 11 additions & 0 deletions src/supervisor/agents/base/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string> | undefined;
}

export type AuthProbe = (ctx: DetectProbeCtx) => Promise<AuthState | undefined>;
Expand Down Expand Up @@ -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<string, string>;
statusProbe?: StatusProbe;
authProbes?: AuthProbe[];
capabilitiesProbe?: (ctx: DetectProbeCtx) => Promise<CapabilitiesProbeResult | undefined>;
Expand Down
25 changes: 24 additions & 1 deletion src/supervisor/agents/commandcode/commandcode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
});
});
});
Expand Down Expand Up @@ -243,14 +245,35 @@ 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?.({
location: project,
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" },
},
]);
});

Expand Down
22 changes: 22 additions & 0 deletions src/supervisor/agents/commandcode/detection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <tgz>` (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<string, string> = {
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
Expand Down Expand Up @@ -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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep Windows terminal login commands shell-safe

When Command Code advertises this terminal-auth env, the native Windows Login/Re-login flow forwards it to runAgentLoginCommand, which renders env vars as a POSIX prefix (KEY='value' command) before writing the script to the login overlay; native Windows shells are started as pwsh/PowerShell/cmd in threadSessionManager, so the command becomes COMMANDCODE_SKIP_UPDATES='1' command-code login and fails before command-code login runs. This only affects native Windows terminal login for Command Code; WSL/POSIX prefixes are valid.

Useful? React with 👍 / 👎.

};

export const commandCodeDetectionSpec: DetectionSpec = {
Expand All @@ -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) : [];
Expand All @@ -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,
};
9 changes: 8 additions & 1 deletion src/supervisor/agents/commandcode/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 ──────────────────────────────────────────
Expand Down Expand Up @@ -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,
};
},
};
Expand Down