|
1 | 1 | import type { Arguments } from 'yargs'; |
2 | | -import { setUI } from '@ui'; |
3 | | -import { LoggingUI } from '@headless/renderers/logging-ui'; |
| 2 | +import { consoleLog } from '@shared/console-log'; |
4 | 3 | import { headlessOption, isHeadless } from '@shared/headless-mode'; |
5 | | -import { Program } from '@programs'; |
6 | | -import { VERSION } from '@shared/version'; |
| 4 | +import { readApiKeyFromEnv } from '@utils/env-api-key'; |
| 5 | +import { addMCPServerToClientsStep, Tool } from '@tools'; |
| 6 | +import { exitWith, underSignals } from '@cli/runners'; |
7 | 7 | import type { Command } from '../command'; |
8 | 8 | import { isTUIUnavailable } from './tui-availability'; |
9 | 9 |
|
@@ -33,56 +33,45 @@ export const mcpAddCommand: Command = { |
33 | 33 |
|
34 | 34 | function runMcpAdd(argv: Arguments): void { |
35 | 35 | const features = parseFeatures(argv.features); |
36 | | - void (async () => { |
37 | | - const { readApiKeyFromEnv } = await import('@utils/env-api-key'); |
38 | | - const apiKey = (argv.apiKey as string | undefined) || readApiKeyFromEnv(); |
39 | | - const debug = argv.debug as boolean | undefined; |
40 | | - const localMcp = argv.local as boolean | undefined; |
41 | | - const args = { local: localMcp, features, apiKey }; |
| 36 | + const apiKey = (argv.apiKey as string | undefined) || readApiKeyFromEnv(); |
| 37 | + const localMcp = argv.local as boolean | undefined; |
| 38 | + // Never forwards `ci`: headless implies session.ci elsewhere, and the step |
| 39 | + // reads that as "skip MCP entirely" — the opposite of what we're here to do. |
| 40 | + const headless = () => |
| 41 | + addMCPServerToClientsStep( |
| 42 | + { local: localMcp, features, apiKey }, |
| 43 | + { log: consoleLog.log }, |
| 44 | + ); |
42 | 45 |
|
43 | | - // Ink renders into a pipe happily and only throws on raw-mode input, so a |
44 | | - // non-TTY run reaches the confirm prompt and stalls there rather than |
45 | | - // hitting the isTUIUnavailable fallback below. The headless flag is the |
46 | | - // only reliable way to install from a script. |
47 | | - if (isHeadless(argv)) { |
48 | | - await runHeadlessAdd(args); |
49 | | - return; |
50 | | - } |
| 46 | + // Ink renders into a pipe happily and only throws on raw-mode input, so a |
| 47 | + // non-TTY run reaches the confirm prompt and stalls there rather than |
| 48 | + // hitting the isTUIUnavailable fallback below. The headless flag is the |
| 49 | + // only reliable way to install from a script. |
| 50 | + if (isHeadless(argv)) { |
| 51 | + exitWith(headless); |
| 52 | + return; |
| 53 | + } |
51 | 54 |
|
| 55 | + exitWith(async () => { |
52 | 56 | try { |
53 | | - const { startTUI } = await import('@tui/start-tui'); |
54 | | - const { buildSession } = await import('@programs/session/wizard-session'); |
55 | | - const tui = startTUI(VERSION, Program.McpAdd); |
56 | | - tui.store.session = buildSession({ |
57 | | - debug, |
58 | | - localMcp, |
59 | | - mcpFeatures: features, |
60 | | - apiKey, |
61 | | - baseUrl: argv.baseUrl as string | undefined, |
62 | | - }); |
| 57 | + const { runTuiTool } = await import('@tui'); |
| 58 | + return await underSignals((signal) => |
| 59 | + runTuiTool(Tool.McpAdd, { |
| 60 | + session: { |
| 61 | + debug: argv.debug as boolean | undefined, |
| 62 | + localMcp, |
| 63 | + mcpFeatures: features, |
| 64 | + apiKey, |
| 65 | + baseUrl: argv.baseUrl as string | undefined, |
| 66 | + }, |
| 67 | + signal, |
| 68 | + }), |
| 69 | + ); |
63 | 70 | } catch (error) { |
64 | 71 | if (!isTUIUnavailable(error)) throw error; |
65 | | - await runHeadlessAdd(args); |
| 72 | + return headless(); |
66 | 73 | } |
67 | | - })(); |
68 | | -} |
69 | | - |
70 | | -async function runHeadlessAdd(args: { |
71 | | - local?: boolean; |
72 | | - features?: string[]; |
73 | | - apiKey?: string; |
74 | | -}): Promise<void> { |
75 | | - setUI(new LoggingUI()); |
76 | | - const { addMCPServerToClientsStep } = await import( |
77 | | - '@shared/mcp-clients/install' |
78 | | - ); |
79 | | - // Never forwards `ci`: headless implies session.ci elsewhere, and the step |
80 | | - // reads that as "skip MCP entirely" — the opposite of what we're here to do. |
81 | | - const { installed, failed } = await addMCPServerToClientsStep(args); |
82 | | - // A scripted caller has no screen to read, so this has to be an exit code. |
83 | | - // Any failure counts, not just a total wipeout: the step installs to every |
84 | | - // detected client, so one succeeding would otherwise mask the rest. |
85 | | - if (failed.length > 0 || installed.length === 0) process.exitCode = 1; |
| 74 | + }); |
86 | 75 | } |
87 | 76 |
|
88 | 77 | function parseFeatures(raw: unknown): string[] | undefined { |
|
0 commit comments