@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
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `zond fixtures` umbrella — manual fixture-bootstrap UX (ARV-195).
|
|
3
|
+
*
|
|
4
|
+
* Two subcommands today, both targeting the case `prepare-fixtures
|
|
5
|
+
* --seed` cannot solve:
|
|
6
|
+
*
|
|
7
|
+
* • `zond fixtures add <var>=<id> [--validate]`
|
|
8
|
+
* Set a fixture by hand. With `--validate` the command GETs the
|
|
9
|
+
* resource's read-by-id endpoint and classifies the value as
|
|
10
|
+
* `live` (200/2xx), `stale` (404), or `unknown` (no read endpoint
|
|
11
|
+
* wired or non-2xx/non-404 status).
|
|
12
|
+
*
|
|
13
|
+
* • `zond fixtures import --from-curl`
|
|
14
|
+
* Paste a curl command (from a vendor dashboard / Chrome
|
|
15
|
+
* devtools) on stdin or via `--curl <text>`. The URL is matched
|
|
16
|
+
* against `apis/<name>/spec.json` paths; every `{var}` segment
|
|
17
|
+
* whose corresponding part of the URL is a literal id contributes
|
|
18
|
+
* a fixture. Reports the inferred map; with `--apply` writes it
|
|
19
|
+
* to `.env.yaml` (with .bak backup).
|
|
20
|
+
*
|
|
21
|
+
* Both commands target `apis/<name>/.env.yaml` resolved via the standard
|
|
22
|
+
* --api / ZOND_API / .zond/current-api chain. They never touch the
|
|
23
|
+
* manifest (.api-fixtures.yaml) — vars not in the manifest are still
|
|
24
|
+
* written but flagged as `not in manifest, ignored` by the next
|
|
25
|
+
* `prepare-fixtures` run, mirroring existing semantics.
|
|
26
|
+
*/
|
|
27
|
+
import type { Command } from "commander";
|
|
28
|
+
import { join } from "node:path";
|
|
29
|
+
import { copyFile } from "node:fs/promises";
|
|
30
|
+
|
|
31
|
+
import { getApi, MISSING_API_MESSAGE } from "../util/api-context.ts";
|
|
32
|
+
import { resolveApiCollection } from "../resolve.ts";
|
|
33
|
+
import { resolveCollectionSpec } from "../../core/setup-api.ts";
|
|
34
|
+
import { findCollectionByNameOrId } from "../../db/queries.ts";
|
|
35
|
+
import { readOpenApiSpec } from "../../core/generator/index.ts";
|
|
36
|
+
import { upsertEnvLine } from "./discover.ts";
|
|
37
|
+
import { jsonOk, jsonError, printJson } from "../json-envelope.ts";
|
|
38
|
+
import { printError, printSuccess } from "../output.ts";
|
|
39
|
+
import { globalJson } from "../resolve.ts";
|
|
40
|
+
import { executeRequest } from "../../core/runner/http-client.ts";
|
|
41
|
+
import { loadEnvFile } from "../../core/parser/variables.ts";
|
|
42
|
+
|
|
43
|
+
interface AddOptions {
|
|
44
|
+
api?: string;
|
|
45
|
+
validate?: boolean;
|
|
46
|
+
apply?: boolean;
|
|
47
|
+
json?: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface ImportOptions {
|
|
51
|
+
api?: string;
|
|
52
|
+
fromCurl?: boolean;
|
|
53
|
+
curl?: string;
|
|
54
|
+
apply?: boolean;
|
|
55
|
+
json?: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function resolveApiContext(
|
|
59
|
+
cmd: Command,
|
|
60
|
+
optsApi: string | undefined,
|
|
61
|
+
json: boolean,
|
|
62
|
+
): { apiName: string; baseDir: string; specPath: string; envPath: string } | { error: string } {
|
|
63
|
+
const apiName = getApi(cmd, { api: optsApi } as Record<string, unknown>);
|
|
64
|
+
if (!apiName) return { error: MISSING_API_MESSAGE };
|
|
65
|
+
const col = resolveApiCollection(apiName, undefined);
|
|
66
|
+
if ("error" in col) return { error: col.error };
|
|
67
|
+
if (!col.baseDir) return { error: `API '${apiName}' has no base_dir registered.` };
|
|
68
|
+
if (!col.spec) return { error: `API '${apiName}' has no spec registered (run 'zond add api ... --spec ...').` };
|
|
69
|
+
return {
|
|
70
|
+
apiName,
|
|
71
|
+
baseDir: col.baseDir,
|
|
72
|
+
specPath: col.spec,
|
|
73
|
+
envPath: join(col.baseDir, ".env.yaml"),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Walk the spec for read-by-id endpoints (GET on a path containing `{var}`
|
|
79
|
+
* exactly). Returns the first one whose template matches `{<varName>}`
|
|
80
|
+
* verbatim — used by `fixtures add --validate` to GET the resource and
|
|
81
|
+
* decide live/stale/unknown.
|
|
82
|
+
*/
|
|
83
|
+
async function findReadEndpointForVar(
|
|
84
|
+
specPath: string,
|
|
85
|
+
varName: string,
|
|
86
|
+
): Promise<{ method: string; path: string } | null> {
|
|
87
|
+
const doc = await readOpenApiSpec(specPath);
|
|
88
|
+
const placeholder = `{${varName}}`;
|
|
89
|
+
for (const [p, item] of Object.entries(doc.paths ?? {})) {
|
|
90
|
+
if (!item) continue;
|
|
91
|
+
if (!p.includes(placeholder)) continue;
|
|
92
|
+
if ((item as Record<string, unknown>).get) {
|
|
93
|
+
return { method: "GET", path: p };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function readEnv(envPath: string): Promise<Record<string, string>> {
|
|
100
|
+
return (await loadEnvFile(envPath)) ?? {};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function applyEnvWrites(
|
|
104
|
+
envPath: string,
|
|
105
|
+
writes: Record<string, string>,
|
|
106
|
+
): Promise<{ backup: string | null }> {
|
|
107
|
+
const file = Bun.file(envPath);
|
|
108
|
+
let text = (await file.exists()) ? await file.text() : "";
|
|
109
|
+
let backup: string | null = `${envPath}.bak`;
|
|
110
|
+
if (await file.exists()) {
|
|
111
|
+
try { await copyFile(envPath, backup); } catch { backup = null; }
|
|
112
|
+
} else {
|
|
113
|
+
backup = null;
|
|
114
|
+
}
|
|
115
|
+
for (const [k, v] of Object.entries(writes)) {
|
|
116
|
+
text = upsertEnvLine(text, k, v);
|
|
117
|
+
}
|
|
118
|
+
if (!text.endsWith("\n")) text += "\n";
|
|
119
|
+
await Bun.write(envPath, text);
|
|
120
|
+
return { backup };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function fillPath(template: string, vars: Record<string, string>): string {
|
|
124
|
+
return template.replace(/\{([^}]+)\}/g, (_, n) => {
|
|
125
|
+
const v = vars[n];
|
|
126
|
+
return typeof v === "string" && v.length > 0 ? encodeURIComponent(v) : `{${n}}`;
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function addAction(
|
|
131
|
+
pairs: string[],
|
|
132
|
+
cmd: Command,
|
|
133
|
+
): Promise<void> {
|
|
134
|
+
const opts = cmd.opts<AddOptions>();
|
|
135
|
+
const json = opts.json === true || globalJson(cmd);
|
|
136
|
+
const ctx = resolveApiContext(cmd, opts.api, json);
|
|
137
|
+
if ("error" in ctx) {
|
|
138
|
+
if (json) printJson(jsonError("fixtures add", [ctx.error]));
|
|
139
|
+
else printError(ctx.error);
|
|
140
|
+
process.exit(2);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Parse "var=value" pairs from positionals.
|
|
145
|
+
const writes: Record<string, string> = {};
|
|
146
|
+
for (const raw of pairs) {
|
|
147
|
+
const idx = raw.indexOf("=");
|
|
148
|
+
if (idx <= 0) {
|
|
149
|
+
const m = `Invalid fixture '${raw}' — expected 'var=value'`;
|
|
150
|
+
if (json) printJson(jsonError("fixtures add", [m])); else printError(m);
|
|
151
|
+
process.exit(2);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
writes[raw.slice(0, idx).trim()] = raw.slice(idx + 1);
|
|
155
|
+
}
|
|
156
|
+
if (Object.keys(writes).length === 0) {
|
|
157
|
+
const m = "No fixtures supplied. Usage: zond fixtures add <var>=<value> [<var>=<value> ...]";
|
|
158
|
+
if (json) printJson(jsonError("fixtures add", [m])); else printError(m);
|
|
159
|
+
process.exit(2);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ARV-32: optional read-by-id validate per fixture.
|
|
164
|
+
type Validation = { var: string; status: "live" | "stale" | "unknown"; httpStatus?: number; reason?: string };
|
|
165
|
+
const validations: Validation[] = [];
|
|
166
|
+
if (opts.validate) {
|
|
167
|
+
const env = await readEnv(ctx.envPath);
|
|
168
|
+
const baseUrl = env.base_url;
|
|
169
|
+
if (!baseUrl) {
|
|
170
|
+
const m = "Cannot --validate: base_url not set in .env.yaml.";
|
|
171
|
+
if (json) printJson(jsonError("fixtures add", [m])); else printError(m);
|
|
172
|
+
process.exit(2);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
for (const [k, v] of Object.entries(writes)) {
|
|
176
|
+
const ep = await findReadEndpointForVar(ctx.specPath, k);
|
|
177
|
+
if (!ep) {
|
|
178
|
+
validations.push({ var: k, status: "unknown", reason: "no GET endpoint with {" + k + "} in path" });
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
const url = `${baseUrl.replace(/\/+$/, "")}${fillPath(ep.path, { ...env, [k]: v })}`;
|
|
182
|
+
try {
|
|
183
|
+
const resp = await executeRequest(
|
|
184
|
+
{ method: "GET", url, headers: { accept: "application/json" } },
|
|
185
|
+
{ timeout: 10_000, retries: 0, network_retries: 1 },
|
|
186
|
+
);
|
|
187
|
+
if (resp.status >= 200 && resp.status < 300) {
|
|
188
|
+
validations.push({ var: k, status: "live", httpStatus: resp.status });
|
|
189
|
+
} else if (resp.status === 404) {
|
|
190
|
+
validations.push({ var: k, status: "stale", httpStatus: 404 });
|
|
191
|
+
} else {
|
|
192
|
+
validations.push({ var: k, status: "unknown", httpStatus: resp.status, reason: `non-2xx/non-404 status` });
|
|
193
|
+
}
|
|
194
|
+
} catch (err) {
|
|
195
|
+
validations.push({ var: k, status: "unknown", reason: (err as Error).message });
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
let backup: string | null = null;
|
|
201
|
+
if (opts.apply) {
|
|
202
|
+
const result = await applyEnvWrites(ctx.envPath, writes);
|
|
203
|
+
backup = result.backup;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (json) {
|
|
207
|
+
printJson(jsonOk("fixtures add", {
|
|
208
|
+
api: ctx.apiName,
|
|
209
|
+
env: ctx.envPath,
|
|
210
|
+
writes,
|
|
211
|
+
applied: opts.apply === true,
|
|
212
|
+
backup,
|
|
213
|
+
validations,
|
|
214
|
+
}));
|
|
215
|
+
} else {
|
|
216
|
+
if (opts.apply) {
|
|
217
|
+
printSuccess(`Wrote ${Object.keys(writes).length} fixture(s) to ${ctx.envPath}` + (backup ? ` (backup: ${backup})` : ""));
|
|
218
|
+
} else {
|
|
219
|
+
printSuccess(`Dry-run — pass --apply to write to ${ctx.envPath}`);
|
|
220
|
+
}
|
|
221
|
+
for (const [k, v] of Object.entries(writes)) {
|
|
222
|
+
const val = validations.find((x) => x.var === k);
|
|
223
|
+
const tag = val ? ` [${val.status}${val.httpStatus ? " " + val.httpStatus : ""}]${val.reason ? " — " + val.reason : ""}` : "";
|
|
224
|
+
console.log(` ${k} = ${v}${tag}`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
process.exit(0);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Strip the `curl` invocation down to the URL. Handles `-X METHOD`,
|
|
231
|
+
* `-H 'Header: v'`, `--data ...`, etc. — we only need the URL here. */
|
|
232
|
+
export function extractUrlFromCurl(curl: string): string | null {
|
|
233
|
+
const cleaned = curl.replace(/\\\n/g, " ").trim();
|
|
234
|
+
// Tokens are space-delimited, but URL values may be quoted. Walk the
|
|
235
|
+
// string with a small state machine so we honour single/double quotes.
|
|
236
|
+
const tokens: string[] = [];
|
|
237
|
+
let buf = "";
|
|
238
|
+
let quote: '"' | "'" | null = null;
|
|
239
|
+
for (let i = 0; i < cleaned.length; i++) {
|
|
240
|
+
const ch = cleaned[i]!;
|
|
241
|
+
if (quote) {
|
|
242
|
+
if (ch === quote) { quote = null; continue; }
|
|
243
|
+
buf += ch;
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
if (ch === '"' || ch === "'") { quote = ch as '"' | "'"; continue; }
|
|
247
|
+
if (/\s/.test(ch)) {
|
|
248
|
+
if (buf) tokens.push(buf);
|
|
249
|
+
buf = "";
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
buf += ch;
|
|
253
|
+
}
|
|
254
|
+
if (buf) tokens.push(buf);
|
|
255
|
+
|
|
256
|
+
// First token starting with http(s):// is the URL. Curl also accepts
|
|
257
|
+
// `--url <url>` and `-:` syntax; stay conservative and just look for
|
|
258
|
+
// a URL-shaped token.
|
|
259
|
+
for (const t of tokens) {
|
|
260
|
+
if (/^https?:\/\//i.test(t)) return t;
|
|
261
|
+
}
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Match a concrete URL path against spec path templates and extract
|
|
266
|
+
* `{var}` → value bindings. Returns the bindings of the FIRST template
|
|
267
|
+
* that matches the whole path, or empty when nothing matches. */
|
|
268
|
+
export function extractFixturesFromPath(
|
|
269
|
+
url: string,
|
|
270
|
+
specPaths: string[],
|
|
271
|
+
): { matchedTemplate: string; bindings: Record<string, string> } | null {
|
|
272
|
+
let pathname: string;
|
|
273
|
+
try { pathname = new URL(url).pathname; } catch { return null; }
|
|
274
|
+
// Sort longest-first so a 3-segment template wins over a 1-segment one.
|
|
275
|
+
const sorted = [...specPaths].sort((a, b) => b.split("/").length - a.split("/").length);
|
|
276
|
+
for (const tpl of sorted) {
|
|
277
|
+
const tplSegs = tpl.split("/").filter(Boolean);
|
|
278
|
+
const urlSegs = pathname.split("/").filter(Boolean);
|
|
279
|
+
if (tplSegs.length !== urlSegs.length) continue;
|
|
280
|
+
const bindings: Record<string, string> = {};
|
|
281
|
+
let ok = true;
|
|
282
|
+
for (let i = 0; i < tplSegs.length; i++) {
|
|
283
|
+
const ts = tplSegs[i]!;
|
|
284
|
+
const us = urlSegs[i]!;
|
|
285
|
+
const m = ts.match(/^\{([^}]+)\}$/);
|
|
286
|
+
if (m) {
|
|
287
|
+
try { bindings[m[1]!] = decodeURIComponent(us); } catch { bindings[m[1]!] = us; }
|
|
288
|
+
} else if (ts !== us) {
|
|
289
|
+
ok = false;
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (ok) return { matchedTemplate: tpl, bindings };
|
|
294
|
+
}
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async function importAction(cmd: Command): Promise<void> {
|
|
299
|
+
const opts = cmd.opts<ImportOptions>();
|
|
300
|
+
const json = opts.json === true || globalJson(cmd);
|
|
301
|
+
const ctx = resolveApiContext(cmd, opts.api, json);
|
|
302
|
+
if ("error" in ctx) {
|
|
303
|
+
if (json) printJson(jsonError("fixtures import", [ctx.error]));
|
|
304
|
+
else printError(ctx.error);
|
|
305
|
+
process.exit(2);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
if (opts.fromCurl !== true) {
|
|
309
|
+
const m = "Required: --from-curl. (Other importers can be added later.)";
|
|
310
|
+
if (json) printJson(jsonError("fixtures import", [m])); else printError(m);
|
|
311
|
+
process.exit(2);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
let curl = opts.curl;
|
|
316
|
+
if (!curl) {
|
|
317
|
+
// Read from stdin so the user can `pbpaste | zond fixtures import --from-curl`.
|
|
318
|
+
curl = (await Bun.stdin.text()).trim();
|
|
319
|
+
}
|
|
320
|
+
if (!curl || curl.length === 0) {
|
|
321
|
+
const m = "No curl input — pipe a 'curl ...' command on stdin or pass --curl '<text>'.";
|
|
322
|
+
if (json) printJson(jsonError("fixtures import", [m])); else printError(m);
|
|
323
|
+
process.exit(2);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const url = extractUrlFromCurl(curl);
|
|
328
|
+
if (!url) {
|
|
329
|
+
const m = "Could not extract a URL from the curl input.";
|
|
330
|
+
if (json) printJson(jsonError("fixtures import", [m])); else printError(m);
|
|
331
|
+
process.exit(2);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const doc = await readOpenApiSpec(ctx.specPath);
|
|
336
|
+
const specPaths = Object.keys(doc.paths ?? {});
|
|
337
|
+
const match = extractFixturesFromPath(url, specPaths);
|
|
338
|
+
if (!match || Object.keys(match.bindings).length === 0) {
|
|
339
|
+
const m = `URL '${url}' did not match any path template in the spec, or had no {var} bindings.`;
|
|
340
|
+
if (json) printJson(jsonError("fixtures import", [m])); else printError(m);
|
|
341
|
+
process.exit(2);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
let backup: string | null = null;
|
|
346
|
+
if (opts.apply) {
|
|
347
|
+
const result = await applyEnvWrites(ctx.envPath, match.bindings);
|
|
348
|
+
backup = result.backup;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (json) {
|
|
352
|
+
printJson(jsonOk("fixtures import", {
|
|
353
|
+
api: ctx.apiName,
|
|
354
|
+
env: ctx.envPath,
|
|
355
|
+
source: { kind: "curl", url, matchedTemplate: match.matchedTemplate },
|
|
356
|
+
writes: match.bindings,
|
|
357
|
+
applied: opts.apply === true,
|
|
358
|
+
backup,
|
|
359
|
+
}));
|
|
360
|
+
} else {
|
|
361
|
+
if (opts.apply) {
|
|
362
|
+
printSuccess(`Imported ${Object.keys(match.bindings).length} fixture(s) from curl URL`);
|
|
363
|
+
console.log(` source: ${url}`);
|
|
364
|
+
console.log(` matched: ${match.matchedTemplate}`);
|
|
365
|
+
console.log(` wrote to: ${ctx.envPath}` + (backup ? ` (backup: ${backup})` : ""));
|
|
366
|
+
} else {
|
|
367
|
+
printSuccess(`Dry-run — pass --apply to write to ${ctx.envPath}`);
|
|
368
|
+
console.log(` matched: ${match.matchedTemplate}`);
|
|
369
|
+
}
|
|
370
|
+
for (const [k, v] of Object.entries(match.bindings)) {
|
|
371
|
+
console.log(` ${k} = ${v}`);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
process.exit(0);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export function registerFixtures(program: Command): void {
|
|
378
|
+
const fixtures = program
|
|
379
|
+
.command("fixtures")
|
|
380
|
+
.description("Manual fixture-bootstrap helpers (ARV-195) — `add` and `import`. Complements `zond prepare-fixtures` for the cases auto-discover/--seed cannot solve (path-FK ids hidden in vendor dashboards, manual sandbox setup).");
|
|
381
|
+
|
|
382
|
+
fixtures
|
|
383
|
+
.command("add <pairs...>")
|
|
384
|
+
.description("Set one or more fixtures: 'var=value'. Optionally validate by GETing the spec's read-by-id endpoint for the var.")
|
|
385
|
+
.option("--api <name>", "Registered API (apis/<name>/.env.yaml). Falls back to ZOND_API / .zond/current-api.")
|
|
386
|
+
.option("--validate", "GET the resource's read-by-id endpoint and classify each value as live/stale/unknown.")
|
|
387
|
+
.option("--apply", "Write the fixtures to .env.yaml (with .env.yaml.bak backup). Default: dry-run.")
|
|
388
|
+
.action(async (pairs: string[], _opts, cmd: Command) => {
|
|
389
|
+
await addAction(pairs, cmd);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
fixtures
|
|
393
|
+
.command("import")
|
|
394
|
+
.description("Import fixtures from an external source. Today: --from-curl (paste a curl command from a vendor dashboard / Chrome devtools).")
|
|
395
|
+
.option("--api <name>", "Registered API (apis/<name>/.env.yaml). Falls back to ZOND_API / .zond/current-api.")
|
|
396
|
+
.option("--from-curl", "Treat input as a curl command. Reads from stdin or --curl <text>.")
|
|
397
|
+
.option("--curl <text>", "Inline curl command (alternative to stdin).")
|
|
398
|
+
.option("--apply", "Write the inferred fixtures to .env.yaml (with .env.yaml.bak backup). Default: dry-run.")
|
|
399
|
+
.action(async (_opts, cmd: Command) => {
|
|
400
|
+
await importAction(cmd);
|
|
401
|
+
});
|
|
402
|
+
}
|