@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.
Files changed (36) hide show
  1. package/README.md +1 -1
  2. package/package.json +4 -3
  3. package/src/cli/commands/ci-init.ts +12 -1
  4. package/src/cli/commands/coverage.ts +21 -1
  5. package/src/cli/commands/db.ts +121 -0
  6. package/src/cli/commands/describe.ts +60 -0
  7. package/src/cli/commands/generate.ts +127 -0
  8. package/src/cli/commands/guide.ts +127 -0
  9. package/src/cli/commands/init.ts +57 -0
  10. package/src/cli/commands/request.ts +57 -0
  11. package/src/cli/commands/run.ts +53 -10
  12. package/src/cli/commands/serve.ts +62 -3
  13. package/src/cli/commands/validate.ts +18 -2
  14. package/src/cli/index.ts +204 -7
  15. package/src/cli/json-envelope.ts +19 -0
  16. package/src/core/diagnostics/db-analysis.ts +351 -0
  17. package/src/core/diagnostics/failure-hints.ts +1 -0
  18. package/src/core/generator/data-factory.ts +19 -8
  19. package/src/core/generator/describe.ts +250 -0
  20. package/src/core/generator/guide-builder.ts +20 -0
  21. package/src/core/generator/suite-generator.ts +133 -20
  22. package/src/core/runner/executor.ts +1 -0
  23. package/src/core/runner/send-request.ts +94 -0
  24. package/src/core/runner/types.ts +1 -0
  25. package/src/db/queries.ts +4 -2
  26. package/src/db/schema.ts +11 -3
  27. package/src/mcp/descriptions.ts +0 -24
  28. package/src/mcp/server.ts +1 -8
  29. package/src/mcp/tools/describe-endpoint.ts +3 -218
  30. package/src/mcp/tools/query-db.ts +6 -222
  31. package/src/mcp/tools/run-tests.ts +1 -0
  32. package/src/mcp/tools/send-request.ts +15 -61
  33. package/src/web/views/suites-tab.ts +1 -1
  34. package/src/mcp/tools/generate-and-save.ts +0 -202
  35. package/src/mcp/tools/save-test-suite.ts +0 -218
  36. package/src/mcp/tools/set-work-dir.ts +0 -35
package/README.md CHANGED
@@ -17,7 +17,7 @@ Zond reads your OpenAPI spec and gives your AI agent everything it needs to test
17
17
 
18
18
  Then say: _"Safely cover the API from openapi.json with tests"_
19
19
 
20
- You get skills, slash commands, and 12 MCP tools in one package.
20
+ You get auto-validation hooks, CLI tools, and 8 MCP tools — all in one package.
21
21
 
22
22
  <details>
23
23
  <summary>Other installation methods (MCP, CLI, binary)</summary>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kirrosh/zond",
3
- "version": "0.14.0",
3
+ "version": "0.16.0",
4
4
  "description": "API testing platform — define tests in YAML, run from CLI or WebUI, generate from OpenAPI specs",
5
5
  "license": "MIT",
6
6
  "module": "index.ts",
@@ -26,12 +26,13 @@
26
26
  "scripts": {
27
27
  "zond": "bun run src/cli/index.ts",
28
28
  "test": "bun run test:unit && bun run test:mocked",
29
- "test:unit": "bun test tests/db/ tests/parser/ tests/runner/ tests/generator/ tests/core/ tests/cli/args.test.ts tests/cli/ci-init.test.ts tests/cli/commands.test.ts tests/cli/safe-run.test.ts tests/integration/ tests/web/ tests/mcp/tools.test.ts tests/mcp/save-test-suite.test.ts tests/reporter/ tests/version-sync.test.ts",
29
+ "test:unit": "bun test tests/db/ tests/parser/ tests/runner/ tests/generator/ tests/core/ tests/cli/args.test.ts tests/cli/ci-init.test.ts tests/cli/commands.test.ts tests/cli/safe-run.test.ts tests/cli/json-envelope.test.ts tests/cli/describe.test.ts tests/cli/db.test.ts tests/cli/request.test.ts tests/cli/init.test.ts tests/cli/guide.test.ts tests/integration/ tests/web/ tests/mcp/tools.test.ts tests/reporter/ tests/version-sync.test.ts",
30
30
  "test:mocked": "bun run scripts/run-mocked-tests.ts",
31
31
  "check": "tsc --noEmit --project tsconfig.json",
32
32
  "build": "bun build --compile src/cli/index.ts --outfile zond",
33
33
  "version:sync": "bun run scripts/sync-version.ts",
34
- "postversion": "bun run scripts/sync-version.ts && git add .claude-plugin/plugin.json .claude-plugin/marketplace.json"
34
+ "postversion": "bun run scripts/sync-version.ts && git add .claude-plugin/plugin.json",
35
+ "bench:api": "bun benchmarks/api/server.ts"
35
36
  },
36
37
  "devDependencies": {
37
38
  "@types/bun": "latest"
@@ -1,11 +1,13 @@
1
1
  import { resolve, dirname } from "path";
2
2
  import { existsSync, mkdirSync, writeFileSync } from "fs";
3
3
  import { printSuccess, printError } from "../output.ts";
4
+ import { jsonOk, printJson } from "../json-envelope.ts";
4
5
 
5
6
  export interface CiInitOptions {
6
7
  platform?: "github" | "gitlab";
7
8
  force: boolean;
8
9
  dir?: string;
10
+ json?: boolean;
9
11
  }
10
12
 
11
13
  const GH_ACTIONS_TEMPLATE = `name: API Tests
@@ -155,7 +157,16 @@ export async function ciInitCommand(options: CiInitOptions): Promise<number> {
155
157
  created = writeIfMissing(targetPath, GITLAB_CI_TEMPLATE, options.force);
156
158
  }
157
159
 
158
- if (created) {
160
+ if (options.json) {
161
+ const targetPath = platform === "github"
162
+ ? resolve(cwd, ".github/workflows/api-tests.yml")
163
+ : resolve(cwd, ".gitlab-ci.yml");
164
+ printJson(jsonOk("ci init", {
165
+ platform,
166
+ filePath: targetPath,
167
+ created,
168
+ }));
169
+ } else if (created) {
159
170
  printSuccess("CI workflow created. Commit and push to activate.");
160
171
  }
161
172
 
@@ -2,12 +2,14 @@ import { readOpenApiSpec, extractEndpoints, scanCoveredEndpoints, filterUncovere
2
2
  import { getDb } from "../../db/schema.ts";
3
3
  import { getResultsByRunId, getRunById } from "../../db/queries.ts";
4
4
  import { printError, printSuccess } from "../output.ts";
5
+ import { jsonOk, jsonError, printJson } from "../json-envelope.ts";
5
6
 
6
7
  export interface CoverageOptions {
7
8
  spec: string;
8
9
  tests: string;
9
10
  failOnCoverage?: number;
10
11
  runId?: number;
12
+ json?: boolean;
11
13
  }
12
14
 
13
15
  const RESET = "\x1b[0m";
@@ -145,12 +147,30 @@ export async function coverageCommand(options: CoverageOptions): Promise<number>
145
147
  }
146
148
  }
147
149
 
150
+ if (options.json) {
151
+ const coveredEndpoints = allEndpoints.filter(ep => !uncovered.includes(ep)).map(ep => `${ep.method} ${ep.path}`);
152
+ const uncoveredEndpoints = uncovered.map(ep => `${ep.method} ${ep.path}`);
153
+ printJson(jsonOk("coverage", {
154
+ covered: coveredCount,
155
+ uncovered: uncovered.length,
156
+ total: allEndpoints.length,
157
+ percentage,
158
+ coveredEndpoints,
159
+ uncoveredEndpoints,
160
+ }));
161
+ }
162
+
148
163
  if (options.failOnCoverage !== undefined) {
149
164
  return percentage < options.failOnCoverage ? 1 : 0;
150
165
  }
151
166
  return uncovered.length > 0 ? 1 : 0;
152
167
  } catch (err) {
153
- printError(err instanceof Error ? err.message : String(err));
168
+ const message = err instanceof Error ? err.message : String(err);
169
+ if (options.json) {
170
+ printJson(jsonError("coverage", [message]));
171
+ } else {
172
+ printError(message);
173
+ }
154
174
  return 2;
155
175
  }
156
176
  }
@@ -0,0 +1,121 @@
1
+ import { getCollections, getRuns, getRunDetail, diagnoseRun, compareRuns } from "../../core/diagnostics/db-analysis.ts";
2
+ import { printError } from "../output.ts";
3
+ import { jsonOk, jsonError, printJson } from "../json-envelope.ts";
4
+
5
+ export interface DbOptions {
6
+ subcommand: string;
7
+ positional: string[];
8
+ limit?: number;
9
+ verbose?: boolean;
10
+ dbPath?: string;
11
+ json?: boolean;
12
+ }
13
+
14
+ export async function dbCommand(options: DbOptions): Promise<number> {
15
+ const { subcommand, positional, json } = options;
16
+
17
+ try {
18
+ switch (subcommand) {
19
+ case "collections": {
20
+ const collections = getCollections(options.dbPath);
21
+ if (json) {
22
+ printJson(jsonOk("db collections", { collections }));
23
+ } else {
24
+ if (collections.length === 0) {
25
+ console.log("No collections found.");
26
+ } else {
27
+ for (const c of collections) {
28
+ console.log(`[${(c as any).id}] ${(c as any).name} — ${(c as any).test_path ?? "no test path"}`);
29
+ }
30
+ }
31
+ }
32
+ return 0;
33
+ }
34
+
35
+ case "runs": {
36
+ const runs = getRuns(options.limit ?? 10, options.dbPath);
37
+ if (json) {
38
+ printJson(jsonOk("db runs", { runs }));
39
+ } else {
40
+ if (runs.length === 0) {
41
+ console.log("No runs found.");
42
+ } else {
43
+ for (const r of runs) {
44
+ const run = r as any;
45
+ const status = run.failed > 0 ? "FAIL" : "PASS";
46
+ console.log(`#${run.id} ${status} ${run.passed}/${run.total} passed (${run.started_at})`);
47
+ }
48
+ }
49
+ }
50
+ return 0;
51
+ }
52
+
53
+ case "run": {
54
+ const id = parseInt(positional[0] ?? "", 10);
55
+ if (isNaN(id)) {
56
+ const msg = "Missing run ID. Usage: zond db run <id>";
57
+ if (json) printJson(jsonError("db run", [msg]));
58
+ else printError(msg);
59
+ return 2;
60
+ }
61
+ const detail = getRunDetail(id, options.verbose, options.dbPath);
62
+ if (json) {
63
+ printJson(jsonOk("db run", detail));
64
+ } else {
65
+ console.log(JSON.stringify(detail, null, 2));
66
+ }
67
+ return 0;
68
+ }
69
+
70
+ case "diagnose": {
71
+ const id = parseInt(positional[0] ?? "", 10);
72
+ if (isNaN(id)) {
73
+ const msg = "Missing run ID. Usage: zond db diagnose <id>";
74
+ if (json) printJson(jsonError("db diagnose", [msg]));
75
+ else printError(msg);
76
+ return 2;
77
+ }
78
+ const result = diagnoseRun(id, options.verbose, options.dbPath);
79
+ if (json) {
80
+ printJson(jsonOk("db diagnose", result));
81
+ } else {
82
+ console.log(JSON.stringify(result, null, 2));
83
+ }
84
+ return 0;
85
+ }
86
+
87
+ case "compare": {
88
+ const idA = parseInt(positional[0] ?? "", 10);
89
+ const idB = parseInt(positional[1] ?? "", 10);
90
+ if (isNaN(idA) || isNaN(idB)) {
91
+ const msg = "Missing run IDs. Usage: zond db compare <idA> <idB>";
92
+ if (json) printJson(jsonError("db compare", [msg]));
93
+ else printError(msg);
94
+ return 2;
95
+ }
96
+ const result = compareRuns(idA, idB, options.dbPath);
97
+ if (json) {
98
+ printJson(jsonOk("db compare", result));
99
+ } else {
100
+ console.log(JSON.stringify(result, null, 2));
101
+ }
102
+ return 0;
103
+ }
104
+
105
+ default: {
106
+ const msg = `Unknown db subcommand: ${subcommand}. Available: collections, runs, run, diagnose, compare`;
107
+ if (json) printJson(jsonError("db", [msg]));
108
+ else printError(msg);
109
+ return 2;
110
+ }
111
+ }
112
+ } catch (err) {
113
+ const message = err instanceof Error ? err.message : String(err);
114
+ if (json) {
115
+ printJson(jsonError(`db ${subcommand}`, [message]));
116
+ } else {
117
+ printError(message);
118
+ }
119
+ return 2;
120
+ }
121
+ }
@@ -0,0 +1,60 @@
1
+ import { describeEndpoint, describeCompact } from "../../core/generator/describe.ts";
2
+ import { printError } from "../output.ts";
3
+ import { jsonOk, jsonError, printJson } from "../json-envelope.ts";
4
+
5
+ export interface DescribeOptions {
6
+ specPath: string;
7
+ method?: string;
8
+ path?: string;
9
+ compact?: boolean;
10
+ json?: boolean;
11
+ }
12
+
13
+ export async function describeCommand(options: DescribeOptions): Promise<number> {
14
+ try {
15
+ if (options.compact) {
16
+ const endpoints = await describeCompact(options.specPath);
17
+
18
+ if (options.json) {
19
+ printJson(jsonOk("describe", { endpoints }));
20
+ } else {
21
+ for (const ep of endpoints) {
22
+ const parts = [ep.method.padEnd(7), ep.path];
23
+ if (ep.operationId) parts.push(`(${ep.operationId})`);
24
+ if (ep.summary) parts.push(`— ${ep.summary}`);
25
+ if (ep.deprecated) parts.push("[deprecated]");
26
+ console.log(parts.join(" "));
27
+ }
28
+ console.log(`\n${endpoints.length} endpoint(s)`);
29
+ }
30
+ return 0;
31
+ }
32
+
33
+ if (!options.method || !options.path) {
34
+ const msg = "Missing --method and --path. Use --compact for all endpoints, or specify --method and --path for one.";
35
+ if (options.json) {
36
+ printJson(jsonError("describe", [msg]));
37
+ } else {
38
+ printError(msg);
39
+ }
40
+ return 2;
41
+ }
42
+
43
+ const result = await describeEndpoint(options.specPath, options.method, options.path);
44
+
45
+ if (options.json) {
46
+ printJson(jsonOk("describe", result));
47
+ } else {
48
+ console.log(JSON.stringify(result, null, 2));
49
+ }
50
+ return 0;
51
+ } catch (err) {
52
+ const message = err instanceof Error ? err.message : String(err);
53
+ if (options.json) {
54
+ printJson(jsonError("describe", [message]));
55
+ } else {
56
+ printError(message);
57
+ }
58
+ return 2;
59
+ }
60
+ }
@@ -0,0 +1,127 @@
1
+ import { join, dirname } from "path";
2
+ import { mkdir } from "fs/promises";
3
+ import {
4
+ readOpenApiSpec,
5
+ extractEndpoints,
6
+ extractSecuritySchemes,
7
+ scanCoveredEndpoints,
8
+ filterUncoveredEndpoints,
9
+ serializeSuite,
10
+ } from "../../core/generator/index.ts";
11
+ import { generateSuites } from "../../core/generator/suite-generator.ts";
12
+ import { filterByTag } from "../../core/generator/chunker.ts";
13
+ import { parse } from "../../core/parser/yaml-parser.ts";
14
+ import { printError, printSuccess } from "../output.ts";
15
+ import { jsonOk, jsonError, printJson } from "../json-envelope.ts";
16
+
17
+ export interface GenerateOptions {
18
+ specPath: string;
19
+ output: string;
20
+ tag?: string;
21
+ uncoveredOnly?: boolean;
22
+ json?: boolean;
23
+ }
24
+
25
+ export async function generateCommand(options: GenerateOptions): Promise<number> {
26
+ try {
27
+ const doc = await readOpenApiSpec(options.specPath);
28
+ let endpoints = extractEndpoints(doc);
29
+ const securitySchemes = extractSecuritySchemes(doc);
30
+ const baseUrl = ((doc as any).servers?.[0]?.url) as string | undefined;
31
+ const warnings: string[] = [];
32
+
33
+ // Filter to uncovered only
34
+ if (options.uncoveredOnly) {
35
+ const covered = await scanCoveredEndpoints(options.output);
36
+ const before = endpoints.length;
37
+ endpoints = filterUncoveredEndpoints(endpoints, covered);
38
+ const coveredCount = before - endpoints.length;
39
+ if (coveredCount > 0) {
40
+ warnings.push(`Skipped ${coveredCount} already-covered endpoints`);
41
+ }
42
+ }
43
+
44
+ // Filter by tag
45
+ if (options.tag) {
46
+ endpoints = filterByTag(endpoints, options.tag);
47
+ }
48
+
49
+ if (endpoints.length === 0) {
50
+ if (options.json) {
51
+ printJson(jsonOk("generate", { files: [], message: "No endpoints to generate tests for" }, warnings));
52
+ } else {
53
+ console.log("No endpoints to generate tests for.");
54
+ }
55
+ return 0;
56
+ }
57
+
58
+ // Generate suites
59
+ const suites = generateSuites({ endpoints, securitySchemes });
60
+
61
+ // Ensure output directory exists
62
+ await mkdir(options.output, { recursive: true });
63
+
64
+ // Write suite files
65
+ const createdFiles: Array<{ file: string; suite: string; tests: number }> = [];
66
+
67
+ for (const suite of suites) {
68
+ const yaml = serializeSuite(suite);
69
+ const fileName = `${suite.fileStem ?? suite.name}.yaml`;
70
+ const filePath = join(options.output, fileName);
71
+ await Bun.write(filePath, yaml);
72
+ createdFiles.push({ file: filePath, suite: suite.name, tests: suite.tests.length });
73
+ }
74
+
75
+ // Create .env.yaml with base_url if it doesn't exist
76
+ const envPath = join(options.output, ".env.yaml");
77
+ const envFile = Bun.file(envPath);
78
+ if (!(await envFile.exists()) && baseUrl) {
79
+ await Bun.write(envPath, `base_url: ${baseUrl}\n`);
80
+ warnings.push(`Created ${envPath} with base_url from spec`);
81
+ }
82
+
83
+ // Validate generated files
84
+ const validationErrors: string[] = [];
85
+ try {
86
+ await parse(options.output);
87
+ } catch (err) {
88
+ validationErrors.push(err instanceof Error ? err.message : String(err));
89
+ }
90
+
91
+ if (validationErrors.length > 0) {
92
+ warnings.push(`Validation warnings: ${validationErrors.join("; ")}`);
93
+ }
94
+
95
+ // Output
96
+ const totalTests = createdFiles.reduce((sum, f) => sum + f.tests, 0);
97
+
98
+ if (options.json) {
99
+ printJson(jsonOk("generate", {
100
+ files: createdFiles,
101
+ totalSuites: suites.length,
102
+ totalTests,
103
+ outputDir: options.output,
104
+ }, warnings));
105
+ } else {
106
+ printSuccess(`Generated ${suites.length} suite(s) with ${totalTests} test(s) in ${options.output}`);
107
+ for (const f of createdFiles) {
108
+ console.log(` ${f.file} (${f.tests} tests)`);
109
+ }
110
+ if (warnings.length > 0) {
111
+ for (const w of warnings) {
112
+ console.log(` ⚠ ${w}`);
113
+ }
114
+ }
115
+ }
116
+
117
+ return 0;
118
+ } catch (err) {
119
+ const message = err instanceof Error ? err.message : String(err);
120
+ if (options.json) {
121
+ printJson(jsonError("generate", [message]));
122
+ } else {
123
+ printError(message);
124
+ }
125
+ return 2;
126
+ }
127
+ }
@@ -0,0 +1,127 @@
1
+ import {
2
+ readOpenApiSpec,
3
+ extractEndpoints,
4
+ extractSecuritySchemes,
5
+ scanCoveredEndpoints,
6
+ filterUncoveredEndpoints,
7
+ } from "../../core/generator/index.ts";
8
+ import { compressEndpointsWithSchemas, buildGenerationGuide } from "../../core/generator/guide-builder.ts";
9
+ import { planChunks, filterByTag } from "../../core/generator/chunker.ts";
10
+ import { findCollectionBySpec } from "../../db/queries.ts";
11
+ import { printError } from "../output.ts";
12
+ import { jsonOk, jsonError, printJson } from "../json-envelope.ts";
13
+
14
+ export interface GuideOptions {
15
+ specPath: string;
16
+ testsDir?: string;
17
+ tag?: string;
18
+ json?: boolean;
19
+ }
20
+
21
+ export async function guideCommand(options: GuideOptions): Promise<number> {
22
+ try {
23
+ const doc = await readOpenApiSpec(options.specPath);
24
+ let endpoints = extractEndpoints(doc);
25
+ const securitySchemes = extractSecuritySchemes(doc);
26
+ const baseUrl = ((doc as any).servers?.[0]?.url) as string | undefined;
27
+ const title = (doc as any).info?.title as string | undefined;
28
+
29
+ let outputDir = options.testsDir;
30
+ if (!outputDir) {
31
+ try {
32
+ const collection = findCollectionBySpec(options.specPath);
33
+ outputDir = collection?.test_path ?? "./tests/";
34
+ } catch {
35
+ outputDir = "./tests/";
36
+ }
37
+ }
38
+
39
+ let coverageInfo: { covered: number; total: number; percentage: number } | undefined;
40
+ if (options.testsDir) {
41
+ const totalBefore = endpoints.length;
42
+ const covered = await scanCoveredEndpoints(options.testsDir);
43
+ const uncovered = filterUncoveredEndpoints(endpoints, covered);
44
+ const coveredCount = totalBefore - uncovered.length;
45
+ const percentage = totalBefore > 0 ? Math.round((coveredCount / totalBefore) * 100) : 100;
46
+ coverageInfo = { covered: coveredCount, total: totalBefore, percentage };
47
+ endpoints = uncovered;
48
+ }
49
+
50
+ if (endpoints.length === 0) {
51
+ if (options.json) {
52
+ printJson(jsonOk("guide", { fullyCovered: true, ...coverageInfo }));
53
+ } else {
54
+ console.log("All endpoints are covered.");
55
+ }
56
+ return 0;
57
+ }
58
+
59
+ if (options.tag) {
60
+ endpoints = filterByTag(endpoints, options.tag);
61
+ if (endpoints.length === 0) {
62
+ const msg = `No endpoints found for tag "${options.tag}"`;
63
+ if (options.json) printJson(jsonError("guide", [msg]));
64
+ else printError(msg);
65
+ return 1;
66
+ }
67
+ }
68
+
69
+ const plan = planChunks(endpoints);
70
+
71
+ if (plan.needsChunking && !options.tag) {
72
+ if (options.json) {
73
+ printJson(jsonOk("guide", {
74
+ mode: "plan",
75
+ title: title ?? "API",
76
+ totalEndpoints: plan.totalEndpoints,
77
+ chunks: plan.chunks,
78
+ ...(coverageInfo ? { coverage: coverageInfo } : {}),
79
+ }));
80
+ } else {
81
+ console.log(`API has ${plan.totalEndpoints} endpoints across ${plan.chunks.length} tags.`);
82
+ console.log("Generate per-tag with --tag <name>:\n");
83
+ for (const chunk of plan.chunks) {
84
+ console.log(` --tag ${chunk.tag} (${chunk.count} endpoints)`);
85
+ }
86
+ }
87
+ return 0;
88
+ }
89
+
90
+ const coverageHeader = coverageInfo
91
+ ? `## Coverage: ${coverageInfo.covered}/${coverageInfo.total} endpoints covered (${coverageInfo.percentage}%). Generating tests for ${endpoints.length} uncovered endpoints:`
92
+ : undefined;
93
+
94
+ const apiContext = compressEndpointsWithSchemas(endpoints, securitySchemes);
95
+ const guide = buildGenerationGuide({
96
+ title: options.tag ? `${title ?? "API"} — tag: ${options.tag}` : (title ?? "API"),
97
+ baseUrl,
98
+ apiContext,
99
+ outputDir,
100
+ securitySchemes,
101
+ endpointCount: endpoints.length,
102
+ coverageHeader,
103
+ includeFormat: true,
104
+ });
105
+
106
+ if (options.json) {
107
+ printJson(jsonOk("guide", {
108
+ title: title ?? "API",
109
+ endpointCount: endpoints.length,
110
+ outputDir,
111
+ guide,
112
+ ...(coverageInfo ? { coverage: coverageInfo } : {}),
113
+ }));
114
+ } else {
115
+ console.log(guide);
116
+ }
117
+ return 0;
118
+ } catch (err) {
119
+ const message = err instanceof Error ? err.message : String(err);
120
+ if (options.json) {
121
+ printJson(jsonError("guide", [message]));
122
+ } else {
123
+ printError(message);
124
+ }
125
+ return 2;
126
+ }
127
+ }
@@ -0,0 +1,57 @@
1
+ import { setupApi } from "../../core/setup-api.ts";
2
+ import { printError, printSuccess } from "../output.ts";
3
+ import { jsonOk, jsonError, printJson } from "../json-envelope.ts";
4
+
5
+ export interface InitOptions {
6
+ name?: string;
7
+ spec?: string;
8
+ baseUrl?: string;
9
+ dir?: string;
10
+ force?: boolean;
11
+ insecure?: boolean;
12
+ dbPath?: string;
13
+ json?: boolean;
14
+ }
15
+
16
+ export async function initCommand(options: InitOptions): Promise<number> {
17
+ try {
18
+ const envVars: Record<string, string> = {};
19
+ if (options.baseUrl) envVars.base_url = options.baseUrl;
20
+
21
+ const result = await setupApi({
22
+ name: options.name,
23
+ spec: options.spec,
24
+ dir: options.dir,
25
+ envVars: Object.keys(envVars).length > 0 ? envVars : undefined,
26
+ dbPath: options.dbPath,
27
+ force: options.force,
28
+ insecure: options.insecure,
29
+ });
30
+
31
+ if (options.json) {
32
+ printJson(jsonOk("init", {
33
+ collectionId: result.collectionId,
34
+ baseDir: result.baseDir,
35
+ testPath: result.testPath,
36
+ endpoints: result.specEndpoints,
37
+ warnings: result.warnings ?? [],
38
+ }, result.warnings));
39
+ } else {
40
+ printSuccess(`Created API '${options.name ?? "api"}' at ${result.baseDir} (${result.specEndpoints} endpoints)`);
41
+ if (result.warnings) {
42
+ for (const w of result.warnings) {
43
+ process.stderr.write(`Warning: ${w}\n`);
44
+ }
45
+ }
46
+ }
47
+ return 0;
48
+ } catch (err) {
49
+ const message = err instanceof Error ? err.message : String(err);
50
+ if (options.json) {
51
+ printJson(jsonError("init", [message]));
52
+ } else {
53
+ printError(message);
54
+ }
55
+ return 2;
56
+ }
57
+ }
@@ -0,0 +1,57 @@
1
+ import { sendAdHocRequest } from "../../core/runner/send-request.ts";
2
+ import { printError } from "../output.ts";
3
+ import { jsonOk, jsonError, printJson } from "../json-envelope.ts";
4
+
5
+ export interface RequestOptions {
6
+ method: string;
7
+ url: string;
8
+ headers?: string[];
9
+ body?: string;
10
+ timeout?: number;
11
+ env?: string;
12
+ api?: string;
13
+ jsonPath?: string;
14
+ dbPath?: string;
15
+ json?: boolean;
16
+ }
17
+
18
+ export async function requestCommand(options: RequestOptions): Promise<number> {
19
+ try {
20
+ const headers: Record<string, string> = {};
21
+ if (options.headers) {
22
+ for (const h of options.headers) {
23
+ const colonIdx = h.indexOf(":");
24
+ if (colonIdx > 0) {
25
+ headers[h.slice(0, colonIdx).trim()] = h.slice(colonIdx + 1).trim();
26
+ }
27
+ }
28
+ }
29
+
30
+ const result = await sendAdHocRequest({
31
+ method: options.method.toUpperCase(),
32
+ url: options.url,
33
+ headers: Object.keys(headers).length > 0 ? headers : undefined,
34
+ body: options.body,
35
+ timeout: options.timeout,
36
+ envName: options.env,
37
+ collectionName: options.api,
38
+ jsonPath: options.jsonPath,
39
+ dbPath: options.dbPath,
40
+ });
41
+
42
+ if (options.json) {
43
+ printJson(jsonOk("request", result));
44
+ } else {
45
+ console.log(JSON.stringify(result, null, 2));
46
+ }
47
+ return 0;
48
+ } catch (err) {
49
+ const message = err instanceof Error ? err.message : String(err);
50
+ if (options.json) {
51
+ printJson(jsonError("request", [message]));
52
+ } else {
53
+ printError(message);
54
+ }
55
+ return 1;
56
+ }
57
+ }