@kirrosh/zond 0.13.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/CHANGELOG.md +7 -0
- package/README.md +1 -1
- package/package.json +4 -7
- 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 +50 -77
- 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 +213 -215
- 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/index.ts +0 -3
- 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/cli/commands/add-api.ts +0 -53
- package/src/cli/commands/ai-generate.ts +0 -106
- package/src/cli/commands/chat.ts +0 -43
- package/src/cli/commands/collections.ts +0 -41
- package/src/cli/commands/compare.ts +0 -129
- package/src/cli/commands/doctor.ts +0 -127
- package/src/cli/commands/runs.ts +0 -108
- package/src/cli/commands/update.ts +0 -142
- package/src/core/agent/agent-loop.ts +0 -116
- package/src/core/agent/context-manager.ts +0 -41
- package/src/core/agent/system-prompt.ts +0 -27
- package/src/core/agent/tools/diagnose-failure.ts +0 -51
- package/src/core/agent/tools/index.ts +0 -42
- package/src/core/agent/tools/query-results.ts +0 -40
- package/src/core/agent/tools/run-tests.ts +0 -38
- package/src/core/agent/tools/send-request.ts +0 -44
- package/src/core/agent/types.ts +0 -22
- package/src/core/generator/ai/ai-generator.ts +0 -61
- package/src/core/generator/ai/llm-client.ts +0 -159
- package/src/core/generator/ai/output-parser.ts +0 -307
- package/src/core/generator/ai/prompt-builder.ts +0 -153
- package/src/core/generator/ai/types.ts +0 -56
- 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/tui/chat-ui.ts +0 -150
package/src/cli/index.ts
CHANGED
|
@@ -3,18 +3,15 @@
|
|
|
3
3
|
import { runCommand } from "./commands/run.ts";
|
|
4
4
|
import { validateCommand } from "./commands/validate.ts";
|
|
5
5
|
import { serveCommand } from "./commands/serve.ts";
|
|
6
|
-
import { collectionsCommand } from "./commands/collections.ts";
|
|
7
|
-
import { aiGenerateCommand } from "./commands/ai-generate.ts";
|
|
8
6
|
import { mcpCommand } from "./commands/mcp.ts";
|
|
9
|
-
import { initCommand } from "./commands/init.ts";
|
|
10
|
-
import { updateCommand } from "./commands/update.ts";
|
|
11
|
-
import { chatCommand } from "./commands/chat.ts";
|
|
12
|
-
import { runsCommand } from "./commands/runs.ts";
|
|
13
7
|
import { coverageCommand } from "./commands/coverage.ts";
|
|
14
|
-
import { doctorCommand } from "./commands/doctor.ts";
|
|
15
|
-
import { addApiCommand } from "./commands/add-api.ts";
|
|
16
8
|
import { ciInitCommand } from "./commands/ci-init.ts";
|
|
17
|
-
import {
|
|
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";
|
|
18
15
|
import { printError } from "./output.ts";
|
|
19
16
|
import { getRuntimeInfo } from "./runtime.ts";
|
|
20
17
|
import { getDb } from "../db/schema.ts";
|
|
@@ -30,6 +27,23 @@ export interface ParsedArgs {
|
|
|
30
27
|
flags: Record<string, string | boolean>;
|
|
31
28
|
}
|
|
32
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
|
+
|
|
33
47
|
export function parseArgs(argv: string[]): ParsedArgs {
|
|
34
48
|
// argv: [bunPath, scriptPath, ...userArgs]
|
|
35
49
|
const args = argv.slice(2);
|
|
@@ -45,12 +59,15 @@ export function parseArgs(argv: string[]): ParsedArgs {
|
|
|
45
59
|
const eqIndex = arg.indexOf("=");
|
|
46
60
|
if (eqIndex !== -1) {
|
|
47
61
|
// --flag=value
|
|
48
|
-
|
|
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;
|
|
49
66
|
} else {
|
|
50
67
|
const key = arg.slice(2);
|
|
51
68
|
const next = args[i + 1];
|
|
52
69
|
if (next !== undefined && !next.startsWith("-")) {
|
|
53
|
-
flags[key] = next;
|
|
70
|
+
flags[key] = API_PATH_FLAGS.has(key) ? stripMsysPath(next) : next;
|
|
54
71
|
i++;
|
|
55
72
|
} else {
|
|
56
73
|
flags[key] = true;
|
|
@@ -75,51 +92,20 @@ function printUsage(): void {
|
|
|
75
92
|
console.log(`zond - API Testing Platform
|
|
76
93
|
|
|
77
94
|
Usage:
|
|
78
|
-
zond add-api <name> Register a new API (collection)
|
|
79
95
|
zond run <path> Run API tests
|
|
80
96
|
zond validate <path> Validate test files without running
|
|
81
|
-
zond
|
|
82
|
-
zond
|
|
83
|
-
zond
|
|
84
|
-
zond
|
|
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
|
|
85
104
|
zond serve Start web dashboard
|
|
86
|
-
zond
|
|
87
|
-
zond ci init Generate CI/CD workflow (GitHub Actions, GitLab CI)
|
|
105
|
+
zond ui Alias for 'serve --open' (start dashboard & open browser)
|
|
88
106
|
zond mcp Start MCP server (stdio transport for AI agents)
|
|
89
107
|
--dir <path> Set working directory (relative paths resolve here)
|
|
90
|
-
zond
|
|
91
|
-
zond compare <runA> <runB> Compare two test runs (regressions/fixes)
|
|
92
|
-
zond doctor Run diagnostic checks
|
|
93
|
-
zond update Update to latest version
|
|
94
|
-
|
|
95
|
-
Options for 'add-api':
|
|
96
|
-
--spec <path-or-url> OpenAPI spec (extracts base_url from servers[0])
|
|
97
|
-
--dir <directory> Base directory (default: ./apis/<name>/)
|
|
98
|
-
--env key=value Set environment variables (repeatable)
|
|
99
|
-
--insecure Skip TLS verification (self-signed certs)
|
|
100
|
-
|
|
101
|
-
Options for 'chat':
|
|
102
|
-
--provider <name> LLM provider: ollama, openai, anthropic, custom (default: ollama)
|
|
103
|
-
--model <name> Model name (default: provider-specific)
|
|
104
|
-
--api-key <key> API key (or set ZOND_AI_KEY env var)
|
|
105
|
-
--base-url <url> Provider base URL override
|
|
106
|
-
--safe Only allow running GET tests (read-only mode)
|
|
107
|
-
|
|
108
|
-
Options for 'runs':
|
|
109
|
-
runs List recent test runs
|
|
110
|
-
runs <id> Show run details with step results
|
|
111
|
-
--limit <n> Number of runs to show (default: 20)
|
|
112
|
-
|
|
113
|
-
Options for 'compare':
|
|
114
|
-
compare <runA> <runB> Compare two run IDs
|
|
115
|
-
Exit code 1 if regressions found, 0 otherwise
|
|
116
|
-
|
|
117
|
-
Options for 'coverage':
|
|
118
|
-
--api <name> Use API collection (auto-resolves spec and tests dir)
|
|
119
|
-
--spec <path> Path to OpenAPI spec (required unless --api used)
|
|
120
|
-
--tests <dir> Path to test files directory (required unless --api used)
|
|
121
|
-
--fail-on-coverage N Exit 1 when coverage percentage is below N (0–100)
|
|
122
|
-
--run-id <number> Cross-reference with a test run for pass/fail/5xx breakdown
|
|
108
|
+
zond ci init Generate CI/CD workflow (GitHub Actions, GitLab CI)
|
|
123
109
|
|
|
124
110
|
Options for 'run':
|
|
125
111
|
--dry-run Show requests without sending them (exit code always 0)
|
|
@@ -135,21 +121,52 @@ Options for 'run':
|
|
|
135
121
|
--safe Run only GET tests (read-only, safe mode)
|
|
136
122
|
--tag <tag> Filter suites by tag (repeatable, comma-separated, OR logic)
|
|
137
123
|
|
|
138
|
-
Options for '
|
|
139
|
-
--
|
|
140
|
-
--
|
|
141
|
-
--
|
|
142
|
-
--
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
--
|
|
146
|
-
--
|
|
147
|
-
|
|
148
|
-
|
|
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
|
+
|
|
158
|
+
Options for 'coverage':
|
|
159
|
+
--api <name> Use API collection (auto-resolves spec and tests dir)
|
|
160
|
+
--spec <path> Path to OpenAPI spec (required unless --api used)
|
|
161
|
+
--tests <dir> Path to test files directory (required unless --api used)
|
|
162
|
+
--fail-on-coverage N Exit 1 when coverage percentage is below N (0–100)
|
|
163
|
+
--run-id <number> Cross-reference with a test run for pass/fail/5xx breakdown
|
|
164
|
+
|
|
165
|
+
Options for 'serve' / 'ui':
|
|
149
166
|
--port <port> Server port (default: 8080)
|
|
150
167
|
--host <host> Server host (default: 0.0.0.0)
|
|
151
|
-
--openapi <spec> Path to OpenAPI spec for Explorer
|
|
152
168
|
--db <path> Path to SQLite database file (default: zond.db)
|
|
169
|
+
--open Open dashboard in browser after starting
|
|
153
170
|
--watch Enable dev mode with hot reload (auto-refresh browser on file changes)
|
|
154
171
|
|
|
155
172
|
Options for 'ci init':
|
|
@@ -159,6 +176,7 @@ Options for 'ci init':
|
|
|
159
176
|
--force Overwrite existing CI config
|
|
160
177
|
|
|
161
178
|
General:
|
|
179
|
+
--json Output in JSON envelope format (available for all commands)
|
|
162
180
|
--help, -h Show this help
|
|
163
181
|
--version, -v Show version`);
|
|
164
182
|
}
|
|
@@ -167,6 +185,7 @@ const VALID_REPORTERS = new Set<string>(["console", "json", "junit"]);
|
|
|
167
185
|
|
|
168
186
|
async function main(): Promise<number> {
|
|
169
187
|
const { command, positional, flags } = parseArgs(process.argv);
|
|
188
|
+
const jsonFlag = flags["json"] === true;
|
|
170
189
|
|
|
171
190
|
// Help
|
|
172
191
|
if (command === "help" || command === "--help" || flags["help"] === true || flags["h"] === true) {
|
|
@@ -186,35 +205,6 @@ async function main(): Promise<number> {
|
|
|
186
205
|
}
|
|
187
206
|
|
|
188
207
|
switch (command) {
|
|
189
|
-
case "add-api": {
|
|
190
|
-
const name = positional[0];
|
|
191
|
-
if (!name) {
|
|
192
|
-
printError("Missing name argument. Usage: zond add-api <name> [--spec <path>] [--dir <dir>]");
|
|
193
|
-
return 2;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// Collect all --env flags (parseArgs only stores last one, so re-parse)
|
|
197
|
-
const envValues: string[] = [];
|
|
198
|
-
const rawArgs = process.argv.slice(2);
|
|
199
|
-
for (let i = 0; i < rawArgs.length; i++) {
|
|
200
|
-
if (rawArgs[i] === "--env" && rawArgs[i + 1] && rawArgs[i + 1]!.includes("=")) {
|
|
201
|
-
envValues.push(rawArgs[i + 1]!);
|
|
202
|
-
i++;
|
|
203
|
-
} else if (rawArgs[i]?.startsWith("--env=") && rawArgs[i]!.slice(6).includes("=")) {
|
|
204
|
-
envValues.push(rawArgs[i]!.slice(6));
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
return addApiCommand({
|
|
209
|
-
name,
|
|
210
|
-
spec: typeof flags["spec"] === "string" ? flags["spec"] : undefined,
|
|
211
|
-
dir: typeof flags["dir"] === "string" ? flags["dir"] : undefined,
|
|
212
|
-
envPairs: envValues.length > 0 ? envValues : undefined,
|
|
213
|
-
dbPath: typeof flags["db"] === "string" ? flags["db"] : undefined,
|
|
214
|
-
insecure: flags["insecure"] === true,
|
|
215
|
-
});
|
|
216
|
-
}
|
|
217
|
-
|
|
218
208
|
case "run": {
|
|
219
209
|
let path = positional[0];
|
|
220
210
|
const apiFlag = typeof flags["api"] === "string" ? flags["api"] : undefined;
|
|
@@ -284,6 +274,7 @@ async function main(): Promise<number> {
|
|
|
284
274
|
tag: tags.length > 0 ? tags : undefined,
|
|
285
275
|
envVars: envVarValues.length > 0 ? envVarValues : undefined,
|
|
286
276
|
dryRun: flags["dry-run"] === true,
|
|
277
|
+
json: jsonFlag,
|
|
287
278
|
});
|
|
288
279
|
}
|
|
289
280
|
|
|
@@ -294,54 +285,10 @@ async function main(): Promise<number> {
|
|
|
294
285
|
return 2;
|
|
295
286
|
}
|
|
296
287
|
|
|
297
|
-
return validateCommand({ path });
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
case "ai-generate": {
|
|
301
|
-
let from = flags["from"] as string | undefined;
|
|
302
|
-
let output = typeof flags["output"] === "string" ? flags["output"] : undefined;
|
|
303
|
-
const aiGenApiFlag = typeof flags["api"] === "string" ? flags["api"] : undefined;
|
|
304
|
-
|
|
305
|
-
// Resolve --api to spec and output dir from collection
|
|
306
|
-
if (aiGenApiFlag) {
|
|
307
|
-
try {
|
|
308
|
-
getDb(typeof flags["db"] === "string" ? flags["db"] : undefined);
|
|
309
|
-
const col = findCollectionByNameOrId(aiGenApiFlag);
|
|
310
|
-
if (!col) { printError(`API '${aiGenApiFlag}' not found`); return 1; }
|
|
311
|
-
if (!from && col.openapi_spec) from = col.openapi_spec;
|
|
312
|
-
if (!output && col.test_path) output = col.test_path;
|
|
313
|
-
} catch (err) {
|
|
314
|
-
printError(`Failed to resolve --api: ${(err as Error).message}`);
|
|
315
|
-
return 2;
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
if (typeof from !== "string") {
|
|
320
|
-
printError("Missing --from <spec>. Usage: zond ai-generate --from <spec> --prompt \"...\"");
|
|
321
|
-
return 2;
|
|
322
|
-
}
|
|
323
|
-
const prompt = flags["prompt"];
|
|
324
|
-
if (typeof prompt !== "string") {
|
|
325
|
-
printError("Missing --prompt <text>. Usage: zond ai-generate --from <spec> --prompt \"...\"");
|
|
326
|
-
return 2;
|
|
327
|
-
}
|
|
328
|
-
return aiGenerateCommand({
|
|
329
|
-
from,
|
|
330
|
-
prompt,
|
|
331
|
-
provider: typeof flags["provider"] === "string" ? flags["provider"] : "ollama",
|
|
332
|
-
model: typeof flags["model"] === "string" ? flags["model"] : undefined,
|
|
333
|
-
apiKey: typeof flags["api-key"] === "string" ? flags["api-key"] : undefined,
|
|
334
|
-
baseUrl: typeof flags["base-url"] === "string" ? flags["base-url"] : undefined,
|
|
335
|
-
output,
|
|
336
|
-
});
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
case "collections": {
|
|
340
|
-
return collectionsCommand(
|
|
341
|
-
typeof flags["db"] === "string" ? flags["db"] : undefined,
|
|
342
|
-
);
|
|
288
|
+
return validateCommand({ path, json: jsonFlag });
|
|
343
289
|
}
|
|
344
290
|
|
|
291
|
+
case "ui":
|
|
345
292
|
case "serve": {
|
|
346
293
|
const portRaw = flags["port"];
|
|
347
294
|
let port: number | undefined;
|
|
@@ -355,15 +302,9 @@ async function main(): Promise<number> {
|
|
|
355
302
|
return serveCommand({
|
|
356
303
|
port,
|
|
357
304
|
host: typeof flags["host"] === "string" ? flags["host"] : undefined,
|
|
358
|
-
openapiSpec: typeof flags["openapi"] === "string" ? flags["openapi"] : undefined,
|
|
359
305
|
dbPath: typeof flags["db"] === "string" ? flags["db"] : undefined,
|
|
360
306
|
watch: flags["watch"] === true,
|
|
361
|
-
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
case "init": {
|
|
365
|
-
return initCommand({
|
|
366
|
-
force: flags["force"] === true,
|
|
307
|
+
open: command === "ui" || flags["open"] === true,
|
|
367
308
|
});
|
|
368
309
|
}
|
|
369
310
|
|
|
@@ -374,49 +315,6 @@ async function main(): Promise<number> {
|
|
|
374
315
|
});
|
|
375
316
|
}
|
|
376
317
|
|
|
377
|
-
case "chat": {
|
|
378
|
-
return chatCommand({
|
|
379
|
-
provider: typeof flags["provider"] === "string" ? flags["provider"] : undefined,
|
|
380
|
-
model: typeof flags["model"] === "string" ? flags["model"] : undefined,
|
|
381
|
-
apiKey: typeof flags["api-key"] === "string" ? flags["api-key"] : undefined,
|
|
382
|
-
baseUrl: typeof flags["base-url"] === "string" ? flags["base-url"] : undefined,
|
|
383
|
-
safe: flags["safe"] === true,
|
|
384
|
-
dbPath: typeof flags["db"] === "string" ? flags["db"] : undefined,
|
|
385
|
-
});
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
case "update": {
|
|
389
|
-
return updateCommand({ force: flags["force"] === true });
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
case "runs": {
|
|
393
|
-
const idRaw = positional[0];
|
|
394
|
-
let runId: number | undefined;
|
|
395
|
-
if (idRaw) {
|
|
396
|
-
runId = parseInt(idRaw, 10);
|
|
397
|
-
if (isNaN(runId)) {
|
|
398
|
-
printError(`Invalid run ID: ${idRaw}`);
|
|
399
|
-
return 2;
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
const limitRaw = flags["limit"];
|
|
404
|
-
let limit: number | undefined;
|
|
405
|
-
if (typeof limitRaw === "string") {
|
|
406
|
-
limit = parseInt(limitRaw, 10);
|
|
407
|
-
if (isNaN(limit) || limit <= 0) {
|
|
408
|
-
printError(`Invalid limit value: ${limitRaw}`);
|
|
409
|
-
return 2;
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
return runsCommand({
|
|
414
|
-
runId,
|
|
415
|
-
limit,
|
|
416
|
-
dbPath: typeof flags["db"] === "string" ? flags["db"] : undefined,
|
|
417
|
-
});
|
|
418
|
-
}
|
|
419
|
-
|
|
420
318
|
case "ci": {
|
|
421
319
|
const ciSub = positional[0];
|
|
422
320
|
if (ciSub !== "init") {
|
|
@@ -430,32 +328,7 @@ async function main(): Promise<number> {
|
|
|
430
328
|
platform,
|
|
431
329
|
force: flags["force"] === true,
|
|
432
330
|
dir: typeof flags["dir"] === "string" ? flags["dir"] : undefined,
|
|
433
|
-
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
case "compare": {
|
|
437
|
-
const rawA = positional[0];
|
|
438
|
-
const rawB = positional[1];
|
|
439
|
-
if (!rawA || !rawB) {
|
|
440
|
-
printError("Usage: zond compare <runA> <runB>");
|
|
441
|
-
return 2;
|
|
442
|
-
}
|
|
443
|
-
const runA = parseInt(rawA, 10);
|
|
444
|
-
const runB = parseInt(rawB, 10);
|
|
445
|
-
if (isNaN(runA) || isNaN(runB)) {
|
|
446
|
-
printError("Run IDs must be integers");
|
|
447
|
-
return 2;
|
|
448
|
-
}
|
|
449
|
-
return compareCommand({
|
|
450
|
-
runA,
|
|
451
|
-
runB,
|
|
452
|
-
dbPath: typeof flags["db"] === "string" ? flags["db"] : undefined,
|
|
453
|
-
});
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
case "doctor": {
|
|
457
|
-
return doctorCommand({
|
|
458
|
-
dbPath: typeof flags["db"] === "string" ? flags["db"] : undefined,
|
|
331
|
+
json: jsonFlag,
|
|
459
332
|
});
|
|
460
333
|
}
|
|
461
334
|
|
|
@@ -503,7 +376,132 @@ async function main(): Promise<number> {
|
|
|
503
376
|
return 2;
|
|
504
377
|
}
|
|
505
378
|
}
|
|
506
|
-
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
|
+
});
|
|
507
505
|
}
|
|
508
506
|
|
|
509
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
|
+
}
|