From 47dbf9beda434a0b3ce4f20a585613a1ca53307a Mon Sep 17 00:00:00 2001 From: Benjamin Jesuiter Date: Tue, 17 Feb 2026 10:42:06 +0100 Subject: [PATCH] CLI: unify interactive root argv scanning --- src/cli/run-main.test.ts | 22 +++++++++ src/cli/run-main.ts | 102 +++++++++++++++------------------------ 2 files changed, 62 insertions(+), 62 deletions(-) diff --git a/src/cli/run-main.test.ts b/src/cli/run-main.test.ts index a91e3d9fd0f..6315708a25c 100644 --- a/src/cli/run-main.test.ts +++ b/src/cli/run-main.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest"; import { isCommanderExitError, rewriteUpdateFlagArgv, + scanInteractiveRootArgv, shouldEnsureCliPath, shouldRegisterPrimarySubcommand, shouldSkipPluginCommandRegistration, @@ -119,6 +120,27 @@ describe("shouldSkipPluginCommandRegistration", () => { }); }); +describe("scanInteractiveRootArgv", () => { + it("extracts interactive flag, primary, and stripped argv in one pass", () => { + expect( + scanInteractiveRootArgv(["node", "openclaw", "-i", "--profile", "dev"]).hasInteractiveFlag, + ).toBe(true); + expect( + scanInteractiveRootArgv(["node", "openclaw", "-i", "--profile", "dev"]).primary, + ).toBeNull(); + expect( + scanInteractiveRootArgv(["node", "openclaw", "-i", "--profile", "dev"]).strippedArgv, + ).toEqual(["node", "openclaw", "--profile", "dev"]); + }); + + it("detects primary commands while stripping interactive flags", () => { + const scanned = scanInteractiveRootArgv(["node", "openclaw", "-i", "status", "--json"]); + expect(scanned.hasInteractiveFlag).toBe(true); + expect(scanned.primary).toBe("status"); + expect(scanned.strippedArgv).toEqual(["node", "openclaw", "status", "--json"]); + }); +}); + describe("shouldUseInteractiveCommandSelector", () => { it("enables selector for -i", () => { expect( diff --git a/src/cli/run-main.ts b/src/cli/run-main.ts index f3662e63c72..e1c31b15905 100644 --- a/src/cli/run-main.ts +++ b/src/cli/run-main.ts @@ -63,42 +63,56 @@ export function shouldEnsureCliPath(argv: string[]): boolean { const ROOT_OPTIONS_WITH_VALUE = new Set(["--profile"]); -function parseRootInvocation(argv: string[]): { +export function scanInteractiveRootArgv(argv: string[]): { primary: string | null; hasInteractiveFlag: boolean; + strippedArgv: string[]; } { const args = argv.slice(2); + const next: string[] = []; let primary: string | null = null; let hasInteractiveFlag = false; let expectOptionValue = false; + let sawPrimary = false; for (const arg of args) { - if (expectOptionValue) { - expectOptionValue = false; - continue; + if (!sawPrimary) { + if (expectOptionValue) { + expectOptionValue = false; + next.push(arg); + continue; + } + if (arg === "--") { + sawPrimary = true; + next.push(arg); + continue; + } + if (arg === "-i" || arg === "--interactive") { + hasInteractiveFlag = true; + continue; + } + if (arg.startsWith("--profile=")) { + next.push(arg); + continue; + } + if (ROOT_OPTIONS_WITH_VALUE.has(arg)) { + expectOptionValue = true; + next.push(arg); + continue; + } + if (!arg.startsWith("-")) { + primary = arg; + sawPrimary = true; + } } - if (arg === "--") { - break; - } - if (arg === "-i" || arg === "--interactive") { - hasInteractiveFlag = true; - continue; - } - if (arg.startsWith("--profile=")) { - continue; - } - if (ROOT_OPTIONS_WITH_VALUE.has(arg)) { - expectOptionValue = true; - continue; - } - if (arg.startsWith("-")) { - continue; - } - primary = arg; - break; + next.push(arg); } - return { primary, hasInteractiveFlag }; + return { + primary, + hasInteractiveFlag, + strippedArgv: [...argv.slice(0, 2), ...next], + }; } export function shouldUseInteractiveCommandSelector(params: { @@ -111,7 +125,7 @@ export function shouldUseInteractiveCommandSelector(params: { if (hasHelpOrVersion(params.argv)) { return false; } - const root = parseRootInvocation(params.argv); + const root = scanInteractiveRootArgv(params.argv); if (!root.hasInteractiveFlag) { return false; } @@ -130,43 +144,7 @@ export function shouldUseInteractiveCommandSelector(params: { } export function stripInteractiveSelectorArgs(argv: string[]): string[] { - const args = argv.slice(2); - const next: string[] = []; - let sawPrimary = false; - let expectOptionValue = false; - - for (const arg of args) { - if (!sawPrimary) { - if (expectOptionValue) { - expectOptionValue = false; - next.push(arg); - continue; - } - if (arg === "--") { - sawPrimary = true; - next.push(arg); - continue; - } - if (arg === "-i" || arg === "--interactive") { - continue; - } - if (arg.startsWith("--profile=")) { - next.push(arg); - continue; - } - if (ROOT_OPTIONS_WITH_VALUE.has(arg)) { - expectOptionValue = true; - next.push(arg); - continue; - } - if (!arg.startsWith("-")) { - sawPrimary = true; - } - } - next.push(arg); - } - - return [...argv.slice(0, 2), ...next]; + return scanInteractiveRootArgv(argv).strippedArgv; } export function isCommanderExitError(error: unknown): boolean {