@vexyo/cli 0.1.0 → 0.1.1
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/api.d.ts +2 -1
- package/dist/api.js +1 -1
- package/dist/{chunk-254CIBOC.js → chunk-HMGCKVG7.js} +48 -6
- package/dist/chunk-HMGCKVG7.js.map +1 -0
- package/dist/index.js +104 -5
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/dist/chunk-254CIBOC.js.map +0 -1
package/dist/api.d.ts
CHANGED
package/dist/api.js
CHANGED
|
@@ -12,11 +12,20 @@ import {
|
|
|
12
12
|
} from "@vexyo/reporters";
|
|
13
13
|
|
|
14
14
|
// src/config.ts
|
|
15
|
+
import { existsSync } from "fs";
|
|
16
|
+
import { createRequire } from "module";
|
|
15
17
|
import { resolve } from "path";
|
|
16
18
|
import { pathToFileURL } from "url";
|
|
17
19
|
import { createJiti } from "jiti";
|
|
18
20
|
import { z } from "zod";
|
|
19
21
|
var SPEC_VERSIONS = ["2025-11-25", "2026-07-28"];
|
|
22
|
+
var DEFAULT_SPEC_VERSION = SPEC_VERSIONS[0];
|
|
23
|
+
var CONFIG_CANDIDATES = [
|
|
24
|
+
"vexyo.config.ts",
|
|
25
|
+
"vexyo.config.mts",
|
|
26
|
+
"vexyo.config.js",
|
|
27
|
+
"vexyo.config.mjs"
|
|
28
|
+
];
|
|
20
29
|
var stdioTargetSchema = z.object({
|
|
21
30
|
transport: z.literal("stdio"),
|
|
22
31
|
command: z.string().min(1),
|
|
@@ -68,14 +77,44 @@ var ConfigError = class extends Error {
|
|
|
68
77
|
this.name = "ConfigError";
|
|
69
78
|
}
|
|
70
79
|
};
|
|
80
|
+
function resolveConfigPath(explicit, cwd = process.cwd()) {
|
|
81
|
+
if (explicit) {
|
|
82
|
+
return resolve(cwd, explicit);
|
|
83
|
+
}
|
|
84
|
+
for (const name of CONFIG_CANDIDATES) {
|
|
85
|
+
const candidate = resolve(cwd, name);
|
|
86
|
+
if (existsSync(candidate)) {
|
|
87
|
+
return candidate;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
throw new ConfigError(
|
|
91
|
+
`No config file found in ${cwd}. Create one with \`vexyo init\`, or pass --config <path>.`
|
|
92
|
+
);
|
|
93
|
+
}
|
|
71
94
|
function formatZodError(error) {
|
|
72
95
|
return error.issues.map((issue) => ` - ${issue.path.join(".") || "(root)"}: ${issue.message}`).join("\n");
|
|
73
96
|
}
|
|
97
|
+
function selfPackageAliases() {
|
|
98
|
+
const aliases = {};
|
|
99
|
+
let req;
|
|
100
|
+
try {
|
|
101
|
+
req = createRequire(import.meta.url);
|
|
102
|
+
} catch {
|
|
103
|
+
return aliases;
|
|
104
|
+
}
|
|
105
|
+
for (const spec of ["@vexyo/cli/config", "@vexyo/cli", "@vexyo/core", "@vexyo/reporters"]) {
|
|
106
|
+
try {
|
|
107
|
+
aliases[spec] = req.resolve(spec);
|
|
108
|
+
} catch {
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return aliases;
|
|
112
|
+
}
|
|
74
113
|
async function loadConfig(configPath) {
|
|
75
114
|
const absolute = resolve(process.cwd(), configPath);
|
|
76
115
|
let mod;
|
|
77
116
|
try {
|
|
78
|
-
const jiti = createJiti(pathToFileURL(absolute).href);
|
|
117
|
+
const jiti = createJiti(pathToFileURL(absolute).href, { alias: selfPackageAliases() });
|
|
79
118
|
mod = await jiti.import(absolute, { default: true });
|
|
80
119
|
} catch (err) {
|
|
81
120
|
throw new ConfigError(
|
|
@@ -141,10 +180,11 @@ var REPORTERS = {
|
|
|
141
180
|
junit: junitReporter
|
|
142
181
|
};
|
|
143
182
|
async function executeRun(opts) {
|
|
144
|
-
const
|
|
183
|
+
const configPath = resolveConfigPath(opts.config);
|
|
184
|
+
const config = await loadConfig(configPath);
|
|
145
185
|
const specVersion = resolveSpecVersion(opts.specVersion, config);
|
|
146
186
|
const failOn = resolveFailOn(opts.failOn, config);
|
|
147
|
-
const extraChecks = await buildRegressionPhase(opts, config, specVersion);
|
|
187
|
+
const extraChecks = await buildRegressionPhase(opts, configPath, config, specVersion);
|
|
148
188
|
return runSuite({
|
|
149
189
|
specVersion,
|
|
150
190
|
target: buildTarget(config),
|
|
@@ -183,20 +223,22 @@ function resolveSpecVersion(override, config) {
|
|
|
183
223
|
function resolveFailOn(override, config) {
|
|
184
224
|
return override ?? config.regression?.failOn ?? "error";
|
|
185
225
|
}
|
|
186
|
-
async function buildRegressionPhase(opts, config, specVersion) {
|
|
226
|
+
async function buildRegressionPhase(opts, configPath, config, specVersion) {
|
|
187
227
|
if (!opts.regression) {
|
|
188
228
|
return void 0;
|
|
189
229
|
}
|
|
190
230
|
if (!config.regression) {
|
|
191
231
|
throw new ConfigError("`--regression` requires a `regression` block in the config.");
|
|
192
232
|
}
|
|
193
|
-
const goldenSet = await readGoldenSet(resolveGoldenDir(
|
|
233
|
+
const goldenSet = await readGoldenSet(resolveGoldenDir(configPath, config.regression));
|
|
194
234
|
const goldenCfg = toGoldenConfig(specVersion, config.regression);
|
|
195
235
|
return (ctx) => runRegression(ctx.client, goldenSet, goldenCfg);
|
|
196
236
|
}
|
|
197
237
|
|
|
198
238
|
export {
|
|
239
|
+
DEFAULT_SPEC_VERSION,
|
|
199
240
|
ConfigError,
|
|
241
|
+
resolveConfigPath,
|
|
200
242
|
loadConfig,
|
|
201
243
|
reportHarnessError,
|
|
202
244
|
resolveGoldenDir,
|
|
@@ -205,4 +247,4 @@ export {
|
|
|
205
247
|
executeRun,
|
|
206
248
|
runCommand
|
|
207
249
|
};
|
|
208
|
-
//# sourceMappingURL=chunk-
|
|
250
|
+
//# sourceMappingURL=chunk-HMGCKVG7.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/run.ts","../src/config.ts","../src/errors.ts","../src/regression.ts","../src/target.ts"],"sourcesContent":["import {\n readGoldenSet,\n runRegression,\n runSuite,\n type RunResult,\n type RunSuiteOptions,\n type Severity,\n type SpecVersion,\n} from '@vexyo/core';\nimport {\n consoleReporter,\n jsonReporter,\n junitReporter,\n markdownReporter,\n type Reporter,\n} from '@vexyo/reporters';\nimport { ConfigError, loadConfig, resolveConfigPath, type Config } from '../config';\nimport { reportHarnessError } from '../errors';\nimport { resolveGoldenDir, toGoldenConfig } from '../regression';\nimport { buildTarget } from '../target';\n\nexport interface RunCommandOptions {\n /** Explicit config path; when omitted, auto-discovered in cwd. */\n config?: string;\n specVersion?: string;\n reporter: string;\n regression?: boolean;\n failOn?: string;\n}\n\nexport interface ExecuteRunOptions {\n /** Explicit config path; when omitted, auto-discovered in cwd. */\n config?: string;\n specVersion?: string;\n regression?: boolean;\n failOn?: string;\n}\n\nconst REPORTERS: Record<string, Reporter> = {\n console: consoleReporter,\n json: jsonReporter,\n markdown: markdownReporter,\n junit: junitReporter,\n};\n\n/**\n * Load config, connect over the configured transport, run the conformance suite\n * (plus regression when requested), and return the {@link RunResult}. Throws\n * `ConfigError`/`GoldenError`/connection errors for anything the caller should\n * map to exit code 2. Shared by the `run` command and the GitHub Action.\n */\nexport async function executeRun(opts: ExecuteRunOptions): Promise<RunResult> {\n const configPath = resolveConfigPath(opts.config);\n const config = await loadConfig(configPath);\n const specVersion = resolveSpecVersion(opts.specVersion, config);\n const failOn = resolveFailOn(opts.failOn, config);\n const extraChecks = await buildRegressionPhase(opts, configPath, config, specVersion);\n\n return runSuite({\n specVersion,\n target: buildTarget(config),\n failOn,\n extraChecks,\n });\n}\n\n/**\n * Execute the `run` command. Returns the process exit code:\n * 0 = pass, 1 = findings at/above threshold, 2 = harness/config error.\n */\nexport async function runCommand(opts: RunCommandOptions): Promise<0 | 1 | 2> {\n const reporter = REPORTERS[opts.reporter];\n if (!reporter) {\n return reportHarnessError(\n new ConfigError(\n `Unknown reporter \"${opts.reporter}\". Available reporters: ${Object.keys(REPORTERS).join(', ')}.`,\n ),\n );\n }\n\n let result: RunResult;\n try {\n result = await executeRun(opts);\n } catch (err) {\n return reportHarnessError(err);\n }\n\n process.stdout.write(`${reporter.format(result)}\\n`);\n return result.exitCode;\n}\n\nfunction resolveSpecVersion(override: string | undefined, config: Config): SpecVersion {\n const specVersion = (override as SpecVersion | undefined) ?? config.specVersion;\n if (specVersion !== '2025-11-25') {\n throw new ConfigError(\n `Spec version \"${specVersion}\" is not wired up in this build. Only 2025-11-25 is supported.`,\n );\n }\n return specVersion;\n}\n\nfunction resolveFailOn(override: string | undefined, config: Config): Severity {\n return (override as Severity | undefined) ?? config.regression?.failOn ?? 'error';\n}\n\nasync function buildRegressionPhase(\n opts: ExecuteRunOptions,\n configPath: string,\n config: Config,\n specVersion: SpecVersion,\n): Promise<RunSuiteOptions['extraChecks']> {\n if (!opts.regression) {\n return undefined;\n }\n if (!config.regression) {\n throw new ConfigError('`--regression` requires a `regression` block in the config.');\n }\n const goldenSet = await readGoldenSet(resolveGoldenDir(configPath, config.regression));\n const goldenCfg = toGoldenConfig(specVersion, config.regression);\n return (ctx) => runRegression(ctx.client, goldenSet, goldenCfg);\n}\n","import { existsSync } from 'node:fs';\nimport { createRequire } from 'node:module';\nimport { resolve } from 'node:path';\nimport { pathToFileURL } from 'node:url';\nimport { createJiti } from 'jiti';\nimport { z } from 'zod';\n\nexport const SPEC_VERSIONS = ['2025-11-25', '2026-07-28'] as const;\n\n/** The spec version a run targets when the config doesn't override it. */\nexport const DEFAULT_SPEC_VERSION = SPEC_VERSIONS[0];\n\n/** Config filenames auto-discovered in cwd, in resolution order. */\nexport const CONFIG_CANDIDATES = [\n 'vexyo.config.ts',\n 'vexyo.config.mts',\n 'vexyo.config.js',\n 'vexyo.config.mjs',\n] as const;\n\nconst stdioTargetSchema = z.object({\n transport: z.literal('stdio'),\n command: z.string().min(1),\n args: z.array(z.string()).optional(),\n cwd: z.string().optional(),\n env: z.record(z.string(), z.string()).optional(),\n});\n\nconst httpTargetSchema = z.object({\n transport: z.literal('http'),\n url: z.string().min(1),\n /** Extra HTTP headers sent on every request (e.g. auth). */\n headers: z.record(z.string(), z.string()).optional(),\n});\n\nconst targetSchema = z.discriminatedUnion('transport', [stdioTargetSchema, httpTargetSchema]);\n\n/** A normalizer is a builtin name or an inline `{ name, apply }` object. */\nconst normalizerRefSchema = z.union([\n z.string(),\n z.object({\n name: z.string(),\n apply: z.custom<(value: unknown) => unknown>((v) => typeof v === 'function'),\n }),\n]);\n\nconst recordCaseSchema = z.object({\n case: z.string().min(1),\n arguments: z.record(z.string(), z.unknown()).default({}),\n});\n\nconst recordToolSchema = z.object({\n cases: z.array(recordCaseSchema).min(1),\n normalizers: z.array(normalizerRefSchema).optional(),\n});\n\nconst regressionSchema = z.object({\n /** Committed golden directory (relative to the config file's cwd). */\n goldenDir: z.string().default('vexyo/goldens'),\n /** Severity at/above which drift fails the run. */\n failOn: z.enum(['error', 'warning']).default('error'),\n /** Default normalizers applied to every recorded tool. */\n normalizers: z.array(normalizerRefSchema).default([]),\n /** Tools to record/compare, keyed by name. Empty = record nothing (opt-in). */\n record: z.record(z.string(), recordToolSchema).default({}),\n});\n\nexport const configSchema = z.object({\n specVersion: z.enum(SPEC_VERSIONS).default('2025-11-25'),\n target: targetSchema,\n security: z.boolean().default(false),\n regression: regressionSchema.optional(),\n});\n\nexport type RegressionConfig = z.infer<typeof regressionSchema>;\n\n/** Validated, defaults-applied config. */\nexport type Config = z.infer<typeof configSchema>;\n/** Shape a user writes in a config file (before defaults are applied). */\nexport type UserConfig = z.input<typeof configSchema>;\nexport type StdioTarget = Extract<Config['target'], { transport: 'stdio' }>;\n\n/** Raised for anything the user must fix in their config or invocation (exit 2). */\nexport class ConfigError extends Error {\n constructor(message: string, cause?: unknown) {\n super(message, cause !== undefined ? { cause } : undefined);\n this.name = 'ConfigError';\n }\n}\n\n/**\n * Resolve which config file to load: an explicit `--config` (relative to cwd),\n * otherwise the first {@link CONFIG_CANDIDATES} present in cwd. Throws a\n * {@link ConfigError} suggesting `vexyo init` when nothing is found. Returns an\n * absolute path.\n */\nexport function resolveConfigPath(\n explicit: string | undefined,\n cwd: string = process.cwd(),\n): string {\n if (explicit) {\n return resolve(cwd, explicit);\n }\n for (const name of CONFIG_CANDIDATES) {\n const candidate = resolve(cwd, name);\n if (existsSync(candidate)) {\n return candidate;\n }\n }\n throw new ConfigError(\n `No config file found in ${cwd}. Create one with \\`vexyo init\\`, or pass --config <path>.`,\n );\n}\n\nfunction formatZodError(error: z.ZodError): string {\n return error.issues\n .map((issue) => ` - ${issue.path.join('.') || '(root)'}: ${issue.message}`)\n .join('\\n');\n}\n\n/**\n * Map vexyo's own package specifiers to the running CLI's installation so a\n * config that imports `@vexyo/cli/config` (defineConfig) loads even when it\n * lives in a directory without a local `node_modules` (a bare project, `/tmp`,\n * etc.). Resolved relative to THIS module — the CLI — not the config's location,\n * which is the pattern vite/eslint use for their own defineConfig imports. A\n * local install, when present, still wins (jiti prefers it over the alias).\n */\nfunction selfPackageAliases(): Record<string, string> {\n const aliases: Record<string, string> = {};\n let req: ReturnType<typeof createRequire>;\n try {\n // Empty `import.meta.url` (e.g. the Action's esbuild CJS bundle) → no alias;\n // there the CLI always runs where `@vexyo/cli` is already resolvable.\n req = createRequire(import.meta.url);\n } catch {\n return aliases;\n }\n for (const spec of ['@vexyo/cli/config', '@vexyo/cli', '@vexyo/core', '@vexyo/reporters']) {\n try {\n aliases[spec] = req.resolve(spec);\n } catch {\n // Unresolvable from here (e.g. dev without a built dist); jiti then\n // resolves the specifier from the config's own location instead.\n }\n }\n return aliases;\n}\n\n/**\n * Load and validate a config file. Supports `.ts`/`.js`/`.mjs` via jiti so the\n * blueprint's `examples/*.config.ts` work without a build step. The default\n * export is validated against {@link configSchema}.\n */\nexport async function loadConfig(configPath: string): Promise<Config> {\n const absolute = resolve(process.cwd(), configPath);\n\n let mod: unknown;\n try {\n const jiti = createJiti(pathToFileURL(absolute).href, { alias: selfPackageAliases() });\n mod = await jiti.import(absolute, { default: true });\n } catch (err) {\n throw new ConfigError(\n `Could not load config file at ${absolute}. Check the path and that the file has a default export.`,\n err,\n );\n }\n\n const parsed = configSchema.safeParse(mod);\n if (!parsed.success) {\n throw new ConfigError(`Invalid config at ${absolute}:\\n${formatZodError(parsed.error)}`);\n }\n return parsed.data;\n}\n","/** Print a harness/config error (with its cause, if any) and return exit code 2. */\nexport function reportHarnessError(err: unknown): 2 {\n const message = err instanceof Error ? err.message : String(err);\n process.stderr.write(`error: ${message}\\n`);\n const cause = err instanceof Error ? err.cause : undefined;\n if (cause !== undefined) {\n process.stderr.write(` cause: ${cause instanceof Error ? cause.message : String(cause)}\\n`);\n }\n return 2;\n}\n","import { dirname, resolve } from 'node:path';\nimport type { GoldenConfig } from '@vexyo/core';\nimport type { RegressionConfig } from './config';\n\n/** Resolve `goldenDir` relative to the config file's directory. */\nexport function resolveGoldenDir(configPath: string, regression: RegressionConfig): string {\n return resolve(dirname(resolve(process.cwd(), configPath)), regression.goldenDir);\n}\n\n/** Map the CLI's regression config onto the core engine's {@link GoldenConfig}. */\nexport function toGoldenConfig(specVersion: string, regression: RegressionConfig): GoldenConfig {\n return {\n specVersion,\n defaultNormalizers: regression.normalizers,\n tools: Object.fromEntries(\n Object.entries(regression.record).map(([name, spec]) => [\n name,\n { cases: spec.cases, normalizers: spec.normalizers },\n ]),\n ),\n };\n}\n","import type { ConnectTarget } from '@vexyo/core';\nimport type { Config } from './config';\n\n/** Map the validated config's target onto the core engine's {@link ConnectTarget}. */\nexport function buildTarget(config: Config): ConnectTarget {\n if (config.target.transport === 'http') {\n return {\n transport: 'http',\n http: { url: config.target.url, headers: config.target.headers },\n };\n }\n return { transport: 'stdio', stdio: config.target };\n}\n"],"mappings":"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","names":["resolve"]}
|
package/dist/index.js
CHANGED
|
@@ -1,24 +1,105 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
ConfigError,
|
|
4
|
+
DEFAULT_SPEC_VERSION,
|
|
4
5
|
buildTarget,
|
|
5
6
|
loadConfig,
|
|
6
7
|
reportHarnessError,
|
|
8
|
+
resolveConfigPath,
|
|
7
9
|
resolveGoldenDir,
|
|
8
10
|
runCommand,
|
|
9
11
|
toGoldenConfig
|
|
10
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-HMGCKVG7.js";
|
|
11
13
|
|
|
12
14
|
// src/index.ts
|
|
13
15
|
import { BRAND } from "@vexyo/core";
|
|
14
16
|
import { Command } from "commander";
|
|
15
17
|
|
|
18
|
+
// src/commands/init.ts
|
|
19
|
+
import { existsSync } from "fs";
|
|
20
|
+
import { writeFile } from "fs/promises";
|
|
21
|
+
import { resolve } from "path";
|
|
22
|
+
var CONFIG_FILENAME = "vexyo.config.ts";
|
|
23
|
+
function renderConfigTemplate(transport, specVersion) {
|
|
24
|
+
const activeTarget = transport === "http" ? ` target: {
|
|
25
|
+
transport: 'http',
|
|
26
|
+
url: 'http://127.0.0.1:3000/mcp',
|
|
27
|
+
// headers: { Authorization: 'Bearer <your-token>' },
|
|
28
|
+
},` : ` target: {
|
|
29
|
+
transport: 'stdio',
|
|
30
|
+
command: 'node',
|
|
31
|
+
args: ['path/to/your/mcp-server.js'],
|
|
32
|
+
// cwd: '.',
|
|
33
|
+
// env: { API_KEY: 'value' },
|
|
34
|
+
},`;
|
|
35
|
+
const alternatives = transport === "http" ? ` // --- Alternative targets (uncomment one, comment out the http target above) ---
|
|
36
|
+
//
|
|
37
|
+
// Node (stdio):
|
|
38
|
+
// target: { transport: 'stdio', command: 'node', args: ['path/to/your/mcp-server.js'] },
|
|
39
|
+
//
|
|
40
|
+
// Python / other runtimes (stdio):
|
|
41
|
+
// target: { transport: 'stdio', command: 'python', args: ['-m', 'your_mcp_server'] },` : ` // --- Alternative targets (uncomment one, comment out the stdio target above) ---
|
|
42
|
+
//
|
|
43
|
+
// Python / other runtimes (stdio):
|
|
44
|
+
// target: { transport: 'stdio', command: 'python', args: ['-m', 'your_mcp_server'] },
|
|
45
|
+
//
|
|
46
|
+
// Streamable HTTP (a running server):
|
|
47
|
+
// target: { transport: 'http', url: 'http://127.0.0.1:3000/mcp' },`;
|
|
48
|
+
return `import { defineConfig } from '@vexyo/cli/config';
|
|
49
|
+
|
|
50
|
+
export default defineConfig({
|
|
51
|
+
specVersion: '${specVersion}',
|
|
52
|
+
|
|
53
|
+
// The MCP server under test.
|
|
54
|
+
${activeTarget}
|
|
55
|
+
|
|
56
|
+
${alternatives}
|
|
57
|
+
|
|
58
|
+
// --- Regression testing (optional) ---
|
|
59
|
+
// Record golden tool outputs with \`vexyo record\`, then \`vexyo run --regression\`
|
|
60
|
+
// fails when behavior drifts.
|
|
61
|
+
// regression: {
|
|
62
|
+
// goldenDir: 'vexyo/goldens',
|
|
63
|
+
// normalizers: ['iso-timestamp', 'uuid'],
|
|
64
|
+
// record: {
|
|
65
|
+
// // greet: { cases: [{ case: 'basic', arguments: { name: 'world' } }] },
|
|
66
|
+
// },
|
|
67
|
+
// },
|
|
68
|
+
});
|
|
69
|
+
`;
|
|
70
|
+
}
|
|
71
|
+
async function initCommand(opts) {
|
|
72
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
73
|
+
const target = resolve(cwd, CONFIG_FILENAME);
|
|
74
|
+
if (existsSync(target) && !opts.force) {
|
|
75
|
+
return reportHarnessError(
|
|
76
|
+
new ConfigError(`${CONFIG_FILENAME} already exists. Pass --force to overwrite it.`)
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
await writeFile(target, renderConfigTemplate(opts.transport, DEFAULT_SPEC_VERSION));
|
|
81
|
+
} catch (err) {
|
|
82
|
+
return reportHarnessError(err);
|
|
83
|
+
}
|
|
84
|
+
process.stdout.write(
|
|
85
|
+
`Created ${CONFIG_FILENAME}.
|
|
86
|
+
|
|
87
|
+
Next steps:
|
|
88
|
+
1. Edit the \`target\` to point at your MCP server.
|
|
89
|
+
2. vexyo run --config ${CONFIG_FILENAME}
|
|
90
|
+
`
|
|
91
|
+
);
|
|
92
|
+
return 0;
|
|
93
|
+
}
|
|
94
|
+
|
|
16
95
|
// src/commands/record.ts
|
|
17
96
|
import { connectTarget, recordGoldens, writeGoldenSet } from "@vexyo/core";
|
|
18
97
|
async function recordCommand(opts) {
|
|
98
|
+
let configPath;
|
|
19
99
|
let config;
|
|
20
100
|
try {
|
|
21
|
-
|
|
101
|
+
configPath = resolveConfigPath(opts.config);
|
|
102
|
+
config = await loadConfig(configPath);
|
|
22
103
|
} catch (err) {
|
|
23
104
|
return reportHarnessError(err);
|
|
24
105
|
}
|
|
@@ -27,7 +108,7 @@ async function recordCommand(opts) {
|
|
|
27
108
|
new ConfigError("No `regression` block in the config \u2014 nothing to record.")
|
|
28
109
|
);
|
|
29
110
|
}
|
|
30
|
-
const goldenDir = resolveGoldenDir(
|
|
111
|
+
const goldenDir = resolveGoldenDir(configPath, config.regression);
|
|
31
112
|
const goldenCfg = toGoldenConfig(config.specVersion, config.regression);
|
|
32
113
|
const toolCount = Object.keys(goldenCfg.tools).length;
|
|
33
114
|
process.stdout.write(
|
|
@@ -60,7 +141,10 @@ async function recordCommand(opts) {
|
|
|
60
141
|
// src/index.ts
|
|
61
142
|
var program = new Command();
|
|
62
143
|
program.name(BRAND.bin).description("CI-native conformance harness for MCP servers");
|
|
63
|
-
program.command("run").description("Run the conformance suite (and optionally regression) against an MCP server").
|
|
144
|
+
program.command("run").description("Run the conformance suite (and optionally regression) against an MCP server").option(
|
|
145
|
+
"-c, --config <path>",
|
|
146
|
+
"path to the vexyo config file (auto-discovered in cwd if omitted)"
|
|
147
|
+
).option("-s, --spec-version <version>", "override the spec version to target").option("-r, --reporter <name>", "output reporter (console, json, markdown)", "console").option("--regression", "also diff current behavior against the recorded golden set").option("--fail-on <severity>", "severity at/above which findings fail the run (error, warning)").action(
|
|
64
148
|
async (opts) => {
|
|
65
149
|
const code = await runCommand({
|
|
66
150
|
config: opts.config,
|
|
@@ -72,10 +156,25 @@ program.command("run").description("Run the conformance suite (and optionally re
|
|
|
72
156
|
process.exit(code);
|
|
73
157
|
}
|
|
74
158
|
);
|
|
75
|
-
program.command("record").description("Record golden files for the tools configured under `regression.record`").
|
|
159
|
+
program.command("record").description("Record golden files for the tools configured under `regression.record`").option(
|
|
160
|
+
"-c, --config <path>",
|
|
161
|
+
"path to the vexyo config file (auto-discovered in cwd if omitted)"
|
|
162
|
+
).action(async (opts) => {
|
|
76
163
|
const code = await recordCommand({ config: opts.config });
|
|
77
164
|
process.exit(code);
|
|
78
165
|
});
|
|
166
|
+
program.command("init").description("Scaffold a vexyo.config.ts in the current directory").option("-t, --transport <kind>", "target transport for the template (stdio, http)", "stdio").option("-f, --force", "overwrite an existing vexyo.config.ts").action(async (opts) => {
|
|
167
|
+
if (opts.transport !== "stdio" && opts.transport !== "http") {
|
|
168
|
+
process.stderr.write(`error: --transport must be "stdio" or "http".
|
|
169
|
+
`);
|
|
170
|
+
process.exit(2);
|
|
171
|
+
}
|
|
172
|
+
const code = await initCommand({
|
|
173
|
+
transport: opts.transport,
|
|
174
|
+
force: Boolean(opts.force)
|
|
175
|
+
});
|
|
176
|
+
process.exit(code);
|
|
177
|
+
});
|
|
79
178
|
var argv = process.argv.slice();
|
|
80
179
|
if (argv[2] === "--") argv.splice(2, 1);
|
|
81
180
|
program.parseAsync(argv).catch((err) => {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/commands/record.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { BRAND } from '@vexyo/core';\nimport { Command } from 'commander';\nimport { recordCommand } from './commands/record';\nimport { runCommand } from './commands/run';\n\nconst program = new Command();\n\nprogram.name(BRAND.bin).description('CI-native conformance harness for MCP servers');\n\nprogram\n .command('run')\n .description('Run the conformance suite (and optionally regression) against an MCP server')\n .requiredOption('-c, --config <path>', 'path to the vexyo config file')\n .option('-s, --spec-version <version>', 'override the spec version to target')\n .option('-r, --reporter <name>', 'output reporter (console, json, markdown)', 'console')\n .option('--regression', 'also diff current behavior against the recorded golden set')\n .option('--fail-on <severity>', 'severity at/above which findings fail the run (error, warning)')\n .action(\n async (opts: {\n config: string;\n specVersion?: string;\n reporter: string;\n regression?: boolean;\n failOn?: string;\n }) => {\n const code = await runCommand({\n config: opts.config,\n specVersion: opts.specVersion,\n reporter: opts.reporter,\n regression: opts.regression,\n failOn: opts.failOn,\n });\n process.exit(code);\n },\n );\n\nprogram\n .command('record')\n .description('Record golden files for the tools configured under `regression.record`')\n .requiredOption('-c, --config <path>', 'path to the vexyo config file')\n .action(async (opts: { config: string }) => {\n const code = await recordCommand({ config: opts.config });\n process.exit(code);\n });\n\n// `pnpm vexyo -- run ...` forwards the `--` separator literally; drop a\n// leading one so both that and `pnpm vexyo run ...` parse identically.\nconst argv = process.argv.slice();\nif (argv[2] === '--') argv.splice(2, 1);\n\nprogram.parseAsync(argv).catch((err: unknown) => {\n process.stderr.write(`fatal: ${err instanceof Error ? err.message : String(err)}\\n`);\n process.exit(2);\n});\n","import { connectTarget, recordGoldens, writeGoldenSet } from '@vexyo/core';\nimport { ConfigError, loadConfig, type Config } from '../config';\nimport { reportHarnessError } from '../errors';\nimport { resolveGoldenDir, toGoldenConfig } from '../regression';\nimport { buildTarget } from '../target';\n\nexport interface RecordCommandOptions {\n config: string;\n}\n\n/**\n * Capture golden files. Connects over the configured transport, snapshots the\n * schema manifest (read-only), and calls only the tools explicitly listed in\n * `regression.record` — never \"call everything\". Returns 0 on success, 2 on any\n * harness/config error.\n */\nexport async function recordCommand(opts: RecordCommandOptions): Promise<0 | 2> {\n let config: Config;\n try {\n config = await loadConfig(opts.config);\n } catch (err) {\n return reportHarnessError(err);\n }\n\n if (!config.regression) {\n return reportHarnessError(\n new ConfigError('No `regression` block in the config — nothing to record.'),\n );\n }\n\n const goldenDir = resolveGoldenDir(opts.config, config.regression);\n const goldenCfg = toGoldenConfig(config.specVersion, config.regression);\n\n const toolCount = Object.keys(goldenCfg.tools).length;\n process.stdout.write(\n toolCount === 0\n ? 'No tools configured under `regression.record`; recording manifest only.\\n'\n : `Recording ${toolCount} tool(s): ${Object.keys(goldenCfg.tools).sort().join(', ')}\\n`,\n );\n\n let conn: Awaited<ReturnType<typeof connectTarget>>;\n try {\n conn = await connectTarget(buildTarget(config));\n } catch (err) {\n return reportHarnessError(err);\n }\n\n try {\n const set = await recordGoldens(conn.client, goldenCfg);\n const written = await writeGoldenSet(goldenDir, set);\n process.stdout.write(`Wrote ${written.length} golden file(s) to ${goldenDir}:\\n`);\n for (const path of written) {\n process.stdout.write(` ${path}\\n`);\n }\n return 0;\n } catch (err) {\n return reportHarnessError(err);\n } finally {\n await conn.close().catch(() => undefined);\n }\n}\n"],"mappings":";;;;;;;;;;;;AACA,SAAS,aAAa;AACtB,SAAS,eAAe;;;ACFxB,SAAS,eAAe,eAAe,sBAAsB;AAgB7D,eAAsB,cAAc,MAA4C;AAC9E,MAAI;AACJ,MAAI;AACF,aAAS,MAAM,WAAW,KAAK,MAAM;AAAA,EACvC,SAAS,KAAK;AACZ,WAAO,mBAAmB,GAAG;AAAA,EAC/B;AAEA,MAAI,CAAC,OAAO,YAAY;AACtB,WAAO;AAAA,MACL,IAAI,YAAY,+DAA0D;AAAA,IAC5E;AAAA,EACF;AAEA,QAAM,YAAY,iBAAiB,KAAK,QAAQ,OAAO,UAAU;AACjE,QAAM,YAAY,eAAe,OAAO,aAAa,OAAO,UAAU;AAEtE,QAAM,YAAY,OAAO,KAAK,UAAU,KAAK,EAAE;AAC/C,UAAQ,OAAO;AAAA,IACb,cAAc,IACV,8EACA,aAAa,SAAS,aAAa,OAAO,KAAK,UAAU,KAAK,EAAE,KAAK,EAAE,KAAK,IAAI,CAAC;AAAA;AAAA,EACvF;AAEA,MAAI;AACJ,MAAI;AACF,WAAO,MAAM,cAAc,YAAY,MAAM,CAAC;AAAA,EAChD,SAAS,KAAK;AACZ,WAAO,mBAAmB,GAAG;AAAA,EAC/B;AAEA,MAAI;AACF,UAAM,MAAM,MAAM,cAAc,KAAK,QAAQ,SAAS;AACtD,UAAM,UAAU,MAAM,eAAe,WAAW,GAAG;AACnD,YAAQ,OAAO,MAAM,SAAS,QAAQ,MAAM,sBAAsB,SAAS;AAAA,CAAK;AAChF,eAAW,QAAQ,SAAS;AAC1B,cAAQ,OAAO,MAAM,KAAK,IAAI;AAAA,CAAI;AAAA,IACpC;AACA,WAAO;AAAA,EACT,SAAS,KAAK;AACZ,WAAO,mBAAmB,GAAG;AAAA,EAC/B,UAAE;AACA,UAAM,KAAK,MAAM,EAAE,MAAM,MAAM,MAAS;AAAA,EAC1C;AACF;;;ADtDA,IAAM,UAAU,IAAI,QAAQ;AAE5B,QAAQ,KAAK,MAAM,GAAG,EAAE,YAAY,+CAA+C;AAEnF,QACG,QAAQ,KAAK,EACb,YAAY,6EAA6E,EACzF,eAAe,uBAAuB,+BAA+B,EACrE,OAAO,gCAAgC,qCAAqC,EAC5E,OAAO,yBAAyB,6CAA6C,SAAS,EACtF,OAAO,gBAAgB,4DAA4D,EACnF,OAAO,wBAAwB,gEAAgE,EAC/F;AAAA,EACC,OAAO,SAMD;AACJ,UAAM,OAAO,MAAM,WAAW;AAAA,MAC5B,QAAQ,KAAK;AAAA,MACb,aAAa,KAAK;AAAA,MAClB,UAAU,KAAK;AAAA,MACf,YAAY,KAAK;AAAA,MACjB,QAAQ,KAAK;AAAA,IACf,CAAC;AACD,YAAQ,KAAK,IAAI;AAAA,EACnB;AACF;AAEF,QACG,QAAQ,QAAQ,EAChB,YAAY,wEAAwE,EACpF,eAAe,uBAAuB,+BAA+B,EACrE,OAAO,OAAO,SAA6B;AAC1C,QAAM,OAAO,MAAM,cAAc,EAAE,QAAQ,KAAK,OAAO,CAAC;AACxD,UAAQ,KAAK,IAAI;AACnB,CAAC;AAIH,IAAM,OAAO,QAAQ,KAAK,MAAM;AAChC,IAAI,KAAK,CAAC,MAAM,KAAM,MAAK,OAAO,GAAG,CAAC;AAEtC,QAAQ,WAAW,IAAI,EAAE,MAAM,CAAC,QAAiB;AAC/C,UAAQ,OAAO,MAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAAA,CAAI;AACnF,UAAQ,KAAK,CAAC;AAChB,CAAC;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/commands/init.ts","../src/commands/record.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { BRAND } from '@vexyo/core';\nimport { Command } from 'commander';\nimport { initCommand, type InitTransport } from './commands/init';\nimport { recordCommand } from './commands/record';\nimport { runCommand } from './commands/run';\n\nconst program = new Command();\n\nprogram.name(BRAND.bin).description('CI-native conformance harness for MCP servers');\n\nprogram\n .command('run')\n .description('Run the conformance suite (and optionally regression) against an MCP server')\n .option(\n '-c, --config <path>',\n 'path to the vexyo config file (auto-discovered in cwd if omitted)',\n )\n .option('-s, --spec-version <version>', 'override the spec version to target')\n .option('-r, --reporter <name>', 'output reporter (console, json, markdown)', 'console')\n .option('--regression', 'also diff current behavior against the recorded golden set')\n .option('--fail-on <severity>', 'severity at/above which findings fail the run (error, warning)')\n .action(\n async (opts: {\n config?: string;\n specVersion?: string;\n reporter: string;\n regression?: boolean;\n failOn?: string;\n }) => {\n const code = await runCommand({\n config: opts.config,\n specVersion: opts.specVersion,\n reporter: opts.reporter,\n regression: opts.regression,\n failOn: opts.failOn,\n });\n process.exit(code);\n },\n );\n\nprogram\n .command('record')\n .description('Record golden files for the tools configured under `regression.record`')\n .option(\n '-c, --config <path>',\n 'path to the vexyo config file (auto-discovered in cwd if omitted)',\n )\n .action(async (opts: { config?: string }) => {\n const code = await recordCommand({ config: opts.config });\n process.exit(code);\n });\n\nprogram\n .command('init')\n .description('Scaffold a vexyo.config.ts in the current directory')\n .option('-t, --transport <kind>', 'target transport for the template (stdio, http)', 'stdio')\n .option('-f, --force', 'overwrite an existing vexyo.config.ts')\n .action(async (opts: { transport: string; force?: boolean }) => {\n if (opts.transport !== 'stdio' && opts.transport !== 'http') {\n process.stderr.write(`error: --transport must be \"stdio\" or \"http\".\\n`);\n process.exit(2);\n }\n const code = await initCommand({\n transport: opts.transport as InitTransport,\n force: Boolean(opts.force),\n });\n process.exit(code);\n });\n\n// `pnpm vexyo -- run ...` forwards the `--` separator literally; drop a\n// leading one so both that and `pnpm vexyo run ...` parse identically.\nconst argv = process.argv.slice();\nif (argv[2] === '--') argv.splice(2, 1);\n\nprogram.parseAsync(argv).catch((err: unknown) => {\n process.stderr.write(`fatal: ${err instanceof Error ? err.message : String(err)}\\n`);\n process.exit(2);\n});\n","import { existsSync } from 'node:fs';\nimport { writeFile } from 'node:fs/promises';\nimport { resolve } from 'node:path';\nimport { ConfigError, DEFAULT_SPEC_VERSION } from '../config';\nimport { reportHarnessError } from '../errors';\n\nexport type InitTransport = 'stdio' | 'http';\n\nexport interface InitCommandOptions {\n transport: InitTransport;\n force: boolean;\n /** Directory to write into (defaults to cwd); injectable for tests. */\n cwd?: string;\n}\n\nconst CONFIG_FILENAME = 'vexyo.config.ts';\n\n/**\n * Render a `vexyo.config.ts` template. The chosen transport's target is active;\n * the other shapes (and a regression block) are included commented out. Pure —\n * no filesystem — so the scaffold output is snapshot-testable.\n */\nexport function renderConfigTemplate(transport: InitTransport, specVersion: string): string {\n const activeTarget =\n transport === 'http'\n ? ` target: {\n transport: 'http',\n url: 'http://127.0.0.1:3000/mcp',\n // headers: { Authorization: 'Bearer <your-token>' },\n },`\n : ` target: {\n transport: 'stdio',\n command: 'node',\n args: ['path/to/your/mcp-server.js'],\n // cwd: '.',\n // env: { API_KEY: 'value' },\n },`;\n\n const alternatives =\n transport === 'http'\n ? ` // --- Alternative targets (uncomment one, comment out the http target above) ---\n //\n // Node (stdio):\n // target: { transport: 'stdio', command: 'node', args: ['path/to/your/mcp-server.js'] },\n //\n // Python / other runtimes (stdio):\n // target: { transport: 'stdio', command: 'python', args: ['-m', 'your_mcp_server'] },`\n : ` // --- Alternative targets (uncomment one, comment out the stdio target above) ---\n //\n // Python / other runtimes (stdio):\n // target: { transport: 'stdio', command: 'python', args: ['-m', 'your_mcp_server'] },\n //\n // Streamable HTTP (a running server):\n // target: { transport: 'http', url: 'http://127.0.0.1:3000/mcp' },`;\n\n return `import { defineConfig } from '@vexyo/cli/config';\n\nexport default defineConfig({\n specVersion: '${specVersion}',\n\n // The MCP server under test.\n${activeTarget}\n\n${alternatives}\n\n // --- Regression testing (optional) ---\n // Record golden tool outputs with \\`vexyo record\\`, then \\`vexyo run --regression\\`\n // fails when behavior drifts.\n // regression: {\n // goldenDir: 'vexyo/goldens',\n // normalizers: ['iso-timestamp', 'uuid'],\n // record: {\n // // greet: { cases: [{ case: 'basic', arguments: { name: 'world' } }] },\n // },\n // },\n});\n`;\n}\n\n/**\n * Scaffold a `vexyo.config.ts` in cwd. Refuses to overwrite an existing file\n * without `--force`. Returns 0 on success, 2 on any error.\n */\nexport async function initCommand(opts: InitCommandOptions): Promise<0 | 2> {\n const cwd = opts.cwd ?? process.cwd();\n const target = resolve(cwd, CONFIG_FILENAME);\n\n if (existsSync(target) && !opts.force) {\n return reportHarnessError(\n new ConfigError(`${CONFIG_FILENAME} already exists. Pass --force to overwrite it.`),\n );\n }\n\n try {\n await writeFile(target, renderConfigTemplate(opts.transport, DEFAULT_SPEC_VERSION));\n } catch (err) {\n return reportHarnessError(err);\n }\n\n process.stdout.write(\n `Created ${CONFIG_FILENAME}.\\n\\n` +\n `Next steps:\\n` +\n ` 1. Edit the \\`target\\` to point at your MCP server.\\n` +\n ` 2. vexyo run --config ${CONFIG_FILENAME}\\n`,\n );\n return 0;\n}\n","import { connectTarget, recordGoldens, writeGoldenSet } from '@vexyo/core';\nimport { ConfigError, loadConfig, resolveConfigPath, type Config } from '../config';\nimport { reportHarnessError } from '../errors';\nimport { resolveGoldenDir, toGoldenConfig } from '../regression';\nimport { buildTarget } from '../target';\n\nexport interface RecordCommandOptions {\n /** Explicit config path; when omitted, auto-discovered in cwd. */\n config?: string;\n}\n\n/**\n * Capture golden files. Connects over the configured transport, snapshots the\n * schema manifest (read-only), and calls only the tools explicitly listed in\n * `regression.record` — never \"call everything\". Returns 0 on success, 2 on any\n * harness/config error.\n */\nexport async function recordCommand(opts: RecordCommandOptions): Promise<0 | 2> {\n let configPath: string;\n let config: Config;\n try {\n configPath = resolveConfigPath(opts.config);\n config = await loadConfig(configPath);\n } catch (err) {\n return reportHarnessError(err);\n }\n\n if (!config.regression) {\n return reportHarnessError(\n new ConfigError('No `regression` block in the config — nothing to record.'),\n );\n }\n\n const goldenDir = resolveGoldenDir(configPath, config.regression);\n const goldenCfg = toGoldenConfig(config.specVersion, config.regression);\n\n const toolCount = Object.keys(goldenCfg.tools).length;\n process.stdout.write(\n toolCount === 0\n ? 'No tools configured under `regression.record`; recording manifest only.\\n'\n : `Recording ${toolCount} tool(s): ${Object.keys(goldenCfg.tools).sort().join(', ')}\\n`,\n );\n\n let conn: Awaited<ReturnType<typeof connectTarget>>;\n try {\n conn = await connectTarget(buildTarget(config));\n } catch (err) {\n return reportHarnessError(err);\n }\n\n try {\n const set = await recordGoldens(conn.client, goldenCfg);\n const written = await writeGoldenSet(goldenDir, set);\n process.stdout.write(`Wrote ${written.length} golden file(s) to ${goldenDir}:\\n`);\n for (const path of written) {\n process.stdout.write(` ${path}\\n`);\n }\n return 0;\n } catch (err) {\n return reportHarnessError(err);\n } finally {\n await conn.close().catch(() => undefined);\n }\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vexyo/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "CLI for vexyo — conformance and regression testing for MCP servers, for local dev and CI.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
|
@@ -41,8 +41,8 @@
|
|
|
41
41
|
"commander": "^13.1.0",
|
|
42
42
|
"jiti": "^2.4.2",
|
|
43
43
|
"zod": "^4.0.0",
|
|
44
|
-
"@vexyo/core": "0.1.
|
|
45
|
-
"@vexyo/reporters": "0.1.
|
|
44
|
+
"@vexyo/core": "0.1.1",
|
|
45
|
+
"@vexyo/reporters": "0.1.1"
|
|
46
46
|
},
|
|
47
47
|
"main": "./dist/index.js",
|
|
48
48
|
"types": "./dist/index.d.ts",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/commands/run.ts","../src/config.ts","../src/errors.ts","../src/regression.ts","../src/target.ts"],"sourcesContent":["import {\n readGoldenSet,\n runRegression,\n runSuite,\n type RunResult,\n type RunSuiteOptions,\n type Severity,\n type SpecVersion,\n} from '@vexyo/core';\nimport {\n consoleReporter,\n jsonReporter,\n junitReporter,\n markdownReporter,\n type Reporter,\n} from '@vexyo/reporters';\nimport { ConfigError, loadConfig, type Config } from '../config';\nimport { reportHarnessError } from '../errors';\nimport { resolveGoldenDir, toGoldenConfig } from '../regression';\nimport { buildTarget } from '../target';\n\nexport interface RunCommandOptions {\n config: string;\n specVersion?: string;\n reporter: string;\n regression?: boolean;\n failOn?: string;\n}\n\nexport interface ExecuteRunOptions {\n config: string;\n specVersion?: string;\n regression?: boolean;\n failOn?: string;\n}\n\nconst REPORTERS: Record<string, Reporter> = {\n console: consoleReporter,\n json: jsonReporter,\n markdown: markdownReporter,\n junit: junitReporter,\n};\n\n/**\n * Load config, connect over the configured transport, run the conformance suite\n * (plus regression when requested), and return the {@link RunResult}. Throws\n * `ConfigError`/`GoldenError`/connection errors for anything the caller should\n * map to exit code 2. Shared by the `run` command and the GitHub Action.\n */\nexport async function executeRun(opts: ExecuteRunOptions): Promise<RunResult> {\n const config = await loadConfig(opts.config);\n const specVersion = resolveSpecVersion(opts.specVersion, config);\n const failOn = resolveFailOn(opts.failOn, config);\n const extraChecks = await buildRegressionPhase(opts, config, specVersion);\n\n return runSuite({\n specVersion,\n target: buildTarget(config),\n failOn,\n extraChecks,\n });\n}\n\n/**\n * Execute the `run` command. Returns the process exit code:\n * 0 = pass, 1 = findings at/above threshold, 2 = harness/config error.\n */\nexport async function runCommand(opts: RunCommandOptions): Promise<0 | 1 | 2> {\n const reporter = REPORTERS[opts.reporter];\n if (!reporter) {\n return reportHarnessError(\n new ConfigError(\n `Unknown reporter \"${opts.reporter}\". Available reporters: ${Object.keys(REPORTERS).join(', ')}.`,\n ),\n );\n }\n\n let result: RunResult;\n try {\n result = await executeRun(opts);\n } catch (err) {\n return reportHarnessError(err);\n }\n\n process.stdout.write(`${reporter.format(result)}\\n`);\n return result.exitCode;\n}\n\nfunction resolveSpecVersion(override: string | undefined, config: Config): SpecVersion {\n const specVersion = (override as SpecVersion | undefined) ?? config.specVersion;\n if (specVersion !== '2025-11-25') {\n throw new ConfigError(\n `Spec version \"${specVersion}\" is not wired up in this build. Only 2025-11-25 is supported.`,\n );\n }\n return specVersion;\n}\n\nfunction resolveFailOn(override: string | undefined, config: Config): Severity {\n return (override as Severity | undefined) ?? config.regression?.failOn ?? 'error';\n}\n\nasync function buildRegressionPhase(\n opts: ExecuteRunOptions,\n config: Config,\n specVersion: SpecVersion,\n): Promise<RunSuiteOptions['extraChecks']> {\n if (!opts.regression) {\n return undefined;\n }\n if (!config.regression) {\n throw new ConfigError('`--regression` requires a `regression` block in the config.');\n }\n const goldenSet = await readGoldenSet(resolveGoldenDir(opts.config, config.regression));\n const goldenCfg = toGoldenConfig(specVersion, config.regression);\n return (ctx) => runRegression(ctx.client, goldenSet, goldenCfg);\n}\n","import { resolve } from 'node:path';\nimport { pathToFileURL } from 'node:url';\nimport { createJiti } from 'jiti';\nimport { z } from 'zod';\n\nexport const SPEC_VERSIONS = ['2025-11-25', '2026-07-28'] as const;\n\nconst stdioTargetSchema = z.object({\n transport: z.literal('stdio'),\n command: z.string().min(1),\n args: z.array(z.string()).optional(),\n cwd: z.string().optional(),\n env: z.record(z.string(), z.string()).optional(),\n});\n\nconst httpTargetSchema = z.object({\n transport: z.literal('http'),\n url: z.string().min(1),\n /** Extra HTTP headers sent on every request (e.g. auth). */\n headers: z.record(z.string(), z.string()).optional(),\n});\n\nconst targetSchema = z.discriminatedUnion('transport', [stdioTargetSchema, httpTargetSchema]);\n\n/** A normalizer is a builtin name or an inline `{ name, apply }` object. */\nconst normalizerRefSchema = z.union([\n z.string(),\n z.object({\n name: z.string(),\n apply: z.custom<(value: unknown) => unknown>((v) => typeof v === 'function'),\n }),\n]);\n\nconst recordCaseSchema = z.object({\n case: z.string().min(1),\n arguments: z.record(z.string(), z.unknown()).default({}),\n});\n\nconst recordToolSchema = z.object({\n cases: z.array(recordCaseSchema).min(1),\n normalizers: z.array(normalizerRefSchema).optional(),\n});\n\nconst regressionSchema = z.object({\n /** Committed golden directory (relative to the config file's cwd). */\n goldenDir: z.string().default('vexyo/goldens'),\n /** Severity at/above which drift fails the run. */\n failOn: z.enum(['error', 'warning']).default('error'),\n /** Default normalizers applied to every recorded tool. */\n normalizers: z.array(normalizerRefSchema).default([]),\n /** Tools to record/compare, keyed by name. Empty = record nothing (opt-in). */\n record: z.record(z.string(), recordToolSchema).default({}),\n});\n\nexport const configSchema = z.object({\n specVersion: z.enum(SPEC_VERSIONS).default('2025-11-25'),\n target: targetSchema,\n security: z.boolean().default(false),\n regression: regressionSchema.optional(),\n});\n\nexport type RegressionConfig = z.infer<typeof regressionSchema>;\n\n/** Validated, defaults-applied config. */\nexport type Config = z.infer<typeof configSchema>;\n/** Shape a user writes in a config file (before defaults are applied). */\nexport type UserConfig = z.input<typeof configSchema>;\nexport type StdioTarget = Extract<Config['target'], { transport: 'stdio' }>;\n\n/** Raised for anything the user must fix in their config or invocation (exit 2). */\nexport class ConfigError extends Error {\n constructor(message: string, cause?: unknown) {\n super(message, cause !== undefined ? { cause } : undefined);\n this.name = 'ConfigError';\n }\n}\n\nfunction formatZodError(error: z.ZodError): string {\n return error.issues\n .map((issue) => ` - ${issue.path.join('.') || '(root)'}: ${issue.message}`)\n .join('\\n');\n}\n\n/**\n * Load and validate a config file. Supports `.ts`/`.js`/`.mjs` via jiti so the\n * blueprint's `examples/*.config.ts` work without a build step. The default\n * export is validated against {@link configSchema}.\n */\nexport async function loadConfig(configPath: string): Promise<Config> {\n const absolute = resolve(process.cwd(), configPath);\n\n let mod: unknown;\n try {\n const jiti = createJiti(pathToFileURL(absolute).href);\n mod = await jiti.import(absolute, { default: true });\n } catch (err) {\n throw new ConfigError(\n `Could not load config file at ${absolute}. Check the path and that the file has a default export.`,\n err,\n );\n }\n\n const parsed = configSchema.safeParse(mod);\n if (!parsed.success) {\n throw new ConfigError(`Invalid config at ${absolute}:\\n${formatZodError(parsed.error)}`);\n }\n return parsed.data;\n}\n","/** Print a harness/config error (with its cause, if any) and return exit code 2. */\nexport function reportHarnessError(err: unknown): 2 {\n const message = err instanceof Error ? err.message : String(err);\n process.stderr.write(`error: ${message}\\n`);\n const cause = err instanceof Error ? err.cause : undefined;\n if (cause !== undefined) {\n process.stderr.write(` cause: ${cause instanceof Error ? cause.message : String(cause)}\\n`);\n }\n return 2;\n}\n","import { dirname, resolve } from 'node:path';\nimport type { GoldenConfig } from '@vexyo/core';\nimport type { RegressionConfig } from './config';\n\n/** Resolve `goldenDir` relative to the config file's directory. */\nexport function resolveGoldenDir(configPath: string, regression: RegressionConfig): string {\n return resolve(dirname(resolve(process.cwd(), configPath)), regression.goldenDir);\n}\n\n/** Map the CLI's regression config onto the core engine's {@link GoldenConfig}. */\nexport function toGoldenConfig(specVersion: string, regression: RegressionConfig): GoldenConfig {\n return {\n specVersion,\n defaultNormalizers: regression.normalizers,\n tools: Object.fromEntries(\n Object.entries(regression.record).map(([name, spec]) => [\n name,\n { cases: spec.cases, normalizers: spec.normalizers },\n ]),\n ),\n };\n}\n","import type { ConnectTarget } from '@vexyo/core';\nimport type { Config } from './config';\n\n/** Map the validated config's target onto the core engine's {@link ConnectTarget}. */\nexport function buildTarget(config: Config): ConnectTarget {\n if (config.target.transport === 'http') {\n return {\n transport: 'http',\n http: { url: config.target.url, headers: config.target.headers },\n };\n }\n return { transport: 'stdio', stdio: config.target };\n}\n"],"mappings":"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","names":["resolve"]}
|