@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kirrosh/zond",
3
- "version": "0.17.0",
3
+ "version": "0.18.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",
@@ -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(),
@@ -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.js" with { type: "file" };
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