@kirrosh/zond 0.17.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/package.json +1 -1
- package/src/cli/commands/generate.ts +2 -1
- package/src/cli/commands/sync.ts +2 -1
- package/src/cli/commands/update.ts +174 -0
- package/src/cli/index.ts +13 -0
- package/src/core/generator/data-factory.ts +9 -6
- package/src/web/server.ts +1 -1
- /package/src/web/static/{htmx.min.js → htmx.min.cjs} +0 -0
package/package.json
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
import { generateSuites } from "../../core/generator/suite-generator.ts";
|
|
12
12
|
import { filterByTag } from "../../core/generator/chunker.ts";
|
|
13
13
|
import { parse } from "../../core/parser/yaml-parser.ts";
|
|
14
|
+
import { decycleSchema } from "../../core/generator/schema-utils.ts";
|
|
14
15
|
import { printError, printSuccess } from "../output.ts";
|
|
15
16
|
import { jsonOk, jsonError, printJson } from "../json-envelope.ts";
|
|
16
17
|
import { readMeta, writeMeta, hashSpec, buildFileMeta } from "../../core/meta/meta-store.ts";
|
|
@@ -82,7 +83,7 @@ export async function generateCommand(options: GenerateOptions): Promise<number>
|
|
|
82
83
|
|
|
83
84
|
// Write .zond-meta.json (merge with existing meta to preserve info about prior files)
|
|
84
85
|
const existingMeta = await readMeta(options.output);
|
|
85
|
-
const specContent = typeof doc === "object" ? JSON.stringify(doc) : String(doc);
|
|
86
|
+
const specContent = typeof doc === "object" ? JSON.stringify(decycleSchema(doc)) : String(doc);
|
|
86
87
|
await writeMeta(options.output, {
|
|
87
88
|
zondVersion: ZOND_VERSION,
|
|
88
89
|
lastSyncedAt: new Date().toISOString(),
|
package/src/cli/commands/sync.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { generateSuites } from "../../core/generator/suite-generator.ts";
|
|
|
10
10
|
import { filterByTag } from "../../core/generator/chunker.ts";
|
|
11
11
|
import { readMeta, writeMeta, hashSpec, buildFileMeta } from "../../core/meta/meta-store.ts";
|
|
12
12
|
import { diffEndpoints } from "../../core/sync/spec-differ.ts";
|
|
13
|
+
import { decycleSchema } from "../../core/generator/schema-utils.ts";
|
|
13
14
|
import { printError, printSuccess, printWarning } from "../output.ts";
|
|
14
15
|
import { jsonOk, jsonError, printJson } from "../json-envelope.ts";
|
|
15
16
|
import { version as ZOND_VERSION } from "../../../package.json";
|
|
@@ -41,7 +42,7 @@ export async function syncCommand(options: SyncOptions): Promise<number> {
|
|
|
41
42
|
|
|
42
43
|
// Load current spec
|
|
43
44
|
const doc = await readOpenApiSpec(options.specPath);
|
|
44
|
-
const specContent = JSON.stringify(doc);
|
|
45
|
+
const specContent = JSON.stringify(decycleSchema(doc));
|
|
45
46
|
const currentHash = hashSpec(specContent);
|
|
46
47
|
|
|
47
48
|
if (currentHash === meta.specHash) {
|
|
@@ -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
|
@@ -13,6 +13,7 @@ import { guideCommand } from "./commands/guide.ts";
|
|
|
13
13
|
import { generateCommand } from "./commands/generate.ts";
|
|
14
14
|
import { exportCommand } from "./commands/export.ts";
|
|
15
15
|
import { syncCommand } from "./commands/sync.ts";
|
|
16
|
+
import { updateCommand } from "./commands/update.ts";
|
|
16
17
|
import { printError } from "./output.ts";
|
|
17
18
|
import { getRuntimeInfo } from "./runtime.ts";
|
|
18
19
|
import { getDb } from "../db/schema.ts";
|
|
@@ -107,6 +108,7 @@ Usage:
|
|
|
107
108
|
zond ci init Generate CI/CD workflow (GitHub Actions, GitLab CI)
|
|
108
109
|
zond export postman <path> Export YAML tests as Postman Collection v2.1
|
|
109
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
|
|
110
112
|
|
|
111
113
|
Options for 'run':
|
|
112
114
|
--dry-run Show requests without sending them (exit code always 0)
|
|
@@ -186,6 +188,9 @@ Options for 'sync':
|
|
|
186
188
|
--dry-run Show what would be generated without writing files
|
|
187
189
|
--tag <tag> Limit sync to endpoints with this tag
|
|
188
190
|
|
|
191
|
+
Options for 'update':
|
|
192
|
+
--check Only check for updates, do not download
|
|
193
|
+
|
|
189
194
|
General:
|
|
190
195
|
--json Output in JSON envelope format (available for all commands)
|
|
191
196
|
--help, -h Show this help
|
|
@@ -528,6 +533,14 @@ async function main(): Promise<number> {
|
|
|
528
533
|
});
|
|
529
534
|
}
|
|
530
535
|
|
|
536
|
+
case "update":
|
|
537
|
+
case "self-update": {
|
|
538
|
+
return updateCommand({
|
|
539
|
+
check: flags["check"] === true,
|
|
540
|
+
json: jsonFlag,
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
|
|
531
544
|
case "sync": {
|
|
532
545
|
const specPath = positional[0];
|
|
533
546
|
if (!specPath) {
|
|
@@ -7,7 +7,10 @@ import type { OpenAPIV3 } from "openapi-types";
|
|
|
7
7
|
export function generateFromSchema(
|
|
8
8
|
schema: OpenAPIV3.SchemaObject,
|
|
9
9
|
propertyName?: string,
|
|
10
|
+
_depth = 0,
|
|
10
11
|
): unknown {
|
|
12
|
+
if (_depth > 5) return {};
|
|
13
|
+
|
|
11
14
|
// allOf: merge all schemas
|
|
12
15
|
if (schema.allOf) {
|
|
13
16
|
const merged: OpenAPIV3.SchemaObject = { type: "object", properties: {} };
|
|
@@ -17,15 +20,15 @@ export function generateFromSchema(
|
|
|
17
20
|
merged.properties = { ...merged.properties, ...s.properties };
|
|
18
21
|
}
|
|
19
22
|
}
|
|
20
|
-
return generateFromSchema(merged, propertyName);
|
|
23
|
+
return generateFromSchema(merged, propertyName, _depth + 1);
|
|
21
24
|
}
|
|
22
25
|
|
|
23
26
|
// oneOf / anyOf: use first variant
|
|
24
27
|
if (schema.oneOf) {
|
|
25
|
-
return generateFromSchema(schema.oneOf[0] as OpenAPIV3.SchemaObject, propertyName);
|
|
28
|
+
return generateFromSchema(schema.oneOf[0] as OpenAPIV3.SchemaObject, propertyName, _depth + 1);
|
|
26
29
|
}
|
|
27
30
|
if (schema.anyOf) {
|
|
28
|
-
return generateFromSchema(schema.anyOf[0] as OpenAPIV3.SchemaObject, propertyName);
|
|
31
|
+
return generateFromSchema(schema.anyOf[0] as OpenAPIV3.SchemaObject, propertyName, _depth + 1);
|
|
29
32
|
}
|
|
30
33
|
|
|
31
34
|
// enum: first value
|
|
@@ -51,7 +54,7 @@ export function generateFromSchema(
|
|
|
51
54
|
|
|
52
55
|
case "array": {
|
|
53
56
|
if (schema.items) {
|
|
54
|
-
const item = generateFromSchema(schema.items as OpenAPIV3.SchemaObject);
|
|
57
|
+
const item = generateFromSchema(schema.items as OpenAPIV3.SchemaObject, undefined, _depth + 1);
|
|
55
58
|
return [item];
|
|
56
59
|
}
|
|
57
60
|
return [];
|
|
@@ -63,14 +66,14 @@ export function generateFromSchema(
|
|
|
63
66
|
if (schema.properties) {
|
|
64
67
|
const obj: Record<string, unknown> = {};
|
|
65
68
|
for (const [key, propSchema] of Object.entries(schema.properties)) {
|
|
66
|
-
obj[key] = generateFromSchema(propSchema as OpenAPIV3.SchemaObject, key);
|
|
69
|
+
obj[key] = generateFromSchema(propSchema as OpenAPIV3.SchemaObject, key, _depth + 1);
|
|
67
70
|
}
|
|
68
71
|
return obj;
|
|
69
72
|
}
|
|
70
73
|
// Record type: additionalProperties defines value schema
|
|
71
74
|
if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
|
|
72
75
|
const valSchema = schema.additionalProperties as OpenAPIV3.SchemaObject;
|
|
73
|
-
return { key1: generateFromSchema(valSchema, "key1"), key2: generateFromSchema(valSchema, "key2") };
|
|
76
|
+
return { key1: generateFromSchema(valSchema, "key1", _depth + 1), key2: generateFromSchema(valSchema, "key2", _depth + 1) };
|
|
74
77
|
}
|
|
75
78
|
if (schema.additionalProperties === true) {
|
|
76
79
|
return { key1: "value1", key2: "value2" };
|
package/src/web/server.ts
CHANGED
|
@@ -4,7 +4,7 @@ import dashboard from "./routes/dashboard.ts";
|
|
|
4
4
|
import runs from "./routes/runs.ts";
|
|
5
5
|
import api from "./routes/api.ts";
|
|
6
6
|
import styleCssPath from "./static/style.css" with { type: "file" };
|
|
7
|
-
import htmxJsPath from "./static/htmx.min.
|
|
7
|
+
import htmxJsPath from "./static/htmx.min.cjs" with { type: "file" };
|
|
8
8
|
|
|
9
9
|
export interface ServerOptions {
|
|
10
10
|
port?: number;
|
|
File without changes
|