@levr-one/cli 0.6.12 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,688 +0,0 @@
1
- import { getApiUrl } from "./env-CHeKHu5S.js";
2
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
- import { delimiter, dirname, join } from "node:path";
4
- import { homedir } from "node:os";
5
- import { applyEdits, modify, parse } from "jsonc-parser";
6
-
7
- //#region ../mcp-harnesses/dist/catalog.js
8
- /**
9
- * @levr/mcp-harnesses — isomorphic catalog (browser + Node).
10
- *
11
- * Single source of truth for MCP-capable clients ("harnesses"): the catalog,
12
- * the OAuth-client → catalog matcher, and the pure config builders. This module
13
- * MUST stay free of Node built-ins (`node:fs`, `node:os`, `node:path`) so the
14
- * client SPA can import it without pulling `node:fs` into the bundle. Detection
15
- * and config-write live in the `@levr/mcp-harnesses/node` subpath.
16
- *
17
- * Plan: specs/plans/mcp-harness-detect-installer.md (ENG-43, P1).
18
- */
19
- /** Stable server key written into every harness config (used by detect/remove).
20
- * Renamed from the legacy brand key pre-first-publish (ENG-2515) — this key
21
- * is a persisted identity in end-users' client config files, so it must not
22
- * carry the old brand. */
23
- const SERVER_NAME = "levr";
24
- /**
25
- * The catalog. Order is presentation order (most common first).
26
- *
27
- * `comingSoon` clients (VS Code, Codex) are listed but not installable in v1:
28
- * their config formats differ enough (VS Code's `servers`/native-http schema,
29
- * Codex's TOML `~/.codex/config.toml`) that faithful writes are deferred to a
30
- * later catalog entry rather than forced through the JSON `mcpServers` builder.
31
- */
32
- const HARNESSES = [
33
- {
34
- id: "claude",
35
- label: "Claude Desktop",
36
- matchers: [
37
- "claude desktop",
38
- "claude-desktop",
39
- "claude"
40
- ],
41
- serverPropertyName: "mcpServers",
42
- installKind: "config-file",
43
- transport: "mcp-remote",
44
- docsUrl: "https://modelcontextprotocol.io/quickstart/user",
45
- comingSoon: false,
46
- locations: [
47
- {
48
- platform: "darwin",
49
- configPath: "~/Library/Application Support/Claude/claude_desktop_config.json",
50
- installSignals: ["~/Library/Application Support/Claude", "/Applications/Claude.app"]
51
- },
52
- {
53
- platform: "win32",
54
- configPath: "~/AppData/Roaming/Claude/claude_desktop_config.json",
55
- installSignals: ["~/AppData/Roaming/Claude"]
56
- },
57
- {
58
- platform: "linux",
59
- configPath: "~/.config/Claude/claude_desktop_config.json",
60
- installSignals: ["~/.config/Claude"]
61
- }
62
- ]
63
- },
64
- {
65
- id: "claude-code",
66
- label: "Claude Code",
67
- matchers: [
68
- "claude code",
69
- "claude-code",
70
- "claude_code",
71
- "claudecode"
72
- ],
73
- serverPropertyName: "mcpServers",
74
- installKind: "cli-command",
75
- transport: "native-http",
76
- docsUrl: "https://docs.anthropic.com/en/docs/claude-code/mcp",
77
- comingSoon: false,
78
- locations: [
79
- {
80
- platform: "darwin",
81
- configPath: "~/.claude.json",
82
- installSignals: [
83
- "which:claude",
84
- "~/.claude.json",
85
- "~/.claude"
86
- ]
87
- },
88
- {
89
- platform: "linux",
90
- configPath: "~/.claude.json",
91
- installSignals: [
92
- "which:claude",
93
- "~/.claude.json",
94
- "~/.claude"
95
- ]
96
- },
97
- {
98
- platform: "win32",
99
- configPath: "~/.claude.json",
100
- installSignals: [
101
- "which:claude",
102
- "~/.claude.json",
103
- "~/.claude"
104
- ]
105
- }
106
- ]
107
- },
108
- {
109
- id: "cursor",
110
- label: "Cursor",
111
- matchers: ["cursor"],
112
- serverPropertyName: "mcpServers",
113
- installKind: "config-file",
114
- transport: "mcp-remote",
115
- docsUrl: "https://docs.cursor.com/context/model-context-protocol",
116
- comingSoon: false,
117
- locations: [
118
- {
119
- platform: "darwin",
120
- configPath: "~/.cursor/mcp.json",
121
- installSignals: [
122
- "~/.cursor",
123
- "which:cursor",
124
- "/Applications/Cursor.app"
125
- ]
126
- },
127
- {
128
- platform: "win32",
129
- configPath: "~/.cursor/mcp.json",
130
- installSignals: ["~/.cursor", "which:cursor"]
131
- },
132
- {
133
- platform: "linux",
134
- configPath: "~/.cursor/mcp.json",
135
- installSignals: ["~/.cursor", "which:cursor"]
136
- }
137
- ]
138
- },
139
- {
140
- id: "windsurf",
141
- label: "Windsurf",
142
- matchers: ["windsurf", "codeium"],
143
- serverPropertyName: "mcpServers",
144
- installKind: "config-file",
145
- transport: "mcp-remote",
146
- docsUrl: "https://docs.windsurf.com/windsurf/mcp",
147
- comingSoon: false,
148
- locations: [
149
- {
150
- platform: "darwin",
151
- configPath: "~/.codeium/windsurf/mcp_config.json",
152
- installSignals: [
153
- "~/.codeium/windsurf",
154
- "/Applications/Windsurf.app",
155
- "which:windsurf"
156
- ]
157
- },
158
- {
159
- platform: "win32",
160
- configPath: "~/.codeium/windsurf/mcp_config.json",
161
- installSignals: ["~/.codeium/windsurf", "which:windsurf"]
162
- },
163
- {
164
- platform: "linux",
165
- configPath: "~/.codeium/windsurf/mcp_config.json",
166
- installSignals: ["~/.codeium/windsurf", "which:windsurf"]
167
- }
168
- ]
169
- },
170
- {
171
- id: "zed",
172
- label: "Zed",
173
- matchers: ["zed"],
174
- serverPropertyName: "context_servers",
175
- installKind: "config-file",
176
- transport: "mcp-remote",
177
- docsUrl: "https://zed.dev/docs/assistant/model-context-protocol",
178
- comingSoon: false,
179
- locations: [
180
- {
181
- platform: "darwin",
182
- configPath: "~/.config/zed/settings.json",
183
- installSignals: [
184
- "~/.config/zed",
185
- "/Applications/Zed.app",
186
- "which:zed"
187
- ]
188
- },
189
- {
190
- platform: "linux",
191
- configPath: "~/.config/zed/settings.json",
192
- installSignals: ["~/.config/zed", "which:zed"]
193
- },
194
- {
195
- platform: "win32",
196
- configPath: "~/AppData/Roaming/Zed/settings.json",
197
- installSignals: ["~/AppData/Roaming/Zed"]
198
- }
199
- ]
200
- },
201
- {
202
- id: "vscode",
203
- label: "VS Code",
204
- matchers: [
205
- "vscode",
206
- "vs code",
207
- "visual studio code"
208
- ],
209
- serverPropertyName: "mcpServers",
210
- installKind: "config-file",
211
- transport: "mcp-remote",
212
- docsUrl: "https://code.visualstudio.com/docs/copilot/chat/mcp-servers",
213
- comingSoon: true,
214
- locations: [
215
- {
216
- platform: "darwin",
217
- configPath: "~/Library/Application Support/Code/User/mcp.json",
218
- installSignals: ["/Applications/Visual Studio Code.app", "which:code"]
219
- },
220
- {
221
- platform: "win32",
222
- configPath: "~/AppData/Roaming/Code/User/mcp.json",
223
- installSignals: ["which:code"]
224
- },
225
- {
226
- platform: "linux",
227
- configPath: "~/.config/Code/User/mcp.json",
228
- installSignals: ["~/.config/Code", "which:code"]
229
- }
230
- ]
231
- },
232
- {
233
- id: "codex",
234
- label: "Codex CLI",
235
- matchers: ["codex"],
236
- serverPropertyName: "mcpServers",
237
- installKind: "config-file",
238
- transport: "mcp-remote",
239
- docsUrl: "https://github.com/openai/codex",
240
- comingSoon: true,
241
- locations: [
242
- {
243
- platform: "darwin",
244
- configPath: "~/.codex/config.toml",
245
- installSignals: ["~/.codex", "which:codex"]
246
- },
247
- {
248
- platform: "linux",
249
- configPath: "~/.codex/config.toml",
250
- installSignals: ["~/.codex", "which:codex"]
251
- },
252
- {
253
- platform: "win32",
254
- configPath: "~/.codex/config.toml",
255
- installSignals: ["~/.codex", "which:codex"]
256
- }
257
- ]
258
- }
259
- ];
260
- /** Look up a harness by id. Returns `undefined` for unknown ids. */
261
- function getHarness(id) {
262
- return HARNESSES.find((h) => h.id === id);
263
- }
264
- /** The `npx -y mcp-remote <url>` invocation shared by mcp-remote harnesses. */
265
- function mcpRemoteInvocation(mcpUrl) {
266
- return {
267
- command: "npx",
268
- args: [
269
- "-y",
270
- "mcp-remote",
271
- mcpUrl
272
- ]
273
- };
274
- }
275
- /**
276
- * The structured server entry to merge under `harness.serverPropertyName`,
277
- * keyed by {@link SERVER_NAME}. Shape is per-harness:
278
- * - `mcpServers` (Claude Desktop, Cursor, Windsurf): flat `{ command, args }`.
279
- * - `context_servers` (Zed): nested `{ source, command: { path, args } }`.
280
- *
281
- * Consumed by the string builder here and, in P2, by the jsonc-preserving
282
- * install merge. Not used for `cli-command` harnesses (see {@link buildHarnessConfig}).
283
- */
284
- function buildServerEntry(harness, mcpUrl) {
285
- const { command, args } = mcpRemoteInvocation(mcpUrl);
286
- if (harness.serverPropertyName === "context_servers") return { [SERVER_NAME]: {
287
- source: "custom",
288
- command: {
289
- path: command,
290
- args
291
- }
292
- } };
293
- return { [SERVER_NAME]: {
294
- command,
295
- args
296
- } };
297
- }
298
- /**
299
- * The customer-facing install snippet for a harness + MCP URL:
300
- * - `cli-command` harnesses (Claude Code) → the exact command to run.
301
- * - `config-file` harnesses → a pretty-printed JSON snippet (the wrapping
302
- * `serverPropertyName` + our server entry) to paste/merge into their config.
303
- */
304
- function buildHarnessConfig(harness, mcpUrl) {
305
- if (harness.installKind === "cli-command") return `claude mcp add --transport http ${SERVER_NAME} ${mcpUrl}`;
306
- const snippet = { [harness.serverPropertyName]: buildServerEntry(harness, mcpUrl) };
307
- return JSON.stringify(snippet, null, 2);
308
- }
309
-
310
- //#endregion
311
- //#region ../mcp-harnesses/dist/node/paths.js
312
- function defaultEnv() {
313
- return {
314
- platform: process.platform,
315
- homedir: homedir(),
316
- pathVar: process.env.PATH ?? ""
317
- };
318
- }
319
- /** Expand a leading `~` / `~/…` to the given home directory. */
320
- function expandTilde(p, home) {
321
- if (p === "~") return home;
322
- if (p.startsWith("~/") || p.startsWith("~\\")) return join(home, p.slice(2));
323
- return p;
324
- }
325
- /** The config location for a harness on the given platform, if any. */
326
- function locationFor(harness, platform) {
327
- return harness.locations.find((l) => l.platform === platform);
328
- }
329
- /** Absolute (tilde-expanded) config path for a harness, or undefined if the
330
- * harness has no location on this platform. */
331
- function resolveConfigPath(harness, env) {
332
- const loc = locationFor(harness, env.platform);
333
- if (!loc) return void 0;
334
- return expandTilde(loc.configPath, env.homedir);
335
- }
336
- const WIN_EXTS = [
337
- "",
338
- ".exe",
339
- ".cmd",
340
- ".bat"
341
- ];
342
- /** Is `bin` resolvable on PATH? Pure `process.env.PATH` scan — never spawns a
343
- * shell (no `which`/`where` subprocess). */
344
- function whichSync(bin, env) {
345
- const exts = env.platform === "win32" ? WIN_EXTS : [""];
346
- for (const dir of env.pathVar.split(delimiter)) {
347
- if (!dir) continue;
348
- for (const ext of exts) if (existsSync(join(dir, bin + ext))) return true;
349
- }
350
- return false;
351
- }
352
- /** Does one `installSignals` entry match on this machine? Supports
353
- * `which:<bin>`, `~`-prefixed paths, and absolute (app-bundle) paths. */
354
- function signalMatches(signal, env) {
355
- if (signal.startsWith("which:")) return whichSync(signal.slice(6), env);
356
- return existsSync(signal.startsWith("~") ? expandTilde(signal, env.homedir) : signal);
357
- }
358
- /** Read a text file, or `null` if it doesn't exist / can't be read. */
359
- function readTextOrNull(path) {
360
- try {
361
- return readFileSync(path, "utf8");
362
- } catch {
363
- return null;
364
- }
365
- }
366
- /** Safe nested lookup over an unknown-typed parsed JSON value. */
367
- function getAtPath(obj, path) {
368
- let cur = obj;
369
- for (const key of path) {
370
- if (cur === null || typeof cur !== "object") return void 0;
371
- cur = cur[key];
372
- }
373
- return cur;
374
- }
375
-
376
- //#endregion
377
- //#region ../mcp-harnesses/dist/node/detect.js
378
- /** Is our server key present under the harness's server property? */
379
- function isServerConfigured(harness, configPath) {
380
- const text = readTextOrNull(configPath);
381
- if (!text) return false;
382
- const val = getAtPath(parse(text), [harness.serverPropertyName, SERVER_NAME]);
383
- return val !== void 0 && val !== null;
384
- }
385
- function detectOne(harness, env) {
386
- const loc = locationFor(harness, env.platform);
387
- const configPath = resolveConfigPath(harness, env);
388
- const available = Boolean(loc);
389
- let installed = false;
390
- let alreadyConfigured = false;
391
- if (loc && configPath) {
392
- installed = loc.installSignals.some((s) => signalMatches(s, env)) || existsSync(configPath);
393
- alreadyConfigured = isServerConfigured(harness, configPath);
394
- }
395
- return {
396
- id: harness.id,
397
- label: harness.label,
398
- installed,
399
- alreadyConfigured,
400
- configPath: configPath ?? "",
401
- available,
402
- comingSoon: harness.comingSoon
403
- };
404
- }
405
- /** Synchronous detection over the whole catalog. Exported for tests. */
406
- function detectSync(env = defaultEnv()) {
407
- return HARNESSES.map((h) => detectOne(h, env));
408
- }
409
-
410
- //#endregion
411
- //#region ../mcp-harnesses/dist/node/install.js
412
- const FORMAT = {
413
- insertSpaces: true,
414
- tabSize: 2,
415
- eol: "\n"
416
- };
417
- /** JSON-structural equality — sufficient for our small config values. */
418
- function sameValue(a, b) {
419
- return JSON.stringify(a) === JSON.stringify(b);
420
- }
421
- /** Merge (or preview) our MCP into a harness config. */
422
- function installHarnessSync(harness, mcpUrl, opts = {}) {
423
- const env = opts.env ?? defaultEnv();
424
- const dryRun = opts.dryRun ?? false;
425
- if (harness.installKind === "cli-command") return {
426
- ok: true,
427
- wrote: false,
428
- path: "",
429
- command: buildHarnessConfig(harness, mcpUrl),
430
- alreadyConfigured: false,
431
- dryRun
432
- };
433
- const path = resolveConfigPath(harness, env);
434
- if (!path) return {
435
- ok: false,
436
- wrote: false,
437
- path: "",
438
- alreadyConfigured: false,
439
- dryRun
440
- };
441
- const entryValue = buildServerEntry(harness, mcpUrl)[SERVER_NAME];
442
- const modPath = [harness.serverPropertyName, SERVER_NAME];
443
- const existing = readTextOrNull(path);
444
- const baseText = existing && existing.trim() ? existing : "{}";
445
- const current = getAtPath(parse(baseText), modPath);
446
- const alreadyConfigured = current !== void 0 && sameValue(current, entryValue);
447
- const nextText = applyEdits(baseText, modify(baseText, modPath, entryValue, { formattingOptions: FORMAT }));
448
- if (alreadyConfigured) return {
449
- ok: true,
450
- wrote: false,
451
- path,
452
- alreadyConfigured: true,
453
- dryRun,
454
- preview: nextText
455
- };
456
- if (dryRun) return {
457
- ok: true,
458
- wrote: false,
459
- path,
460
- alreadyConfigured: false,
461
- dryRun: true,
462
- preview: nextText
463
- };
464
- mkdirSync(dirname(path), { recursive: true });
465
- const finalText = nextText.endsWith("\n") ? nextText : `${nextText}\n`;
466
- writeFileSync(path, finalText, "utf8");
467
- return {
468
- ok: true,
469
- wrote: true,
470
- path,
471
- alreadyConfigured: false,
472
- dryRun: false,
473
- preview: finalText
474
- };
475
- }
476
-
477
- //#endregion
478
- //#region src/mcp/url.ts
479
- const KNOWN_MCP_URLS = {
480
- "api.levr.one": "https://ai.levr.one/api/v1/mcp",
481
- "api.levr.now": "https://ai.levr.now/api/v1/mcp"
482
- };
483
- /**
484
- * Resolve the MCP server URL: `--url` flag > `LEVR_MCP_URL` env > derived
485
- * from the resolved API URL (which itself honors `LEVR_URL` > the URL stored
486
- * at login > production default, ENG-2361). Known Levr hosts map to their
487
- * app-host MCP resource; anything else (localhost dev stacks, custom
488
- * deployments) derives `<api-url>/v1/mcp`.
489
- */
490
- function resolveMcpUrl(flagUrl) {
491
- if (flagUrl) return {
492
- url: stripSlash(flagUrl),
493
- source: "flag"
494
- };
495
- const envVar = process.env["LEVR_MCP_URL"];
496
- if (envVar) return {
497
- url: stripSlash(envVar),
498
- source: "env:LEVR_MCP_URL"
499
- };
500
- const apiUrl = getApiUrl();
501
- return {
502
- url: knownMcpUrl(apiUrl) ?? `${apiUrl}/v1/mcp`,
503
- source: `derived:${apiUrl}`
504
- };
505
- }
506
- function knownMcpUrl(apiUrl) {
507
- try {
508
- return KNOWN_MCP_URLS[new URL(apiUrl).host];
509
- } catch {
510
- return;
511
- }
512
- }
513
- function stripSlash(url) {
514
- return url.replace(/\/+$/, "");
515
- }
516
-
517
- //#endregion
518
- //#region src/mcp/run.ts
519
- /** Harness ids to pre-select in interactive mode: detected + installable +
520
- * not-already-configured. */
521
- function autoSelectIds(detected) {
522
- return detected.filter((d) => d.available && !d.comingSoon && d.installed && !d.alreadyConfigured).map((d) => d.id);
523
- }
524
- /** Resolve `--all` / `--client` into concrete, installable harness ids. */
525
- function resolveRequestedIds(options, detected) {
526
- if (options.all) return {
527
- ids: detected.filter((d) => d.available && !d.comingSoon).map((d) => d.id),
528
- unknown: [],
529
- comingSoon: []
530
- };
531
- const ids = [];
532
- const unknown = [];
533
- const comingSoon = [];
534
- for (const c of options.clients ?? []) {
535
- const harness = getHarness(c);
536
- if (!harness) unknown.push(c);
537
- else if (harness.comingSoon) comingSoon.push(c);
538
- else ids.push(c);
539
- }
540
- return {
541
- ids,
542
- unknown,
543
- comingSoon
544
- };
545
- }
546
- /** Install each selected id, collecting structured outcomes. */
547
- function installSelected(ids, mcpUrl, dryRun, install) {
548
- const outcomes = [];
549
- for (const id of ids) {
550
- const harness = getHarness(id);
551
- if (!harness) continue;
552
- outcomes.push({
553
- id,
554
- label: harness.label,
555
- result: install(harness, mcpUrl, dryRun)
556
- });
557
- }
558
- return outcomes;
559
- }
560
- /**
561
- * The non-interactive run: detect, pick ids from `--all`/`--client` (or
562
- * auto-select when only `--yes` is given), install, and return a structured
563
- * report. No console output — the caller formats it.
564
- */
565
- function runNonInteractive(options, url, urlSource, deps) {
566
- const detected = deps.detect();
567
- let ids;
568
- let unknown = [];
569
- let comingSoon = [];
570
- if (options.all || options.clients && options.clients.length > 0) {
571
- const requested = resolveRequestedIds(options, detected);
572
- ids = requested.ids;
573
- unknown = requested.unknown;
574
- comingSoon = requested.comingSoon;
575
- } else ids = autoSelectIds(detected);
576
- return {
577
- url,
578
- urlSource,
579
- outcomes: installSelected(ids, url, options.dryRun, deps.install),
580
- unknownClients: unknown,
581
- comingSoonClients: comingSoon,
582
- dryRun: options.dryRun
583
- };
584
- }
585
- /** One human-readable status line per outcome. */
586
- function outcomeLine(o, dryRun) {
587
- const r = o.result;
588
- if (r.command) return `${o.label}: run \`${r.command}\``;
589
- if (!r.ok) return `${o.label}: failed (no config location on this platform)`;
590
- if (r.alreadyConfigured) return `${o.label}: already set up (${r.path})`;
591
- if (dryRun) return `${o.label}: would update ${r.path} (dry run — no changes)`;
592
- if (r.wrote) return `${o.label}: installed → ${r.path}`;
593
- return `${o.label}: no change (${r.path})`;
594
- }
595
- /** Render a report as a plain multi-line summary (used by the CLI + tests). */
596
- function formatReport(report) {
597
- const lines = [];
598
- lines.push(`MCP URL: ${report.url} (${report.urlSource})`);
599
- if (report.outcomes.length === 0) lines.push("No clients selected.");
600
- else for (const o of report.outcomes) lines.push(outcomeLine(o, report.dryRun));
601
- if (report.unknownClients.length > 0) lines.push(`Unknown clients (skipped): ${report.unknownClients.join(", ")}`);
602
- if (report.comingSoonClients.length > 0) lines.push(`Coming soon (skipped): ${report.comingSoonClients.join(", ")}`);
603
- return lines.join("\n");
604
- }
605
- /** Next-steps blurb after a run. */
606
- function nextStepsText(report) {
607
- if (report.dryRun) return "Dry run — re-run without --dry-run to apply these changes.";
608
- if (!report.outcomes.some((o) => o.result.wrote || o.result.command)) return "Nothing to do.";
609
- return ["Next: restart the client(s) above — each will prompt you to authorize", "Levr once in the browser. Then ask it: \"What issues are assigned to me?\""].join("\n");
610
- }
611
-
612
- //#endregion
613
- //#region src/commands/mcp/addHandler.ts
614
- const defaultInstall = (harness, mcpUrl, dryRun) => installHarnessSync(harness, mcpUrl, { dryRun });
615
- const defaultDeps = {
616
- detect: () => detectSync(),
617
- install: defaultInstall
618
- };
619
- async function mcpAddHandler(flags) {
620
- const { url, source } = resolveMcpUrl(flags.url);
621
- const clients = (flags.client ?? []).flatMap((c) => c.split(",").map((s) => s.trim()).filter(Boolean));
622
- const options = {
623
- all: flags.all,
624
- clients,
625
- yes: flags.yes,
626
- dryRun: flags["dry-run"]
627
- };
628
- if (options.all || clients.length > 0 || options.yes || !process.stdout.isTTY) {
629
- const report = runNonInteractive(options, url, source, defaultDeps);
630
- this.process.stdout.write(`${formatReport(report)}\n`);
631
- this.process.stdout.write(`\n${nextStepsText(report)}\n`);
632
- if (report.unknownClients.length > 0 || hasFailure(report)) this.process.exitCode = 1;
633
- return;
634
- }
635
- await interactive(this, options.dryRun, url, source);
636
- }
637
- function hasFailure(report) {
638
- return report.outcomes.some((o) => !o.result.ok);
639
- }
640
- async function interactive(ctx, dryRun, url, urlSource) {
641
- const p = await import("@clack/prompts");
642
- p.intro("Levr MCP setup");
643
- p.note(`${url}\n(${urlSource})`, "MCP endpoint");
644
- const detected = defaultDeps.detect();
645
- const installable = detected.filter((d) => d.available && !d.comingSoon);
646
- if (installable.length === 0) {
647
- p.outro("No supported MCP clients found on this machine.");
648
- return;
649
- }
650
- const preselect = new Set(autoSelectIds(detected));
651
- const selection = await p.multiselect({
652
- message: "Select clients to set up",
653
- options: installable.map((d) => ({
654
- value: d.id,
655
- label: d.label,
656
- hint: d.alreadyConfigured ? "already set up" : d.installed ? "detected" : "not detected"
657
- })),
658
- initialValues: installable.filter((d) => preselect.has(d.id)).map((d) => d.id),
659
- required: false
660
- });
661
- if (p.isCancel(selection)) {
662
- p.cancel("Cancelled.");
663
- ctx.process.exitCode = 1;
664
- return;
665
- }
666
- if (selection.length === 0) {
667
- p.outro("Nothing selected — bye.");
668
- return;
669
- }
670
- const spin = p.spinner();
671
- spin.start(dryRun ? "Previewing changes" : "Installing");
672
- const outcomes = installSelected(selection, url, dryRun, defaultDeps.install);
673
- spin.stop(dryRun ? "Preview ready" : "Done");
674
- const report = {
675
- url,
676
- urlSource,
677
- outcomes,
678
- unknownClients: [],
679
- comingSoonClients: [],
680
- dryRun
681
- };
682
- p.note(formatReport(report), "Results");
683
- p.outro(nextStepsText(report));
684
- if (hasFailure(report)) ctx.process.exitCode = 1;
685
- }
686
-
687
- //#endregion
688
- export { mcpAddHandler };