From 24f6e714c4f51802f966d5b4e85312d2b07f4a5c Mon Sep 17 00:00:00 2001 From: suibianwanwan Date: Mon, 24 Aug 2026 16:12:33 +0800 Subject: [PATCH] fix(cli): harden the argument and output surface against silent misreports Co-Authored-By: Claude Opus 5 (1M context) --- packages/cz-cli/UPSTREAM-PATCHES.md | 20 + packages/cz-cli/src/agent-mcp.ts | 19 +- packages/cz-cli/src/bootstrap/runtime.ts | 71 ++ packages/cz-cli/src/cli.ts | 152 +++- packages/cz-cli/src/command-group.ts | 15 +- packages/cz-cli/src/commands/agent-llm.ts | 21 +- .../cz-cli/src/commands/analytics-agent.ts | 3 + packages/cz-cli/src/commands/auth.ts | 10 +- packages/cz-cli/src/commands/datasource.ts | 3 + .../cz-cli/src/commands/profile-bootstrap.ts | 3 + packages/cz-cli/src/commands/schema.ts | 2 +- packages/cz-cli/src/commands/sql.ts | 41 +- packages/cz-cli/src/commands/status.ts | 43 +- packages/cz-cli/src/commands/task.ts | 5 +- packages/cz-cli/src/connection/config.ts | 30 +- packages/cz-cli/src/main.ts | 6 +- packages/cz-cli/src/output/index.ts | 213 +++-- packages/cz-cli/src/run-cli.ts | 128 ++- packages/cz-cli/src/telemetry.ts | 63 +- packages/cz-cli/src/usage-error.ts | 16 + packages/cz-cli/test/agent-mcp.test.ts | 34 + packages/cz-cli/test/auth-list-format.test.ts | 125 +++ .../cz-cli/test/connection-config.test.ts | 90 ++ .../cz-cli/test/output-row-projection.test.ts | 425 ++++++++++ .../cz-cli/test/parameter-hardening.test.ts | 777 ++++++++++++++++++ packages/cz-cli/test/status-exit-code.test.ts | 74 ++ packages/cz-cli/test/telemetry.test.ts | 36 +- 27 files changed, 2291 insertions(+), 134 deletions(-) create mode 100644 packages/cz-cli/src/usage-error.ts create mode 100644 packages/cz-cli/test/auth-list-format.test.ts create mode 100644 packages/cz-cli/test/output-row-projection.test.ts create mode 100644 packages/cz-cli/test/parameter-hardening.test.ts create mode 100644 packages/cz-cli/test/status-exit-code.test.ts diff --git a/packages/cz-cli/UPSTREAM-PATCHES.md b/packages/cz-cli/UPSTREAM-PATCHES.md index 0166cafaad..cd047fe337 100644 --- a/packages/cz-cli/UPSTREAM-PATCHES.md +++ b/packages/cz-cli/UPSTREAM-PATCHES.md @@ -410,6 +410,26 @@ the hooks they depend on still exist in the new upstream. --- +### 6. `cz-cli serve` flag surface + +- **cz files:** `packages/cz-cli/src/bootstrap/runtime.ts` (the `serve` branch: + `applyServeLogFlags`, `serveInheritedGlobals`). +- **Mechanism:** `cz-cli serve` runs its own yargs instance around upstream's + `ServeCommand`, so it never passes through upstream's ROOT parser. Two things are + re-created there: the root parser's logging flags (`--print-logs`, `--log-level`, + `--pure`), wired to the same env vars upstream's root middleware sets + (`OPENCODE_PRINT_LOGS`, `OPENCODE_LOG_LEVEL`, `OPENCODE_PURE`), and the cz global + flags that run-cli has already consumed off the same argv, declared hidden so + `.strict()` can reject a real typo without rejecting a working invocation. +- **Upstream hooks to re-verify:** `packages/opencode/src/cli/index.ts` still declares + those three flags on the root parser and its middleware still reads them from those + env var names; `packages/opencode/src/cli/cmd/serve.ts` still takes its network + options from `withNetworkOptions`. +- **Failure mode if the hook moves:** the logging flags silently go back to being + accepted and doing nothing (an env var rename), or `serve` starts rejecting a flag + it should accept (a new global in cli.ts's `KNOWN_GLOBAL_FLAGS` is covered + automatically; a new UPSTREAM root flag is not). + ## Re-baseline procedure (quick) 1. Fast-forward upstream packages to the new opencode version. diff --git a/packages/cz-cli/src/agent-mcp.ts b/packages/cz-cli/src/agent-mcp.ts index f7d86804c3..3c9e6e300f 100644 --- a/packages/cz-cli/src/agent-mcp.ts +++ b/packages/cz-cli/src/agent-mcp.ts @@ -196,10 +196,21 @@ function manifestName(relativePath: string) { } function resolveClickZettaRemote(manifest: ClickZettaRemoteManifest, cliArgs: Partial): RemoteMcpConfig | undefined { - const connection = resolveConnectionConfig({ - ...cliArgs, - profile: manifest.profile ?? cliArgs.profile, - }) + // `manifest.profile` is a field in a config file on disk, not something the caller + // typed, so a profile that has since been renamed or deleted must skip THIS entry — + // the way a manifest with no usable auth already does — rather than abort the whole + // invocation. resolveConnectionConfig rejects an explicitly named missing profile, + // and nothing between here and run-cli's injectAgentMcp call would catch it. + let connection: ReturnType + try { + connection = resolveConnectionConfig({ + ...cliArgs, + profile: manifest.profile ?? cliArgs.profile, + }) + } catch (err) { + if ((err as { code?: unknown } | null)?.code === "PROFILE_NOT_FOUND") return undefined + throw err + } if (!hasLakehouseAuth(connection) && manifest.enabled !== false) return undefined return compactEntry({ type: "remote", diff --git a/packages/cz-cli/src/bootstrap/runtime.ts b/packages/cz-cli/src/bootstrap/runtime.ts index 5a3f450bce..d8dd6141bb 100644 --- a/packages/cz-cli/src/bootstrap/runtime.ts +++ b/packages/cz-cli/src/bootstrap/runtime.ts @@ -7,10 +7,53 @@ import { flushOtel } from "../opencode-plugin/otel/index.js" import { flushLangfuse, initLangfuse } from "../langfuse.js" import { CLICKZETTA_AGENT_SYSTEM_PROMPT } from "../agent-system-prompt.js" import { parseAgentTimeoutMs } from "./runtime-config.js" +import { KNOWN_GLOBAL_FLAGS } from "../cli.js" import { applyBaseOpencodeEnv, applyAgentRuntimeInjection } from "./opencode-injection.js" let globalHandlersRegistered = false +/** + * The cz global flags that reach `cz-cli serve`'s own parser, as hidden no-ops. + * + * Built from cli.ts's KNOWN_GLOBAL_FLAGS — the single list the top-level parser and + * both fail handlers already use — so adding a global there cannot silently start + * failing `serve`. `help`/`version` are declared separately above with their real + * behavior, and the short aliases are declared as their own entries because they + * arrive un-canonicalized on this path. + */ +function serveInheritedGlobals(): Record { + const booleans = new Set(["debug", "d"]) + const skip = new Set(["help", "h", "version", "v"]) + const options: Record = {} + for (const flag of KNOWN_GLOBAL_FLAGS) { + if (skip.has(flag)) continue + options[flag] = { type: booleans.has(flag) ? "boolean" : "string", hidden: true } + } + return options +} + +/** The logging flags `cz-cli serve` accepts, mirroring upstream's root parser. */ +export interface ServeLogFlags { + "print-logs"?: boolean + "log-level"?: string + pure?: boolean +} + +/** + * cz_change: wire `cz-cli serve`'s logging flags to the env vars opencode reads. + * + * These three are declared on upstream's ROOT parser (opencode's index.ts), which + * `cz-cli serve` never goes through, so before this they were accepted and did + * nothing — and could not be rejected either, because turning on .strict() without + * declaring them would have failed a documented invocation. Exported so a test can + * assert the wiring without starting a server. + */ +export function applyServeLogFlags(flags: ServeLogFlags): void { + if (flags["print-logs"]) process.env.OPENCODE_PRINT_LOGS = "1" + if (flags["log-level"]) process.env.OPENCODE_LOG_LEVEL = String(flags["log-level"]) + if (flags.pure) process.env.OPENCODE_PURE = "1" +} + export async function main(args: string[], agentRuntime = false): Promise { // cz_change: apply the base opencode env injection (kill upstream auto-updater, // disable repo-local project config, telemetry defaults) at the very top of main() @@ -118,11 +161,35 @@ export async function main(args: string[], agentRuntime = false): Promise