mirror of
https://github.com/wu736139669/hapi.git
synced 2026-11-14 02:51:43 +00:00
feat(cli): add agent picker and remove Claude default
Separate top-level help and version flags from agent arguments. Require explicit agents in scripts and preserve command argument boundaries. Validation: bun typecheck, bun run test, targeted runner integration tests, and PTY/source/compiled argv smoke checks.
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
import { basename } from 'node:path'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { normalizeCliArgs } from './cliArgs'
|
||||
|
||||
describe('CLI argv normalization', () => {
|
||||
const exec = process.execPath
|
||||
const entrypoint = '/workspace/cli/src/index.ts'
|
||||
|
||||
afterEach(() => { vi.unstubAllGlobals() })
|
||||
|
||||
it.each([
|
||||
[exec],
|
||||
[basename(exec)],
|
||||
[exec, entrypoint],
|
||||
['bun', entrypoint],
|
||||
[exec, entrypoint, '--']
|
||||
])('strips the runtime wrapper %j without consuming command arguments', (...prefix) => {
|
||||
const args = ['codex', '--', '--help', '--version', 'prompt with spaces']
|
||||
expect(normalizeCliArgs([...prefix, ...args])).toEqual(args)
|
||||
})
|
||||
|
||||
it('preserves a separator in already normalized arguments', () => {
|
||||
expect(normalizeCliArgs(['claude', '--', '--model', 'literal']))
|
||||
.toEqual(['claude', '--', '--model', 'literal'])
|
||||
expect(normalizeCliArgs(['--', '--help'])).toEqual(['--', '--help'])
|
||||
})
|
||||
|
||||
it('preserves runner handoff arguments for compiled and source entrypoints', () => {
|
||||
const args = ['runner', 'start-sync', '--workspace-root', '/my project']
|
||||
expect(normalizeCliArgs([exec, ...args])).toEqual(args)
|
||||
expect(normalizeCliArgs([exec, entrypoint, ...args])).toEqual(args)
|
||||
})
|
||||
|
||||
it('returns an empty command for bare invocations', () => {
|
||||
expect(normalizeCliArgs([])).toEqual([])
|
||||
expect(normalizeCliArgs([exec])).toEqual([])
|
||||
expect(normalizeCliArgs([exec, entrypoint])).toEqual([])
|
||||
})
|
||||
|
||||
it('preserves filenames used as prompts instead of treating them as entrypoints', () => {
|
||||
vi.stubGlobal('Bun', { main: entrypoint })
|
||||
expect(normalizeCliArgs([exec, entrypoint, 'prompt.ts', 'another.js']))
|
||||
.toEqual(['prompt.ts', 'another.js'])
|
||||
expect(normalizeCliArgs(['prompt.ts'])).toEqual(['prompt.ts'])
|
||||
})
|
||||
|
||||
it('handles Bun virtual entrypoints and compiled process argv', () => {
|
||||
const main = '/$bunfs/root/hapi'
|
||||
vi.stubGlobal('Bun', { main })
|
||||
expect(normalizeCliArgs(['bun', main, 'codex', '--', '--help']))
|
||||
.toEqual(['codex', '--', '--help'])
|
||||
expect(normalizeCliArgs(['bun', main, 'prompt.ts'])).toEqual(['prompt.ts'])
|
||||
expect(normalizeCliArgs([exec, 'prompt.ts'])).toEqual(['prompt.ts'])
|
||||
expect(normalizeCliArgs(['bun', main])).toEqual([])
|
||||
})
|
||||
})
|
||||
+22
-42
@@ -12,28 +12,12 @@ function isEntrypointPath(value: string, bunMain: string): boolean {
|
||||
if (!value) {
|
||||
return false;
|
||||
}
|
||||
if (value === bunMain) {
|
||||
return true;
|
||||
if (bunMain) {
|
||||
return value === bunMain;
|
||||
}
|
||||
return /\.(c|m)?(ts|js)$/.test(value);
|
||||
}
|
||||
|
||||
function hasRuntimeWrapper(preArgs: string[], execPath: string, execBase: string, bunMain: string): boolean {
|
||||
if (preArgs.length === 0) {
|
||||
return false;
|
||||
}
|
||||
if (preArgs[0] === 'bun') {
|
||||
return true;
|
||||
}
|
||||
if (preArgs.length < 2) {
|
||||
return false;
|
||||
}
|
||||
if (preArgs[0] !== execPath && preArgs[0] !== execBase) {
|
||||
return false;
|
||||
}
|
||||
return isEntrypointPath(preArgs[1], bunMain);
|
||||
}
|
||||
|
||||
export function normalizeCliArgs(rawArgv: string[]): string[] {
|
||||
if (!Array.isArray(rawArgv) || rawArgv.length === 0) {
|
||||
return [];
|
||||
@@ -42,34 +26,30 @@ export function normalizeCliArgs(rawArgv: string[]): string[] {
|
||||
const execPath = process.execPath;
|
||||
const execBase = basename(execPath);
|
||||
const bunMain = globalThis.Bun?.main ?? '';
|
||||
const dashIndex = rawArgv.indexOf('--');
|
||||
let argv = rawArgv.slice();
|
||||
if (dashIndex >= 0) {
|
||||
const preArgs = rawArgv.slice(0, dashIndex);
|
||||
const postArgs = rawArgv.slice(dashIndex + 1);
|
||||
argv = hasRuntimeWrapper(preArgs, execPath, execBase, bunMain)
|
||||
? postArgs
|
||||
: [...preArgs, ...postArgs];
|
||||
}
|
||||
const argv = rawArgv;
|
||||
|
||||
let startIndex = 0;
|
||||
while (startIndex < argv.length) {
|
||||
const value = argv[startIndex] || '';
|
||||
const nextValue = argv[startIndex + 1] || '';
|
||||
if (
|
||||
value === 'bun' &&
|
||||
(nextValue === bunMain || nextValue === execPath || nextValue === execBase || isEntrypointPath(nextValue, bunMain))
|
||||
) {
|
||||
startIndex += 2;
|
||||
continue;
|
||||
}
|
||||
if (value === execPath || value === execBase || isEntrypointPath(value, bunMain)) {
|
||||
startIndex += 1;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
const nextValue = argv[1] || '';
|
||||
if (argv[0] === 'bun' && (
|
||||
nextValue === execPath || nextValue === execBase || isEntrypointPath(nextValue, bunMain)
|
||||
)) {
|
||||
startIndex += 1;
|
||||
}
|
||||
if (argv[startIndex] === execPath || argv[startIndex] === execBase) {
|
||||
startIndex += 1;
|
||||
}
|
||||
// Consume at most one entrypoint. Later filenames, even *.ts / *.js, are
|
||||
// user arguments, not additional runtime wrappers.
|
||||
if ((startIndex > 0 || (bunMain && argv[0] === bunMain))
|
||||
&& isEntrypointPath(argv[startIndex] || '', bunMain)) {
|
||||
startIndex += 1;
|
||||
}
|
||||
|
||||
// Only a separator immediately after the runtime/entrypoint is a wrapper
|
||||
// separator. A later `--` belongs to the selected command and its arguments.
|
||||
if (startIndex > 0 && argv[startIndex] === '--') {
|
||||
startIndex += 1;
|
||||
}
|
||||
return argv.slice(startIndex);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user