@kirrosh/zond 0.21.0 → 0.23.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 +758 -3
- package/README.md +78 -15
- package/package.json +17 -10
- package/src/cli/argv.ts +122 -0
- package/src/cli/commands/add-api.ts +134 -0
- package/src/cli/commands/api/annotate/idempotency.ts +59 -0
- package/src/cli/commands/api/annotate/index.ts +525 -0
- package/src/cli/commands/api/annotate/lifecycle.ts +74 -0
- package/src/cli/commands/api/annotate/overlay.ts +206 -0
- package/src/cli/commands/api/annotate/pagination.ts +60 -0
- package/src/cli/commands/api/annotate/prompts.ts +183 -0
- package/src/cli/commands/api/annotate/readback.ts +58 -0
- package/src/cli/commands/api/annotate/resources.ts +91 -0
- package/src/cli/commands/api/annotate/seed-bodies.ts +61 -0
- package/src/cli/commands/audit.ts +480 -0
- package/src/cli/commands/bootstrap.ts +710 -0
- package/src/cli/commands/catalog.ts +35 -0
- package/src/cli/commands/check.ts +348 -0
- package/src/cli/commands/checks.ts +756 -0
- package/src/cli/commands/ci-init.ts +55 -6
- package/src/cli/commands/clean.ts +212 -0
- package/src/cli/commands/cleanup.ts +262 -0
- package/src/cli/commands/completions.ts +192 -0
- package/src/cli/commands/coverage.ts +605 -132
- package/src/cli/commands/db.ts +180 -8
- package/src/cli/commands/describe.ts +37 -2
- package/src/cli/commands/discover.ts +1236 -0
- package/src/cli/commands/doctor.ts +607 -0
- package/src/cli/commands/fixtures.ts +402 -0
- package/src/cli/commands/generate.ts +420 -47
- package/src/cli/commands/init/agents-md.ts +61 -0
- package/src/cli/commands/init/bootstrap.ts +108 -0
- package/src/cli/commands/init/index.ts +244 -0
- package/src/cli/commands/init/skills.ts +98 -0
- package/src/cli/commands/init/templates/agents.md +77 -0
- package/src/cli/commands/init/templates/markdown.d.ts +4 -0
- package/src/cli/commands/init/templates/skills/zond-checks.md +397 -0
- package/src/cli/commands/init/templates/skills/zond-triage.md +210 -0
- package/src/cli/commands/init/templates/skills/zond.md +651 -0
- package/src/cli/commands/init/templates/zond-config.yml +14 -0
- package/src/cli/commands/prepare-fixtures.ts +135 -0
- package/src/cli/commands/probe/mass-assignment.ts +503 -0
- package/src/cli/commands/probe/security.ts +454 -0
- package/src/cli/commands/probe/static.ts +255 -0
- package/src/cli/commands/probe/webhooks.ts +161 -0
- package/src/cli/commands/probe.ts +459 -0
- package/src/cli/commands/reference.ts +87 -0
- package/src/cli/commands/refresh-api.ts +169 -0
- package/src/cli/commands/remove-api.ts +150 -0
- package/src/cli/commands/report-bundle.ts +318 -0
- package/src/cli/commands/report.ts +241 -0
- package/src/cli/commands/request.ts +379 -4
- package/src/cli/commands/run.ts +911 -33
- package/src/cli/commands/session.ts +244 -0
- package/src/cli/commands/use.ts +74 -0
- package/src/cli/index.ts +36 -607
- package/src/cli/json-envelope.ts +112 -3
- package/src/cli/json-schemas.ts +263 -0
- package/src/cli/program.ts +218 -0
- package/src/cli/resolve.ts +105 -0
- package/src/cli/status-filter.ts +124 -0
- package/src/cli/util/api-context.ts +85 -0
- package/src/cli/version.ts +8 -0
- package/src/core/anti-fp/bootstrap.ts +34 -0
- package/src/core/anti-fp/index.ts +33 -0
- package/src/core/anti-fp/registry.ts +44 -0
- package/src/core/anti-fp/rules/baseline-echo.ts +74 -0
- package/src/core/anti-fp/rules/schemathesis/body_negation_becomes_valid.ts +52 -0
- package/src/core/anti-fp/rules/schemathesis/coverage_phase_boundary_positive.ts +38 -0
- package/src/core/anti-fp/rules/schemathesis/has_unverifiable_mutations.ts +35 -0
- package/src/core/anti-fp/rules/schemathesis/index.ts +24 -0
- package/src/core/anti-fp/rules/schemathesis/string_type_mutation_becomes_valid.ts +53 -0
- package/src/core/anti-fp/rules/subscription-gated/index.ts +11 -0
- package/src/core/anti-fp/rules/subscription-gated/paid-plan-403.ts +75 -0
- package/src/core/anti-fp/types.ts +68 -0
- package/src/core/checks/checks/_crud-helpers.ts +133 -0
- package/src/core/checks/checks/_negative_mutator.ts +133 -0
- package/src/core/checks/checks/_readback-helpers.ts +133 -0
- package/src/core/checks/checks/content_type_conformance.ts +39 -0
- package/src/core/checks/checks/cross_call_references.ts +134 -0
- package/src/core/checks/checks/ensure_resource_availability.ts +62 -0
- package/src/core/checks/checks/idempotency_replay.ts +246 -0
- package/src/core/checks/checks/ignored_auth.ts +211 -0
- package/src/core/checks/checks/index.ts +65 -0
- package/src/core/checks/checks/lifecycle_transitions.ts +273 -0
- package/src/core/checks/checks/missing_required_header.ts +40 -0
- package/src/core/checks/checks/negative_data_rejection.ts +45 -0
- package/src/core/checks/checks/not_a_server_error.ts +27 -0
- package/src/core/checks/checks/open_cors_on_sensitive.ts +131 -0
- package/src/core/checks/checks/pagination_invariants.ts +238 -0
- package/src/core/checks/checks/positive_data_acceptance.ts +36 -0
- package/src/core/checks/checks/rate_limit_headers_absent.ts +77 -0
- package/src/core/checks/checks/response_headers_conformance.ts +74 -0
- package/src/core/checks/checks/response_schema_conformance.ts +30 -0
- package/src/core/checks/checks/status_code_conformance.ts +61 -0
- package/src/core/checks/checks/unsupported_method.ts +63 -0
- package/src/core/checks/checks/use_after_free.ts +78 -0
- package/src/core/checks/index.ts +30 -0
- package/src/core/checks/mode.ts +79 -0
- package/src/core/checks/recommended-action.ts +64 -0
- package/src/core/checks/registry.ts +78 -0
- package/src/core/checks/runner.ts +874 -0
- package/src/core/checks/sarif.ts +230 -0
- package/src/core/checks/stateful.ts +121 -0
- package/src/core/checks/types.ts +189 -0
- package/src/core/classifier/recommended-action.ts +222 -0
- package/src/core/context/current.ts +51 -0
- package/src/core/context/session.ts +78 -0
- package/src/core/coverage/loader.ts +185 -0
- package/src/core/coverage/reasons.ts +300 -0
- package/src/core/diagnostics/db-analysis.ts +161 -12
- package/src/core/diagnostics/failure-class.ts +120 -0
- package/src/core/diagnostics/failure-hints.ts +212 -9
- package/src/core/diagnostics/spec-pointer.ts +99 -0
- package/src/core/diagnostics/suggested-fixes.ts +156 -0
- package/src/core/exporter/case-study/index.ts +270 -0
- package/src/core/exporter/curl.ts +40 -0
- package/src/core/exporter/exporter.ts +48 -0
- package/src/core/exporter/html-report/escape.ts +24 -0
- package/src/core/exporter/html-report/index.ts +479 -0
- package/src/core/exporter/html-report/script.ts +100 -0
- package/src/core/exporter/html-report/styles.ts +408 -0
- package/src/core/generator/chunker.ts +53 -15
- package/src/core/generator/coverage-phase.ts +0 -0
- package/src/core/generator/create-body.ts +89 -0
- package/src/core/generator/data-factory.ts +490 -33
- package/src/core/generator/describe.ts +1 -1
- package/src/core/generator/fixtures-builder.ts +325 -0
- package/src/core/generator/index.ts +7 -5
- package/src/core/generator/openapi-reader.ts +55 -3
- package/src/core/generator/path-param-disambig.ts +114 -0
- package/src/core/generator/resources-builder.ts +648 -0
- package/src/core/generator/schema-utils.ts +11 -3
- package/src/core/generator/serializer.ts +114 -15
- package/src/core/generator/suite-generator.ts +484 -77
- package/src/core/generator/types.ts +8 -0
- package/src/core/identity/identity-file.ts +129 -0
- package/src/core/lint/affects.ts +28 -0
- package/src/core/lint/config.ts +96 -0
- package/src/core/lint/format.ts +42 -0
- package/src/core/lint/index.ts +94 -0
- package/src/core/lint/reporter.ts +128 -0
- package/src/core/lint/rules/consistency.ts +158 -0
- package/src/core/lint/rules/heuristics.ts +97 -0
- package/src/core/lint/rules/strictness.ts +109 -0
- package/src/core/lint/types.ts +96 -0
- package/src/core/lint/walker.ts +248 -0
- package/src/core/meta/meta-store.ts +6 -73
- package/src/core/output/README.md +91 -0
- package/src/core/output/index.ts +13 -0
- package/src/core/output/run.ts +126 -0
- package/src/core/output/types.ts +129 -0
- package/src/core/parser/env-interpolation.ts +104 -0
- package/src/core/parser/filter.ts +57 -0
- package/src/core/parser/schema.ts +132 -5
- package/src/core/parser/types.ts +29 -2
- package/src/core/parser/variables.ts +0 -0
- package/src/core/parser/yaml-parser.ts +108 -13
- package/src/core/probe/bootstrap.ts +34 -0
- package/src/core/probe/dry-run-envelope.ts +57 -0
- package/src/core/probe/mass-assignment-probe-class.ts +198 -0
- package/src/core/probe/mass-assignment-probe.ts +1122 -0
- package/src/core/probe/mass-assignment-template.ts +212 -0
- package/src/core/probe/method-probe.ts +164 -0
- package/src/core/probe/method-shared.ts +69 -0
- package/src/core/probe/negative-probe.ts +691 -0
- package/src/core/probe/orphan-tracker.ts +188 -0
- package/src/core/probe/path-discovery.ts +440 -0
- package/src/core/probe/probe-harness.ts +120 -0
- package/src/core/probe/registry.ts +89 -0
- package/src/core/probe/runner.ts +136 -0
- package/src/core/probe/security-probe-class.ts +201 -0
- package/src/core/probe/security-probe.ts +1453 -0
- package/src/core/probe/shared.ts +505 -0
- package/src/core/probe/static-probe-class.ts +125 -0
- package/src/core/probe/types.ts +165 -0
- package/src/core/probe/verdict-aggregator.ts +33 -0
- package/src/core/probe/webhooks-probe.ts +284 -0
- package/src/core/reporter/console.ts +69 -4
- package/src/core/reporter/index.ts +2 -3
- package/src/core/reporter/json.ts +15 -2
- package/src/core/reporter/junit.ts +27 -12
- package/src/core/reporter/ndjson.ts +37 -0
- package/src/core/reporter/types.ts +3 -0
- package/src/core/runner/assertions.ts +62 -2
- package/src/core/runner/async-pool.ts +108 -0
- package/src/core/runner/auth-path.ts +8 -0
- package/src/core/runner/ci-context.ts +72 -0
- package/src/core/runner/executor.ts +391 -52
- package/src/core/runner/form-encode.ts +51 -0
- package/src/core/runner/http-client.ts +115 -7
- package/src/core/runner/learn-drift.ts +293 -0
- package/src/core/runner/preflight-vars.ts +149 -0
- package/src/core/runner/progress-tracker.ts +73 -0
- package/src/core/runner/rate-limiter.ts +203 -0
- package/src/core/runner/run-kind.ts +39 -0
- package/src/core/runner/schema-validator.ts +312 -0
- package/src/core/runner/send-request.ts +153 -20
- package/src/core/runner/types.ts +38 -0
- package/src/core/secrets/registry.ts +164 -0
- package/src/core/secrets/secrets-file.ts +115 -0
- package/src/core/selectors/operation-filter.ts +144 -0
- package/src/core/setup-api.ts +419 -17
- package/src/core/severity/category.ts +94 -0
- package/src/core/severity/index.ts +121 -0
- package/src/core/spec/layers.ts +154 -0
- package/src/core/util/format-eta.ts +21 -0
- package/src/core/utils.ts +5 -1
- package/src/core/workspace/config.ts +129 -0
- package/src/core/workspace/manifest.ts +283 -0
- package/src/core/workspace/output-rotation.ts +62 -0
- package/src/core/workspace/root.ts +94 -0
- package/src/core/workspace/triage-path.ts +87 -0
- package/src/db/lint-runs.ts +47 -0
- package/src/db/migrate.ts +126 -0
- package/src/db/migrations/0001_run_kind.sql +25 -0
- package/src/db/migrations/sql.d.ts +4 -0
- package/src/db/queries/collections.ts +133 -0
- package/src/db/queries/coverage.ts +9 -0
- package/src/db/queries/dashboard.ts +59 -0
- package/src/db/queries/results.ts +128 -0
- package/src/db/queries/runs.ts +235 -0
- package/src/db/queries/sessions.ts +42 -0
- package/src/db/queries/settings.ts +28 -0
- package/src/db/queries/types.ts +172 -0
- package/src/db/queries.ts +72 -802
- package/src/db/schema.ts +179 -48
- package/src/cli/commands/export.ts +0 -144
- package/src/cli/commands/guide.ts +0 -127
- package/src/cli/commands/init.ts +0 -57
- package/src/cli/commands/serve.ts +0 -81
- package/src/cli/commands/sync.ts +0 -269
- package/src/cli/commands/update.ts +0 -189
- package/src/cli/commands/validate.ts +0 -34
- package/src/core/exporter/postman.ts +0 -963
- package/src/core/generator/guide-builder.ts +0 -253
- package/src/core/meta/types.ts +0 -21
- package/src/core/parser/index.ts +0 -21
- package/src/core/runner/execute-run.ts +0 -132
- package/src/core/runner/index.ts +0 -12
- package/src/core/sync/spec-differ.ts +0 -38
- package/src/web/data/collection-state.ts +0 -362
- package/src/web/routes/api.ts +0 -314
- package/src/web/routes/dashboard.ts +0 -350
- package/src/web/routes/runs.ts +0 -64
- package/src/web/schemas.ts +0 -121
- package/src/web/server.ts +0 -134
- package/src/web/static/htmx.min.cjs +0 -1
- package/src/web/static/style.css +0 -1148
- package/src/web/views/endpoints-tab.ts +0 -174
- package/src/web/views/explorer-tab.ts +0 -402
- package/src/web/views/health-strip.ts +0 -92
- package/src/web/views/layout.ts +0 -48
- package/src/web/views/results.ts +0 -210
- package/src/web/views/runs-tab.ts +0 -126
- package/src/web/views/suites-tab.ts +0 -181
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import { startServer } from "../../web/server.ts";
|
|
2
|
-
|
|
3
|
-
export interface ServeOptions {
|
|
4
|
-
port?: number;
|
|
5
|
-
host?: string;
|
|
6
|
-
dbPath?: string;
|
|
7
|
-
watch?: boolean;
|
|
8
|
-
open?: boolean;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/** Kill any existing process listening on the given port (Windows + Unix) */
|
|
12
|
-
async function killPortHolder(port: number): Promise<void> {
|
|
13
|
-
const isWin = process.platform === "win32";
|
|
14
|
-
try {
|
|
15
|
-
if (isWin) {
|
|
16
|
-
// PowerShell: find PID on port, then kill it
|
|
17
|
-
const find = Bun.spawn(["powershell", "-NoProfile", "-Command",
|
|
18
|
-
`(Get-NetTCPConnection -LocalPort ${port} -ErrorAction SilentlyContinue).OwningProcess`], {
|
|
19
|
-
stdout: "pipe", stderr: "ignore",
|
|
20
|
-
});
|
|
21
|
-
const out = await new Response(find.stdout).text();
|
|
22
|
-
const pids = [...new Set(out.trim().split(/\s+/).filter(s => /^\d+$/.test(s) && s !== "0"))];
|
|
23
|
-
for (const pid of pids) {
|
|
24
|
-
Bun.spawn(["powershell", "-NoProfile", "-Command",
|
|
25
|
-
`Stop-Process -Id ${pid} -Force -ErrorAction SilentlyContinue`], {
|
|
26
|
-
stdout: "ignore", stderr: "ignore",
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
if (pids.length > 0) {
|
|
30
|
-
// Give OS time to release the port
|
|
31
|
-
await Bun.sleep(500);
|
|
32
|
-
}
|
|
33
|
-
} else {
|
|
34
|
-
// Unix: lsof + kill
|
|
35
|
-
const find = Bun.spawn(["lsof", "-ti", `:${port}`], {
|
|
36
|
-
stdout: "pipe", stderr: "ignore",
|
|
37
|
-
});
|
|
38
|
-
const out = await new Response(find.stdout).text();
|
|
39
|
-
const pids = out.trim().split(/\s+/).filter(s => /^\d+$/.test(s));
|
|
40
|
-
for (const pid of pids) {
|
|
41
|
-
Bun.spawn(["kill", "-9", pid], { stdout: "ignore", stderr: "ignore" });
|
|
42
|
-
}
|
|
43
|
-
if (pids.length > 0) {
|
|
44
|
-
await Bun.sleep(300);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
} catch {
|
|
48
|
-
// Best effort — if we can't kill, startServer will fail with port-in-use
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export async function serveCommand(options: ServeOptions): Promise<number> {
|
|
53
|
-
const port = options.port ?? 8080;
|
|
54
|
-
|
|
55
|
-
// Kill previous instance on the same port
|
|
56
|
-
await killPortHolder(port);
|
|
57
|
-
|
|
58
|
-
await startServer({
|
|
59
|
-
port,
|
|
60
|
-
host: options.host,
|
|
61
|
-
dbPath: options.dbPath,
|
|
62
|
-
dev: options.watch,
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
// Open browser if requested
|
|
66
|
-
if (options.open) {
|
|
67
|
-
const host = options.host === "0.0.0.0" || !options.host ? "localhost" : options.host;
|
|
68
|
-
const url = `http://${host}:${port}`;
|
|
69
|
-
try {
|
|
70
|
-
const cmd = process.platform === "win32" ? ["cmd", "/c", "start", url]
|
|
71
|
-
: process.platform === "darwin" ? ["open", url]
|
|
72
|
-
: ["xdg-open", url];
|
|
73
|
-
Bun.spawn(cmd, { stdout: "ignore", stderr: "ignore" });
|
|
74
|
-
} catch {
|
|
75
|
-
// Best effort — if browser can't open, server still runs
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Keep running — Bun.serve keeps the process alive
|
|
80
|
-
return 0;
|
|
81
|
-
}
|
package/src/cli/commands/sync.ts
DELETED
|
@@ -1,269 +0,0 @@
|
|
|
1
|
-
import { join } from "path";
|
|
2
|
-
import { mkdir } from "fs/promises";
|
|
3
|
-
import {
|
|
4
|
-
readOpenApiSpec,
|
|
5
|
-
extractEndpoints,
|
|
6
|
-
extractSecuritySchemes,
|
|
7
|
-
serializeSuite,
|
|
8
|
-
buildCatalog,
|
|
9
|
-
serializeCatalog,
|
|
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 { readMeta, writeMeta, hashSpec, buildFileMeta } from "../../core/meta/meta-store.ts";
|
|
14
|
-
import { diffEndpoints } from "../../core/sync/spec-differ.ts";
|
|
15
|
-
import { decycleSchema } from "../../core/generator/schema-utils.ts";
|
|
16
|
-
import { printError, printSuccess, printWarning } from "../output.ts";
|
|
17
|
-
import { jsonOk, jsonError, printJson } from "../json-envelope.ts";
|
|
18
|
-
import { version as ZOND_VERSION } from "../../../package.json";
|
|
19
|
-
import { getDb } from "../../db/schema.ts";
|
|
20
|
-
import { findCollectionByTestPath, updateCollection } from "../../db/queries.ts";
|
|
21
|
-
|
|
22
|
-
export interface SyncOptions {
|
|
23
|
-
specPath: string;
|
|
24
|
-
testsDir: string;
|
|
25
|
-
dryRun?: boolean;
|
|
26
|
-
tag?: string;
|
|
27
|
-
json?: boolean;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export async function syncCommand(options: SyncOptions): Promise<number> {
|
|
31
|
-
try {
|
|
32
|
-
// Load existing metadata
|
|
33
|
-
const meta = await readMeta(options.testsDir);
|
|
34
|
-
if (!meta) {
|
|
35
|
-
const msg =
|
|
36
|
-
"No .zond-meta.json found. Run `zond generate <spec> --output <dir>` first to initialize metadata.";
|
|
37
|
-
if (options.json) {
|
|
38
|
-
printJson(jsonError("sync", [msg]));
|
|
39
|
-
} else {
|
|
40
|
-
printError(msg);
|
|
41
|
-
}
|
|
42
|
-
return 2;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Load current spec
|
|
46
|
-
const doc = await readOpenApiSpec(options.specPath);
|
|
47
|
-
const specContent = JSON.stringify(decycleSchema(doc));
|
|
48
|
-
const currentHash = hashSpec(specContent);
|
|
49
|
-
|
|
50
|
-
if (currentHash === meta.specHash) {
|
|
51
|
-
const msg = "Spec unchanged — nothing to sync.";
|
|
52
|
-
if (options.json) {
|
|
53
|
-
printJson(jsonOk("sync", { newEndpoints: [], generatedFiles: [], removedKeys: [], specChanged: false }, [msg]));
|
|
54
|
-
} else {
|
|
55
|
-
console.log(msg);
|
|
56
|
-
}
|
|
57
|
-
return 0;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Extract current endpoints
|
|
61
|
-
let currentEndpoints = extractEndpoints(doc);
|
|
62
|
-
const securitySchemes = extractSecuritySchemes(doc);
|
|
63
|
-
|
|
64
|
-
if (options.tag) {
|
|
65
|
-
currentEndpoints = filterByTag(currentEndpoints, options.tag);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Collect all previously known endpoint keys from meta
|
|
69
|
-
const prevKeys = Object.values(meta.files).flatMap((f) => f.endpoints);
|
|
70
|
-
|
|
71
|
-
// Compute diff
|
|
72
|
-
const { newEndpoints, removedKeys } = diffEndpoints(prevKeys, currentEndpoints);
|
|
73
|
-
|
|
74
|
-
const warnings: string[] = [];
|
|
75
|
-
|
|
76
|
-
if (removedKeys.length > 0) {
|
|
77
|
-
for (const key of removedKeys) {
|
|
78
|
-
warnings.push(`Removed endpoint not deleted from tests (review manually): ${key}`);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (newEndpoints.length === 0) {
|
|
83
|
-
// Update catalog even when no new endpoints — spec schema may have changed
|
|
84
|
-
const allEndpoints = extractEndpoints(doc);
|
|
85
|
-
const catalog = buildCatalog({
|
|
86
|
-
endpoints: allEndpoints,
|
|
87
|
-
securitySchemes,
|
|
88
|
-
specSource: options.specPath,
|
|
89
|
-
specHash: currentHash,
|
|
90
|
-
apiName: (doc as any).info?.title,
|
|
91
|
-
apiVersion: (doc as any).info?.version,
|
|
92
|
-
baseUrl: (doc as any).servers?.[0]?.url,
|
|
93
|
-
});
|
|
94
|
-
await Bun.write(join(options.testsDir, ".api-catalog.yaml"), serializeCatalog(catalog));
|
|
95
|
-
|
|
96
|
-
const msg = "Spec changed (hash differs) but no new endpoints detected. Existing tests may need manual review.";
|
|
97
|
-
warnings.push(msg);
|
|
98
|
-
if (options.json) {
|
|
99
|
-
printJson(jsonOk("sync", {
|
|
100
|
-
newEndpoints: [],
|
|
101
|
-
removedKeys,
|
|
102
|
-
generatedFiles: [],
|
|
103
|
-
specChanged: true,
|
|
104
|
-
}, warnings));
|
|
105
|
-
} else {
|
|
106
|
-
console.log(msg);
|
|
107
|
-
for (const w of warnings) {
|
|
108
|
-
printWarning(w);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
return 0;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// Generate suites for new endpoints only
|
|
115
|
-
const suites = generateSuites({ endpoints: newEndpoints, securitySchemes });
|
|
116
|
-
|
|
117
|
-
if (options.dryRun) {
|
|
118
|
-
const newEndpointKeys = newEndpoints.map((ep) => `${ep.method.toUpperCase()} ${ep.path}`);
|
|
119
|
-
const plannedFiles = suites.map((s) => ({
|
|
120
|
-
file: `${s.fileStem ?? s.name}.yaml`,
|
|
121
|
-
suite: s.name,
|
|
122
|
-
tests: s.tests.length,
|
|
123
|
-
}));
|
|
124
|
-
|
|
125
|
-
if (options.json) {
|
|
126
|
-
printJson(jsonOk("sync", {
|
|
127
|
-
dryRun: true,
|
|
128
|
-
newEndpoints: newEndpointKeys,
|
|
129
|
-
removedKeys,
|
|
130
|
-
plannedFiles,
|
|
131
|
-
specChanged: true,
|
|
132
|
-
}, warnings));
|
|
133
|
-
} else {
|
|
134
|
-
console.log(`[dry-run] Detected ${newEndpoints.length} new endpoint(s):`);
|
|
135
|
-
for (const ep of newEndpoints) {
|
|
136
|
-
console.log(` + ${ep.method.toUpperCase()} ${ep.path}`);
|
|
137
|
-
}
|
|
138
|
-
console.log(`\nWould generate ${suites.length} new suite file(s):`);
|
|
139
|
-
for (const f of plannedFiles) {
|
|
140
|
-
console.log(` ${f.file} (${f.tests} tests)`);
|
|
141
|
-
}
|
|
142
|
-
if (removedKeys.length > 0) {
|
|
143
|
-
console.log("\nRemoved endpoints (not deleted — review tests):");
|
|
144
|
-
for (const key of removedKeys) {
|
|
145
|
-
console.log(` - ${key}`);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
console.log("\nNo files written (dry-run).");
|
|
149
|
-
}
|
|
150
|
-
return 0;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// Write new files (skip if file already exists)
|
|
154
|
-
await mkdir(options.testsDir, { recursive: true });
|
|
155
|
-
|
|
156
|
-
const generatedFiles: Array<{ file: string; suite: string; tests: number }> = [];
|
|
157
|
-
const skippedFiles: string[] = [];
|
|
158
|
-
const updatedMetaFiles: Record<string, import("../../core/meta/types.ts").FileMeta> = {};
|
|
159
|
-
|
|
160
|
-
for (const suite of suites) {
|
|
161
|
-
const fileName = `${suite.fileStem ?? suite.name}.yaml`;
|
|
162
|
-
const filePath = join(options.testsDir, fileName);
|
|
163
|
-
const existing = Bun.file(filePath);
|
|
164
|
-
|
|
165
|
-
if (await existing.exists()) {
|
|
166
|
-
skippedFiles.push(fileName);
|
|
167
|
-
warnings.push(`Skipped ${fileName} (already exists — add new endpoints manually)`);
|
|
168
|
-
continue;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const yaml = serializeSuite(suite);
|
|
172
|
-
await Bun.write(filePath, yaml);
|
|
173
|
-
generatedFiles.push({ file: filePath, suite: suite.name, tests: suite.tests.length });
|
|
174
|
-
updatedMetaFiles[fileName] = buildFileMeta(suite, ZOND_VERSION);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// Update metadata: merge new file entries, update hash and timestamp
|
|
178
|
-
await writeMeta(options.testsDir, {
|
|
179
|
-
zondVersion: ZOND_VERSION,
|
|
180
|
-
lastSyncedAt: new Date().toISOString(),
|
|
181
|
-
specUrl: options.specPath,
|
|
182
|
-
specHash: currentHash,
|
|
183
|
-
files: { ...meta.files, ...updatedMetaFiles },
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
// Update .api-catalog.yaml with current spec state
|
|
187
|
-
const allEndpoints = extractEndpoints(doc);
|
|
188
|
-
const catalog = buildCatalog({
|
|
189
|
-
endpoints: allEndpoints,
|
|
190
|
-
securitySchemes,
|
|
191
|
-
specSource: options.specPath,
|
|
192
|
-
specHash: currentHash,
|
|
193
|
-
apiName: (doc as any).info?.title,
|
|
194
|
-
apiVersion: (doc as any).info?.version,
|
|
195
|
-
baseUrl: (doc as any).servers?.[0]?.url,
|
|
196
|
-
});
|
|
197
|
-
await Bun.write(join(options.testsDir, ".api-catalog.yaml"), serializeCatalog(catalog));
|
|
198
|
-
|
|
199
|
-
// Sync DB collection if one is registered for this tests directory
|
|
200
|
-
try {
|
|
201
|
-
getDb();
|
|
202
|
-
const collection = findCollectionByTestPath(options.testsDir);
|
|
203
|
-
if (collection && collection.openapi_spec !== options.specPath) {
|
|
204
|
-
updateCollection(collection.id, { openapi_spec: options.specPath });
|
|
205
|
-
warnings.push(`Updated collection '${collection.name}' spec reference: ${collection.openapi_spec ?? "(none)"} → ${options.specPath}`);
|
|
206
|
-
}
|
|
207
|
-
} catch {
|
|
208
|
-
// DB unavailable (e.g. no zond.db yet) — not a fatal error for sync
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
const newEndpointKeys = newEndpoints.map((ep) => `${ep.method.toUpperCase()} ${ep.path}`);
|
|
212
|
-
|
|
213
|
-
if (options.json) {
|
|
214
|
-
printJson(jsonOk("sync", {
|
|
215
|
-
newEndpoints: newEndpointKeys,
|
|
216
|
-
removedKeys,
|
|
217
|
-
generatedFiles,
|
|
218
|
-
skippedFiles,
|
|
219
|
-
specChanged: true,
|
|
220
|
-
}, warnings));
|
|
221
|
-
} else {
|
|
222
|
-
console.log(`Spec changed. Detected ${newEndpoints.length} new endpoint(s):`);
|
|
223
|
-
for (const ep of newEndpoints) {
|
|
224
|
-
console.log(` + ${ep.method.toUpperCase()} ${ep.path}`);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
if (generatedFiles.length > 0) {
|
|
228
|
-
console.log(`\nGenerated ${generatedFiles.length} new suite file(s):`);
|
|
229
|
-
for (const f of generatedFiles) {
|
|
230
|
-
console.log(` ${f.file} (${f.tests} tests)`);
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
if (skippedFiles.length > 0) {
|
|
235
|
-
console.log("\nSkipped (file exists, review manually):");
|
|
236
|
-
for (const f of skippedFiles) {
|
|
237
|
-
console.log(` ${f}`);
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
if (removedKeys.length > 0) {
|
|
242
|
-
console.log("\nRemoved endpoints (not deleted — review tests):");
|
|
243
|
-
for (const key of removedKeys) {
|
|
244
|
-
console.log(` - ${key}`);
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
if (generatedFiles.length > 0) {
|
|
249
|
-
printSuccess(`\nSync complete. ${generatedFiles.length} file(s) written.`);
|
|
250
|
-
} else {
|
|
251
|
-
printWarning("No new files written — all target files already exist.");
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
for (const w of warnings) {
|
|
255
|
-
printWarning(w);
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
return 0;
|
|
260
|
-
} catch (err) {
|
|
261
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
262
|
-
if (options.json) {
|
|
263
|
-
printJson(jsonError("sync", [message]));
|
|
264
|
-
} else {
|
|
265
|
-
printError(message);
|
|
266
|
-
}
|
|
267
|
-
return 2;
|
|
268
|
-
}
|
|
269
|
-
}
|
|
@@ -1,189 +0,0 @@
|
|
|
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. Install binary: curl -fsSL https://raw.githubusercontent.com/kirrosh/zond/master/install.sh | sh";
|
|
43
|
-
if (options.json) {
|
|
44
|
-
printJson(jsonOk("update", { action: "skip", reason: "not-standalone", installHint: "curl -fsSL https://raw.githubusercontent.com/kirrosh/zond/master/install.sh | sh" }, [msg]));
|
|
45
|
-
} else {
|
|
46
|
-
printWarning(msg);
|
|
47
|
-
}
|
|
48
|
-
return 3;
|
|
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
|
-
try {
|
|
145
|
-
if (process.platform === "win32") {
|
|
146
|
-
// Windows: rename current to .old, move new, clean up
|
|
147
|
-
const oldBinary = currentBinary + ".old";
|
|
148
|
-
try { await rm(oldBinary, { force: true }); } catch {}
|
|
149
|
-
await rename(currentBinary, oldBinary);
|
|
150
|
-
await rename(newBinary, currentBinary);
|
|
151
|
-
try { await rm(oldBinary, { force: true }); } catch {}
|
|
152
|
-
} else {
|
|
153
|
-
await rename(newBinary, currentBinary);
|
|
154
|
-
await chmod(currentBinary, 0o755);
|
|
155
|
-
}
|
|
156
|
-
} catch (replaceErr: any) {
|
|
157
|
-
if (replaceErr?.code === "EACCES" || replaceErr?.code === "EPERM") {
|
|
158
|
-
const hint = process.platform === "win32"
|
|
159
|
-
? `Permission denied. Run the terminal as Administrator.`
|
|
160
|
-
: `Permission denied writing to ${currentBinary}. Run: sudo zond update`;
|
|
161
|
-
if (options.json) {
|
|
162
|
-
printJson(jsonError("update", [hint]));
|
|
163
|
-
} else {
|
|
164
|
-
printError(hint);
|
|
165
|
-
}
|
|
166
|
-
return 2;
|
|
167
|
-
}
|
|
168
|
-
throw replaceErr;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
if (options.json) {
|
|
172
|
-
printJson(jsonOk("update", { action: "updated", previousVersion: VERSION, newVersion: latest }));
|
|
173
|
-
} else {
|
|
174
|
-
printSuccess(`Updated zond ${VERSION} → ${latest}`);
|
|
175
|
-
}
|
|
176
|
-
return 0;
|
|
177
|
-
} finally {
|
|
178
|
-
try { await rm(tmpDir, { recursive: true, force: true }); } catch {}
|
|
179
|
-
}
|
|
180
|
-
} catch (err) {
|
|
181
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
182
|
-
if (options.json) {
|
|
183
|
-
printJson(jsonError("update", [message]));
|
|
184
|
-
} else {
|
|
185
|
-
printError(message);
|
|
186
|
-
}
|
|
187
|
-
return 2;
|
|
188
|
-
}
|
|
189
|
-
}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { parse } from "../../core/parser/yaml-parser.ts";
|
|
2
|
-
import { printError, printSuccess } from "../output.ts";
|
|
3
|
-
import { jsonOk, jsonError, printJson } from "../json-envelope.ts";
|
|
4
|
-
|
|
5
|
-
export interface ValidateOptions {
|
|
6
|
-
path: string;
|
|
7
|
-
json?: boolean;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export async function validateCommand(options: ValidateOptions): Promise<number> {
|
|
11
|
-
try {
|
|
12
|
-
const suites = await parse(options.path);
|
|
13
|
-
const totalSteps = suites.reduce((sum, s) => sum + s.tests.length, 0);
|
|
14
|
-
if (options.json) {
|
|
15
|
-
printJson(jsonOk("validate", {
|
|
16
|
-
files: suites.length,
|
|
17
|
-
suites: suites.length,
|
|
18
|
-
tests: totalSteps,
|
|
19
|
-
valid: true,
|
|
20
|
-
}));
|
|
21
|
-
} else {
|
|
22
|
-
printSuccess(`OK: ${suites.length} suite(s), ${totalSteps} test(s) validated successfully`);
|
|
23
|
-
}
|
|
24
|
-
return 0;
|
|
25
|
-
} catch (err) {
|
|
26
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
27
|
-
if (options.json) {
|
|
28
|
-
printJson(jsonError("validate", [message]));
|
|
29
|
-
} else {
|
|
30
|
-
printError(message);
|
|
31
|
-
}
|
|
32
|
-
return 2;
|
|
33
|
-
}
|
|
34
|
-
}
|