From 992856ac5ef12c67a23781e7911ae8127c3cb0c7 Mon Sep 17 00:00:00 2001 From: sudacode Date: Sat, 25 Apr 2026 20:03:44 -0700 Subject: [PATCH] fix(launcher): reject --candidates and --select when used together - Validate mutually exclusive dictionary CLI flags; exit 1 with clear error - Add parseArgs test covering the conflicting-flags rejection path - Fix test helper to overwrite capture file (>) instead of appending (>>) --- launcher/config/cli-parser-builder.ts | 9 +++++++-- launcher/main.test.ts | 2 +- launcher/parse-args.test.ts | 8 ++++++++ 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/launcher/config/cli-parser-builder.ts b/launcher/config/cli-parser-builder.ts index eb0e06dc..b92a31c5 100644 --- a/launcher/config/cli-parser-builder.ts +++ b/launcher/config/cli-parser-builder.ts @@ -219,12 +219,17 @@ export function parseCliPrograms( .option('--select ', 'Pin an AniList media ID for the target series') .option('--log-level ', 'Log level') .action((target: string | undefined, options: Record) => { + const selectValue = typeof options.select === 'string' ? options.select.trim() : ''; + const hasSelect = selectValue.length > 0; + if (options.candidates === true && hasSelect) { + throw new Error('Dictionary --candidates and --select cannot be combined.'); + } dictionaryTriggered = true; dictionaryTarget = target ?? null; dictionaryLogLevel = typeof options.logLevel === 'string' ? options.logLevel : null; dictionaryCandidates = options.candidates === true; - dictionarySelect = typeof options.select === 'string'; - dictionaryAnilistId = typeof options.select === 'string' ? options.select : null; + dictionarySelect = hasSelect; + dictionaryAnilistId = hasSelect ? selectValue : null; }); commandProgram diff --git a/launcher/main.test.ts b/launcher/main.test.ts index 5fb01224..4fc6d7a7 100644 --- a/launcher/main.test.ts +++ b/launcher/main.test.ts @@ -477,7 +477,7 @@ test('dictionary command forwards manual AniList selection modes to app command const capturePath = path.join(root, 'captured-args.txt'); fs.writeFileSync( appPath, - '#!/bin/sh\nif [ -n "$SUBMINER_TEST_CAPTURE" ]; then printf "%s\\n" "$@" >> "$SUBMINER_TEST_CAPTURE"; fi\nexit 0\n', + '#!/bin/sh\nif [ -n "$SUBMINER_TEST_CAPTURE" ]; then printf "%s\\n" "$@" > "$SUBMINER_TEST_CAPTURE"; fi\nexit 0\n', ); fs.chmodSync(appPath, 0o755); diff --git a/launcher/parse-args.test.ts b/launcher/parse-args.test.ts index 4bb8f3f9..7e7ebb02 100644 --- a/launcher/parse-args.test.ts +++ b/launcher/parse-args.test.ts @@ -110,6 +110,14 @@ test('parseArgs maps dictionary candidate lookup and manual selection', () => { assert.equal(selectParsed.dictionaryTarget, process.cwd()); }); +test('parseArgs rejects conflicting dictionary candidate and selection modes', () => { + const exit = withProcessExitIntercept(() => { + parseArgs(['dictionary', '--candidates', '--select', '21355', '.'], 'subminer', {}); + }); + + assert.equal(exit.code, 1); +}); + test('parseArgs maps stats command and log-level override', () => { const parsed = parseArgs(['stats', '--log-level', 'debug'], 'subminer', {});