Skip to content

Commit 4be5b3f

Browse files
committed
refactor: split the tools out of programs
Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589
1 parent 6086517 commit 4be5b3f

37 files changed

Lines changed: 1934 additions & 704 deletions

‎src/cli/commands/cli/add.ts‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import { consoleLog } from '@shared/console-log';
12
import { CLI_STEERING_TARGETS } from '@shared/install-cli-steering';
2-
import { runCliAdd } from '@tools/cli-steering/index';
3+
import { runCliAdd } from '@tools';
4+
import { exitWith } from '@cli/runners';
35
import type { Command } from '../command';
46

57
export const cliAddCommand: Command = {
@@ -42,6 +44,15 @@ export const cliAddCommand: Command = {
4244
return true;
4345
},
4446
handler: (argv) => {
45-
void runCliAdd(argv);
47+
exitWith(() =>
48+
runCliAdd(
49+
{
50+
agent: typeof argv.agent === 'string' ? argv.agent : undefined,
51+
path: typeof argv.path === 'string' ? argv.path : undefined,
52+
all: argv.all === true,
53+
},
54+
{ log: consoleLog },
55+
),
56+
);
4657
},
4758
};

‎src/cli/commands/doctor.ts‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { consoleLog } from '@shared/console-log';
2+
import { readApiKeyFromEnv } from '@utils/env-api-key';
3+
import { DOCTOR, runDoctorReport, Tool } from '@tools';
4+
import { exitWith, tuiSessionArgs, withSignals } from '@cli/runners';
5+
import { skillProgramOptions } from './skill-program-options';
6+
import type { Command } from './command';
7+
8+
export const doctorCommand: Command = {
9+
name: 'doctor',
10+
description: DOCTOR.description,
11+
options: { ...skillProgramOptions },
12+
handler: (argv) => {
13+
const options = { ...argv } as Record<string, unknown>;
14+
// In CI there is no screen: fetch the project's health issues and print them.
15+
if (options.ci) {
16+
exitWith(() =>
17+
runDoctorReport(
18+
{
19+
apiKey:
20+
(options.apiKey as string | undefined) ??
21+
readApiKeyFromEnv() ??
22+
undefined,
23+
projectId: options.projectId
24+
? Number(options.projectId as string)
25+
: undefined,
26+
baseUrl: options.baseUrl as string | undefined,
27+
},
28+
{ log: consoleLog },
29+
),
30+
);
31+
return;
32+
}
33+
withSignals(async (signal) => {
34+
// Loaded here, not at startup: a console run never loads the TUI.
35+
const { runTuiTool } = await import('@tui');
36+
return runTuiTool(Tool.PosthogDoctor, {
37+
session: tuiSessionArgs(options),
38+
signal,
39+
});
40+
});
41+
},
42+
};

‎src/cli/commands/mcp/add.ts‎

Lines changed: 37 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
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';
43
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';
77
import type { Command } from '../command';
88
import { isTUIUnavailable } from './tui-availability';
99

@@ -33,56 +33,45 @@ export const mcpAddCommand: Command = {
3333

3434
function runMcpAdd(argv: Arguments): void {
3535
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+
);
4245

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+
}
5154

55+
exitWith(async () => {
5256
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+
);
6370
} catch (error) {
6471
if (!isTUIUnavailable(error)) throw error;
65-
await runHeadlessAdd(args);
72+
return headless();
6673
}
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+
});
8675
}
8776

8877
function parseFeatures(raw: unknown): string[] | undefined {

‎src/cli/commands/mcp/remove.ts‎

Lines changed: 30 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
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';
43
import { headlessOption, isHeadless } from '@shared/headless-mode';
5-
import { Program } from '@programs';
6-
import { VERSION } from '@shared/version';
4+
import { removeMCPServerFromClientsStep, Tool } from '@tools';
5+
import { exitWith, underSignals } from '@cli/runners';
76
import type { Command } from '../command';
87
import { isTUIUnavailable } from './tui-availability';
98

@@ -23,40 +22,38 @@ export const mcpRemoveCommand: Command = {
2322
};
2423

2524
function runMcpRemove(argv: Arguments): void {
26-
void (async () => {
27-
const debug = argv.debug as boolean | undefined;
28-
const localMcp = argv.local as boolean | undefined;
25+
const localMcp = argv.local as boolean | undefined;
26+
const headless = () =>
27+
removeMCPServerFromClientsStep(
28+
{ local: localMcp },
29+
{ log: consoleLog.log },
30+
);
2931

30-
// See the note in add.ts: a non-TTY run stalls on the confirm prompt
31-
// instead of falling back, so scripts need an explicit flag.
32-
if (isHeadless(argv)) {
33-
await runHeadlessRemove(localMcp);
34-
return;
35-
}
32+
// See the note in add.ts: a non-TTY run stalls on the confirm prompt
33+
// instead of falling back, so scripts need an explicit flag.
34+
if (isHeadless(argv)) {
35+
exitWith(headless);
36+
return;
37+
}
3638

39+
exitWith(async () => {
3740
try {
38-
const { startTUI } = await import('@tui/start-tui');
39-
const { buildSession } = await import('@programs/session/wizard-session');
40-
const tui = startTUI(VERSION, Program.McpRemove);
41-
tui.store.session = buildSession({
42-
debug,
43-
localMcp,
44-
baseUrl: argv.baseUrl as string | undefined,
45-
});
41+
const { runTuiTool } = await import('@tui');
42+
return await underSignals((signal) =>
43+
runTuiTool(Tool.McpRemove, {
44+
session: {
45+
debug: argv.debug as boolean | undefined,
46+
localMcp,
47+
baseUrl: argv.baseUrl as string | undefined,
48+
},
49+
signal,
50+
}),
51+
);
4652
} catch (error) {
47-
// Same guard as `mcp add`: only a missing TTY falls back to LoggingUI,
53+
// Same guard as `mcp add`: only a missing TTY falls back to the console,
4854
// so a genuine TUI bug surfaces instead of looking like a plain shell.
4955
if (!isTUIUnavailable(error)) throw error;
50-
await runHeadlessRemove(localMcp);
56+
return headless();
5157
}
52-
})();
53-
}
54-
55-
/** No exit code on an empty result: nothing to remove is the requested end state. */
56-
async function runHeadlessRemove(local?: boolean): Promise<void> {
57-
setUI(new LoggingUI());
58-
const { removeMCPServerFromClientsStep } = await import(
59-
'@shared/mcp-clients/install'
60-
);
61-
await removeMCPServerFromClientsStep({ local });
58+
});
6259
}

‎src/cli/commands/mcp/tui-availability.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ import { ErrorCodes } from '@shared/errors';
33
/**
44
* startTUI throws CliInteractiveRequired, and Ink throws "Raw mode is not
55
* supported", when stdin has no TTY (piped input, CI, some IDE terminals).
6-
* That is the only TUI failure the mcp commands
7-
* degrade to LoggingUI for — any other error from the TUI path is a real bug
8-
* and must surface rather than be silently swallowed.
6+
* That is the only TUI failure the mcp commands degrade to console output for
7+
* — any other error from the TUI path is a real bug and must surface rather
8+
* than be silently swallowed.
99
*/
1010
export function isTUIUnavailable(error: unknown): boolean {
1111
if (!(error instanceof Error)) return false;

‎src/cli/commands/mcp/tutorial.ts‎

Lines changed: 19 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
import type { Arguments } from 'yargs';
2-
import { getUI, setUI } from '@ui';
3-
import { LoggingUI } from '@headless/renderers/logging-ui';
4-
import { Program } from '@programs';
5-
import { VERSION } from '@shared/version';
6-
import { ErrorCodes } from '@shared/errors';
7-
import { emitWizardError } from '@shared/errors';
2+
import { consoleLog } from '@shared/console-log';
3+
import { ErrorCodes, emitWizardError } from '@shared/errors';
4+
import { Tool } from '@tools';
5+
import { exitWith, underSignals } from '@cli/runners';
86
import type { Command } from '../command';
97

108
export const mcpTutorialCommand: Command = {
@@ -22,23 +20,22 @@ export const mcpTutorialCommand: Command = {
2220
};
2321

2422
function runMcpTutorial(argv: Arguments): void {
25-
void (async () => {
26-
const debug = argv.debug as boolean | undefined;
27-
const localMcp = argv.local as boolean | undefined;
28-
23+
exitWith(async () => {
2924
try {
30-
const { startTUI } = await import('@tui/start-tui');
31-
const { buildSession } = await import('@programs/session/wizard-session');
32-
const tui = startTUI(VERSION, Program.McpTutorial);
33-
tui.store.session = buildSession({
34-
debug,
35-
localMcp,
36-
baseUrl: argv.baseUrl as string | undefined,
37-
});
25+
const { runTuiTool } = await import('@tui');
26+
return await underSignals((signal) =>
27+
runTuiTool(Tool.McpTutorial, {
28+
session: {
29+
debug: argv.debug as boolean | undefined,
30+
localMcp: argv.local as boolean | undefined,
31+
baseUrl: argv.baseUrl as string | undefined,
32+
},
33+
signal,
34+
}),
35+
);
3836
} catch (err) {
3937
// TUI unavailable — the tutorial has no headless fallback.
40-
setUI(new LoggingUI());
41-
getUI().log.error(
38+
consoleLog.log.error(
4239
`The MCP tutorial requires an interactive terminal. ${
4340
err instanceof Error ? err.message : String(err)
4441
}`,
@@ -47,7 +44,7 @@ function runMcpTutorial(argv: Arguments): void {
4744
code: ErrorCodes.CliInteractiveRequired,
4845
message: 'The MCP tutorial requires an interactive terminal.',
4946
});
50-
process.exit(1);
47+
return 1;
5148
}
52-
})();
49+
});
5350
}

0 commit comments

Comments
 (0)