瀏覽代碼

fix(cli): reject leaked config replacement flags

Turtle 2 月之前
父節點
當前提交
030b044350
共有 2 個文件被更改,包括 9 次插入 和 3 次删除
  1. 3 3
      apps/cli/src/args.ts
  2. 6 0
      apps/cli/tests/args.spec.ts

+ 3 - 3
apps/cli/src/args.ts

@@ -226,9 +226,9 @@ Examples:
       // the subcommand into `program.opts()`. `meta` accepts only `--resume`, so
       // a leaked `--config`/`-p` is a mistyped invocation that must fail loud
       // rather than silently be dropped.
-      const parent = program.opts<{ config?: string; prompt?: string; resume?: string }>()
-      if (parent.config !== undefined || parent.prompt !== undefined) {
-        program.error('error: meta takes neither --config nor -p/--prompt')
+      const parent = program.opts<{ config?: string; configReplace?: string; prompt?: string; resume?: string }>()
+      if (parent.config !== undefined || parent.configReplace !== undefined || parent.prompt !== undefined) {
+        program.error('error: meta takes none of --config, --config-replace, or -p/--prompt')
       }
       // Same reason as the default surface: an empty id would start a fresh
       // session downstream instead of failing the mistyped resume.

+ 6 - 0
apps/cli/tests/args.spec.ts

@@ -27,6 +27,7 @@ describe('parseDshArgs', () => {
   it('routes each mode by its shape: default TUI, -p headless, meta and web subcommands', () => {
     expect(parse([])).toEqual({ mode: 'tui' })
     expect(parse(['--config', 'custom.yml'])).toEqual({ mode: 'tui', config: 'custom.yml' })
+    expect(parse(['--config-replace', 'tree.yml'])).toEqual({ mode: 'tui', configReplace: 'tree.yml' })
     expect(parse(['--resume', 'sess', '--config', 'app.yml'])).toEqual({ mode: 'tui', config: 'app.yml', resume: 'sess' })
     expect(parse(['-p', 'do the thing'])).toEqual({ mode: 'headless', prompt: 'do the thing' })
     // `meta` accepts `--resume` but does not redeclare it: a shared option parses
@@ -58,6 +59,8 @@ describe('parseDshArgs', () => {
     expect(exitCode(['--resume='])).toBe(1)
     expect(exitCode(['-p', ''])).toBe(1)
     expect(exitCode(['-p', 'x', '--config', 'c.yml'])).toBe(1)
+    expect(exitCode(['-p', 'x', '--config-replace', 'tree.yml'])).toBe(1)
+    expect(exitCode(['--config', 'c.yml', '--config-replace', 'tree.yml'])).toBe(1)
     expect(exitCode(['-p', 'x', '--resume', 's'])).toBe(1)
     expect(exitCode(['--bogus'])).toBe(1)
     expect(exitCode(['bogus-positional'])).toBe(1)
@@ -66,17 +69,20 @@ describe('parseDshArgs', () => {
     expect(exitCode(['web', '-p', 'task'])).toBe(1)
     expect(exitCode(['web', '--resume', 's'])).toBe(1)
     expect(exitCode(['--config', 'c.yml', 'web'])).toBe(1)
+    expect(exitCode(['--config-replace', 'tree.yml', 'web'])).toBe(1)
     // Same rule for credential setup: it shares no option with the default
     // surface, so a leaked flag is a typo, not something to ignore.
     // `meta` fixes its own config tree and is interactive, so --config/-p are
     // rejected; an empty id is swallowed downstream exactly as above.
     expect(exitCode(['meta', '--resume='])).toBe(1)
     expect(exitCode(['meta', '--config', 'c.yml'])).toBe(1)
+    expect(exitCode(['meta', '--config-replace', 'tree.yml'])).toBe(1)
     expect(exitCode(['meta', '-p', 'task'])).toBe(1)
     // `migrate`/`upgrade` take no options: any leaked default-surface flag is a
     // mistyped invocation, not a silently-dropped input.
     expect(exitCode(['migrate', '--resume', 's'])).toBe(1)
     expect(exitCode(['migrate', '--config', 'c.yml'])).toBe(1)
+    expect(exitCode(['migrate', '--config-replace', 'tree.yml'])).toBe(1)
     expect(exitCode(['migrate', '-p', 'task'])).toBe(1)
     expect(exitCode(['upgrade', '--resume', 's'])).toBe(1)
     expect(exitCode(['upgrade', '--config', 'c.yml'])).toBe(1)