diff --git a/CHANGELOG.md b/CHANGELOG.md index 168d522d8fd..2a572a60384 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ Docs: https://docs.openclaw.ai - Provider/HTTP: treat HTTP 503 as failover-eligible for LLM provider errors. (#21086) Thanks @Protocol-zero-0. - Slack: pass `recipient_team_id` / `recipient_user_id` through Slack native streaming calls so `chat.startStream`/`appendStream`/`stopStream` work reliably across DMs and Slack Connect setups, and disable block streaming when native streaming is active. (#20988) Thanks @Dithilli. Earlier recipient-ID groundwork was contributed in #20377 by @AsserAl1012. - CLI/Config: add canonical `--strict-json` parsing for `config set` and keep `--json` as a legacy alias to reduce help/behavior drift. (#21332) thanks @adhitShet. +- CLI: keep `openclaw -v` as a root-only version alias so subcommand `-v, --verbose` flags (for example ACP/hooks/skills) are no longer intercepted globally. (#21303) thanks @adhitShet. - Discord/Gateway: handle close code 4014 (missing privileged gateway intents) without crashing the gateway. Thanks @thewilloftheshadow. - Security/Net: strip sensitive headers (`Authorization`, `Proxy-Authorization`, `Cookie`, `Cookie2`) on cross-origin redirects in `fetchWithSsrFGuard` to prevent credential forwarding across origin boundaries. (#20313) Thanks @afurm. diff --git a/src/cli/acp-cli.ts b/src/cli/acp-cli.ts index 130c9ab3b07..c4c7b09aeaf 100644 --- a/src/cli/acp-cli.ts +++ b/src/cli/acp-cli.ts @@ -45,7 +45,7 @@ export function registerAcpCli(program: Command) { .option("--require-existing", "Fail if the session key/label does not exist", false) .option("--reset-session", "Reset the session key before first use", false) .option("--no-prefix-cwd", "Do not prefix prompts with the working directory", false) - .option("--verbose, -v", "Verbose logging to stderr", false) + .option("-v, --verbose", "Verbose logging to stderr", false) .addHelpText( "after", () => `\n${theme.muted("Docs:")} ${formatDocsLink("/cli/acp", "docs.openclaw.ai/cli/acp")}\n`, @@ -96,7 +96,7 @@ export function registerAcpCli(program: Command) { .option("--server ", "ACP server command (default: openclaw)") .option("--server-args ", "Extra arguments for the ACP server") .option("--server-verbose", "Enable verbose logging on the ACP server", false) - .option("--verbose, -v", "Verbose client logging", false) + .option("-v, --verbose", "Verbose client logging", false) .action(async (opts, command) => { const inheritedVerbose = inheritOptionFromParent(command, "verbose"); try { diff --git a/src/cli/argv.test.ts b/src/cli/argv.test.ts index 0b7a9d66931..19e431a04f9 100644 --- a/src/cli/argv.test.ts +++ b/src/cli/argv.test.ts @@ -29,6 +29,31 @@ describe("argv helpers", () => { argv: ["node", "openclaw", "status"], expected: false, }, + { + name: "root -v alias", + argv: ["node", "openclaw", "-v"], + expected: true, + }, + { + name: "root -v alias with profile", + argv: ["node", "openclaw", "--profile", "work", "-v"], + expected: true, + }, + { + name: "subcommand -v should not be treated as version", + argv: ["node", "openclaw", "acp", "-v"], + expected: false, + }, + { + name: "root -v alias with equals profile", + argv: ["node", "openclaw", "--profile=work", "-v"], + expected: true, + }, + { + name: "subcommand path after global root flags should not be treated as version", + argv: ["node", "openclaw", "--dev", "skills", "list", "-v"], + expected: false, + }, ])("detects help/version flags: $name", ({ argv, expected }) => { expect(hasHelpOrVersion(argv)).toBe(expected); }); diff --git a/src/cli/argv.ts b/src/cli/argv.ts index 1489cec4fc3..a3e20d3e4c0 100644 --- a/src/cli/argv.ts +++ b/src/cli/argv.ts @@ -1,9 +1,14 @@ const HELP_FLAGS = new Set(["-h", "--help"]); -const VERSION_FLAGS = new Set(["-v", "-V", "--version"]); +const VERSION_FLAGS = new Set(["-V", "--version"]); +const ROOT_VERSION_ALIAS_FLAG = "-v"; +const ROOT_BOOLEAN_FLAGS = new Set(["--dev", "--no-color"]); +const ROOT_VALUE_FLAGS = new Set(["--profile"]); const FLAG_TERMINATOR = "--"; export function hasHelpOrVersion(argv: string[]): boolean { - return argv.some((arg) => HELP_FLAGS.has(arg) || VERSION_FLAGS.has(arg)); + return ( + argv.some((arg) => HELP_FLAGS.has(arg) || VERSION_FLAGS.has(arg)) || hasRootVersionAlias(argv) + ); } function isValueToken(arg: string | undefined): boolean { @@ -40,6 +45,42 @@ export function hasFlag(argv: string[], name: string): boolean { return false; } +export function hasRootVersionAlias(argv: string[]): boolean { + const args = argv.slice(2); + let hasAlias = false; + for (let i = 0; i < args.length; i += 1) { + const arg = args[i]; + if (!arg) { + continue; + } + if (arg === FLAG_TERMINATOR) { + break; + } + if (arg === ROOT_VERSION_ALIAS_FLAG) { + hasAlias = true; + continue; + } + if (ROOT_BOOLEAN_FLAGS.has(arg)) { + continue; + } + if (arg.startsWith("--profile=")) { + continue; + } + if (ROOT_VALUE_FLAGS.has(arg)) { + const next = args[i + 1]; + if (isValueToken(next)) { + i += 1; + } + continue; + } + if (arg.startsWith("-")) { + continue; + } + return false; + } + return hasAlias; +} + export function getFlagValue(argv: string[], name: string): string | null | undefined { const args = argv.slice(2); for (let i = 0; i < args.length; i += 1) { diff --git a/src/cli/banner.ts b/src/cli/banner.ts index 629a1f24c95..2417566548b 100644 --- a/src/cli/banner.ts +++ b/src/cli/banner.ts @@ -1,6 +1,7 @@ import { resolveCommitHash } from "../infra/git-commit.js"; import { visibleWidth } from "../terminal/ansi.js"; import { isRich, theme } from "../terminal/theme.js"; +import { hasRootVersionAlias } from "./argv.js"; import { pickTagline, type TaglineOptions } from "./tagline.js"; type BannerOptions = TaglineOptions & { @@ -32,7 +33,7 @@ const hasJsonFlag = (argv: string[]) => argv.some((arg) => arg === "--json" || arg.startsWith("--json=")); const hasVersionFlag = (argv: string[]) => - argv.some((arg) => arg === "--version" || arg === "-V" || arg === "-v"); + argv.some((arg) => arg === "--version" || arg === "-V") || hasRootVersionAlias(argv); export function formatCliBannerLine(version: string, options: BannerOptions = {}): string { const commit = options.commit ?? resolveCommitHash({ env: options.env }); diff --git a/src/cli/program/build-program.version-alias.test.ts b/src/cli/program/build-program.version-alias.test.ts new file mode 100644 index 00000000000..1d2ef41b8d4 --- /dev/null +++ b/src/cli/program/build-program.version-alias.test.ts @@ -0,0 +1,39 @@ +import process from "node:process"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const { buildProgram } = await import("./build-program.js"); + +describe("buildProgram version alias handling", () => { + let originalArgv: string[]; + + beforeEach(() => { + originalArgv = [...process.argv]; + }); + + afterEach(() => { + process.argv = originalArgv; + vi.restoreAllMocks(); + }); + + it("exits with version output for root -v", () => { + process.argv = ["node", "openclaw", "-v"]; + const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + const exitSpy = vi.spyOn(process, "exit").mockImplementation(((code?: number) => { + throw new Error(`process.exit:${String(code)}`); + }) as typeof process.exit); + + expect(() => buildProgram()).toThrow("process.exit:0"); + expect(logSpy).toHaveBeenCalledTimes(1); + expect(exitSpy).toHaveBeenCalledWith(0); + }); + + it("does not treat subcommand -v as root version alias", () => { + process.argv = ["node", "openclaw", "acp", "-v"]; + const exitSpy = vi.spyOn(process, "exit").mockImplementation(((code?: number) => { + throw new Error(`unexpected process.exit:${String(code)}`); + }) as typeof process.exit); + + expect(() => buildProgram()).not.toThrow(); + expect(exitSpy).not.toHaveBeenCalled(); + }); +}); diff --git a/src/cli/program/help.ts b/src/cli/program/help.ts index 8769a08db98..94bb5ac7a1e 100644 --- a/src/cli/program/help.ts +++ b/src/cli/program/help.ts @@ -2,6 +2,7 @@ import type { Command } from "commander"; import { formatDocsLink } from "../../terminal/links.js"; import { isRich, theme } from "../../terminal/theme.js"; import { escapeRegExp } from "../../utils.js"; +import { hasFlag, hasRootVersionAlias } from "../argv.js"; import { formatCliBannerLine, hasEmittedCliBanner } from "../banner.js"; import { replaceCliName, resolveCliName } from "../cli-name.js"; import { getCoreCliCommandsWithSubcommands } from "./command-registry.js"; @@ -98,9 +99,9 @@ export function configureProgramHelp(program: Command, ctx: ProgramContext) { }); if ( - process.argv.includes("-V") || - process.argv.includes("--version") || - process.argv.includes("-v") + hasFlag(process.argv, "-V") || + hasFlag(process.argv, "--version") || + hasRootVersionAlias(process.argv) ) { console.log(ctx.programVersion); process.exit(0); diff --git a/src/cli/run-main.test.ts b/src/cli/run-main.test.ts index c86071f7d80..0884d05b65e 100644 --- a/src/cli/run-main.test.ts +++ b/src/cli/run-main.test.ts @@ -44,10 +44,12 @@ describe("shouldRegisterPrimarySubcommand", () => { it("skips eager primary registration for help/version invocations", () => { expect(shouldRegisterPrimarySubcommand(["node", "openclaw", "status", "--help"])).toBe(false); expect(shouldRegisterPrimarySubcommand(["node", "openclaw", "-V"])).toBe(false); + expect(shouldRegisterPrimarySubcommand(["node", "openclaw", "-v"])).toBe(false); }); it("keeps eager primary registration for regular command runs", () => { expect(shouldRegisterPrimarySubcommand(["node", "openclaw", "status"])).toBe(true); + expect(shouldRegisterPrimarySubcommand(["node", "openclaw", "acp", "-v"])).toBe(true); }); }); @@ -107,6 +109,7 @@ describe("shouldEnsureCliPath", () => { it("skips path bootstrap for help/version invocations", () => { expect(shouldEnsureCliPath(["node", "openclaw", "--help"])).toBe(false); expect(shouldEnsureCliPath(["node", "openclaw", "-V"])).toBe(false); + expect(shouldEnsureCliPath(["node", "openclaw", "-v"])).toBe(false); }); it("skips path bootstrap for read-only fast paths", () => { @@ -119,5 +122,6 @@ describe("shouldEnsureCliPath", () => { it("keeps path bootstrap for mutating or unknown commands", () => { expect(shouldEnsureCliPath(["node", "openclaw", "message", "send"])).toBe(true); expect(shouldEnsureCliPath(["node", "openclaw", "voicecall", "status"])).toBe(true); + expect(shouldEnsureCliPath(["node", "openclaw", "acp", "-v"])).toBe(true); }); });