diff --git a/src/argv.test.ts b/src/argv.test.ts new file mode 100644 index 0000000..37b86e3 --- /dev/null +++ b/src/argv.test.ts @@ -0,0 +1,57 @@ +import { describe, expect, it } from 'vitest'; +import { detectInitInvocation, firstOperand } from './argv.js'; + +const argv = (...args: string[]): string[] => ['/node', '/bitrefill', ...args]; + +describe('firstOperand', () => { + it('returns the first non-option token', () => { + expect(firstOperand(argv('--json', 'search-products'))).toBe( + 'search-products' + ); + }); + + it('returns undefined when there is no operand', () => { + expect(firstOperand(argv('--json'))).toBeUndefined(); + expect(firstOperand(argv())).toBeUndefined(); + }); +}); + +describe('detectInitInvocation', () => { + it('detects the init subcommand', () => { + expect(detectInitInvocation(argv('init')).isInit).toBe(true); + expect(detectInitInvocation(argv('init', '--openclaw')).isInit).toBe( + true + ); + expect(detectInitInvocation(argv('--json', 'init')).isInit).toBe(true); + }); + + it('reports --help and -h on the init subcommand', () => { + expect(detectInitInvocation(argv('init', '--help'))).toEqual({ + isInit: true, + hasHelp: true, + }); + expect(detectInitInvocation(argv('init', '-h'))).toEqual({ + isInit: true, + hasHelp: true, + }); + expect(detectInitInvocation(argv('init')).hasHelp).toBe(false); + }); + + it('does not treat an option value of "init" as the subcommand', () => { + expect( + detectInitInvocation(argv('search-products', '--query', 'init')) + .isInit + ).toBe(false); + expect( + detectInitInvocation(argv('llm-context', '-o', 'init')).isInit + ).toBe(false); + }); + + it('leaves `help init` to the help command', () => { + expect(detectInitInvocation(argv('help', 'init')).isInit).toBe(false); + }); + + it('returns false without arguments', () => { + expect(detectInitInvocation(argv()).isInit).toBe(false); + }); +}); diff --git a/src/argv.ts b/src/argv.ts new file mode 100644 index 0000000..0d02870 --- /dev/null +++ b/src/argv.ts @@ -0,0 +1,21 @@ +/** + * Subcommand detection for the pre-connect dispatch in `index.ts`, kept here + * so it can be unit tested without importing the CLI entry point. + */ +export interface InitInvocation { + isInit: boolean; + hasHelp: boolean; +} + +/** The subcommand is the first operand; an option value is never one. */ +export function firstOperand(argv: readonly string[]): string | undefined { + return argv.slice(2).find((arg) => !arg.startsWith('-')); +} + +export function detectInitInvocation(argv: readonly string[]): InitInvocation { + const args = argv.slice(2); + return { + isInit: firstOperand(argv) === 'init', + hasHelp: args.includes('--help') || args.includes('-h'), + }; +} diff --git a/src/index.ts b/src/index.ts index 8a5b0d0..368212f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -29,6 +29,7 @@ import { buildOptionsForTool, parseToolArgs } from './tools.js'; import { generateLlmContextMarkdown } from './llm-context.js'; import { buildManifest } from './manifest.js'; import { runInit } from './init.js'; +import { detectInitInvocation } from './argv.js'; import { BASE_MCP_URL, IS_DEFAULT_BASE_MCP_URL } from './config.js'; import { VERSION } from './version.js'; @@ -248,13 +249,6 @@ Options: --openclaw Force OpenClaw integration even if not auto-detected -h, --help Display help for command`; -function detectInitInvocation(): { isInit: boolean; hasHelp: boolean } { - const isInit = process.argv.some((arg, i) => arg === 'init' && i >= 2); - const hasHelp = - process.argv.includes('--help') || process.argv.includes('-h'); - return { isInit, hasHelp }; -} - async function handleInit(): Promise { const formatter = createOutputFormatter(resolveJsonMode()); try { @@ -484,7 +478,7 @@ function getClientId(serverUrl: string): string | undefined { // --- Main --- async function main(): Promise { - const init = detectInitInvocation(); + const init = detectInitInvocation(process.argv); if (init.isInit && init.hasHelp) { console.log(INIT_HELP); return;