From d86c2b7f0785d7d6b6c6d1f354dfae327efa4d42 Mon Sep 17 00:00:00 2001 From: Aleksandr Stasko Date: Tue, 2 Jun 2026 16:42:27 +0400 Subject: [PATCH 1/4] fix(agents): pass --version to downstream binary and suppress banners in non-TTY mode --- src/agents/core/AgentCLI.ts | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/src/agents/core/AgentCLI.ts b/src/agents/core/AgentCLI.ts index 4b83c6021..75672f5b9 100644 --- a/src/agents/core/AgentCLI.ts +++ b/src/agents/core/AgentCLI.ts @@ -71,7 +71,6 @@ export class AgentCLI { this.program .name(programName) .description(`CodeMie ${this.adapter.displayName} - ${this.adapter.description}`) - .version(this.version) .option('-s, --silent', 'Enable silent mode') .option('--status', 'Enable status bar (shows model, context usage, git branch, and cost)') .option('--profile ', 'Use specific provider profile') @@ -171,10 +170,11 @@ export class AgentCLI { process.exit(1); } - // Auto-enable silent mode in non-interactive mode (--task flag present) - // or when only printing the generated config (--print-config). - // This suppresses welcome/goodbye messages and interactive prompts. - const isNonInteractiveMode = !!options.task; + // Auto-enable silent mode in non-interactive mode: + // - --task flag is present (explicit single-task invocation) + // - stdout is not a TTY (piped, redirected, or CI environment) + // - --print-config is set (only printing the generated config) + const isNonInteractiveMode = !!options.task || !process.stdout.isTTY; const shouldBeSilent = options.silent || isNonInteractiveMode || !!options.printConfig; // Apply silent mode from CLI flag or auto-detected non-interactive mode @@ -764,6 +764,22 @@ export class AgentCLI { async run(argv: string[]): Promise { // Auth validation and SSO re-login use global fetch before the adapter runs. installSystemProxyDispatcher(); + + // Intercept --version / -V before Commander handles it. + // We delegate to the downstream agent binary so the user sees the actual + // installed agent version rather than the CodeMie wrapper version. + const args = argv.slice(2); // strip node + script + if (args.includes('--version') || args.includes('-V')) { + const agentVersion = await this.adapter.getVersion(); + if (agentVersion !== null) { + console.log(agentVersion); + } else { + // Fallback: print CodeMie wrapper version when agent version is unavailable + console.log(this.version); + } + process.exit(0); + } + await this.program.parseAsync(argv); } } From dc08cd89e337f65ad76f3c61cb7a7825da643f40 Mon Sep 17 00:00:00 2001 From: Aleksandr Stasko Date: Mon, 31 Aug 2026 20:01:19 +0400 Subject: [PATCH 2/4] fix(agents): resolve comments --- src/agents/core/AgentCLI.ts | 142 +++++++++++++++++++++++++++--------- 1 file changed, 109 insertions(+), 33 deletions(-) diff --git a/src/agents/core/AgentCLI.ts b/src/agents/core/AgentCLI.ts index 75672f5b9..a23748ff1 100644 --- a/src/agents/core/AgentCLI.ts +++ b/src/agents/core/AgentCLI.ts @@ -11,6 +11,7 @@ import { JWTTemplate } from '../../providers/plugins/jwt/jwt.template.js'; import { logger } from '../../utils/logger.js'; import { getDirname } from '../../utils/paths.js'; import { installSystemProxyDispatcher } from '../../utils/system-proxy-dispatcher.js'; +import { isNonInteractiveEnvironment } from '../../utils/interactive.js'; import { BUILTIN_AGENT_NAME } from '../registry.js'; import { ClaudePluginMetadata } from '../plugins/claude/claude.plugin.js'; import { CodeMieCodePluginMetadata } from '../plugins/codemie-code.plugin.js'; @@ -147,9 +148,11 @@ export class AgentCLI { */ private displayWindowsPathGuidance(): void { if (process.platform === 'win32') { - console.log(chalk.yellow(`⚠️ Windows users: If you just installed ${this.adapter.displayName},`)); - console.log(chalk.yellow(' you may need to restart your terminal/PowerShell/CMD')); - console.log(chalk.yellow(' for PATH changes to take effect.\n')); + // stderr: this guidance only ever accompanies an error, and stdout must + // stay clean for wrappers that parse it. + console.error(chalk.yellow(`⚠️ Windows users: If you just installed ${this.adapter.displayName},`)); + console.error(chalk.yellow(' you may need to restart your terminal/PowerShell/CMD')); + console.error(chalk.yellow(' for PATH changes to take effect.\n')); } } @@ -160,9 +163,11 @@ export class AgentCLI { try { // Check if agent is installed if (!(await this.adapter.isInstalled())) { - console.log(chalk.red(`\n✗ ${this.adapter.displayName} is not installed\n`)); - console.log(chalk.white('Install it with:\n')); - console.log(chalk.cyan(` ${getAgentInstallCommand(this.adapter.name)}\n`)); + // stderr, not stdout: callers that capture our stdout (t3code, SDKs) + // must not receive install instructions in the stream they parse. + console.error(chalk.red(`\n✗ ${this.adapter.displayName} is not installed\n`)); + console.error(chalk.white('Install it with:\n')); + console.error(chalk.cyan(` ${getAgentInstallCommand(this.adapter.name)}\n`)); // Windows-specific guidance for PATH refresh issue this.displayWindowsPathGuidance(); @@ -170,12 +175,19 @@ export class AgentCLI { process.exit(1); } - // Auto-enable silent mode in non-interactive mode: + // Auto-enable silent mode whenever this wrapper is not driving an + // interactive terminal session. Programmatic consumers (t3code, the Claude + // SDK, ACP editors, CI pipelines) capture our stdio, so the ASCII logo, + // welcome/goodbye messages and update notices would corrupt the stream + // they parse. Triggers: // - --task flag is present (explicit single-task invocation) - // - stdout is not a TTY (piped, redirected, or CI environment) + // - stdin is not a TTY (spawned by a parent process / piped input), + // which also means we must never reach an inquirer prompt + // - stdout is not a TTY (captured to a pipe or redirected to a file) // - --print-config is set (only printing the generated config) - const isNonInteractiveMode = !!options.task || !process.stdout.isTTY; - const shouldBeSilent = options.silent || isNonInteractiveMode || !!options.printConfig; + const isNonInteractiveMode = + !!options.task || isNonInteractiveEnvironment() || !process.stdout.isTTY; + const shouldBeSilent = Boolean(options.silent) || isNonInteractiveMode || !!options.printConfig; // Apply silent mode from CLI flag or auto-detected non-interactive mode if (shouldBeSilent) { @@ -502,18 +514,14 @@ export class AgentCLI { console.log(`Version: ${version}`); } } else { - const isWindows = process.platform === 'win32'; - - console.log(chalk.red(`\n✗ ${this.adapter.displayName} is not installed\n`)); - console.log(chalk.white('Install it with:\n')); - console.log(chalk.cyan(` ${getAgentInstallCommand(this.adapter.name)}\n`)); + // Failure diagnostics go to stderr; the non-zero exit code is the + // machine-readable signal for health-check callers. + console.error(chalk.red(`\n✗ ${this.adapter.displayName} is not installed\n`)); + console.error(chalk.white('Install it with:\n')); + console.error(chalk.cyan(` ${getAgentInstallCommand(this.adapter.name)}\n`)); // Windows-specific guidance for PATH refresh issue - if (isWindows) { - console.log(chalk.yellow(`⚠️ Windows users: If you just installed ${this.adapter.displayName},`)); - console.log(chalk.yellow(' you may need to restart your terminal/PowerShell/CMD')); - console.log(chalk.yellow(' for PATH changes to take effect.\n')); - } + this.displayWindowsPathGuidance(); process.exit(1); } @@ -758,6 +766,81 @@ export class AgentCLI { } } + /** + * Handle a bare `--version` / `-V` invocation by reporting the DOWNSTREAM + * agent version rather than the CodeMie wrapper version. + * + * External tooling (e.g. t3code) shells out to `codemie- --version` to + * decide whether the underlying harness is a supported version and whether it + * needs an update. It must therefore see the agent's own version on stdout and + * nothing else — no banner, no colour, no wrapper version. + * + * IMPORTANT: this is deliberately NOT registered via Commander's `.version()`, + * because resolving the agent version is async and Commander's version handler + * is synchronous. That makes `run()` the SOLE entry point for version handling: + * calling `this.program.parseAsync()` directly (in a test or a future refactor) + * bypasses this interception and silently reverts to wrapper-version output. + * Route every invocation through `run()`. + * + * @param argv - Raw process argv (node + script + args) + * @returns true when the invocation was handled and the caller must stop + */ + private async tryHandleVersionRequest(argv: string[]): Promise { + const args = argv.slice(2); // strip node + script + + // Only intercept when a version flag is the sole argument. A raw scan for + // `--version` anywhere would hijack invocations that merely carry the flag + // (e.g. `codemie- run --version`) and exit early instead of + // forwarding them to the downstream agent. + const isVersionOnlyRequest = + args.length === 1 && (args[0] === '--version' || args[0] === '-V'); + + if (!isVersionOnlyRequest) { + return false; + } + + let agentVersion: string | null = null; + try { + agentVersion = await this.adapter.getVersion(); + } catch (error) { + // Adapters are expected to swallow exec/spawn failures and return null, + // but a throwing adapter must not surface as an unhandled rejection. + logger.debug(`[AgentCLI] getVersion() threw while resolving agent version: ${error instanceof Error ? error.message : String(error)}`); + agentVersion = null; + } + + if (agentVersion !== null) { + console.log(agentVersion); + return true; + } + + // No agent version available. Distinguish the two reasons, because a + // version-checking caller must not be handed a plausible-but-wrong number: + // - agent missing -> reporting the CodeMie wrapper version here would be + // read as the agent's own version (e.g. "Gemini CLI 0.14.1") and silently + // produce a bogus supported/needs-update verdict. Stay silent on stdout + // and fail, so the caller sees the agent as unavailable. + // - agent present but versionless (built-in agents, ACP adapters that + // intentionally report no version) -> the wrapper version is the only + // meaningful answer, so keep `--version` parseable. + let installed = false; + try { + installed = await this.adapter.isInstalled(); + } catch (error) { + logger.debug(`[AgentCLI] isInstalled() threw while resolving agent version: ${error instanceof Error ? error.message : String(error)}`); + } + + if (!installed) { + console.error(chalk.red(`✗ ${this.adapter.displayName} is not installed`)); + console.error(chalk.white(` Install it with: ${getAgentInstallCommand(this.adapter.name)}`)); + process.exitCode = 1; + return true; + } + + console.log(this.version); + return true; + } + /** * Run the CLI */ @@ -765,19 +848,12 @@ export class AgentCLI { // Auth validation and SSO re-login use global fetch before the adapter runs. installSystemProxyDispatcher(); - // Intercept --version / -V before Commander handles it. - // We delegate to the downstream agent binary so the user sees the actual - // installed agent version rather than the CodeMie wrapper version. - const args = argv.slice(2); // strip node + script - if (args.includes('--version') || args.includes('-V')) { - const agentVersion = await this.adapter.getVersion(); - if (agentVersion !== null) { - console.log(agentVersion); - } else { - // Fallback: print CodeMie wrapper version when agent version is unavailable - console.log(this.version); - } - process.exit(0); + if (await this.tryHandleVersionRequest(argv)) { + // Return instead of process.exit(0): when stdout is a pipe (which is + // exactly how t3code and other wrappers read us) writes are asynchronous, + // and exiting immediately can truncate the version line. Letting run() + // return lets Node drain stdout and exit 0 on its own. + return; } await this.program.parseAsync(argv); From c48edd32cf64d8377e69bc54ac3cb3aaa594770a Mon Sep 17 00:00:00 2001 From: Aleksandr Stasko Date: Tue, 1 Sep 2026 13:39:24 +0400 Subject: [PATCH 3/4] fix(tests): fix flaky tests for metrics-upload-contract --- .../metrics/__tests__/metrics-upload-contract.test.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/providers/plugins/sso/session/processors/metrics/__tests__/metrics-upload-contract.test.ts b/src/providers/plugins/sso/session/processors/metrics/__tests__/metrics-upload-contract.test.ts index c53bf7006..b5f921dd0 100644 --- a/src/providers/plugins/sso/session/processors/metrics/__tests__/metrics-upload-contract.test.ts +++ b/src/providers/plugins/sso/session/processors/metrics/__tests__/metrics-upload-contract.test.ts @@ -24,6 +24,7 @@ import { join } from 'node:path'; import { MetricsSender } from '../metrics-api-client.js'; import type { SessionMetric } from '../metrics-types.js'; +import { logger } from '../../../../../../../utils/logger.js'; interface CapturedRequest { method: string | undefined; @@ -107,6 +108,10 @@ describe('MetricsSender upload contract', () => { } if (originalHome === undefined) delete process.env.CODEMIE_HOME; else process.env.CODEMIE_HOME = originalHome; + // Flush and close the singleton logger's write stream before deleting the + // temp home: it opens the log file asynchronously, and deleting the + // directory mid-open surfaces as an unhandled ENOENT later in the run. + await logger.close(); // Windows can briefly hold a handle (logger) on files under the temp home; // retry and never let a cleanup failure fail the test. try { rmSync(home, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); } catch { /* best-effort */ } From 525165ba8cfd62648cadf815d394b7f366fe1d17 Mon Sep 17 00:00:00 2001 From: Aleksandr Stasko Date: Mon, 28 Sep 2026 17:40:20 +0400 Subject: [PATCH 4/4] fix(tests): fix flaky CI tests --- .../__tests__/migration-runner-ordering.test.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/migrations/__tests__/migration-runner-ordering.test.ts b/src/migrations/__tests__/migration-runner-ordering.test.ts index 647526695..41db67bb7 100644 --- a/src/migrations/__tests__/migration-runner-ordering.test.ts +++ b/src/migrations/__tests__/migration-runner-ordering.test.ts @@ -86,8 +86,13 @@ afterEach(async () => { if (originalCodemieHome !== undefined) process.env.CODEMIE_HOME = originalCodemieHome; else delete process.env.CODEMIE_HOME; // maxRetries absorbs Windows ENOTEMPTY/EBUSY when the OS still holds a handle - // on a just-closed log stream under tmpHome/logs. - rmSync(tmpHome, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); + // on a just-closed log stream under tmpHome/logs; try/catch is the final + // best-effort fallback if retries are exhausted, so cleanup can't fail the test. + try { + rmSync(tmpHome, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); + } catch { + /* best-effort */ + } vi.restoreAllMocks(); });