@kirrosh/zond 0.14.0 → 0.16.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/README.md +1 -1
- package/package.json +4 -3
- package/src/cli/commands/ci-init.ts +12 -1
- package/src/cli/commands/coverage.ts +21 -1
- package/src/cli/commands/db.ts +121 -0
- package/src/cli/commands/describe.ts +60 -0
- package/src/cli/commands/generate.ts +127 -0
- package/src/cli/commands/guide.ts +127 -0
- package/src/cli/commands/init.ts +57 -0
- package/src/cli/commands/request.ts +57 -0
- package/src/cli/commands/run.ts +53 -10
- package/src/cli/commands/serve.ts +62 -3
- package/src/cli/commands/validate.ts +18 -2
- package/src/cli/index.ts +204 -7
- package/src/cli/json-envelope.ts +19 -0
- package/src/core/diagnostics/db-analysis.ts +351 -0
- package/src/core/diagnostics/failure-hints.ts +1 -0
- package/src/core/generator/data-factory.ts +19 -8
- package/src/core/generator/describe.ts +250 -0
- package/src/core/generator/guide-builder.ts +20 -0
- package/src/core/generator/suite-generator.ts +133 -20
- package/src/core/runner/executor.ts +1 -0
- package/src/core/runner/send-request.ts +94 -0
- package/src/core/runner/types.ts +1 -0
- package/src/db/queries.ts +4 -2
- package/src/db/schema.ts +11 -3
- package/src/mcp/descriptions.ts +0 -24
- package/src/mcp/server.ts +1 -8
- package/src/mcp/tools/describe-endpoint.ts +3 -218
- package/src/mcp/tools/query-db.ts +6 -222
- package/src/mcp/tools/run-tests.ts +1 -0
- package/src/mcp/tools/send-request.ts +15 -61
- package/src/web/views/suites-tab.ts +1 -1
- package/src/mcp/tools/generate-and-save.ts +0 -202
- package/src/mcp/tools/save-test-suite.ts +0 -218
- package/src/mcp/tools/set-work-dir.ts +0 -35
package/src/cli/commands/run.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type { ReporterName } from "../../core/reporter/types.ts";
|
|
|
9
9
|
import type { TestSuite } from "../../core/parser/types.ts";
|
|
10
10
|
import type { TestRunResult } from "../../core/runner/types.ts";
|
|
11
11
|
import { printError, printWarning } from "../output.ts";
|
|
12
|
+
import { jsonOk, jsonError, printJson } from "../json-envelope.ts";
|
|
12
13
|
import { getDb } from "../../db/schema.ts";
|
|
13
14
|
import { createRun, finalizeRun, saveResults, findCollectionByTestPath } from "../../db/queries.ts";
|
|
14
15
|
|
|
@@ -25,6 +26,7 @@ export interface RunOptions {
|
|
|
25
26
|
tag?: string[];
|
|
26
27
|
envVars?: string[];
|
|
27
28
|
dryRun?: boolean;
|
|
29
|
+
json?: boolean;
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
export async function runCommand(options: RunOptions): Promise<number> {
|
|
@@ -51,14 +53,19 @@ export async function runCommand(options: RunOptions): Promise<number> {
|
|
|
51
53
|
}
|
|
52
54
|
}
|
|
53
55
|
|
|
54
|
-
// 1c. Safe mode:
|
|
56
|
+
// 1c. Safe mode: keep GET, set-only steps, and auth-related requests
|
|
55
57
|
if (options.safe) {
|
|
58
|
+
const AUTH_PATH_RE = /\/(auth|login|signin|token|oauth)\b/i;
|
|
56
59
|
for (const suite of suites) {
|
|
57
|
-
suite.tests = suite.tests.filter(t =>
|
|
60
|
+
suite.tests = suite.tests.filter(t => {
|
|
61
|
+
if (t.method === "GET" || !t.method) return true;
|
|
62
|
+
if (AUTH_PATH_RE.test(t.path)) return true;
|
|
63
|
+
return false;
|
|
64
|
+
});
|
|
58
65
|
}
|
|
59
66
|
suites = suites.filter(s => s.tests.length > 0);
|
|
60
67
|
if (suites.length === 0) {
|
|
61
|
-
printWarning("No
|
|
68
|
+
printWarning("No safe tests found. Nothing to run in safe mode.");
|
|
62
69
|
return 0;
|
|
63
70
|
}
|
|
64
71
|
}
|
|
@@ -128,29 +135,65 @@ export async function runCommand(options: RunOptions): Promise<number> {
|
|
|
128
135
|
results.push(...all);
|
|
129
136
|
}
|
|
130
137
|
|
|
131
|
-
// 5.
|
|
132
|
-
const
|
|
133
|
-
|
|
138
|
+
// 5. Collect warnings
|
|
139
|
+
const warnings: string[] = [];
|
|
140
|
+
const rateLimited = results.flatMap(r => r.steps)
|
|
141
|
+
.filter(s => s.response?.status === 429);
|
|
142
|
+
if (rateLimited.length > 0) {
|
|
143
|
+
warnings.push(`${rateLimited.length} request(s) hit rate limit (429). Consider: consolidating login steps, adding --bail, or using retry_until with delay.`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// 5b. Report
|
|
147
|
+
if (!options.json) {
|
|
148
|
+
const reporter = getReporter(options.report);
|
|
149
|
+
reporter.report(results);
|
|
150
|
+
for (const w of warnings) {
|
|
151
|
+
printWarning(w);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
134
154
|
|
|
135
155
|
// 6. Save to DB
|
|
156
|
+
let savedRunId: number | undefined;
|
|
136
157
|
if (!options.noDb) {
|
|
137
158
|
try {
|
|
138
159
|
getDb(options.dbPath);
|
|
139
160
|
const collection = findCollectionByTestPath(options.path);
|
|
140
|
-
|
|
161
|
+
savedRunId = createRun({
|
|
141
162
|
started_at: results[0]?.started_at ?? new Date().toISOString(),
|
|
142
163
|
environment: options.env,
|
|
143
164
|
collection_id: collection?.id,
|
|
144
165
|
});
|
|
145
|
-
finalizeRun(
|
|
146
|
-
saveResults(
|
|
166
|
+
finalizeRun(savedRunId, results);
|
|
167
|
+
saveResults(savedRunId, results);
|
|
147
168
|
} catch (err) {
|
|
148
169
|
printWarning(`Failed to save results to DB: ${(err as Error).message}`);
|
|
149
170
|
}
|
|
150
171
|
}
|
|
151
172
|
|
|
152
173
|
// 7. Exit code (always 0 in dry-run mode)
|
|
153
|
-
if (dryRun)
|
|
174
|
+
if (dryRun) {
|
|
175
|
+
if (options.json) {
|
|
176
|
+
printJson(jsonOk("run", { summary: { total: results.length, passed: 0, failed: 0 }, dryRun: true }));
|
|
177
|
+
}
|
|
178
|
+
return 0;
|
|
179
|
+
}
|
|
154
180
|
const hasFailures = results.some((r) => r.failed > 0 || r.steps.some((s) => s.status === "error"));
|
|
181
|
+
|
|
182
|
+
if (options.json) {
|
|
183
|
+
const total = results.reduce((s, r) => s + r.total, 0);
|
|
184
|
+
const passed = results.reduce((s, r) => s + r.passed, 0);
|
|
185
|
+
const failed = results.reduce((s, r) => s + r.failed, 0);
|
|
186
|
+
const failures = results.flatMap(r =>
|
|
187
|
+
r.steps.filter(s => s.status === "fail" || s.status === "error").map(s => ({
|
|
188
|
+
suite: r.suite_name,
|
|
189
|
+
test: s.name,
|
|
190
|
+
...(r.suite_file ? { file: r.suite_file } : {}),
|
|
191
|
+
status: s.status,
|
|
192
|
+
error: s.error,
|
|
193
|
+
}))
|
|
194
|
+
);
|
|
195
|
+
printJson(jsonOk("run", { summary: { total, passed, failed }, failures, warnings, runId: savedRunId }));
|
|
196
|
+
}
|
|
197
|
+
|
|
155
198
|
return hasFailures ? 1 : 0;
|
|
156
199
|
}
|
|
@@ -3,20 +3,79 @@ import { startServer } from "../../web/server.ts";
|
|
|
3
3
|
export interface ServeOptions {
|
|
4
4
|
port?: number;
|
|
5
5
|
host?: string;
|
|
6
|
-
openapiSpec?: string;
|
|
7
|
-
testsDir?: string;
|
|
8
6
|
dbPath?: string;
|
|
9
7
|
watch?: boolean;
|
|
8
|
+
open?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Kill any existing process listening on the given port (Windows + Unix) */
|
|
12
|
+
async function killPortHolder(port: number): Promise<void> {
|
|
13
|
+
const isWin = process.platform === "win32";
|
|
14
|
+
try {
|
|
15
|
+
if (isWin) {
|
|
16
|
+
// PowerShell: find PID on port, then kill it
|
|
17
|
+
const find = Bun.spawn(["powershell", "-NoProfile", "-Command",
|
|
18
|
+
`(Get-NetTCPConnection -LocalPort ${port} -ErrorAction SilentlyContinue).OwningProcess`], {
|
|
19
|
+
stdout: "pipe", stderr: "ignore",
|
|
20
|
+
});
|
|
21
|
+
const out = await new Response(find.stdout).text();
|
|
22
|
+
const pids = [...new Set(out.trim().split(/\s+/).filter(s => /^\d+$/.test(s) && s !== "0"))];
|
|
23
|
+
for (const pid of pids) {
|
|
24
|
+
Bun.spawn(["powershell", "-NoProfile", "-Command",
|
|
25
|
+
`Stop-Process -Id ${pid} -Force -ErrorAction SilentlyContinue`], {
|
|
26
|
+
stdout: "ignore", stderr: "ignore",
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
if (pids.length > 0) {
|
|
30
|
+
// Give OS time to release the port
|
|
31
|
+
await Bun.sleep(500);
|
|
32
|
+
}
|
|
33
|
+
} else {
|
|
34
|
+
// Unix: lsof + kill
|
|
35
|
+
const find = Bun.spawn(["lsof", "-ti", `:${port}`], {
|
|
36
|
+
stdout: "pipe", stderr: "ignore",
|
|
37
|
+
});
|
|
38
|
+
const out = await new Response(find.stdout).text();
|
|
39
|
+
const pids = out.trim().split(/\s+/).filter(s => /^\d+$/.test(s));
|
|
40
|
+
for (const pid of pids) {
|
|
41
|
+
Bun.spawn(["kill", "-9", pid], { stdout: "ignore", stderr: "ignore" });
|
|
42
|
+
}
|
|
43
|
+
if (pids.length > 0) {
|
|
44
|
+
await Bun.sleep(300);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
} catch {
|
|
48
|
+
// Best effort — if we can't kill, startServer will fail with port-in-use
|
|
49
|
+
}
|
|
10
50
|
}
|
|
11
51
|
|
|
12
52
|
export async function serveCommand(options: ServeOptions): Promise<number> {
|
|
53
|
+
const port = options.port ?? 8080;
|
|
54
|
+
|
|
55
|
+
// Kill previous instance on the same port
|
|
56
|
+
await killPortHolder(port);
|
|
57
|
+
|
|
13
58
|
await startServer({
|
|
14
|
-
port
|
|
59
|
+
port,
|
|
15
60
|
host: options.host,
|
|
16
61
|
dbPath: options.dbPath,
|
|
17
62
|
dev: options.watch,
|
|
18
63
|
});
|
|
19
64
|
|
|
65
|
+
// Open browser if requested
|
|
66
|
+
if (options.open) {
|
|
67
|
+
const host = options.host === "0.0.0.0" || !options.host ? "localhost" : options.host;
|
|
68
|
+
const url = `http://${host}:${port}`;
|
|
69
|
+
try {
|
|
70
|
+
const cmd = process.platform === "win32" ? ["cmd", "/c", "start", url]
|
|
71
|
+
: process.platform === "darwin" ? ["open", url]
|
|
72
|
+
: ["xdg-open", url];
|
|
73
|
+
Bun.spawn(cmd, { stdout: "ignore", stderr: "ignore" });
|
|
74
|
+
} catch {
|
|
75
|
+
// Best effort — if browser can't open, server still runs
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
20
79
|
// Keep running — Bun.serve keeps the process alive
|
|
21
80
|
return 0;
|
|
22
81
|
}
|
|
@@ -1,18 +1,34 @@
|
|
|
1
1
|
import { parse } from "../../core/parser/yaml-parser.ts";
|
|
2
2
|
import { printError, printSuccess } from "../output.ts";
|
|
3
|
+
import { jsonOk, jsonError, printJson } from "../json-envelope.ts";
|
|
3
4
|
|
|
4
5
|
export interface ValidateOptions {
|
|
5
6
|
path: string;
|
|
7
|
+
json?: boolean;
|
|
6
8
|
}
|
|
7
9
|
|
|
8
10
|
export async function validateCommand(options: ValidateOptions): Promise<number> {
|
|
9
11
|
try {
|
|
10
12
|
const suites = await parse(options.path);
|
|
11
13
|
const totalSteps = suites.reduce((sum, s) => sum + s.tests.length, 0);
|
|
12
|
-
|
|
14
|
+
if (options.json) {
|
|
15
|
+
printJson(jsonOk("validate", {
|
|
16
|
+
files: suites.length,
|
|
17
|
+
suites: suites.length,
|
|
18
|
+
tests: totalSteps,
|
|
19
|
+
valid: true,
|
|
20
|
+
}));
|
|
21
|
+
} else {
|
|
22
|
+
printSuccess(`OK: ${suites.length} suite(s), ${totalSteps} test(s) validated successfully`);
|
|
23
|
+
}
|
|
13
24
|
return 0;
|
|
14
25
|
} catch (err) {
|
|
15
|
-
|
|
26
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
27
|
+
if (options.json) {
|
|
28
|
+
printJson(jsonError("validate", [message]));
|
|
29
|
+
} else {
|
|
30
|
+
printError(message);
|
|
31
|
+
}
|
|
16
32
|
return 2;
|
|
17
33
|
}
|
|
18
34
|
}
|
package/src/cli/index.ts
CHANGED
|
@@ -6,6 +6,12 @@ import { serveCommand } from "./commands/serve.ts";
|
|
|
6
6
|
import { mcpCommand } from "./commands/mcp.ts";
|
|
7
7
|
import { coverageCommand } from "./commands/coverage.ts";
|
|
8
8
|
import { ciInitCommand } from "./commands/ci-init.ts";
|
|
9
|
+
import { initCommand } from "./commands/init.ts";
|
|
10
|
+
import { describeCommand } from "./commands/describe.ts";
|
|
11
|
+
import { dbCommand } from "./commands/db.ts";
|
|
12
|
+
import { requestCommand } from "./commands/request.ts";
|
|
13
|
+
import { guideCommand } from "./commands/guide.ts";
|
|
14
|
+
import { generateCommand } from "./commands/generate.ts";
|
|
9
15
|
import { printError } from "./output.ts";
|
|
10
16
|
import { getRuntimeInfo } from "./runtime.ts";
|
|
11
17
|
import { getDb } from "../db/schema.ts";
|
|
@@ -21,6 +27,23 @@ export interface ParsedArgs {
|
|
|
21
27
|
flags: Record<string, string | boolean>;
|
|
22
28
|
}
|
|
23
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Strip MSYS/Git Bash automatic path conversion.
|
|
32
|
+
* Git Bash on Windows converts "/foo" → "C:/Program Files/Git/foo".
|
|
33
|
+
* Detect and reverse this for flags that expect API paths (e.g. --path /users).
|
|
34
|
+
*/
|
|
35
|
+
const MSYS_PREFIX_RE = /^[A-Z]:[\\/](?:Program Files[\\/]Git|msys64|usr)[\\/]/i;
|
|
36
|
+
|
|
37
|
+
function stripMsysPath(value: string): string {
|
|
38
|
+
if (!MSYS_PREFIX_RE.test(value)) return value;
|
|
39
|
+
// Extract the original path: "C:/Program Files/Git/products" → "/products"
|
|
40
|
+
const stripped = value.replace(MSYS_PREFIX_RE, "/");
|
|
41
|
+
return stripped;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Flags whose values are API paths, not filesystem paths — subject to MSYS fix */
|
|
45
|
+
const API_PATH_FLAGS = new Set(["path", "json-path"]);
|
|
46
|
+
|
|
24
47
|
export function parseArgs(argv: string[]): ParsedArgs {
|
|
25
48
|
// argv: [bunPath, scriptPath, ...userArgs]
|
|
26
49
|
const args = argv.slice(2);
|
|
@@ -36,12 +59,15 @@ export function parseArgs(argv: string[]): ParsedArgs {
|
|
|
36
59
|
const eqIndex = arg.indexOf("=");
|
|
37
60
|
if (eqIndex !== -1) {
|
|
38
61
|
// --flag=value
|
|
39
|
-
|
|
62
|
+
const key = arg.slice(2, eqIndex);
|
|
63
|
+
let val = arg.slice(eqIndex + 1);
|
|
64
|
+
if (API_PATH_FLAGS.has(key)) val = stripMsysPath(val);
|
|
65
|
+
flags[key] = val;
|
|
40
66
|
} else {
|
|
41
67
|
const key = arg.slice(2);
|
|
42
68
|
const next = args[i + 1];
|
|
43
69
|
if (next !== undefined && !next.startsWith("-")) {
|
|
44
|
-
flags[key] = next;
|
|
70
|
+
flags[key] = API_PATH_FLAGS.has(key) ? stripMsysPath(next) : next;
|
|
45
71
|
i++;
|
|
46
72
|
} else {
|
|
47
73
|
flags[key] = true;
|
|
@@ -69,7 +95,14 @@ Usage:
|
|
|
69
95
|
zond run <path> Run API tests
|
|
70
96
|
zond validate <path> Validate test files without running
|
|
71
97
|
zond coverage Analyze API test coverage
|
|
98
|
+
zond init Register a new API for testing
|
|
99
|
+
zond describe <spec> Describe endpoints from OpenAPI spec
|
|
100
|
+
zond db <subcommand> Query the test database
|
|
101
|
+
zond request <method> <url> Send an ad-hoc HTTP request
|
|
102
|
+
zond generate <spec> Generate test suites from OpenAPI spec
|
|
103
|
+
zond guide <spec> Generate test generation guide from OpenAPI spec
|
|
72
104
|
zond serve Start web dashboard
|
|
105
|
+
zond ui Alias for 'serve --open' (start dashboard & open browser)
|
|
73
106
|
zond mcp Start MCP server (stdio transport for AI agents)
|
|
74
107
|
--dir <path> Set working directory (relative paths resolve here)
|
|
75
108
|
zond ci init Generate CI/CD workflow (GitHub Actions, GitLab CI)
|
|
@@ -88,6 +121,40 @@ Options for 'run':
|
|
|
88
121
|
--safe Run only GET tests (read-only, safe mode)
|
|
89
122
|
--tag <tag> Filter suites by tag (repeatable, comma-separated, OR logic)
|
|
90
123
|
|
|
124
|
+
Options for 'init':
|
|
125
|
+
--name <name> API name (auto-detected from spec title if omitted)
|
|
126
|
+
--spec <path> Path to OpenAPI spec file
|
|
127
|
+
--base-url <url> Override base URL
|
|
128
|
+
--force Overwrite existing API collection
|
|
129
|
+
|
|
130
|
+
Options for 'describe':
|
|
131
|
+
--compact List all endpoints briefly
|
|
132
|
+
--method <method> HTTP method for single endpoint detail
|
|
133
|
+
--path <path> Endpoint path for single endpoint detail
|
|
134
|
+
|
|
135
|
+
Options for 'db':
|
|
136
|
+
zond db collections List all API collections
|
|
137
|
+
zond db runs [--limit N] List recent test runs
|
|
138
|
+
zond db run <id> [--verbose] Show run details
|
|
139
|
+
zond db diagnose <id> Diagnose run failures
|
|
140
|
+
zond db compare <idA> <idB> Compare two runs
|
|
141
|
+
|
|
142
|
+
Options for 'request':
|
|
143
|
+
--header <H> Request header "Name: Value" (repeatable)
|
|
144
|
+
--body <json> Request body (JSON string)
|
|
145
|
+
--env <name> Environment for variable interpolation
|
|
146
|
+
--api <name> Collection name (loads env from its directory)
|
|
147
|
+
--json-path <path> Extract value from response (dot notation)
|
|
148
|
+
|
|
149
|
+
Options for 'generate':
|
|
150
|
+
--output <dir> Output directory for generated test files (required)
|
|
151
|
+
--tag <tag> Generate only for endpoints with this tag
|
|
152
|
+
--uncovered-only Skip endpoints already covered by existing tests
|
|
153
|
+
|
|
154
|
+
Options for 'guide':
|
|
155
|
+
--tests-dir <dir> Filter to uncovered endpoints only
|
|
156
|
+
--tag <tag> Generate only for endpoints with this tag
|
|
157
|
+
|
|
91
158
|
Options for 'coverage':
|
|
92
159
|
--api <name> Use API collection (auto-resolves spec and tests dir)
|
|
93
160
|
--spec <path> Path to OpenAPI spec (required unless --api used)
|
|
@@ -95,11 +162,11 @@ Options for 'coverage':
|
|
|
95
162
|
--fail-on-coverage N Exit 1 when coverage percentage is below N (0–100)
|
|
96
163
|
--run-id <number> Cross-reference with a test run for pass/fail/5xx breakdown
|
|
97
164
|
|
|
98
|
-
Options for 'serve':
|
|
165
|
+
Options for 'serve' / 'ui':
|
|
99
166
|
--port <port> Server port (default: 8080)
|
|
100
167
|
--host <host> Server host (default: 0.0.0.0)
|
|
101
|
-
--openapi <spec> Path to OpenAPI spec for Explorer
|
|
102
168
|
--db <path> Path to SQLite database file (default: zond.db)
|
|
169
|
+
--open Open dashboard in browser after starting
|
|
103
170
|
--watch Enable dev mode with hot reload (auto-refresh browser on file changes)
|
|
104
171
|
|
|
105
172
|
Options for 'ci init':
|
|
@@ -109,6 +176,7 @@ Options for 'ci init':
|
|
|
109
176
|
--force Overwrite existing CI config
|
|
110
177
|
|
|
111
178
|
General:
|
|
179
|
+
--json Output in JSON envelope format (available for all commands)
|
|
112
180
|
--help, -h Show this help
|
|
113
181
|
--version, -v Show version`);
|
|
114
182
|
}
|
|
@@ -117,6 +185,7 @@ const VALID_REPORTERS = new Set<string>(["console", "json", "junit"]);
|
|
|
117
185
|
|
|
118
186
|
async function main(): Promise<number> {
|
|
119
187
|
const { command, positional, flags } = parseArgs(process.argv);
|
|
188
|
+
const jsonFlag = flags["json"] === true;
|
|
120
189
|
|
|
121
190
|
// Help
|
|
122
191
|
if (command === "help" || command === "--help" || flags["help"] === true || flags["h"] === true) {
|
|
@@ -205,6 +274,7 @@ async function main(): Promise<number> {
|
|
|
205
274
|
tag: tags.length > 0 ? tags : undefined,
|
|
206
275
|
envVars: envVarValues.length > 0 ? envVarValues : undefined,
|
|
207
276
|
dryRun: flags["dry-run"] === true,
|
|
277
|
+
json: jsonFlag,
|
|
208
278
|
});
|
|
209
279
|
}
|
|
210
280
|
|
|
@@ -215,9 +285,10 @@ async function main(): Promise<number> {
|
|
|
215
285
|
return 2;
|
|
216
286
|
}
|
|
217
287
|
|
|
218
|
-
return validateCommand({ path });
|
|
288
|
+
return validateCommand({ path, json: jsonFlag });
|
|
219
289
|
}
|
|
220
290
|
|
|
291
|
+
case "ui":
|
|
221
292
|
case "serve": {
|
|
222
293
|
const portRaw = flags["port"];
|
|
223
294
|
let port: number | undefined;
|
|
@@ -231,9 +302,9 @@ async function main(): Promise<number> {
|
|
|
231
302
|
return serveCommand({
|
|
232
303
|
port,
|
|
233
304
|
host: typeof flags["host"] === "string" ? flags["host"] : undefined,
|
|
234
|
-
openapiSpec: typeof flags["openapi"] === "string" ? flags["openapi"] : undefined,
|
|
235
305
|
dbPath: typeof flags["db"] === "string" ? flags["db"] : undefined,
|
|
236
306
|
watch: flags["watch"] === true,
|
|
307
|
+
open: command === "ui" || flags["open"] === true,
|
|
237
308
|
});
|
|
238
309
|
}
|
|
239
310
|
|
|
@@ -257,6 +328,7 @@ async function main(): Promise<number> {
|
|
|
257
328
|
platform,
|
|
258
329
|
force: flags["force"] === true,
|
|
259
330
|
dir: typeof flags["dir"] === "string" ? flags["dir"] : undefined,
|
|
331
|
+
json: jsonFlag,
|
|
260
332
|
});
|
|
261
333
|
}
|
|
262
334
|
|
|
@@ -304,7 +376,132 @@ async function main(): Promise<number> {
|
|
|
304
376
|
return 2;
|
|
305
377
|
}
|
|
306
378
|
}
|
|
307
|
-
return coverageCommand({ spec, tests, failOnCoverage, runId });
|
|
379
|
+
return coverageCommand({ spec, tests, failOnCoverage, runId, json: jsonFlag });
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
case "init": {
|
|
383
|
+
return initCommand({
|
|
384
|
+
name: typeof flags["name"] === "string" ? flags["name"] : undefined,
|
|
385
|
+
spec: typeof flags["spec"] === "string" ? flags["spec"] : positional[0],
|
|
386
|
+
baseUrl: typeof flags["base-url"] === "string" ? flags["base-url"] : undefined,
|
|
387
|
+
dir: typeof flags["dir"] === "string" ? flags["dir"] : undefined,
|
|
388
|
+
force: flags["force"] === true,
|
|
389
|
+
insecure: flags["insecure"] === true,
|
|
390
|
+
dbPath: typeof flags["db"] === "string" ? flags["db"] : undefined,
|
|
391
|
+
json: jsonFlag,
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
case "describe": {
|
|
396
|
+
const specPath = positional[0];
|
|
397
|
+
if (!specPath) {
|
|
398
|
+
printError("Missing spec path. Usage: zond describe <spec> [--compact | --method <M> --path <P>]");
|
|
399
|
+
return 2;
|
|
400
|
+
}
|
|
401
|
+
return describeCommand({
|
|
402
|
+
specPath,
|
|
403
|
+
compact: flags["compact"] === true,
|
|
404
|
+
method: typeof flags["method"] === "string" ? flags["method"] : undefined,
|
|
405
|
+
path: typeof flags["path"] === "string" ? flags["path"] : undefined,
|
|
406
|
+
json: jsonFlag,
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
case "db": {
|
|
411
|
+
const dbSub = positional[0];
|
|
412
|
+
if (!dbSub) {
|
|
413
|
+
printError("Missing subcommand. Usage: zond db <collections|runs|run|diagnose|compare> [args]");
|
|
414
|
+
return 2;
|
|
415
|
+
}
|
|
416
|
+
const limitRaw = flags["limit"];
|
|
417
|
+
let limit: number | undefined;
|
|
418
|
+
if (typeof limitRaw === "string") {
|
|
419
|
+
limit = parseInt(limitRaw, 10);
|
|
420
|
+
if (isNaN(limit) || limit <= 0) limit = undefined;
|
|
421
|
+
}
|
|
422
|
+
return dbCommand({
|
|
423
|
+
subcommand: dbSub,
|
|
424
|
+
positional: positional.slice(1),
|
|
425
|
+
limit,
|
|
426
|
+
verbose: flags["verbose"] === true,
|
|
427
|
+
dbPath: typeof flags["db"] === "string" ? flags["db"] : undefined,
|
|
428
|
+
json: jsonFlag,
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
case "request": {
|
|
433
|
+
const method = positional[0];
|
|
434
|
+
const url = positional[1];
|
|
435
|
+
if (!method || !url) {
|
|
436
|
+
printError("Missing arguments. Usage: zond request <METHOD> <URL> [--header H] [--body JSON]");
|
|
437
|
+
return 2;
|
|
438
|
+
}
|
|
439
|
+
// Collect all --header flags
|
|
440
|
+
const headerValues: string[] = [];
|
|
441
|
+
const rawArgs = process.argv.slice(2);
|
|
442
|
+
for (let i = 0; i < rawArgs.length; i++) {
|
|
443
|
+
const arg = rawArgs[i]!;
|
|
444
|
+
if (arg === "--header" && rawArgs[i + 1]) {
|
|
445
|
+
headerValues.push(rawArgs[i + 1]!);
|
|
446
|
+
i++;
|
|
447
|
+
} else if (arg.startsWith("--header=")) {
|
|
448
|
+
headerValues.push(arg.slice("--header=".length));
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const timeoutRaw = flags["timeout"];
|
|
453
|
+
let timeout: number | undefined;
|
|
454
|
+
if (typeof timeoutRaw === "string") {
|
|
455
|
+
timeout = parseInt(timeoutRaw, 10);
|
|
456
|
+
if (isNaN(timeout) || timeout <= 0) timeout = undefined;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
return requestCommand({
|
|
460
|
+
method,
|
|
461
|
+
url,
|
|
462
|
+
headers: headerValues.length > 0 ? headerValues : undefined,
|
|
463
|
+
body: typeof flags["body"] === "string" ? flags["body"] : undefined,
|
|
464
|
+
timeout,
|
|
465
|
+
env: typeof flags["env"] === "string" ? flags["env"] : undefined,
|
|
466
|
+
api: typeof flags["api"] === "string" ? flags["api"] : undefined,
|
|
467
|
+
jsonPath: typeof flags["json-path"] === "string" ? flags["json-path"] : undefined,
|
|
468
|
+
dbPath: typeof flags["db"] === "string" ? flags["db"] : undefined,
|
|
469
|
+
json: jsonFlag,
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
case "generate": {
|
|
474
|
+
const specPath = positional[0];
|
|
475
|
+
if (!specPath) {
|
|
476
|
+
printError("Missing spec path. Usage: zond generate <spec> --output <dir> [--tag <tag>] [--uncovered-only] [--json]");
|
|
477
|
+
return 2;
|
|
478
|
+
}
|
|
479
|
+
const output = typeof flags["output"] === "string" ? flags["output"] : undefined;
|
|
480
|
+
if (!output) {
|
|
481
|
+
printError("Missing --output <dir>. Usage: zond generate <spec> --output <dir>");
|
|
482
|
+
return 2;
|
|
483
|
+
}
|
|
484
|
+
return generateCommand({
|
|
485
|
+
specPath,
|
|
486
|
+
output,
|
|
487
|
+
tag: typeof flags["tag"] === "string" ? flags["tag"] : undefined,
|
|
488
|
+
uncoveredOnly: flags["uncovered-only"] === true,
|
|
489
|
+
json: jsonFlag,
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
case "guide": {
|
|
494
|
+
const specPath = positional[0];
|
|
495
|
+
if (!specPath) {
|
|
496
|
+
printError("Missing spec path. Usage: zond guide <spec> [--tests-dir <dir>] [--tag <tag>]");
|
|
497
|
+
return 2;
|
|
498
|
+
}
|
|
499
|
+
return guideCommand({
|
|
500
|
+
specPath,
|
|
501
|
+
testsDir: typeof flags["tests-dir"] === "string" ? flags["tests-dir"] : undefined,
|
|
502
|
+
tag: typeof flags["tag"] === "string" ? flags["tag"] : undefined,
|
|
503
|
+
json: jsonFlag,
|
|
504
|
+
});
|
|
308
505
|
}
|
|
309
506
|
|
|
310
507
|
default: {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface JsonEnvelope<T = unknown> {
|
|
2
|
+
ok: boolean;
|
|
3
|
+
command: string;
|
|
4
|
+
data: T;
|
|
5
|
+
warnings: string[];
|
|
6
|
+
errors: string[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function jsonOk<T>(command: string, data: T, warnings?: string[]): JsonEnvelope<T> {
|
|
10
|
+
return { ok: true, command, data, warnings: warnings ?? [], errors: [] };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function jsonError(command: string, errors: string[], warnings?: string[]): JsonEnvelope<null> {
|
|
14
|
+
return { ok: false, command, data: null, warnings: warnings ?? [], errors };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function printJson(envelope: JsonEnvelope): void {
|
|
18
|
+
process.stdout.write(JSON.stringify(envelope, null, 2) + "\n");
|
|
19
|
+
}
|