diff --git a/src/cli/argv-invocation.test.ts b/src/cli/argv-invocation.test.ts index 68c0237f2def..97aab17e3b92 100644 --- a/src/cli/argv-invocation.test.ts +++ b/src/cli/argv-invocation.test.ts @@ -25,6 +25,47 @@ describe("argv-invocation", () => { }); }); + it.each([ + { + name: "version-pinned install", + argv: ["node", "openclaw", "skills", "install", "@owner/weather", "--version", "1.2.3"], + commandPath: ["skills", "install"], + }, + { + name: "version-pinned verification", + argv: ["node", "openclaw", "skills", "verify", "@owner/weather", "--version", "1.2.3"], + commandPath: ["skills", "verify"], + }, + { + name: "equals-form version-pinned install", + argv: ["node", "openclaw", "skills", "install", "@owner/weather", "--version=1.2.3"], + commandPath: ["skills", "install"], + }, + { + name: "profiled version-pinned verification", + argv: [ + "node", + "openclaw", + "--profile", + "work", + "skills", + "verify", + "@owner/weather", + "--version", + "1.2.3", + ], + commandPath: ["skills", "verify"], + }, + ])("keeps $name in command execution mode", ({ argv, commandPath }) => { + expect(resolveCliArgvInvocation(argv)).toEqual({ + argv, + commandPath, + primary: "skills", + hasHelpOrVersion: false, + isRootHelpInvocation: false, + }); + }); + it("consumes agent parent option values before the exec subcommand", () => { expect( resolveCliArgvInvocation([ diff --git a/src/cli/argv.test.ts b/src/cli/argv.test.ts index caf7ed0490d6..686214200e99 100644 --- a/src/cli/argv.test.ts +++ b/src/cli/argv.test.ts @@ -293,6 +293,75 @@ describe("argv helpers", () => { argv: ["node", "openclaw", "nodes", "invoke", "--", "--version"], expected: false, }, + { + name: "root version flag", + argv: ["node", "openclaw", "--version"], + expected: true, + }, + { + name: "root short version flag", + argv: ["node", "openclaw", "-V"], + expected: true, + }, + { + name: "root version alias after profile", + argv: ["node", "openclaw", "--profile", "work", "-v"], + expected: true, + }, + { + name: "root version flag after profile", + argv: ["node", "openclaw", "--profile", "work", "--version"], + expected: true, + }, + { + name: "version-pinned skill install", + argv: ["node", "openclaw", "skills", "install", "@owner/weather", "--version", "1.2.3"], + expected: false, + }, + { + name: "version-pinned skill verification", + argv: ["node", "openclaw", "skills", "verify", "@owner/weather", "--version", "1.2.3"], + expected: false, + }, + { + name: "equals-form version-pinned skill install", + argv: ["node", "openclaw", "skills", "install", "@owner/weather", "--version=1.2.3"], + expected: false, + }, + { + name: "profiled version-pinned skill verification", + argv: [ + "node", + "openclaw", + "--profile", + "work", + "skills", + "verify", + "@owner/weather", + "--version", + "1.2.3", + ], + expected: false, + }, + { + name: "help for a version-pinned skill command", + argv: [ + "node", + "openclaw", + "skills", + "verify", + "@owner/weather", + "--version", + "1.2.3", + "--help", + ], + expected: true, + }, + { + name: "unknown root option does not turn version into root help", + argv: ["node", "openclaw", "--unknown", "--version"], + expected: false, + }, ])("detects help/version invocations: $name", ({ argv, expected }) => { expect(isHelpOrVersionInvocation(argv)).toBe(expected); }); diff --git a/src/cli/argv.ts b/src/cli/argv.ts index ca3049eca7ad..c5980162660d 100644 --- a/src/cli/argv.ts +++ b/src/cli/argv.ts @@ -30,7 +30,7 @@ const ROOT_COMMANDS_WITH_SUBCOMMANDS: ReadonlySet = new Set( ); export function isHelpOrVersionInvocation(argv: string[]): boolean { - if (hasRootVersionAlias(argv)) { + if (isRootVersionInvocation(argv)) { return true; } @@ -47,7 +47,7 @@ export function isHelpOrVersionInvocation(argv: string[]): boolean { i += rootConsumed - 1; continue; } - if (HELP_FLAGS.has(arg) || VERSION_FLAGS.has(arg)) { + if (HELP_FLAGS.has(arg)) { return true; } if (arg.startsWith("-")) { diff --git a/src/cli/program/preaction.test.ts b/src/cli/program/preaction.test.ts index 525fb0beccc8..e0ad5a81e2cf 100644 --- a/src/cli/program/preaction.test.ts +++ b/src/cli/program/preaction.test.ts @@ -201,6 +201,14 @@ describe("registerPreActionHooks", () => { .action(() => {}); programLocal.command("completion").action(() => {}); programLocal.command("secrets").action(() => {}); + const skills = programLocal.command("skills"); + for (const skillCommand of ["install", "verify"]) { + skills + .command(skillCommand) + .argument("") + .option("--version ") + .action(() => {}); + } programLocal .command("qa") .command("suite") @@ -655,6 +663,51 @@ describe("registerPreActionHooks", () => { expect(ensureConfigReadyMock).toHaveBeenCalledTimes(1); }); + it.each([ + { + name: "version-pinned skill install", + action: "install", + argv: ["node", "openclaw", "skills", "install", "@owner/weather", "--version", "1.2.3"], + }, + { + name: "version-pinned skill verification", + action: "verify", + argv: ["node", "openclaw", "skills", "verify", "@owner/weather", "--version", "1.2.3"], + }, + { + name: "equals-form version-pinned skill install", + action: "install", + argv: ["node", "openclaw", "skills", "install", "@owner/weather", "--version=1.2.3"], + }, + { + name: "profiled version-pinned skill verification", + action: "verify", + argv: [ + "node", + "openclaw", + "--profile", + "work", + "skills", + "verify", + "@owner/weather", + "--version", + "1.2.3", + ], + }, + ])("runs the execution bootstrap for $name", async ({ action, argv }) => { + await runPreAction({ + parseArgv: ["skills", action], + processArgv: argv, + }); + + expect(ensureConfigReadyMock).toHaveBeenCalledWith( + expect.objectContaining({ + runtime: runtimeMock, + commandPath: ["skills", action], + }), + ); + }); + it("applies --json stdout suppression only for explicit JSON output commands", async () => { await runPreAction({ parseArgv: ["status"], diff --git a/src/cli/run-main.exit.test.ts b/src/cli/run-main.exit.test.ts index d369131969a5..5eaac1f19370 100644 --- a/src/cli/run-main.exit.test.ts +++ b/src/cli/run-main.exit.test.ts @@ -2211,6 +2211,52 @@ describe("runCli exit behavior", () => { expect(startProxyMock).toHaveBeenCalledWith(undefined); }); + it.each([ + { + name: "version-pinned skill install", + argv: ["node", "openclaw", "skills", "install", "@owner/weather", "--version", "1.2.3"], + }, + { + name: "version-pinned skill verification", + argv: ["node", "openclaw", "skills", "verify", "@owner/weather", "--version", "1.2.3"], + }, + { + name: "equals-form version-pinned skill install", + argv: ["node", "openclaw", "skills", "install", "@owner/weather", "--version=1.2.3"], + }, + { + name: "profiled version-pinned skill verification", + argv: [ + "node", + "openclaw", + "--profile", + "work", + "skills", + "verify", + "@owner/weather", + "--version", + "1.2.3", + ], + }, + ])("starts the managed proxy for $name", async ({ argv }) => { + await withEnvAsync( + { + OPENCLAW_PROFILE: undefined, + OPENCLAW_STATE_DIR: undefined, + OPENCLAW_CONFIG_PATH: undefined, + }, + async () => { + hasEnvHttpProxyAgentConfiguredMock.mockReturnValue(true); + tryRouteCliMock.mockResolvedValueOnce(true); + + await runCli(argv); + + expect(startProxyMock).toHaveBeenCalledWith(undefined); + expect(ensureGlobalUndiciEnvProxyDispatcherMock).toHaveBeenCalledOnce(); + }, + ); + }); + it.each([ ["JSON flag", ["node", "openclaw", "plugins", "marketplace", "list", "--json"]], ["models status JSON alias", ["node", "openclaw", "models", "--status-json"]],