@kirrosh/zond 0.16.0 → 0.18.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 +132 -112
- package/README.md +3 -10
- package/package.json +2 -3
- package/src/cli/commands/export.ts +144 -0
- package/src/cli/commands/generate.ts +32 -0
- package/src/cli/commands/run.ts +22 -5
- package/src/cli/commands/sync.ts +241 -0
- package/src/cli/commands/update.ts +174 -0
- package/src/cli/index.ts +67 -10
- package/src/core/diagnostics/db-analysis.ts +79 -7
- package/src/core/diagnostics/failure-hints.ts +39 -0
- package/src/core/exporter/postman.ts +963 -0
- package/src/core/generator/data-factory.ts +47 -9
- package/src/core/generator/index.ts +1 -1
- package/src/core/generator/openapi-reader.ts +6 -0
- package/src/core/generator/serializer.ts +17 -2
- package/src/core/generator/suite-generator.ts +163 -14
- package/src/core/generator/types.ts +1 -0
- package/src/core/meta/meta-store.ts +78 -0
- package/src/core/meta/types.ts +21 -0
- package/src/core/parser/schema.ts +12 -2
- package/src/core/parser/types.ts +12 -1
- package/src/core/parser/variables.ts +3 -0
- package/src/core/parser/yaml-parser.ts +2 -1
- package/src/core/runner/assertions.ts +44 -20
- package/src/core/runner/execute-run.ts +31 -8
- package/src/core/runner/executor.ts +34 -8
- package/src/core/runner/http-client.ts +1 -1
- package/src/core/runner/types.ts +1 -0
- package/src/core/sync/spec-differ.ts +38 -0
- package/src/web/server.ts +1 -1
- package/src/cli/commands/mcp.ts +0 -16
- package/src/mcp/descriptions.ts +0 -47
- package/src/mcp/server.ts +0 -38
- package/src/mcp/tools/ci-init.ts +0 -54
- package/src/mcp/tools/coverage-analysis.ts +0 -141
- package/src/mcp/tools/describe-endpoint.ts +0 -27
- package/src/mcp/tools/manage-server.ts +0 -86
- package/src/mcp/tools/query-db.ts +0 -84
- package/src/mcp/tools/run-tests.ts +0 -116
- package/src/mcp/tools/send-request.ts +0 -51
- package/src/mcp/tools/setup-api.ts +0 -88
- /package/src/web/static/{htmx.min.js → htmx.min.cjs} +0 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { join } from "path";
|
|
2
|
+
import { mkdir } from "fs/promises";
|
|
3
|
+
import {
|
|
4
|
+
readOpenApiSpec,
|
|
5
|
+
extractEndpoints,
|
|
6
|
+
extractSecuritySchemes,
|
|
7
|
+
serializeSuite,
|
|
8
|
+
} from "../../core/generator/index.ts";
|
|
9
|
+
import { generateSuites } from "../../core/generator/suite-generator.ts";
|
|
10
|
+
import { filterByTag } from "../../core/generator/chunker.ts";
|
|
11
|
+
import { readMeta, writeMeta, hashSpec, buildFileMeta } from "../../core/meta/meta-store.ts";
|
|
12
|
+
import { diffEndpoints } from "../../core/sync/spec-differ.ts";
|
|
13
|
+
import { decycleSchema } from "../../core/generator/schema-utils.ts";
|
|
14
|
+
import { printError, printSuccess, printWarning } from "../output.ts";
|
|
15
|
+
import { jsonOk, jsonError, printJson } from "../json-envelope.ts";
|
|
16
|
+
import { version as ZOND_VERSION } from "../../../package.json";
|
|
17
|
+
import { getDb } from "../../db/schema.ts";
|
|
18
|
+
import { findCollectionByTestPath, updateCollection } from "../../db/queries.ts";
|
|
19
|
+
|
|
20
|
+
export interface SyncOptions {
|
|
21
|
+
specPath: string;
|
|
22
|
+
testsDir: string;
|
|
23
|
+
dryRun?: boolean;
|
|
24
|
+
tag?: string;
|
|
25
|
+
json?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function syncCommand(options: SyncOptions): Promise<number> {
|
|
29
|
+
try {
|
|
30
|
+
// Load existing metadata
|
|
31
|
+
const meta = await readMeta(options.testsDir);
|
|
32
|
+
if (!meta) {
|
|
33
|
+
const msg =
|
|
34
|
+
"No .zond-meta.json found. Run `zond generate <spec> --output <dir>` first to initialize metadata.";
|
|
35
|
+
if (options.json) {
|
|
36
|
+
printJson(jsonError("sync", [msg]));
|
|
37
|
+
} else {
|
|
38
|
+
printError(msg);
|
|
39
|
+
}
|
|
40
|
+
return 2;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Load current spec
|
|
44
|
+
const doc = await readOpenApiSpec(options.specPath);
|
|
45
|
+
const specContent = JSON.stringify(decycleSchema(doc));
|
|
46
|
+
const currentHash = hashSpec(specContent);
|
|
47
|
+
|
|
48
|
+
if (currentHash === meta.specHash) {
|
|
49
|
+
const msg = "Spec unchanged — nothing to sync.";
|
|
50
|
+
if (options.json) {
|
|
51
|
+
printJson(jsonOk("sync", { newEndpoints: [], generatedFiles: [], removedKeys: [], specChanged: false }, [msg]));
|
|
52
|
+
} else {
|
|
53
|
+
console.log(msg);
|
|
54
|
+
}
|
|
55
|
+
return 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Extract current endpoints
|
|
59
|
+
let currentEndpoints = extractEndpoints(doc);
|
|
60
|
+
const securitySchemes = extractSecuritySchemes(doc);
|
|
61
|
+
|
|
62
|
+
if (options.tag) {
|
|
63
|
+
currentEndpoints = filterByTag(currentEndpoints, options.tag);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Collect all previously known endpoint keys from meta
|
|
67
|
+
const prevKeys = Object.values(meta.files).flatMap((f) => f.endpoints);
|
|
68
|
+
|
|
69
|
+
// Compute diff
|
|
70
|
+
const { newEndpoints, removedKeys } = diffEndpoints(prevKeys, currentEndpoints);
|
|
71
|
+
|
|
72
|
+
const warnings: string[] = [];
|
|
73
|
+
|
|
74
|
+
if (removedKeys.length > 0) {
|
|
75
|
+
for (const key of removedKeys) {
|
|
76
|
+
warnings.push(`Removed endpoint not deleted from tests (review manually): ${key}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (newEndpoints.length === 0) {
|
|
81
|
+
const msg = "Spec changed (hash differs) but no new endpoints detected. Existing tests may need manual review.";
|
|
82
|
+
warnings.push(msg);
|
|
83
|
+
if (options.json) {
|
|
84
|
+
printJson(jsonOk("sync", {
|
|
85
|
+
newEndpoints: [],
|
|
86
|
+
removedKeys,
|
|
87
|
+
generatedFiles: [],
|
|
88
|
+
specChanged: true,
|
|
89
|
+
}, warnings));
|
|
90
|
+
} else {
|
|
91
|
+
console.log(msg);
|
|
92
|
+
for (const w of warnings) {
|
|
93
|
+
printWarning(w);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return 0;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Generate suites for new endpoints only
|
|
100
|
+
const suites = generateSuites({ endpoints: newEndpoints, securitySchemes });
|
|
101
|
+
|
|
102
|
+
if (options.dryRun) {
|
|
103
|
+
const newEndpointKeys = newEndpoints.map((ep) => `${ep.method.toUpperCase()} ${ep.path}`);
|
|
104
|
+
const plannedFiles = suites.map((s) => ({
|
|
105
|
+
file: `${s.fileStem ?? s.name}.yaml`,
|
|
106
|
+
suite: s.name,
|
|
107
|
+
tests: s.tests.length,
|
|
108
|
+
}));
|
|
109
|
+
|
|
110
|
+
if (options.json) {
|
|
111
|
+
printJson(jsonOk("sync", {
|
|
112
|
+
dryRun: true,
|
|
113
|
+
newEndpoints: newEndpointKeys,
|
|
114
|
+
removedKeys,
|
|
115
|
+
plannedFiles,
|
|
116
|
+
specChanged: true,
|
|
117
|
+
}, warnings));
|
|
118
|
+
} else {
|
|
119
|
+
console.log(`[dry-run] Detected ${newEndpoints.length} new endpoint(s):`);
|
|
120
|
+
for (const ep of newEndpoints) {
|
|
121
|
+
console.log(` + ${ep.method.toUpperCase()} ${ep.path}`);
|
|
122
|
+
}
|
|
123
|
+
console.log(`\nWould generate ${suites.length} new suite file(s):`);
|
|
124
|
+
for (const f of plannedFiles) {
|
|
125
|
+
console.log(` ${f.file} (${f.tests} tests)`);
|
|
126
|
+
}
|
|
127
|
+
if (removedKeys.length > 0) {
|
|
128
|
+
console.log("\nRemoved endpoints (not deleted — review tests):");
|
|
129
|
+
for (const key of removedKeys) {
|
|
130
|
+
console.log(` - ${key}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
console.log("\nNo files written (dry-run).");
|
|
134
|
+
}
|
|
135
|
+
return 0;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Write new files (skip if file already exists)
|
|
139
|
+
await mkdir(options.testsDir, { recursive: true });
|
|
140
|
+
|
|
141
|
+
const generatedFiles: Array<{ file: string; suite: string; tests: number }> = [];
|
|
142
|
+
const skippedFiles: string[] = [];
|
|
143
|
+
const updatedMetaFiles: Record<string, import("../../core/meta/types.ts").FileMeta> = {};
|
|
144
|
+
|
|
145
|
+
for (const suite of suites) {
|
|
146
|
+
const fileName = `${suite.fileStem ?? suite.name}.yaml`;
|
|
147
|
+
const filePath = join(options.testsDir, fileName);
|
|
148
|
+
const existing = Bun.file(filePath);
|
|
149
|
+
|
|
150
|
+
if (await existing.exists()) {
|
|
151
|
+
skippedFiles.push(fileName);
|
|
152
|
+
warnings.push(`Skipped ${fileName} (already exists — add new endpoints manually)`);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const yaml = serializeSuite(suite);
|
|
157
|
+
await Bun.write(filePath, yaml);
|
|
158
|
+
generatedFiles.push({ file: filePath, suite: suite.name, tests: suite.tests.length });
|
|
159
|
+
updatedMetaFiles[fileName] = buildFileMeta(suite, ZOND_VERSION);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Update metadata: merge new file entries, update hash and timestamp
|
|
163
|
+
await writeMeta(options.testsDir, {
|
|
164
|
+
zondVersion: ZOND_VERSION,
|
|
165
|
+
lastSyncedAt: new Date().toISOString(),
|
|
166
|
+
specUrl: options.specPath,
|
|
167
|
+
specHash: currentHash,
|
|
168
|
+
files: { ...meta.files, ...updatedMetaFiles },
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// Sync DB collection if one is registered for this tests directory
|
|
172
|
+
try {
|
|
173
|
+
getDb();
|
|
174
|
+
const collection = findCollectionByTestPath(options.testsDir);
|
|
175
|
+
if (collection && collection.openapi_spec !== options.specPath) {
|
|
176
|
+
updateCollection(collection.id, { openapi_spec: options.specPath });
|
|
177
|
+
warnings.push(`Updated collection '${collection.name}' spec reference: ${collection.openapi_spec ?? "(none)"} → ${options.specPath}`);
|
|
178
|
+
}
|
|
179
|
+
} catch {
|
|
180
|
+
// DB unavailable (e.g. no zond.db yet) — not a fatal error for sync
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const newEndpointKeys = newEndpoints.map((ep) => `${ep.method.toUpperCase()} ${ep.path}`);
|
|
184
|
+
|
|
185
|
+
if (options.json) {
|
|
186
|
+
printJson(jsonOk("sync", {
|
|
187
|
+
newEndpoints: newEndpointKeys,
|
|
188
|
+
removedKeys,
|
|
189
|
+
generatedFiles,
|
|
190
|
+
skippedFiles,
|
|
191
|
+
specChanged: true,
|
|
192
|
+
}, warnings));
|
|
193
|
+
} else {
|
|
194
|
+
console.log(`Spec changed. Detected ${newEndpoints.length} new endpoint(s):`);
|
|
195
|
+
for (const ep of newEndpoints) {
|
|
196
|
+
console.log(` + ${ep.method.toUpperCase()} ${ep.path}`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (generatedFiles.length > 0) {
|
|
200
|
+
console.log(`\nGenerated ${generatedFiles.length} new suite file(s):`);
|
|
201
|
+
for (const f of generatedFiles) {
|
|
202
|
+
console.log(` ${f.file} (${f.tests} tests)`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (skippedFiles.length > 0) {
|
|
207
|
+
console.log("\nSkipped (file exists, review manually):");
|
|
208
|
+
for (const f of skippedFiles) {
|
|
209
|
+
console.log(` ${f}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (removedKeys.length > 0) {
|
|
214
|
+
console.log("\nRemoved endpoints (not deleted — review tests):");
|
|
215
|
+
for (const key of removedKeys) {
|
|
216
|
+
console.log(` - ${key}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (generatedFiles.length > 0) {
|
|
221
|
+
printSuccess(`\nSync complete. ${generatedFiles.length} file(s) written.`);
|
|
222
|
+
} else {
|
|
223
|
+
printWarning("No new files written — all target files already exist.");
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
for (const w of warnings) {
|
|
227
|
+
printWarning(w);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return 0;
|
|
232
|
+
} catch (err) {
|
|
233
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
234
|
+
if (options.json) {
|
|
235
|
+
printJson(jsonError("sync", [message]));
|
|
236
|
+
} else {
|
|
237
|
+
printError(message);
|
|
238
|
+
}
|
|
239
|
+
return 2;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { VERSION } from "../index.ts";
|
|
2
|
+
import { isCompiledBinary } from "../runtime.ts";
|
|
3
|
+
import { printError, printSuccess, printWarning } from "../output.ts";
|
|
4
|
+
import { jsonOk, jsonError, printJson } from "../json-envelope.ts";
|
|
5
|
+
|
|
6
|
+
export interface UpdateOptions {
|
|
7
|
+
json?: boolean;
|
|
8
|
+
check?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const REPO = "kirrosh/zond";
|
|
12
|
+
const GITHUB_API = `https://api.github.com/repos/${REPO}/releases/latest`;
|
|
13
|
+
|
|
14
|
+
interface GitHubRelease {
|
|
15
|
+
tag_name: string;
|
|
16
|
+
assets: { name: string; browser_download_url: string }[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function getTarget(): { target: string; ext: string } | null {
|
|
20
|
+
const platform = process.platform;
|
|
21
|
+
const arch = process.arch;
|
|
22
|
+
|
|
23
|
+
if (platform === "linux" && arch === "x64") return { target: "linux-x64", ext: "tar.gz" };
|
|
24
|
+
if (platform === "darwin" && arch === "arm64") return { target: "darwin-arm64", ext: "tar.gz" };
|
|
25
|
+
if (platform === "win32" && arch === "x64") return { target: "win-x64", ext: "zip" };
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function fetchLatestRelease(): Promise<GitHubRelease> {
|
|
30
|
+
const resp = await fetch(GITHUB_API, {
|
|
31
|
+
headers: { "User-Agent": `zond/${VERSION}` },
|
|
32
|
+
});
|
|
33
|
+
if (!resp.ok) {
|
|
34
|
+
throw new Error(`GitHub API returned ${resp.status}: ${resp.statusText}`);
|
|
35
|
+
}
|
|
36
|
+
return resp.json() as Promise<GitHubRelease>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function updateCommand(options: UpdateOptions): Promise<number> {
|
|
40
|
+
try {
|
|
41
|
+
if (!isCompiledBinary()) {
|
|
42
|
+
const msg = "Self-update is only available for standalone binaries. Update via: npm update -g @kirrosh/zond or bun update -g @kirrosh/zond";
|
|
43
|
+
if (options.json) {
|
|
44
|
+
printJson(jsonOk("update", { action: "skip", reason: "not-standalone" }, [msg]));
|
|
45
|
+
} else {
|
|
46
|
+
printWarning(msg);
|
|
47
|
+
}
|
|
48
|
+
return 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const target = getTarget();
|
|
52
|
+
if (!target) {
|
|
53
|
+
const msg = `Unsupported platform: ${process.platform}-${process.arch}`;
|
|
54
|
+
if (options.json) {
|
|
55
|
+
printJson(jsonError("update", [msg]));
|
|
56
|
+
} else {
|
|
57
|
+
printError(msg);
|
|
58
|
+
}
|
|
59
|
+
return 2;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const release = await fetchLatestRelease();
|
|
63
|
+
const latest = release.tag_name.replace(/^v/, "");
|
|
64
|
+
|
|
65
|
+
if (latest === VERSION) {
|
|
66
|
+
const msg = `Already up to date (${VERSION})`;
|
|
67
|
+
if (options.json) {
|
|
68
|
+
printJson(jsonOk("update", { action: "none", currentVersion: VERSION, latestVersion: latest }));
|
|
69
|
+
} else {
|
|
70
|
+
console.log(msg);
|
|
71
|
+
}
|
|
72
|
+
return 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (options.check) {
|
|
76
|
+
const msg = `Update available: ${VERSION} → ${latest}`;
|
|
77
|
+
if (options.json) {
|
|
78
|
+
printJson(jsonOk("update", { action: "available", currentVersion: VERSION, latestVersion: latest }));
|
|
79
|
+
} else {
|
|
80
|
+
console.log(msg);
|
|
81
|
+
}
|
|
82
|
+
return 0;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Find the right asset
|
|
86
|
+
const assetName = `zond-${target.target}.${target.ext}`;
|
|
87
|
+
const asset = release.assets.find(a => a.name === assetName);
|
|
88
|
+
if (!asset) {
|
|
89
|
+
const msg = `Binary not found for ${target.target} in release ${release.tag_name}`;
|
|
90
|
+
if (options.json) {
|
|
91
|
+
printJson(jsonError("update", [msg]));
|
|
92
|
+
} else {
|
|
93
|
+
printError(msg);
|
|
94
|
+
}
|
|
95
|
+
return 2;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
console.log(`Updating zond ${VERSION} → ${latest}...`);
|
|
99
|
+
console.log(`Downloading ${assetName}...`);
|
|
100
|
+
|
|
101
|
+
// Download the archive
|
|
102
|
+
const resp = await fetch(asset.browser_download_url, {
|
|
103
|
+
headers: { "User-Agent": `zond/${VERSION}` },
|
|
104
|
+
});
|
|
105
|
+
if (!resp.ok) {
|
|
106
|
+
throw new Error(`Download failed: ${resp.status} ${resp.statusText}`);
|
|
107
|
+
}
|
|
108
|
+
const archiveData = new Uint8Array(await resp.arrayBuffer());
|
|
109
|
+
|
|
110
|
+
const currentBinary = process.execPath;
|
|
111
|
+
const { join, dirname } = await import("path");
|
|
112
|
+
const tmpDir = join(dirname(currentBinary), `.zond-update-${Date.now()}`);
|
|
113
|
+
const { mkdir, rm, rename, chmod } = await import("fs/promises");
|
|
114
|
+
await mkdir(tmpDir, { recursive: true });
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const archivePath = join(tmpDir, assetName);
|
|
118
|
+
await Bun.write(archivePath, archiveData);
|
|
119
|
+
|
|
120
|
+
// Extract
|
|
121
|
+
if (target.ext === "tar.gz") {
|
|
122
|
+
const proc = Bun.spawn(["tar", "-xzf", archivePath, "-C", tmpDir]);
|
|
123
|
+
const exitCode = await proc.exited;
|
|
124
|
+
if (exitCode !== 0) throw new Error(`tar extraction failed (exit ${exitCode})`);
|
|
125
|
+
} else {
|
|
126
|
+
// Windows zip
|
|
127
|
+
const proc = Bun.spawn([
|
|
128
|
+
"powershell", "-NoProfile", "-Command",
|
|
129
|
+
`Expand-Archive -Path '${archivePath}' -DestinationPath '${tmpDir}' -Force`,
|
|
130
|
+
]);
|
|
131
|
+
const exitCode = await proc.exited;
|
|
132
|
+
if (exitCode !== 0) throw new Error(`Zip extraction failed (exit ${exitCode})`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Find the extracted binary
|
|
136
|
+
const binaryName = process.platform === "win32" ? "zond.exe" : "zond";
|
|
137
|
+
const newBinary = join(tmpDir, binaryName);
|
|
138
|
+
const file = Bun.file(newBinary);
|
|
139
|
+
if (!await file.exists()) {
|
|
140
|
+
throw new Error(`Binary '${binaryName}' not found in archive`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Replace current binary
|
|
144
|
+
if (process.platform === "win32") {
|
|
145
|
+
// Windows: rename current to .old, move new, clean up
|
|
146
|
+
const oldBinary = currentBinary + ".old";
|
|
147
|
+
try { await rm(oldBinary, { force: true }); } catch {}
|
|
148
|
+
await rename(currentBinary, oldBinary);
|
|
149
|
+
await rename(newBinary, currentBinary);
|
|
150
|
+
try { await rm(oldBinary, { force: true }); } catch {}
|
|
151
|
+
} else {
|
|
152
|
+
await rename(newBinary, currentBinary);
|
|
153
|
+
await chmod(currentBinary, 0o755);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (options.json) {
|
|
157
|
+
printJson(jsonOk("update", { action: "updated", previousVersion: VERSION, newVersion: latest }));
|
|
158
|
+
} else {
|
|
159
|
+
printSuccess(`Updated zond ${VERSION} → ${latest}`);
|
|
160
|
+
}
|
|
161
|
+
return 0;
|
|
162
|
+
} finally {
|
|
163
|
+
try { await rm(tmpDir, { recursive: true, force: true }); } catch {}
|
|
164
|
+
}
|
|
165
|
+
} catch (err) {
|
|
166
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
167
|
+
if (options.json) {
|
|
168
|
+
printJson(jsonError("update", [message]));
|
|
169
|
+
} else {
|
|
170
|
+
printError(message);
|
|
171
|
+
}
|
|
172
|
+
return 2;
|
|
173
|
+
}
|
|
174
|
+
}
|
package/src/cli/index.ts
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
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 { mcpCommand } from "./commands/mcp.ts";
|
|
7
6
|
import { coverageCommand } from "./commands/coverage.ts";
|
|
8
7
|
import { ciInitCommand } from "./commands/ci-init.ts";
|
|
9
8
|
import { initCommand } from "./commands/init.ts";
|
|
@@ -12,6 +11,9 @@ import { dbCommand } from "./commands/db.ts";
|
|
|
12
11
|
import { requestCommand } from "./commands/request.ts";
|
|
13
12
|
import { guideCommand } from "./commands/guide.ts";
|
|
14
13
|
import { generateCommand } from "./commands/generate.ts";
|
|
14
|
+
import { exportCommand } from "./commands/export.ts";
|
|
15
|
+
import { syncCommand } from "./commands/sync.ts";
|
|
16
|
+
import { updateCommand } from "./commands/update.ts";
|
|
15
17
|
import { printError } from "./output.ts";
|
|
16
18
|
import { getRuntimeInfo } from "./runtime.ts";
|
|
17
19
|
import { getDb } from "../db/schema.ts";
|
|
@@ -103,9 +105,10 @@ Usage:
|
|
|
103
105
|
zond guide <spec> Generate test generation guide from OpenAPI spec
|
|
104
106
|
zond serve Start web dashboard
|
|
105
107
|
zond ui Alias for 'serve --open' (start dashboard & open browser)
|
|
106
|
-
zond mcp Start MCP server (stdio transport for AI agents)
|
|
107
|
-
--dir <path> Set working directory (relative paths resolve here)
|
|
108
108
|
zond ci init Generate CI/CD workflow (GitHub Actions, GitLab CI)
|
|
109
|
+
zond export postman <path> Export YAML tests as Postman Collection v2.1
|
|
110
|
+
zond sync <spec> Detect new/removed endpoints and generate tests for new ones
|
|
111
|
+
zond update Check for updates and self-update the binary
|
|
109
112
|
|
|
110
113
|
Options for 'run':
|
|
111
114
|
--dry-run Show requests without sending them (exit code always 0)
|
|
@@ -175,6 +178,19 @@ Options for 'ci init':
|
|
|
175
178
|
--dir <path> Project root directory (default: current directory)
|
|
176
179
|
--force Overwrite existing CI config
|
|
177
180
|
|
|
181
|
+
Options for 'export postman':
|
|
182
|
+
--output <file> Output file path (default: collection.postman.json)
|
|
183
|
+
--env <file> Also export .env.yaml as Postman environment
|
|
184
|
+
--collection-name <name> Collection name (default: derived from path)
|
|
185
|
+
|
|
186
|
+
Options for 'sync':
|
|
187
|
+
--tests <dir> Path to test files directory (required)
|
|
188
|
+
--dry-run Show what would be generated without writing files
|
|
189
|
+
--tag <tag> Limit sync to endpoints with this tag
|
|
190
|
+
|
|
191
|
+
Options for 'update':
|
|
192
|
+
--check Only check for updates, do not download
|
|
193
|
+
|
|
178
194
|
General:
|
|
179
195
|
--json Output in JSON envelope format (available for all commands)
|
|
180
196
|
--help, -h Show this help
|
|
@@ -308,13 +324,6 @@ async function main(): Promise<number> {
|
|
|
308
324
|
});
|
|
309
325
|
}
|
|
310
326
|
|
|
311
|
-
case "mcp": {
|
|
312
|
-
return mcpCommand({
|
|
313
|
-
dbPath: typeof flags["db"] === "string" ? flags["db"] : undefined,
|
|
314
|
-
dir: typeof flags["dir"] === "string" ? flags["dir"] : undefined,
|
|
315
|
-
});
|
|
316
|
-
}
|
|
317
|
-
|
|
318
327
|
case "ci": {
|
|
319
328
|
const ciSub = positional[0];
|
|
320
329
|
if (ciSub !== "init") {
|
|
@@ -504,6 +513,54 @@ async function main(): Promise<number> {
|
|
|
504
513
|
});
|
|
505
514
|
}
|
|
506
515
|
|
|
516
|
+
case "export": {
|
|
517
|
+
const subcommand = positional[0];
|
|
518
|
+
if (subcommand !== "postman") {
|
|
519
|
+
printError(`Unknown export subcommand: ${subcommand ?? "(none)"}. Usage: zond export postman <path>`);
|
|
520
|
+
return 2;
|
|
521
|
+
}
|
|
522
|
+
const testsPath = positional[1];
|
|
523
|
+
if (!testsPath) {
|
|
524
|
+
printError("Missing tests path. Usage: zond export postman <path> [--output <file>]");
|
|
525
|
+
return 2;
|
|
526
|
+
}
|
|
527
|
+
return exportCommand({
|
|
528
|
+
testsPath,
|
|
529
|
+
output: typeof flags["output"] === "string" ? flags["output"] : "collection.postman.json",
|
|
530
|
+
env: typeof flags["env"] === "string" ? flags["env"] : undefined,
|
|
531
|
+
collectionName: typeof flags["collection-name"] === "string" ? flags["collection-name"] : undefined,
|
|
532
|
+
json: jsonFlag,
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
case "update":
|
|
537
|
+
case "self-update": {
|
|
538
|
+
return updateCommand({
|
|
539
|
+
check: flags["check"] === true,
|
|
540
|
+
json: jsonFlag,
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
case "sync": {
|
|
545
|
+
const specPath = positional[0];
|
|
546
|
+
if (!specPath) {
|
|
547
|
+
printError("Missing spec path. Usage: zond sync <spec> --tests <dir> [--dry-run] [--tag <tag>]");
|
|
548
|
+
return 2;
|
|
549
|
+
}
|
|
550
|
+
const testsDir = typeof flags["tests"] === "string" ? flags["tests"] : undefined;
|
|
551
|
+
if (!testsDir) {
|
|
552
|
+
printError("Missing --tests <dir>. Usage: zond sync <spec> --tests <dir>");
|
|
553
|
+
return 2;
|
|
554
|
+
}
|
|
555
|
+
return syncCommand({
|
|
556
|
+
specPath,
|
|
557
|
+
testsDir,
|
|
558
|
+
dryRun: flags["dry-run"] === true,
|
|
559
|
+
tag: typeof flags["tag"] === "string" ? flags["tag"] : undefined,
|
|
560
|
+
json: jsonFlag,
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
|
|
507
564
|
default: {
|
|
508
565
|
printError(`Unknown command: ${command}`);
|
|
509
566
|
printUsage();
|