Skip to content

Commit 3deffe9

Browse files
committed
feat(review): implement review command argument parsing and tests
1 parent 23ac4c1 commit 3deffe9

4 files changed

Lines changed: 217 additions & 223 deletions

File tree

source/cli.spec.ts

Lines changed: 0 additions & 178 deletions
Original file line numberDiff line numberDiff line change
@@ -575,178 +575,6 @@ test('resume flags: --continue without `run` is not a non-interactive error', t
575575
t.false(nonInteractiveError);
576576
});
577577

578-
// Review command parsing tests. Mirrors the review arg parsing in cli.tsx:
579-
// `nanocoder review <target>` injects `/review <target>` as the prompt.
580-
581-
type ReviewParseResult = {
582-
prompt: string | undefined;
583-
error: boolean;
584-
};
585-
586-
function parseReviewArgs(args: string[]): ReviewParseResult {
587-
const isReviewCommand = args[0] === 'review';
588-
if (!isReviewCommand) {
589-
return {prompt: undefined, error: false};
590-
}
591-
const afterReviewArgs = args.slice(1);
592-
const reviewArgs: string[] = [];
593-
for (let i = 0; i < afterReviewArgs.length; i++) {
594-
const arg = afterReviewArgs[i];
595-
if (
596-
arg === '--vscode' ||
597-
arg === '--json' ||
598-
arg === '--trust-directory' ||
599-
arg === '--plain' ||
600-
arg === '--no-plain' ||
601-
arg === '--no-alt-screen' ||
602-
arg === '--alt-screen'
603-
) {
604-
continue;
605-
} else if (
606-
arg === '--vscode-port' ||
607-
arg === '--provider' ||
608-
arg === '--model' ||
609-
arg === '--context-max' ||
610-
arg === '--output-format'
611-
) {
612-
i++; // skip this flag and its value
613-
continue;
614-
} else if (arg === '--mode') {
615-
i++; // skip this flag and its value
616-
continue;
617-
} else if (arg.startsWith('--mode=')) {
618-
continue;
619-
} else if (arg.startsWith('--output-format=')) {
620-
continue;
621-
} else {
622-
reviewArgs.push(arg);
623-
}
624-
}
625-
if (reviewArgs.length === 0) {
626-
return {prompt: '/review', error: false};
627-
}
628-
return {prompt: `/review ${reviewArgs[0]}`, error: false};
629-
}
630-
631-
test('review CLI: parses review with branch name', t => {
632-
const result = parseReviewArgs(['review', 'feature/auth']);
633-
t.is(result.prompt, '/review feature/auth');
634-
t.false(result.error);
635-
});
636-
637-
test('review CLI: parses review with PR number', t => {
638-
const result = parseReviewArgs(['review', '42']);
639-
t.is(result.prompt, '/review 42');
640-
t.false(result.error);
641-
});
642-
643-
test('review CLI: no args produces /review', t => {
644-
const result = parseReviewArgs(['review']);
645-
t.is(result.prompt, '/review');
646-
t.false(result.error);
647-
});
648-
649-
test('review CLI: returns undefined when not a review command', t => {
650-
const result = parseReviewArgs(['run', 'hello']);
651-
t.is(result.prompt, undefined);
652-
t.false(result.error);
653-
});
654-
655-
test('review CLI: anchors on args[0], not findIndex', t => {
656-
const result = parseReviewArgs(['--vscode', 'review', 'main']);
657-
t.is(result.prompt, undefined);
658-
t.false(result.error);
659-
});
660-
661-
test('review CLI: filters --vscode flag', t => {
662-
const result = parseReviewArgs(['review', 'main', '--vscode']);
663-
t.is(result.prompt, '/review main');
664-
});
665-
666-
test('review CLI: filters --provider flag and value', t => {
667-
const result = parseReviewArgs([
668-
'review',
669-
'main',
670-
'--provider',
671-
'openrouter',
672-
]);
673-
t.is(result.prompt, '/review main');
674-
});
675-
676-
test('review CLI: filters --model flag and value', t => {
677-
const result = parseReviewArgs(['review', 'main', '--model', 'gpt-4']);
678-
t.is(result.prompt, '/review main');
679-
});
680-
681-
test('review CLI: filters --mode flag and value (two-token)', t => {
682-
const result = parseReviewArgs(['review', 'main', '--mode', 'plan']);
683-
t.is(result.prompt, '/review main');
684-
});
685-
686-
test('review CLI: filters --mode fused form', t => {
687-
const result = parseReviewArgs(['review', 'main', '--mode=plan']);
688-
t.is(result.prompt, '/review main');
689-
});
690-
691-
test('review CLI: filters --json flag', t => {
692-
const result = parseReviewArgs(['review', 'main', '--json']);
693-
t.is(result.prompt, '/review main');
694-
});
695-
696-
test('review CLI: filters --output-format flag and value', t => {
697-
const result = parseReviewArgs([
698-
'review',
699-
'main',
700-
'--output-format',
701-
'json',
702-
]);
703-
t.is(result.prompt, '/review main');
704-
});
705-
706-
test('review CLI: filters --context-max flag and value', t => {
707-
const result = parseReviewArgs([
708-
'review',
709-
'main',
710-
'--context-max',
711-
'128k',
712-
]);
713-
t.is(result.prompt, '/review main');
714-
});
715-
716-
test('review CLI: filters --plain and --no-plain flags', t => {
717-
const result1 = parseReviewArgs(['review', 'main', '--plain']);
718-
t.is(result1.prompt, '/review main');
719-
720-
const result2 = parseReviewArgs(['review', 'main', '--no-plain']);
721-
t.is(result2.prompt, '/review main');
722-
});
723-
724-
test('review CLI: filters --no-alt-screen and --alt-screen flags', t => {
725-
const result1 = parseReviewArgs(['review', 'main', '--no-alt-screen']);
726-
t.is(result1.prompt, '/review main');
727-
728-
const result2 = parseReviewArgs(['review', 'main', '--alt-screen']);
729-
t.is(result2.prompt, '/review main');
730-
});
731-
732-
test('review CLI: filters --trust-directory flag', t => {
733-
const result = parseReviewArgs(['review', 'main', '--trust-directory']);
734-
t.is(result.prompt, '/review main');
735-
});
736-
737-
test('review CLI: handles multiple mixed flags', t => {
738-
const result = parseReviewArgs([
739-
'review',
740-
'feature',
741-
'--provider',
742-
'ollama',
743-
'--mode',
744-
'plan',
745-
'--json',
746-
]);
747-
t.is(result.prompt, '/review feature');
748-
});
749-
750578
// Run command with flags before 'run' (the blocker fix)
751579
test('CLI parsing: handles flags before run command', t => {
752580
const args = ['--plain', 'run', 'say', 'hi'];
@@ -765,9 +593,3 @@ test('CLI parsing: handles --mode before run command', t => {
765593
const prompt = parsePrompt(args);
766594
t.is(prompt, 'audit module');
767595
});
768-
769-
// Review should only use first arg, not join all
770-
test('review CLI: only uses first positional arg', t => {
771-
const result = parseReviewArgs(['review', 'feature', 'extra', 'args']);
772-
t.is(result.prompt, '/review feature');
773-
});

source/cli.tsx

Lines changed: 5 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,8 @@ async function main(): Promise<void> {
199199
// Those packages pull ~thousand+ modules; --acp / --plain / auth must stay
200200
// on the lightweight path. Ink + App load only in the final TUI branch.
201201

202+
const {parseReviewCliArgs} = await import('./commands/review-cli');
203+
202204
const vscodeMode = args.includes('--vscode');
203205

204206
// Extract VS Code port if specified
@@ -371,52 +373,10 @@ async function main(): Promise<void> {
371373
// `nanocoder run /review <target>`. The target is the branch or PR number
372374
// to review. Flags between `review` and the target are filtered the same
373375
// way as `run`.
374-
const isReviewCommand = args[0] === 'review';
376+
const {isReviewCommand, prompt: reviewPrompt} = parseReviewCliArgs(args);
375377
if (isReviewCommand) {
376-
const afterReviewArgs = args.slice(1);
377-
const reviewArgs: string[] = [];
378-
for (let i = 0; i < afterReviewArgs.length; i++) {
379-
const arg = afterReviewArgs[i];
380-
if (
381-
arg === '--vscode' ||
382-
arg === '--json' ||
383-
arg === '--trust-directory' ||
384-
arg === '--plain' ||
385-
arg === '--no-plain' ||
386-
arg === '--no-alt-screen' ||
387-
arg === '--alt-screen'
388-
) {
389-
continue;
390-
} else if (
391-
arg === '--vscode-port' ||
392-
arg === '--provider' ||
393-
arg === '--model' ||
394-
arg === '--context-max' ||
395-
arg === '--output-format'
396-
) {
397-
i++; // skip this flag and its value
398-
continue;
399-
} else if (arg === '--mode') {
400-
i++; // skip this flag and its value
401-
continue;
402-
} else if (arg.startsWith('--mode=')) {
403-
continue;
404-
} else if (arg.startsWith('--output-format=')) {
405-
continue;
406-
} else {
407-
reviewArgs.push(arg);
408-
}
409-
}
410-
if (reviewArgs.length === 0) {
411-
// No target provided — review current branch against default.
412-
// This matches the /review behavior in the interactive TUI.
413-
nonInteractivePrompt = '/review';
414-
nonInteractiveMode = true;
415-
} else {
416-
// Inject as a slash command prompt.
417-
nonInteractivePrompt = `/review ${reviewArgs[0]}`;
418-
nonInteractiveMode = true;
419-
}
378+
nonInteractivePrompt = reviewPrompt;
379+
nonInteractiveMode = true;
420380
}
421381

422382
// --continue/-c and --resume/-r: session resume flags for the interactive

0 commit comments

Comments
 (0)