@jmoak/vibrissa 0.1.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.
package/dist/cli.js ADDED
@@ -0,0 +1,726 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ ConsoleResultsDisplay,
4
+ McpSession,
5
+ PrettyConsoleResultsDisplay,
6
+ Runner,
7
+ defaultRunCasesOptions,
8
+ resolveRootDir,
9
+ resolveServerConfig
10
+ } from "./chunk-FTVETYUK.js";
11
+
12
+ // src/cli/args.ts
13
+ import path from "path";
14
+ function takeFlag(argv2, name) {
15
+ const index = argv2.indexOf(name);
16
+ if (index === -1) return void 0;
17
+ return argv2[index + 1];
18
+ }
19
+ function hasFlag(argv2, name) {
20
+ return argv2.includes(name);
21
+ }
22
+ function takeAllFlags(argv2, name) {
23
+ const values = [];
24
+ for (let i = 0; i < argv2.length; i++) {
25
+ if (argv2[i] === name && argv2[i + 1] !== void 0) {
26
+ values.push(argv2[i + 1]);
27
+ i++;
28
+ }
29
+ }
30
+ return values;
31
+ }
32
+ function splitCommandLine(input) {
33
+ const tokens = [];
34
+ let current = "";
35
+ let quote = null;
36
+ for (let i = 0; i < input.length; i++) {
37
+ const ch = input[i];
38
+ if (quote) {
39
+ if (ch === quote) quote = null;
40
+ else current += ch;
41
+ continue;
42
+ }
43
+ if (ch === '"' || ch === "'") {
44
+ quote = ch;
45
+ continue;
46
+ }
47
+ if (/\s/.test(ch)) {
48
+ if (current) {
49
+ tokens.push(current);
50
+ current = "";
51
+ }
52
+ continue;
53
+ }
54
+ current += ch;
55
+ }
56
+ if (current) tokens.push(current);
57
+ if (tokens.length === 0) throw new Error("Empty --server command");
58
+ return { cmd: tokens[0], args: tokens.slice(1) };
59
+ }
60
+ function parseEnvPairs(pairs) {
61
+ const env = {};
62
+ for (const pair of pairs) {
63
+ const eq = pair.indexOf("=");
64
+ if (eq <= 0) throw new Error(`Invalid --env value "${pair}" (expected KEY=VALUE)`);
65
+ env[pair.slice(0, eq)] = pair.slice(eq + 1);
66
+ }
67
+ return env;
68
+ }
69
+ function parseArgValue(raw) {
70
+ if (raw === "true") return true;
71
+ if (raw === "false") return false;
72
+ if (raw === "null") return null;
73
+ if (/^-?\d+(\.\d+)?$/.test(raw)) return Number(raw);
74
+ if (raw.startsWith("{") && raw.endsWith("}") || raw.startsWith("[") && raw.endsWith("]") || raw.startsWith('"') && raw.endsWith('"')) {
75
+ try {
76
+ return JSON.parse(raw);
77
+ } catch {
78
+ return raw;
79
+ }
80
+ }
81
+ return raw;
82
+ }
83
+ function parseToolArgs(argv2) {
84
+ const json = takeFlag(argv2, "--args");
85
+ const pairs = takeAllFlags(argv2, "--arg");
86
+ let args = {};
87
+ if (json !== void 0) {
88
+ let parsed;
89
+ try {
90
+ parsed = JSON.parse(json);
91
+ } catch (err) {
92
+ throw new Error(`Invalid --args JSON: ${err instanceof Error ? err.message : String(err)}`);
93
+ }
94
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
95
+ throw new Error("--args must be a JSON object");
96
+ }
97
+ args = { ...parsed };
98
+ }
99
+ for (const pair of pairs) {
100
+ const eq = pair.indexOf("=");
101
+ if (eq <= 0) throw new Error(`Invalid --arg value "${pair}" (expected key=value)`);
102
+ args[pair.slice(0, eq)] = parseArgValue(pair.slice(eq + 1));
103
+ }
104
+ return args;
105
+ }
106
+ function resolvePathFromCwd(value) {
107
+ return path.isAbsolute(value) ? value : path.resolve(process.cwd(), value);
108
+ }
109
+ function printJson(value) {
110
+ console.log(JSON.stringify(value, null, 2));
111
+ }
112
+
113
+ // src/cli/format.ts
114
+ var RESET = "\x1B[0m";
115
+ var BOLD = "\x1B[1m";
116
+ var DIM = "\x1B[2m";
117
+ var FG_CYAN = "\x1B[36m";
118
+ var FG_GRAY = "\x1B[90m";
119
+ var FG_GREEN = "\x1B[32m";
120
+ var FG_RED = "\x1B[31m";
121
+ var FG_YELLOW = "\x1B[33m";
122
+ function color(text, code2) {
123
+ return `${code2}${text}${RESET}`;
124
+ }
125
+ function formatToolList(tools) {
126
+ if (tools.length === 0) return "No tools registered on this server.";
127
+ const lines = [
128
+ color(`${tools.length} tool${tools.length === 1 ? "" : "s"}`, BOLD + FG_CYAN),
129
+ ""
130
+ ];
131
+ for (const tool of tools) {
132
+ lines.push(`${color("\u25B8", FG_GREEN)} ${color(tool.name, BOLD + FG_CYAN)}`);
133
+ if (tool.description) lines.push(` ${color(tool.description, FG_GRAY)}`);
134
+ const schema = tool.inputSchema;
135
+ if (schema && typeof schema === "object" && !Array.isArray(schema)) {
136
+ const props = typeof schema.properties === "object" && schema.properties !== null ? Object.keys(schema.properties) : [];
137
+ const required = Array.isArray(schema.required) ? schema.required.filter((v) => typeof v === "string") : [];
138
+ if (props.length > 0) {
139
+ const rendered = props.map((name) => required.includes(name) ? `${name}*` : name).join(", ");
140
+ lines.push(` ${color(`args: ${rendered}`, DIM)}`);
141
+ }
142
+ }
143
+ lines.push("");
144
+ }
145
+ return lines.join("\n").trimEnd();
146
+ }
147
+ function formatCallHeader(tool, args) {
148
+ return `${color("call", FG_CYAN)} ${color(tool, BOLD)} ${color(JSON.stringify(args), FG_GRAY)}`;
149
+ }
150
+ function formatSuccessBanner(text) {
151
+ return color(text, FG_GREEN);
152
+ }
153
+ function formatErrorBanner(text) {
154
+ return color(text, FG_RED);
155
+ }
156
+ function formatWarnBanner(text) {
157
+ return color(text, FG_YELLOW);
158
+ }
159
+
160
+ // src/cli/resolve.ts
161
+ import path3 from "path";
162
+
163
+ // src/config.ts
164
+ import fs from "fs";
165
+ import path2 from "path";
166
+ function stripJsonComments(input) {
167
+ let result = "";
168
+ let inString = false;
169
+ let stringQuote = null;
170
+ let escaped = false;
171
+ let inBlockComment = false;
172
+ let inLineComment = false;
173
+ for (let i = 0; i < input.length; i++) {
174
+ const char = input[i];
175
+ const next = i + 1 < input.length ? input[i + 1] : "";
176
+ if (inLineComment) {
177
+ if (char === "\n") {
178
+ inLineComment = false;
179
+ result += char;
180
+ }
181
+ continue;
182
+ }
183
+ if (inBlockComment) {
184
+ if (char === "*" && next === "/") {
185
+ inBlockComment = false;
186
+ i++;
187
+ }
188
+ continue;
189
+ }
190
+ if (!inString && char === "/" && next === "*") {
191
+ inBlockComment = true;
192
+ i++;
193
+ continue;
194
+ }
195
+ if (!inString && char === "/" && next === "/") {
196
+ inLineComment = true;
197
+ i++;
198
+ continue;
199
+ }
200
+ if (inString) {
201
+ result += char;
202
+ if (!escaped && char === stringQuote) {
203
+ inString = false;
204
+ stringQuote = null;
205
+ }
206
+ escaped = !escaped && char === "\\";
207
+ continue;
208
+ }
209
+ if (char === '"' || char === "'" || char === "`") {
210
+ inString = true;
211
+ stringQuote = char;
212
+ result += char;
213
+ escaped = false;
214
+ continue;
215
+ }
216
+ result += char;
217
+ }
218
+ return result;
219
+ }
220
+ function loadConfigObjectFromPath(configPath) {
221
+ const raw = fs.readFileSync(configPath, "utf8");
222
+ const json = JSON.parse(stripJsonComments(raw));
223
+ return json;
224
+ }
225
+ function loadConfigFile(cwd, explicitPath) {
226
+ if (explicitPath) {
227
+ const abs = path2.isAbsolute(explicitPath) ? explicitPath : path2.join(cwd, explicitPath);
228
+ if (!fs.existsSync(abs)) throw new Error(`Config not found: ${abs}`);
229
+ return {
230
+ config: loadConfigObjectFromPath(abs),
231
+ rootDir: path2.dirname(abs),
232
+ configPath: abs
233
+ };
234
+ }
235
+ const candidates = ["vibrissa.json", "vibrissa.jsonc"];
236
+ for (const name of candidates) {
237
+ const file = path2.join(cwd, name);
238
+ if (fs.existsSync(file)) {
239
+ return {
240
+ config: loadConfigObjectFromPath(file),
241
+ rootDir: path2.dirname(file),
242
+ configPath: file
243
+ };
244
+ }
245
+ }
246
+ const pkgPath = path2.join(cwd, "package.json");
247
+ if (fs.existsSync(pkgPath)) {
248
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
249
+ if (pkg?.vibrissa && typeof pkg.vibrissa === "object") {
250
+ return {
251
+ config: pkg.vibrissa,
252
+ rootDir: path2.dirname(pkgPath),
253
+ configPath: pkgPath
254
+ };
255
+ }
256
+ }
257
+ return void 0;
258
+ }
259
+ function resolveOptions(cwd, explicitPath) {
260
+ const loaded = loadConfigFile(cwd, explicitPath);
261
+ const fileConfig = loaded?.config;
262
+ const rootDir = loaded?.rootDir ?? path2.resolve(cwd);
263
+ const resolved = {
264
+ ...defaultRunCasesOptions,
265
+ ...fileConfig,
266
+ server: {
267
+ ...defaultRunCasesOptions.server,
268
+ ...fileConfig?.server ?? {}
269
+ },
270
+ rootDir
271
+ };
272
+ return resolved;
273
+ }
274
+
275
+ // src/cli/resolve.ts
276
+ function parseCommonFlags(argv2) {
277
+ const configPath = takeFlag(argv2, "--config");
278
+ const serverRaw = takeFlag(argv2, "--server");
279
+ const serverCwd = takeFlag(argv2, "--server-cwd");
280
+ const timeoutRaw = takeFlag(argv2, "--timeout");
281
+ const env = parseEnvPairs(takeAllFlags(argv2, "--env"));
282
+ let serverCmd;
283
+ let serverArgs;
284
+ if (serverRaw !== void 0) {
285
+ const split = splitCommandLine(serverRaw);
286
+ serverCmd = split.cmd;
287
+ serverArgs = split.args;
288
+ }
289
+ return {
290
+ configPath,
291
+ serverCmd,
292
+ serverArgs,
293
+ serverCwd,
294
+ env: Object.keys(env).length > 0 ? env : void 0,
295
+ timeoutMs: timeoutRaw !== void 0 && Number.isFinite(Number(timeoutRaw)) ? Number(timeoutRaw) : void 0,
296
+ json: hasFlag(argv2, "--json")
297
+ };
298
+ }
299
+ function mergeCliOptions(flags, overrides = {}) {
300
+ const resolved = resolveOptions(process.cwd(), flags.configPath);
301
+ const serverCwd = flags.serverCwd ? path3.isAbsolute(flags.serverCwd) ? flags.serverCwd : resolvePathFromCwd(flags.serverCwd) : void 0;
302
+ return {
303
+ ...resolved,
304
+ ...overrides,
305
+ server: {
306
+ ...resolved.server,
307
+ ...overrides.server ?? {},
308
+ ...flags.serverCmd ? { cmd: flags.serverCmd } : {},
309
+ ...flags.serverArgs ? { args: flags.serverArgs } : {},
310
+ ...serverCwd ? { cwd: serverCwd } : {},
311
+ env: {
312
+ ...resolved.server.env ?? {},
313
+ ...overrides.server?.env ?? {},
314
+ ...flags.env ?? {}
315
+ }
316
+ },
317
+ ...typeof flags.timeoutMs === "number" ? { timeoutMs: flags.timeoutMs } : {}
318
+ };
319
+ }
320
+ function openSessionOptions(flags) {
321
+ const options = mergeCliOptions(flags);
322
+ return {
323
+ options,
324
+ server: resolveServerConfig(options),
325
+ rootDir: resolveRootDir(options),
326
+ timeoutMs: options.timeoutMs
327
+ };
328
+ }
329
+
330
+ // src/cli/call.ts
331
+ async function callCommand(argv2) {
332
+ const tool = argv2[0] && !argv2[0].startsWith("--") ? argv2[0] : void 0;
333
+ if (!tool) {
334
+ console.error(`Usage: vib-test call <tool> [--args '{"key":"value"}'] [--arg key=value]`);
335
+ return 1;
336
+ }
337
+ const flags = parseCommonFlags(argv2.slice(1));
338
+ const args = parseToolArgs(argv2.slice(1));
339
+ const { server, timeoutMs } = openSessionOptions(flags);
340
+ const spawnTimeoutMs = Math.max(2e3, Math.min(1e4, timeoutMs));
341
+ const session = await McpSession.start(server, spawnTimeoutMs);
342
+ try {
343
+ if (!flags.json) console.log(formatCallHeader(tool, args));
344
+ const outcome = await session.callTool(tool, args, timeoutMs);
345
+ if (outcome.kind === "error") {
346
+ if (flags.json) {
347
+ printJson({ ok: false, error: outcome.error });
348
+ } else {
349
+ const label = outcome.error.codeName ? ` [${outcome.error.codeName}]` : "";
350
+ console.error(
351
+ formatErrorBanner(`error${label} (${outcome.error.kind}): ${outcome.error.message}`)
352
+ );
353
+ }
354
+ return 1;
355
+ }
356
+ if (flags.json) printJson({ ok: true, result: outcome.result });
357
+ else {
358
+ console.log(formatSuccessBanner("ok"));
359
+ printJson(outcome.result);
360
+ }
361
+ return 0;
362
+ } finally {
363
+ await session.close();
364
+ }
365
+ }
366
+
367
+ // src/cli/init.ts
368
+ import fs2 from "fs";
369
+ import path4 from "path";
370
+ function writeFile(pathName, contents, force) {
371
+ if (fs2.existsSync(pathName) && !force) return "skipped";
372
+ fs2.mkdirSync(path4.dirname(pathName), { recursive: true });
373
+ fs2.writeFileSync(pathName, contents, "utf8");
374
+ return "wrote";
375
+ }
376
+ function detectServerCommand(dir) {
377
+ const distCandidates = ["dist/index.js", "build/index.js", "src/index.js"];
378
+ for (const candidate of distCandidates) {
379
+ if (fs2.existsSync(path4.join(dir, candidate))) {
380
+ return { cmd: "node", args: [candidate] };
381
+ }
382
+ }
383
+ return { cmd: "node", args: ["dist/index.js"] };
384
+ }
385
+ function schemaRef(fromFile, rootDir, schemaName) {
386
+ const local = path4.join(rootDir, "node_modules", "vibrissa", "schema", schemaName);
387
+ if (fs2.existsSync(local)) {
388
+ return path4.relative(path4.dirname(fromFile), local).replace(/\\/g, "/");
389
+ }
390
+ return `https://unpkg.com/vibrissa/schema/${schemaName}`;
391
+ }
392
+ async function initCommand(argv2) {
393
+ const flags = parseCommonFlags(argv2);
394
+ const dir = resolvePathFromCwd(takeFlag(argv2, "--dir") ?? ".");
395
+ const force = hasFlag(argv2, "--force");
396
+ const serverOverride = flags.serverCmd ? { cmd: flags.serverCmd, args: flags.serverArgs ?? [] } : detectServerCommand(dir);
397
+ const configPath = path4.join(dir, "vibrissa.json");
398
+ const casesDir = path4.join(dir, "tests", "integration");
399
+ const samplePath = path4.join(casesDir, "echo.basic.json");
400
+ const config = {
401
+ $schema: schemaRef(configPath, dir, "vibrissa.schema.json"),
402
+ server: {
403
+ cmd: serverOverride.cmd,
404
+ args: serverOverride.args,
405
+ cwd: "."
406
+ },
407
+ globs: ["tests/integration/**/*.json"],
408
+ timeoutMs: 15e3,
409
+ failFast: false
410
+ };
411
+ const sample = {
412
+ $schema: schemaRef(samplePath, dir, "case.schema.json"),
413
+ name: "echo basic",
414
+ tool: "echo",
415
+ args: { text: "hello", uppercase: true },
416
+ expect: {
417
+ content: [{ type: "text", text: "HELLO" }]
418
+ }
419
+ };
420
+ const configStatus = writeFile(configPath, `${JSON.stringify(config, null, 2)}
421
+ `, force);
422
+ fs2.mkdirSync(casesDir, { recursive: true });
423
+ const sampleStatus = writeFile(samplePath, `${JSON.stringify(sample, null, 2)}
424
+ `, force);
425
+ const pkgPath = path4.join(dir, "package.json");
426
+ let pkgNote = "no package.json found (skipped script)";
427
+ if (fs2.existsSync(pkgPath)) {
428
+ const pkg = JSON.parse(fs2.readFileSync(pkgPath, "utf8"));
429
+ pkg.scripts ??= {};
430
+ if (!pkg.scripts["test:integration"] || force) {
431
+ pkg.scripts["test:integration"] = "vib-test run";
432
+ fs2.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}
433
+ `, "utf8");
434
+ pkgNote = "added scripts.test:integration";
435
+ } else {
436
+ pkgNote = "scripts.test:integration already present";
437
+ }
438
+ }
439
+ console.log(formatSuccessBanner("vibrissa init"));
440
+ console.log(` config: ${configStatus.padEnd(7)} ${configPath}`);
441
+ console.log(` sample: ${sampleStatus.padEnd(7)} ${samplePath}`);
442
+ console.log(` package: ${pkgNote}`);
443
+ console.log("");
444
+ console.log("Next:");
445
+ console.log(" 1. vib-test tools");
446
+ console.log(" 2. vib-test call <tool> --arg key=value");
447
+ console.log(" 3. vib-test record <tool> --arg key=value");
448
+ console.log(" 4. vib-test run");
449
+ console.log(" 5. vib-test inspect # open MCP Inspector with this server");
450
+ if (configStatus === "skipped" || sampleStatus === "skipped") {
451
+ console.log(
452
+ formatWarnBanner("Some files already existed; re-run with --force to replace them.")
453
+ );
454
+ }
455
+ return 0;
456
+ }
457
+
458
+ // src/cli/inspect.ts
459
+ import { spawn } from "child_process";
460
+ async function inspectCommand(argv2) {
461
+ const dash = argv2.indexOf("--");
462
+ const ownArgs = dash === -1 ? argv2 : argv2.slice(0, dash);
463
+ const passthrough = dash === -1 ? [] : argv2.slice(dash + 1);
464
+ const flags = parseCommonFlags(ownArgs);
465
+ const { server } = openSessionOptions(flags);
466
+ const cliMode = hasFlag(ownArgs, "--cli") || passthrough[0] === "--cli";
467
+ const rest2 = passthrough[0] === "--cli" ? passthrough.slice(1) : passthrough;
468
+ const inspectorArgs = [
469
+ "--yes",
470
+ "@modelcontextprotocol/inspector",
471
+ ...cliMode ? ["--cli"] : [],
472
+ server.cmd,
473
+ ...server.args ?? [],
474
+ ...rest2
475
+ ];
476
+ console.log(
477
+ formatSuccessBanner(
478
+ `Opening MCP Inspector for: ${server.cmd} ${(server.args ?? []).join(" ")}`.trim()
479
+ )
480
+ );
481
+ console.log(
482
+ formatWarnBanner(
483
+ "Inspector = visual exploration \xB7 Vibrissa tools/call/record/run = the regression loop"
484
+ )
485
+ );
486
+ const child = spawn("npx", inspectorArgs, {
487
+ cwd: server.cwd ?? process.cwd(),
488
+ env: { ...process.env, ...server.env ?? {} },
489
+ stdio: "inherit",
490
+ shell: process.platform === "win32"
491
+ });
492
+ return await new Promise((resolve) => {
493
+ child.on("error", (err) => {
494
+ console.error(`Failed to launch MCP Inspector: ${err.message}`);
495
+ resolve(1);
496
+ });
497
+ child.on("exit", (code2, signal) => {
498
+ if (signal) {
499
+ resolve(1);
500
+ return;
501
+ }
502
+ resolve(code2 ?? 1);
503
+ });
504
+ });
505
+ }
506
+
507
+ // src/cli/record.ts
508
+ import fs3 from "fs";
509
+ import path5 from "path";
510
+ function slugify(input) {
511
+ return input.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48);
512
+ }
513
+ function defaultOutPath(rootDir, tool, name) {
514
+ const dir = path5.join(rootDir, "tests", "integration");
515
+ return path5.join(dir, `${slugify(name) || slugify(tool) || "case"}.json`);
516
+ }
517
+ function caseSchemaRef(outPath, rootDir) {
518
+ const localSchema = path5.join(rootDir, "node_modules", "vibrissa", "schema", "case.schema.json");
519
+ if (fs3.existsSync(localSchema)) {
520
+ return path5.relative(path5.dirname(outPath), localSchema).replace(/\\/g, "/");
521
+ }
522
+ return "https://unpkg.com/vibrissa/schema/case.schema.json";
523
+ }
524
+ async function recordCommand(argv2) {
525
+ const tool = argv2[0] && !argv2[0].startsWith("--") ? argv2[0] : void 0;
526
+ if (!tool) {
527
+ console.error(
528
+ `Usage: vib-test record <tool> [--args '{"key":"value"}'] [--out path] [--name "..."] [--partial]`
529
+ );
530
+ return 1;
531
+ }
532
+ const rest2 = argv2.slice(1);
533
+ const flags = parseCommonFlags(rest2);
534
+ const args = parseToolArgs(rest2);
535
+ const name = takeFlag(rest2, "--name") ?? `${tool} recorded`;
536
+ const outFlag = takeFlag(rest2, "--out");
537
+ const partial = hasFlag(rest2, "--partial");
538
+ const force = hasFlag(rest2, "--force");
539
+ const { server, rootDir, timeoutMs } = openSessionOptions(flags);
540
+ const outPath = outFlag ? resolvePathFromCwd(outFlag) : defaultOutPath(rootDir, tool, name);
541
+ if (fs3.existsSync(outPath) && !force) {
542
+ console.error(formatErrorBanner(`Refusing to overwrite ${outPath} (pass --force to replace)`));
543
+ return 1;
544
+ }
545
+ const spawnTimeoutMs = Math.max(2e3, Math.min(1e4, timeoutMs));
546
+ const session = await McpSession.start(server, spawnTimeoutMs);
547
+ try {
548
+ if (!flags.json) console.log(formatCallHeader(tool, args));
549
+ const outcome = await session.callTool(tool, args, timeoutMs);
550
+ const testCase = {
551
+ $schema: caseSchemaRef(outPath, rootDir),
552
+ name,
553
+ tool,
554
+ args
555
+ };
556
+ if (outcome.kind === "error") {
557
+ testCase.expectError = {
558
+ ...outcome.error.codeName ? { code: outcome.error.codeName } : {},
559
+ message: { $regex: escapeRegex(outcome.error.message.slice(0, 80)) }
560
+ };
561
+ if (!flags.json) {
562
+ console.log(
563
+ formatWarnBanner(
564
+ `Captured ${outcome.error.kind} error into expectError (${outcome.error.codeName ?? "tool"})`
565
+ )
566
+ );
567
+ }
568
+ } else {
569
+ const expect = outcome.result;
570
+ testCase.expect = partial ? { $partial: true, ...expect } : expect;
571
+ if (!flags.json) console.log(formatSuccessBanner("Captured result into expect"));
572
+ }
573
+ fs3.mkdirSync(path5.dirname(outPath), { recursive: true });
574
+ fs3.writeFileSync(outPath, `${JSON.stringify(testCase, null, 2)}
575
+ `, "utf8");
576
+ if (flags.json) printJson({ ok: true, path: outPath, case: testCase });
577
+ else console.log(`${formatSuccessBanner("wrote")} ${outPath}`);
578
+ return 0;
579
+ } finally {
580
+ await session.close();
581
+ }
582
+ }
583
+ function escapeRegex(input) {
584
+ return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
585
+ }
586
+
587
+ // src/cli/run.ts
588
+ import path6 from "path";
589
+ async function runCommand(argv2) {
590
+ const flags = parseCommonFlags(argv2);
591
+ const casesGlob = takeFlag(argv2, "--cases");
592
+ const failFast = hasFlag(argv2, "--fail-fast");
593
+ const allowEmpty = hasFlag(argv2, "--allow-empty");
594
+ let display;
595
+ if (hasFlag(argv2, "--pretty")) display = "pretty";
596
+ if (hasFlag(argv2, "--no-pretty")) display = "plain";
597
+ const displayFlag = takeFlag(argv2, "--display");
598
+ if (displayFlag === "plain" || displayFlag === "pretty") display = displayFlag;
599
+ const options = mergeCliOptions(flags, {
600
+ ...display ? { display } : {},
601
+ ...casesGlob ? {
602
+ globs: [
603
+ path6.isAbsolute(casesGlob) ? casesGlob : resolvePathFromCwd(casesGlob).replace(/\\/g, "/")
604
+ ]
605
+ } : {},
606
+ ...failFast ? { failFast: true } : {},
607
+ ...allowEmpty ? { allowEmpty: true } : {}
608
+ });
609
+ const runner = new Runner(
610
+ options,
611
+ options.display === "pretty" ? new PrettyConsoleResultsDisplay() : new ConsoleResultsDisplay()
612
+ );
613
+ return runner.run();
614
+ }
615
+
616
+ // src/cli/tools.ts
617
+ async function toolsCommand(argv2) {
618
+ const flags = parseCommonFlags(argv2);
619
+ const { server, timeoutMs } = openSessionOptions(flags);
620
+ const spawnTimeoutMs = Math.max(2e3, Math.min(1e4, timeoutMs));
621
+ const session = await McpSession.start(server, spawnTimeoutMs);
622
+ try {
623
+ const tools = await session.listTools();
624
+ if (flags.json) printJson(tools);
625
+ else console.log(formatToolList(tools));
626
+ return 0;
627
+ } finally {
628
+ await session.close();
629
+ }
630
+ }
631
+
632
+ // src/cli.ts
633
+ var COMMANDS = /* @__PURE__ */ new Set(["run", "tools", "call", "record", "init", "inspect"]);
634
+ function printHelp() {
635
+ console.log(
636
+ [
637
+ "vib-test \u2014 JSON-driven MCP integration runner + authoring loop",
638
+ "",
639
+ "Usage:",
640
+ " vib-test run [options] Run JSON cases (default)",
641
+ " vib-test tools [options] List tools on the server",
642
+ " vib-test call <tool> [options] Call a tool and print the result",
643
+ " vib-test record <tool> [options] Call a tool and write a case file",
644
+ " vib-test init [options] Scaffold vibrissa.json + sample case",
645
+ " vib-test inspect [options] [-- ...] Open MCP Inspector with this server",
646
+ "",
647
+ "Shared options:",
648
+ " --config <path> vibrissa.json / .jsonc / package.json#vibrissa",
649
+ ' --server "<cmd ...>" Override server command',
650
+ " --server-cwd <dir> Working directory for the server process",
651
+ " --env KEY=VALUE Extra env var (repeatable)",
652
+ " --timeout <ms> Per-call / spawn timeout",
653
+ " --json Machine-readable output (tools/call/record)",
654
+ "",
655
+ "run options:",
656
+ " --cases <glob> Case glob (relative to shell cwd)",
657
+ " --fail-fast Stop on first failure",
658
+ " --allow-empty Exit 0 when no cases match",
659
+ " --pretty / --no-pretty",
660
+ "",
661
+ "call / record options:",
662
+ " --args '{...}' JSON object of tool arguments",
663
+ " --arg key=value Tool argument (repeatable; JSON-ish values ok)",
664
+ "",
665
+ "record options:",
666
+ " --out <path> Case file path (default: tests/integration/<name>.json)",
667
+ " --name <string> Case name",
668
+ ' --partial Wrap expect in "$partial": true',
669
+ " --force Overwrite an existing case file",
670
+ "",
671
+ "init options:",
672
+ " --dir <path> Target directory (default: .)",
673
+ " --force Overwrite existing scaffold files",
674
+ "",
675
+ "inspect options:",
676
+ " --cli Use Inspector CLI mode",
677
+ " -- ... Extra args forwarded after the server command",
678
+ "",
679
+ "Workflow:",
680
+ " init \u2192 tools \u2192 call \u2192 record \u2192 run \u2192 inspect (when you want the UI)"
681
+ ].join("\n")
682
+ );
683
+ }
684
+ var argv = process.argv.slice(2);
685
+ if (argv.includes("-h") || argv.includes("--help")) {
686
+ printHelp();
687
+ process.exit(0);
688
+ }
689
+ var command = "run";
690
+ var rest = argv;
691
+ if (argv[0] && COMMANDS.has(argv[0])) {
692
+ command = argv[0];
693
+ rest = argv.slice(1);
694
+ }
695
+ var code = 1;
696
+ try {
697
+ switch (command) {
698
+ case "run":
699
+ code = await runCommand(rest);
700
+ break;
701
+ case "tools":
702
+ code = await toolsCommand(rest);
703
+ break;
704
+ case "call":
705
+ code = await callCommand(rest);
706
+ break;
707
+ case "record":
708
+ code = await recordCommand(rest);
709
+ break;
710
+ case "init":
711
+ code = await initCommand(rest);
712
+ break;
713
+ case "inspect":
714
+ code = await inspectCommand(rest);
715
+ break;
716
+ default:
717
+ printHelp();
718
+ code = 1;
719
+ }
720
+ } catch (err) {
721
+ const message = err instanceof Error ? err.message : String(err);
722
+ console.error(message);
723
+ code = 1;
724
+ }
725
+ process.exit(code);
726
+ //# sourceMappingURL=cli.js.map