@mcpeak/cli 0.9.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.
@@ -0,0 +1,186 @@
1
+ import { McpStdioConnection, ToolDef } from "@mcpeak/core";
2
+ import { AuthoringExecutionSnapshot, AuthoringSessionView, BaselineGenerationResult, PreFillProvider } from "@mcpeak/generate";
3
+ import { Cassette } from "@mcpeak/record";
4
+ import { FinalizeRunnerExecutionOptions, RunSuiteOptions, RunnerExecution, RunnerReport, SuiteValidationIssue, SuiteValidationResult, TestSuiteSpec } from "@mcpeak/runner";
5
+ //#region src/generate-command.d.ts
6
+ interface GenerateCommandInput {
7
+ readonly suiteId: string;
8
+ readonly name: string;
9
+ readonly outPath: string;
10
+ readonly command: string;
11
+ readonly args: readonly string[];
12
+ readonly baselineOnly: boolean;
13
+ readonly provider?: "codex" | "claude";
14
+ readonly model?: string;
15
+ /** 승인 전 시험 실행 여부. 기본은 실행이고 `--no-dry-run` 이 끈다. 설계 문서 §4.3. */
16
+ readonly dryRun: boolean;
17
+ /** `--cassette` 경로. 없으면 서버를 직접 부른다. */
18
+ readonly cassettePath?: string;
19
+ /** `--record`. 카세트 파일이 있어도 새로 녹화한다. */
20
+ readonly forceRecord: boolean;
21
+ /** `--force`. `--out` 에 파일이 있으면 지우고 새로 쓴다. 설계 문서 §5. */
22
+ readonly force: boolean;
23
+ /** `--reset-cmd`. 시험 실행 직전 1회 실행한다. */
24
+ readonly resetCmd?: string;
25
+ /** 입력값 교정 단계를 돌릴지 여부. 기본은 실행이고 `--no-repair` 가 끈다. 설계 문서 §7. */
26
+ readonly repair: boolean;
27
+ }
28
+ interface GenerateCommandDependencies {
29
+ connect(options: {
30
+ command: string;
31
+ args: readonly string[];
32
+ }): Promise<McpStdioConnection>;
33
+ createBaselineSuite(tools: readonly ToolDef[], options: {
34
+ suiteId: string;
35
+ suiteName: string;
36
+ }): BaselineGenerationResult;
37
+ createAuthoringSession(baseline: BaselineGenerationResult, options?: {
38
+ readonly preFilledCaseIds?: readonly string[];
39
+ }): AuthoringSessionView;
40
+ finalizeAuthoringDraft(options: {
41
+ session: AuthoringSessionView;
42
+ approval: {
43
+ approved: boolean;
44
+ fingerprint: string;
45
+ };
46
+ }): {
47
+ finalized: true;
48
+ snapshot: AuthoringExecutionSnapshot;
49
+ } | {
50
+ finalized: false;
51
+ };
52
+ getAuthoringExecutionSuite(snapshot: AuthoringExecutionSnapshot): TestSuiteSpec;
53
+ validateSuite(value: unknown): SuiteValidationResult;
54
+ exists(path: string): Promise<boolean>;
55
+ openTemp(path: string): Promise<{
56
+ writeFile(data: string, encoding: "utf8"): Promise<void>;
57
+ sync(): Promise<void>;
58
+ close(): Promise<void>;
59
+ }>;
60
+ readFile(path: string): Promise<Uint8Array>;
61
+ /**
62
+ * 임시 파일을 최종 경로에 커밋한다. `rename`이 아니라 `link`인 것이 요점이다.
63
+ * `rename`은 대상이 있으면 말없이 덮어쓴다. `link`는 EEXIST로 실패하므로
64
+ * 원자성과 no-clobber를 동시에 얻는다. 덮어쓸 수 있는 primitive를 아예 두지 않는다.
65
+ */
66
+ link(from: string, to: string): Promise<void>;
67
+ unlink(path: string): Promise<void>;
68
+ writeStdout(text: string): void;
69
+ writeStderr(text: string): void;
70
+ reviewIO?: ReviewIO;
71
+ providers?: Partial<Record<"codex" | "claude", (model: string) => TestAuthoringProvider | undefined>>;
72
+ /**
73
+ * 거절 근거 진단 통로 (#89 · 설계 문서 §6). `providers` 와 **따로 둔다.** 그쪽은
74
+ * `TestAuthoringProvider`(`author`)이고 이쪽은 `diagnoseRejection` 이라 다른 계약이다.
75
+ * `preFillProviders` 와 같은 선례다. 없으면 진단을 아예 묻지 않는다.
76
+ */
77
+ rejectionProviders?: Partial<Record<"codex" | "claude", (model: string) => import("@mcpeak/generate").RejectionDiagnosisProvider | undefined>>;
78
+ prepareRejectionDiagnosisRequests?: typeof import("@mcpeak/generate").prepareRejectionDiagnosisRequests;
79
+ dispatchRejectionDiagnosis?: typeof import("@mcpeak/generate").dispatchRejectionDiagnosis;
80
+ prepareAuthoringRequest?: typeof import("@mcpeak/generate").prepareAuthoringRequest;
81
+ dispatchAuthoringRequest?: typeof import("@mcpeak/generate").dispatchAuthoringRequest;
82
+ createAuthoringDiff?: typeof import("@mcpeak/generate").createAuthoringDiff;
83
+ applyAuthoringChanges?: typeof import("@mcpeak/generate").applyAuthoringChanges;
84
+ reviewLocalAuthoringCandidate?: typeof import("@mcpeak/generate").reviewLocalAuthoringCandidate;
85
+ computeCoverage?: typeof import("@mcpeak/generate").computeCoverage;
86
+ /**
87
+ * AI 사전보완 통로. 넷 다 주입돼야 사전보완이 돈다. 하나라도 없으면 건너뛴다.
88
+ * 위 authoring 통로와 같은 이유로 값 import 가 아니라 주입이다.
89
+ */
90
+ preparePreFillRequest?: typeof import("@mcpeak/generate").preparePreFillRequest;
91
+ previewPreFillRequest?: typeof import("@mcpeak/generate").previewPreFillRequest;
92
+ dispatchPreFillRequest?: typeof import("@mcpeak/generate").dispatchPreFillRequest;
93
+ preFillProviders?: Partial<Record<"codex" | "claude", (model: string) => PreFillProvider | undefined>>;
94
+ /**
95
+ * `instanceof` 용 클래스. 값 import 가 아니라 주입인 것이 요점이다.
96
+ *
97
+ * 이 파일은 `@mcpeak/generate` 에서 타입만 가져온다(위 `import type`). 클래스를 값으로
98
+ * 가져오면 `index.ts` 가 정적으로 끌어와 `test` 경로까지 `generate` 를 로드한다.
99
+ * `typeof import(...)` 는 타입 위치라 런타임 import 가 생기지 않고, 주입하면 클래스
100
+ * 동일성도 보장된다. 위 6개 필드가 같은 방식이다.
101
+ */
102
+ GenerateTestsError?: typeof import("@mcpeak/generate").GenerateTestsError;
103
+ /**
104
+ * 카세트 파일 입출력. 주입점을 여기 하나만 두는 이유는 시험 실행 경로에서 파일시스템을
105
+ * 만지는 곳이 이것뿐이기 때문이다. 나머지(`runDryRun`·`reviewDryRun`)는 주입한 client 와
106
+ * io 만 쓰므로 테스트가 실제 구현을 그대로 돌린다.
107
+ */
108
+ cassetteIo?: {
109
+ load(path: string): Promise<Cassette | null>;
110
+ save(path: string, cassette: Cassette): Promise<void>;
111
+ };
112
+ }
113
+ interface ReviewIO {
114
+ input(message: string): Promise<string>;
115
+ choose(message: string, choices: readonly string[]): Promise<string>;
116
+ confirm(message: string): Promise<boolean>;
117
+ write(text: string): void;
118
+ readonly interactive: boolean;
119
+ close?(): void;
120
+ }
121
+ type TestAuthoringProvider = import("@mcpeak/generate").TestAuthoringProvider;
122
+ declare function runGenerateCommand(argv: readonly string[], deps: GenerateCommandDependencies): Promise<number>;
123
+ declare const nodeGenerateDependencies: () => Omit<GenerateCommandDependencies, "connect" | "createBaselineSuite" | "createAuthoringSession" | "finalizeAuthoringDraft" | "getAuthoringExecutionSuite" | "validateSuite">;
124
+ declare function nodeReviewIO(input?: NodeJS.ReadableStream, output?: NodeJS.WritableStream): ReviewIO;
125
+ //#endregion
126
+ //#region src/repair-command.d.ts
127
+ interface RepairCommandInput {
128
+ readonly bundlePath: string;
129
+ readonly providerId: "codex" | "claude";
130
+ readonly model: string;
131
+ readonly yes: boolean;
132
+ readonly includeStderr: boolean;
133
+ readonly maxCases: number;
134
+ }
135
+ interface RepairCommandDependencies {
136
+ readFile(path: string): Promise<string>;
137
+ writeStdout(text: string): void;
138
+ writeStderr(text: string): void;
139
+ /** 전송 확인 화면. 없으면 비대화형으로 본다. `generate` 가 쓰는 그 인터페이스다. */
140
+ readonly reviewIO?: ReviewIO;
141
+ /**
142
+ * 진단 통로. `@mcpeak/generate` 를 값으로 import 하지 않고 주입받는다. 정적으로 import 하면
143
+ * `test` 경로가 `generate` 를 함께 로드한다. 계획서 §8 위험표 첫 줄이다.
144
+ */
145
+ readonly diagnosis?: {
146
+ readonly prepare: typeof import("@mcpeak/generate").prepareDiagnosisRequest;
147
+ readonly dispatch: typeof import("@mcpeak/generate").dispatchDiagnosisRequest;
148
+ readonly providers: {
149
+ readonly codex: (model: string) => import("@mcpeak/generate").ServerDiagnosisProvider;
150
+ readonly claude: (model: string) => import("@mcpeak/generate").ServerDiagnosisProvider;
151
+ };
152
+ };
153
+ }
154
+ declare function parseRepairCommand(argv: readonly string[]): RepairCommandInput;
155
+ /**
156
+ * `repair` 명령. 번들을 읽고, 전송 내용을 확인받고, provider 에게 진단을 물어 화면에 찍는다.
157
+ * 파일도 명세도 고치지 않는다. 종료 코드는 운영 실패에만 1 이다(설계서 §7.1).
158
+ */
159
+ declare function runRepairCommand(argv: readonly string[], deps: RepairCommandDependencies): Promise<number>;
160
+ //#endregion
161
+ //#region src/replay-command.d.ts
162
+ interface ReplayCommandInput {
163
+ readonly suitePath: string;
164
+ readonly cassettePath: string;
165
+ }
166
+ /**
167
+ * `connect` 가 없다. replay 가 서버를 띄우지 않는다는 것이 주석이 아니라 타입에 드러난다.
168
+ * ADR-0028.
169
+ */
170
+ interface ReplayCommandDependencies {
171
+ readFile(path: string): Promise<Uint8Array>;
172
+ validateSuite(input: unknown): SuiteValidationResult;
173
+ loadCassette(path: string): Promise<Cassette | null>;
174
+ startRunner(options: RunSuiteOptions): RunnerExecution;
175
+ finalize(options: FinalizeRunnerExecutionOptions): Promise<RunnerReport>;
176
+ renderReport(report: RunnerReport, options?: {
177
+ color?: boolean;
178
+ }): string;
179
+ colorEnabled: boolean;
180
+ writeStdout(text: string): void;
181
+ writeStderr(text: string): void;
182
+ }
183
+ declare function parseReplayCommand(argv: readonly string[]): ReplayCommandInput;
184
+ declare function runReplayCommand(argv: readonly string[], dependencies: ReplayCommandDependencies): Promise<number>;
185
+ //#endregion
186
+ export { RepairCommandDependencies as a, runRepairCommand as c, ReviewIO as d, nodeGenerateDependencies as f, runReplayCommand as i, GenerateCommandDependencies as l, runGenerateCommand as m, ReplayCommandInput as n, RepairCommandInput as o, nodeReviewIO as p, parseReplayCommand as r, parseRepairCommand as s, ReplayCommandDependencies as t, GenerateCommandInput as u };
@@ -0,0 +1,459 @@
1
+ import { a as runReplayCommand, c as nodeGenerateDependencies, d as GLOBAL_HELP, f as VERIFY_USAGE_HINT, l as nodeReviewIO, m as version, n as runCli, p as commandHelp, r as ReplayRuntimeUnavailableError, s as runRepairCommand, t as parseTestCommand, u as runGenerateCommand } from "./test-command-CsJfojut.mjs";
2
+ import { readFile, writeFile } from "node:fs/promises";
3
+ //#region src/verify-command.ts
4
+ /**
5
+ * 런타임 모듈을 못 불러 fallback 의존성이 쓰일 때 던진다.
6
+ *
7
+ * 타입을 따로 두는 이유는 이 실패가 **카세트 손상과 구분돼야** 하기 때문이다. 그냥 던지면
8
+ * `CASSETTE_READ_FAILED` 로 잡혀 "카세트 파일이 손상되지 않았는지 확인하세요" 가 나가고,
9
+ * 사용자는 멀쩡한 파일을 들여다보게 된다. 고칠 곳은 설치다.
10
+ */
11
+ var VerifyRuntimeUnavailableError = class extends Error {
12
+ constructor() {
13
+ super("verify 실행에 필요한 모듈을 로드하지 못했습니다.");
14
+ this.name = "VerifyRuntimeUnavailableError";
15
+ }
16
+ };
17
+ const dictionary = {
18
+ VERIFY_RUNTIME_UNAVAILABLE: {
19
+ message: "verify 에 필요한 @mcpeak/record 를 로드하지 못했습니다.",
20
+ hint: "의존성을 설치한 뒤 다시 실행하세요. 카세트 파일의 문제가 아닙니다."
21
+ },
22
+ CASSETTE_NOT_FOUND: {
23
+ message: "카세트 파일이 없습니다.",
24
+ hint: "`mcpeak generate --cassette <path> --record` 로 먼저 녹화하세요."
25
+ },
26
+ CASSETTE_READ_FAILED: {
27
+ message: "카세트를 읽지 못했습니다.",
28
+ hint: "카세트 파일이 손상되지 않았는지 확인하세요."
29
+ },
30
+ SERVER_CONNECT_FAILED: {
31
+ message: "MCP 서버에 연결하지 못했습니다.",
32
+ hint: "--command 와 --arg 가 카세트를 녹화할 때 쓴 서버와 같은지 확인하세요."
33
+ },
34
+ CASSETTE_DRIFTED: {
35
+ message: "카세트가 실서버 응답과 다릅니다.",
36
+ hint: "서버가 바뀐 것이라면 `--record` 로 카세트를 다시 만드세요."
37
+ }
38
+ };
39
+ var VerifyCommandError = class extends Error {
40
+ failure;
41
+ constructor(failure) {
42
+ super(failure.message);
43
+ this.failure = failure;
44
+ }
45
+ };
46
+ const usage = (message) => {
47
+ throw new VerifyCommandError({
48
+ code: "CLI_USAGE",
49
+ message,
50
+ hint: VERIFY_USAGE_HINT
51
+ });
52
+ };
53
+ /**
54
+ * 값을 하나 읽는다. `--opt value` 와 `--opt=value` 를 모두 받는다.
55
+ *
56
+ * `--arg` 만 값의 모양을 검사하지 않는다. 서버 인자는 대부분 플래그 모양이고(`-y`, `--with`,
57
+ * `--db-path`) 빈 문자열도 정상적인 인자다. `generate` 와 `test` 가 이미 `--arg` 를 그렇게
58
+ * 받으므로, 여기서 거절하면 **`generate --record` 로 녹화한 바로 그 서버를 `verify` 로는
59
+ * 부를 수 없다.** 같은 명령줄을 못 받는 verify 는 그 카세트를 확인할 방법이 아예 없다.
60
+ *
61
+ * `--command` 는 그대로 검사한다. 실행 파일 자리에 들어온 플래그는 값을 빠뜨린 오타다.
62
+ */
63
+ function optionValue(argv, index, name) {
64
+ const anyShape = name === "--arg";
65
+ const token = argv[index] ?? "";
66
+ if (token.startsWith(`${name}=`)) {
67
+ const value = token.slice(name.length + 1);
68
+ if (value === "" && !anyShape) usage(`${name} 옵션 값이 필요합니다.`);
69
+ return [value, index];
70
+ }
71
+ const next = argv[index + 1];
72
+ if (next === void 0 || !anyShape && (next === "" || next.startsWith("--"))) usage(`${name} 옵션 값이 필요합니다.`);
73
+ return [next, index + 1];
74
+ }
75
+ function parseVerifyCommand(argv) {
76
+ let cassettePath;
77
+ let command;
78
+ const args = [];
79
+ for (let index = 0; index < argv.length; index++) {
80
+ const token = argv[index] ?? "";
81
+ const [name] = token.startsWith("--") ? token.split("=", 1) : [token];
82
+ if (name === "--command") {
83
+ if (command !== void 0) usage("--command 는 한 번만 사용할 수 있습니다.");
84
+ [command, index] = optionValue(argv, index, "--command");
85
+ continue;
86
+ }
87
+ if (name === "--arg") {
88
+ let value;
89
+ [value, index] = optionValue(argv, index, "--arg");
90
+ args.push(value);
91
+ continue;
92
+ }
93
+ if (name === "--record") usage("verify 는 카세트를 고치지 않습니다. 다시 녹화하려면 generate --record 를 쓰세요.");
94
+ if (token.startsWith("--")) usage(`알 수 없는 옵션입니다: ${token}`);
95
+ if (cassettePath !== void 0) usage(`카세트는 하나만 받습니다: ${token}`);
96
+ cassettePath = token;
97
+ }
98
+ if (cassettePath === void 0 || cassettePath === "") usage("카세트 JSON 경로가 필요합니다.");
99
+ if (command === void 0) usage("--command 에 MCP 서버 실행 명령이 필요합니다.");
100
+ return Object.freeze({
101
+ cassettePath,
102
+ command,
103
+ args: Object.freeze([...args])
104
+ });
105
+ }
106
+ const format = (failure) => `오류 [${failure.code}]: ${failure.message}\n해결: ${failure.hint}\n`;
107
+ const writeFailure = (dependencies, failure) => {
108
+ dependencies.writeStderr(format(failure));
109
+ return 1;
110
+ };
111
+ /**
112
+ * 결과 요약. 숫자만 찍고 끝내지 않는다 — 무엇이 어떻게 다른지가 이 명령의 산출물이다.
113
+ * `record` 가 만든 문장을 그대로 싣는다. 여기서 다시 쓰면 두 곳이 갈린다.
114
+ */
115
+ function formatVerifyResult(result, cassettePath) {
116
+ const lines = [];
117
+ const total = result.matched + result.mismatched.length + result.failed.length + result.skipped.length;
118
+ for (const item of [
119
+ ...result.mismatched,
120
+ ...result.failed,
121
+ ...result.skipped
122
+ ]) lines.push(item.message, "");
123
+ lines.push(`카세트: ${cassettePath}`);
124
+ lines.push(` 일치 ${result.matched} · 불일치 ${result.mismatched.length} · 실패 ${result.failed.length} · 확인불가 ${result.skipped.length} (총 ${total})`);
125
+ if (result.skipped.length > 0) lines.push(" 확인불가는 args 에 마스킹된 비밀값이 있어 실서버에 그대로 보낼 수 없는 요청입니다.");
126
+ if (result.mismatched.length === 0 && result.failed.length === 0) lines.push(result.skipped.length === 0 ? " 카세트가 실서버와 일치합니다." : ` 확인한 ${result.matched}개는 실서버와 일치합니다. ${result.skipped.length}개는 확인하지 못했습니다.`);
127
+ return `${lines.join("\n")}\n`;
128
+ }
129
+ async function runVerifyCommand(argv, dependencies) {
130
+ let input;
131
+ try {
132
+ input = parseVerifyCommand(argv);
133
+ } catch (error) {
134
+ if (error instanceof VerifyCommandError) return writeFailure(dependencies, error.failure);
135
+ throw error;
136
+ }
137
+ let cassette;
138
+ try {
139
+ cassette = await dependencies.loadCassette(input.cassettePath);
140
+ } catch (error) {
141
+ const code = error instanceof VerifyRuntimeUnavailableError ? "VERIFY_RUNTIME_UNAVAILABLE" : "CASSETTE_READ_FAILED";
142
+ return writeFailure(dependencies, {
143
+ code,
144
+ ...dictionary[code]
145
+ });
146
+ }
147
+ if (cassette === null) return writeFailure(dependencies, {
148
+ code: "CASSETTE_NOT_FOUND",
149
+ ...dictionary.CASSETTE_NOT_FOUND
150
+ });
151
+ let client;
152
+ try {
153
+ client = await dependencies.connect({
154
+ command: input.command,
155
+ args: input.args
156
+ });
157
+ } catch {
158
+ return writeFailure(dependencies, {
159
+ code: "SERVER_CONNECT_FAILED",
160
+ ...dictionary.SERVER_CONNECT_FAILED
161
+ });
162
+ }
163
+ let result;
164
+ try {
165
+ result = await dependencies.verifyCassette(client, cassette, { cassettePath: input.cassettePath });
166
+ } finally {
167
+ await client.close().catch(() => {});
168
+ }
169
+ dependencies.writeStdout(formatVerifyResult(result, input.cassettePath));
170
+ if (result.mismatched.length === 0 && result.failed.length === 0) return 0;
171
+ dependencies.writeStderr(format({
172
+ code: "CASSETTE_DRIFTED",
173
+ ...dictionary.CASSETTE_DRIFTED
174
+ }));
175
+ return 1;
176
+ }
177
+ //#endregion
178
+ //#region src/index.ts
179
+ const COMMANDS = [
180
+ "test",
181
+ "generate",
182
+ "repair",
183
+ "record",
184
+ "replay",
185
+ "verify",
186
+ "mock"
187
+ ];
188
+ const unavailableDependencies = {
189
+ readFile: async () => {
190
+ throw new Error("runtime dependencies unavailable");
191
+ },
192
+ validateSuite: () => {
193
+ throw new Error("runtime dependencies unavailable");
194
+ },
195
+ connect: async () => {
196
+ throw new Error("runtime dependencies unavailable");
197
+ },
198
+ startRunner: () => {
199
+ throw new Error("runtime dependencies unavailable");
200
+ },
201
+ finalize: async () => {
202
+ throw new Error("runtime dependencies unavailable");
203
+ },
204
+ renderReport: () => {
205
+ throw new Error("runtime dependencies unavailable");
206
+ },
207
+ renderJUnit: () => {
208
+ throw new Error("runtime dependencies unavailable");
209
+ },
210
+ writeFile: async () => {
211
+ throw new Error("runtime dependencies unavailable");
212
+ },
213
+ colorEnabled: false,
214
+ writeStdout: (text) => process.stdout.write(text),
215
+ writeStderr: (text) => process.stderr.write(text)
216
+ };
217
+ const unavailableRuntimeDependencies = {
218
+ ...unavailableDependencies,
219
+ readFile
220
+ };
221
+ /**
222
+ * `repair` 실행 의존성. 함수로 빼 둔 이유는 **주입 자체를 테스트가 단언할 수 있게** 하기
223
+ * 위해서다. 분기 안에 리터럴로 두면 `reviewIO` 를 빠뜨려도 아무 테스트도 안 깨진다.
224
+ * 실제로 그렇게 한 번 빠뜨렸다(T10 보고서).
225
+ *
226
+ * 대화형 판정은 `generate` 와 같은 기준이다. 같은 `nodeReviewIO()` 를 쓰고, 그 구현이
227
+ * stdin·stdout 이 둘 다 TTY 일 때만 `interactive` 를 참으로 만든다.
228
+ */
229
+ function nodeRepairDependencies(generate) {
230
+ return {
231
+ readFile: (path) => readFile(path, "utf8"),
232
+ writeStdout: (text) => void process.stdout.write(text),
233
+ writeStderr: (text) => void process.stderr.write(text),
234
+ reviewIO: nodeReviewIO(),
235
+ diagnosis: {
236
+ prepare: generate.prepareDiagnosisRequest,
237
+ dispatch: generate.dispatchDiagnosisRequest,
238
+ providers: {
239
+ codex: (model) => generate.createCodexProvider({ model }),
240
+ claude: (model) => generate.createClaudeProvider({ model })
241
+ }
242
+ }
243
+ };
244
+ }
245
+ /**
246
+ * replay 는 `connect` 를 쓰지 않는다. 대신 카세트 로더가 필요하다. 런타임 의존성을 못 불러도
247
+ * 사용 오류는 정상적으로 내야 하므로, 실제로 쓰이기 전에 끝나는 경로를 위해 자리만 채운다.
248
+ *
249
+ * **테스트가 배선을 직접 단언할 수 있도록 내보낸다.** 여기서 평범한 Error 로 되돌아가면
250
+ * 사용자는 "이슈를 보고하세요" 를 보게 되는데, 모듈 모킹으로는 이 경로를 재현할 수 없다 —
251
+ * `record` 와 `runner` 가 `generate-command` 를 통해 정적 import 체인에 있어서, 그것을
252
+ * 모킹하면 `index.ts` 로드 자체가 깨진다. `nodeRepairDependencies` 를 함수로 뺀 것과 같은
253
+ * 이유다(주입을 빠뜨려도 아무 테스트가 안 깨지는 상황을 막는다).
254
+ */
255
+ const unavailableReplayDependencies = {
256
+ ...unavailableRuntimeDependencies,
257
+ /**
258
+ * 런타임에서 오는 의존성은 전용 오류 타입을 던진다. 평범한 Error 로 두면 `validateSuite`
259
+ * 자리에서 `CLI_INTERNAL_ERROR` 로 잡혀 "이슈를 보고하세요" 가 나가고, 사용자는 자기
260
+ * 설치 문제로 버그 리포트를 쓰게 된다. 실제로 가장 먼저 걸리는 것이 `validateSuite` 다.
261
+ *
262
+ * `unavailableRuntimeDependencies` 자체는 `test` 경로와 공유하므로 여기서만 덮는다.
263
+ */
264
+ validateSuite: () => {
265
+ throw new ReplayRuntimeUnavailableError();
266
+ },
267
+ loadCassette: async () => {
268
+ throw new ReplayRuntimeUnavailableError();
269
+ },
270
+ startRunner: () => {
271
+ throw new ReplayRuntimeUnavailableError();
272
+ },
273
+ finalize: async () => {
274
+ throw new ReplayRuntimeUnavailableError();
275
+ },
276
+ renderReport: () => {
277
+ throw new ReplayRuntimeUnavailableError();
278
+ }
279
+ };
280
+ /**
281
+ * verify 도 런타임을 못 불러도 사용 오류는 정상적으로 내야 한다. 파싱은 의존성 없이 끝나므로
282
+ * `--command` 누락 같은 오류는 여기까지 오지 않는다.
283
+ */
284
+ const unavailableVerifyDependencies = {
285
+ loadCassette: async () => {
286
+ throw new VerifyRuntimeUnavailableError();
287
+ },
288
+ connect: async () => {
289
+ throw new VerifyRuntimeUnavailableError();
290
+ },
291
+ verifyCassette: async () => {
292
+ throw new VerifyRuntimeUnavailableError();
293
+ },
294
+ writeStdout: (text) => void process.stdout.write(text),
295
+ writeStderr: (text) => void process.stderr.write(text)
296
+ };
297
+ async function run(argv) {
298
+ if (argv.length === 0 || argv.length === 1 && [
299
+ "--help",
300
+ "-h",
301
+ "help"
302
+ ].includes(argv[0] ?? "")) {
303
+ process.stdout.write(GLOBAL_HELP);
304
+ return 0;
305
+ }
306
+ if (argv.length === 2) {
307
+ const command = argv[0] === "help" ? argv[1] : argv[1] === "--help" ? argv[0] : void 0;
308
+ if (command === "test" || command === "generate" || command === "repair" || command === "replay" || command === "verify") {
309
+ process.stdout.write(commandHelp(command));
310
+ return 0;
311
+ }
312
+ }
313
+ if (argv.length === 1 && argv[0] === "--version") {
314
+ process.stdout.write(`mcpeak ${version}\n`);
315
+ return 0;
316
+ }
317
+ if (argv[0] === "generate") {
318
+ let core;
319
+ let runner;
320
+ let generate;
321
+ try {
322
+ [core, runner, generate] = await Promise.all([
323
+ import("@mcpeak/core"),
324
+ import("@mcpeak/runner"),
325
+ import("@mcpeak/generate")
326
+ ]);
327
+ } catch {
328
+ return runGenerateCommand(argv, {
329
+ ...nodeGenerateDependencies(),
330
+ connect: unavailableDependencies.connect,
331
+ createBaselineSuite: unavailableDependencies.validateSuite,
332
+ createAuthoringSession: unavailableDependencies.validateSuite,
333
+ finalizeAuthoringDraft: unavailableDependencies.validateSuite,
334
+ getAuthoringExecutionSuite: unavailableDependencies.validateSuite,
335
+ validateSuite: unavailableDependencies.validateSuite
336
+ });
337
+ }
338
+ return runGenerateCommand(argv, {
339
+ ...nodeGenerateDependencies(),
340
+ connect: core.connectStdio,
341
+ createBaselineSuite: generate.createBaselineSuite,
342
+ createAuthoringSession: generate.createAuthoringSession,
343
+ finalizeAuthoringDraft: generate.finalizeAuthoringDraft,
344
+ getAuthoringExecutionSuite: generate.getAuthoringExecutionSuite,
345
+ validateSuite: runner.validateMcpSuite,
346
+ reviewIO: nodeReviewIO(),
347
+ providers: {
348
+ codex: (model) => generate.createCodexAuthoringProvider({ model }),
349
+ claude: (model) => generate.createClaudeAuthoringProvider({ model })
350
+ },
351
+ prepareAuthoringRequest: generate.prepareAuthoringRequest,
352
+ dispatchAuthoringRequest: generate.dispatchAuthoringRequest,
353
+ createAuthoringDiff: generate.createAuthoringDiff,
354
+ applyAuthoringChanges: generate.applyAuthoringChanges,
355
+ reviewLocalAuthoringCandidate: generate.reviewLocalAuthoringCandidate,
356
+ computeCoverage: generate.computeCoverage,
357
+ preparePreFillRequest: generate.preparePreFillRequest,
358
+ previewPreFillRequest: generate.previewPreFillRequest,
359
+ dispatchPreFillRequest: generate.dispatchPreFillRequest,
360
+ preFillProviders: {
361
+ codex: (model) => generate.createCodexProvider({ model }),
362
+ claude: (model) => generate.createClaudeProvider({ model })
363
+ },
364
+ prepareRejectionDiagnosisRequests: generate.prepareRejectionDiagnosisRequests,
365
+ dispatchRejectionDiagnosis: generate.dispatchRejectionDiagnosis,
366
+ rejectionProviders: {
367
+ codex: (model) => generate.createCodexProvider({ model }),
368
+ claude: (model) => generate.createClaudeProvider({ model })
369
+ },
370
+ GenerateTestsError: generate.GenerateTestsError
371
+ });
372
+ }
373
+ if (argv[0] === "repair") {
374
+ /**
375
+ * `generate` 분기와 같은 모양으로 동적 import 한다. `test` 경로는 이 분기를 지나지 않으므로
376
+ * 여전히 `core` 와 `runner` 만 로드한다. 계획서 §8 위험표 첫 줄.
377
+ */
378
+ let generate;
379
+ try {
380
+ generate = await import("@mcpeak/generate");
381
+ } catch {
382
+ process.stderr.write("오류 [REPAIR_RUNTIME_UNAVAILABLE]: 진단에 필요한 @mcpeak/generate 를 로드하지 못했습니다.\n해결: 의존성을 설치한 뒤 다시 실행하세요.\n");
383
+ return 1;
384
+ }
385
+ const dependencies = nodeRepairDependencies(generate);
386
+ try {
387
+ return await runRepairCommand(argv, dependencies);
388
+ } finally {
389
+ dependencies.reviewIO?.close?.();
390
+ }
391
+ }
392
+ if (argv[0] === "replay") {
393
+ let runner;
394
+ let record;
395
+ try {
396
+ [runner, record] = await Promise.all([import("@mcpeak/runner"), import("@mcpeak/record")]);
397
+ } catch {
398
+ return runReplayCommand(argv.slice(1), unavailableReplayDependencies);
399
+ }
400
+ return runReplayCommand(argv.slice(1), {
401
+ readFile,
402
+ validateSuite: runner.validateMcpSuite,
403
+ loadCassette: record.loadCassette,
404
+ startRunner: runner.runSuite,
405
+ finalize: runner.finalizeRunnerExecution,
406
+ renderReport: runner.renderReport,
407
+ colorEnabled: process.stdout.isTTY === true && process.env.NO_COLOR === void 0,
408
+ writeStdout: (text) => process.stdout.write(text),
409
+ writeStderr: (text) => process.stderr.write(text)
410
+ });
411
+ }
412
+ if (argv[0] === "verify") {
413
+ let core;
414
+ let record;
415
+ try {
416
+ [core, record] = await Promise.all([import("@mcpeak/core"), import("@mcpeak/record")]);
417
+ } catch {
418
+ return runVerifyCommand(argv.slice(1), unavailableVerifyDependencies);
419
+ }
420
+ return runVerifyCommand(argv.slice(1), {
421
+ loadCassette: record.loadCassette,
422
+ connect: (options) => core.connect({
423
+ command: options.command,
424
+ args: options.args
425
+ }),
426
+ verifyCassette: record.verifyCassette,
427
+ writeStdout: (text) => void process.stdout.write(text),
428
+ writeStderr: (text) => void process.stderr.write(text)
429
+ });
430
+ }
431
+ if (argv[0] !== "test") return runCli(argv, unavailableDependencies);
432
+ try {
433
+ if (!parseTestCommand(argv.slice(1)).suitePath.toLowerCase().endsWith(".json")) return runCli(argv, unavailableDependencies);
434
+ } catch {
435
+ return runCli(argv, unavailableDependencies);
436
+ }
437
+ let core;
438
+ let runner;
439
+ try {
440
+ [core, runner] = await Promise.all([import("@mcpeak/core"), import("@mcpeak/runner")]);
441
+ } catch {
442
+ return runCli(argv, unavailableRuntimeDependencies);
443
+ }
444
+ return runCli(argv, {
445
+ readFile,
446
+ validateSuite: runner.validateMcpSuite,
447
+ connect: core.connectStdio,
448
+ startRunner: runner.runSuite,
449
+ finalize: runner.finalizeRunnerExecution,
450
+ renderReport: runner.renderReport,
451
+ renderJUnit: runner.renderJUnit,
452
+ writeFile: (path, text) => writeFile(path, text, "utf8"),
453
+ colorEnabled: process.stdout.isTTY === true && process.env.NO_COLOR === void 0,
454
+ writeStdout: (text) => process.stdout.write(text),
455
+ writeStderr: (text) => process.stderr.write(text)
456
+ });
457
+ }
458
+ //#endregion
459
+ export { unavailableReplayDependencies as i, nodeRepairDependencies as n, run as r, COMMANDS as t };