Skip to content

Commit a9840ca

Browse files
committed
fix(mcp): use Clack multiselect for the client picker
The #305 Clack migration removed @inquirer/prompts, but the mcp install client picker still imported checkbox from it — undeclared after the rebase and broken on a clean install. Add a multiselect wrapper to lib/prompts.ts (matching the existing confirm/text/password Clack wrappers, with the same cancel→throwUserAbort handling) and route pickClients through it. No behavior change to the picker.
1 parent 67af23c commit a9840ca

2 files changed

Lines changed: 36 additions & 23 deletions

File tree

packages/cli-core/src/commands/mcp/shared.ts

Lines changed: 10 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
* Shared options and helpers for `clerk mcp` subcommands.
33
*/
44

5-
import { ttyContext } from "../../lib/listage.ts";
65
import { getMcpUrl } from "../../lib/environment.ts";
76
import { CliError, ERROR_CODE, errorMessage, throwUsageError } from "../../lib/errors.ts";
87
import { log } from "../../lib/log.ts";
@@ -67,28 +66,16 @@ export function resolveClients(ids: readonly string[]): McpClient[] {
6766
export async function pickClients(detected: McpClient[]): Promise<McpClient[]> {
6867
if (detected.length === 0) return [];
6968
if (detected.length === 1) return detected;
70-
// Imported lazily (like `doctor`/`update` do): a top-level import of
71-
// `@inquirer/prompts` is resolved at module load, which breaks tests that
72-
// mock the module with a partial shape that omits `checkbox`.
73-
const { checkbox } = await import("@inquirer/prompts");
74-
const tty = ttyContext();
75-
try {
76-
const selected = await checkbox<ClientId>(
77-
{
78-
message: "Select MCP clients to install into:",
79-
choices: detected.map((c) => ({
80-
name: `${c.displayName} (${c.scope})`,
81-
value: c.id,
82-
checked: true,
83-
})),
84-
required: true,
85-
},
86-
tty ? { input: tty.input } : undefined,
87-
);
88-
return resolveClients(selected);
89-
} finally {
90-
tty?.close();
91-
}
69+
// Imported lazily (like `doctor` does) so the prompt layer stays off the
70+
// module-load path for non-interactive callers and tests.
71+
const { multiselect } = await import("../../lib/prompts.ts");
72+
const selected = await multiselect<ClientId>({
73+
message: "Select MCP clients to install into:",
74+
options: detected.map((c) => ({ value: c.id, label: `${c.displayName} (${c.scope})` })),
75+
initialValues: detected.map((c) => c.id),
76+
required: true,
77+
});
78+
return resolveClients(selected);
9279
}
9380

9481
export async function targetClients(options: McpOptions, cwd: string): Promise<McpClient[]> {

packages/cli-core/src/lib/prompts.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import {
99
isCancel,
1010
text as clackText,
1111
password as clackPassword,
12+
multiselect as clackMultiselect,
13+
type Option as ClackOption,
1214
} from "@clack/prompts";
1315
import { editAsync } from "external-editor";
1416
import { throwUserAbort } from "./errors.ts";
@@ -80,6 +82,30 @@ export async function confirm(config: { message: string; default?: boolean }): P
8082
}
8183
}
8284

85+
/** Multi-select checklist. Returns the chosen values (at least one when required). */
86+
export async function multiselect<T>(config: {
87+
message: string;
88+
options: { value: T; label: string; hint?: string }[];
89+
initialValues?: T[];
90+
required?: boolean;
91+
}): Promise<T[]> {
92+
const tty = ttyContext();
93+
try {
94+
const result = await clackMultiselect<T>({
95+
message: config.message,
96+
// `Option<T>` is a conditional type a naked generic can't satisfy
97+
// structurally; our shape provides `value` + `label`, valid in both branches.
98+
options: config.options as ClackOption<T>[],
99+
initialValues: config.initialValues,
100+
required: config.required ?? true,
101+
input: tty?.input,
102+
});
103+
return unwrap(result);
104+
} finally {
105+
tty?.close();
106+
}
107+
}
108+
83109
/** Single-line text input. */
84110
export async function text(config: {
85111
message: string;

0 commit comments

Comments
 (0)