From 92e1e87034ddc0dc327ee11fa3d76e8dd74b180b Mon Sep 17 00:00:00 2001 From: Benjamin Jesuiter Date: Mon, 16 Feb 2026 21:47:20 +0100 Subject: [PATCH] CLI: add run-current and nested picker for mixed commands --- src/cli/program/command-selector.test.ts | 7 ++++- src/cli/program/command-selector.ts | 35 +++++++++++++++++------- 2 files changed, 31 insertions(+), 11 deletions(-) diff --git a/src/cli/program/command-selector.test.ts b/src/cli/program/command-selector.test.ts index 612b139550..4e2e53bd6a 100644 --- a/src/cli/program/command-selector.test.ts +++ b/src/cli/program/command-selector.test.ts @@ -65,7 +65,11 @@ describe("command-selector", () => { it("detects commands that require subcommands", () => { const program = new Command(); const models = program.command("models").description("Model commands"); - models.command("auth").description("Auth command"); + const auth = models + .command("auth") + .description("Auth command") + .action(() => undefined); + auth.command("login").description("Login auth profile"); const status = program .command("status") @@ -73,6 +77,7 @@ describe("command-selector", () => { .action(() => undefined); expect(commandRequiresSubcommand(models)).toBe(true); + expect(commandRequiresSubcommand(auth)).toBe(true); expect(commandRequiresSubcommand(status)).toBe(false); }); diff --git a/src/cli/program/command-selector.ts b/src/cli/program/command-selector.ts index fa0f50fa97..e19b61ab5f 100644 --- a/src/cli/program/command-selector.ts +++ b/src/cli/program/command-selector.ts @@ -8,6 +8,7 @@ import { getSubCliEntries, registerSubCliByName } from "./register.subclis.js"; const SHOW_HELP_VALUE = "__show_help__"; const BACK_TO_MAIN_VALUE = "__back_to_main__"; +const RUN_CURRENT_VALUE = "__run_current__"; const PATH_SEPARATOR = "\u0000"; const MAX_RESULTS = 200; @@ -22,7 +23,7 @@ type PreparedCommandSelectorCandidate = CommandSelectorCandidate & { searchTextLower: string; }; -type SelectorPromptResult = string[] | "back_to_main" | null; +type SelectorPromptResult = string[] | "back_to_main" | "run_current" | null; function isHiddenCommand(command: Command): boolean { // Commander stores hidden state on a private field. @@ -111,16 +112,9 @@ export function resolveCommandByPath(program: Command, path: string[]): Command return current; } -function hasActionHandler(command: Command): boolean { - return Boolean((command as Command & { _actionHandler?: unknown })._actionHandler); -} - export function commandRequiresSubcommand(command: Command): boolean { const visibleChildren = command.commands.filter((child) => !shouldSkipCommand(child, 1)); - if (visibleChildren.length === 0) { - return false; - } - return !hasActionHandler(command); + return visibleChildren.length > 0; } export function collectDirectSubcommandSelectorCandidates( @@ -196,6 +190,8 @@ async function promptForCommandSelection(params: { placeholder: string; candidates: PreparedCommandSelectorCandidate[]; includeBackToMain?: boolean; + includeRunCurrent?: boolean; + currentPath?: string[]; }): Promise { const selection = await clackAutocomplete({ message: params.message, @@ -213,6 +209,17 @@ async function promptForCommandSelection(params: { label: candidate.label, hint: stylePromptHint(candidate.description), })), + ...(params.includeRunCurrent + ? [ + { + value: RUN_CURRENT_VALUE, + label: "./", + hint: stylePromptHint( + `Run ${params.currentPath?.join(" ") ?? "selected command"} directly`, + ), + }, + ] + : []), ...(params.includeBackToMain ? [ { @@ -237,6 +244,9 @@ async function promptForCommandSelection(params: { if (selection === BACK_TO_MAIN_VALUE) { return "back_to_main"; } + if (selection === RUN_CURRENT_VALUE) { + return "run_current"; + } return deserializePath(selection); } @@ -254,7 +264,7 @@ export async function runInteractiveCommandSelector(program: Command): Promise