@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,4 +1,5 @@
|
|
|
1
|
-
import { join,
|
|
1
|
+
import { join, resolve as resolvePath } from "path";
|
|
2
|
+
import { existsSync } from "fs";
|
|
2
3
|
import { mkdir } from "fs/promises";
|
|
3
4
|
import {
|
|
4
5
|
readOpenApiSpec,
|
|
@@ -7,26 +8,86 @@ import {
|
|
|
7
8
|
scanCoveredEndpoints,
|
|
8
9
|
filterUncoveredEndpoints,
|
|
9
10
|
serializeSuite,
|
|
10
|
-
buildCatalog,
|
|
11
|
-
serializeCatalog,
|
|
12
11
|
} from "../../core/generator/index.ts";
|
|
13
|
-
import {
|
|
14
|
-
|
|
12
|
+
import {
|
|
13
|
+
generateSuites,
|
|
14
|
+
findUnresolvedVars,
|
|
15
|
+
detectCrudGroupsWithDiagnostics,
|
|
16
|
+
} from "../../core/generator/suite-generator.ts";
|
|
17
|
+
import { generateFromSchema, classifyFieldSource } from "../../core/generator/data-factory.ts";
|
|
18
|
+
import { filterByTag, collectTags } from "../../core/generator/chunker.ts";
|
|
19
|
+
import { compileOperationFilter } from "../../core/selectors/operation-filter.ts";
|
|
15
20
|
import { parse } from "../../core/parser/yaml-parser.ts";
|
|
16
|
-
import {
|
|
21
|
+
import { loadEnvironment } from "../../core/parser/variables.ts";
|
|
17
22
|
import { printError, printSuccess } from "../output.ts";
|
|
18
23
|
import { jsonOk, jsonError, printJson } from "../json-envelope.ts";
|
|
19
|
-
import { readMeta, writeMeta, hashSpec, buildFileMeta } from "../../core/meta/meta-store.ts";
|
|
20
|
-
import { version as ZOND_VERSION } from "../../../package.json";
|
|
21
24
|
import { getDb } from "../../db/schema.ts";
|
|
22
25
|
import { findCollectionByTestPath, updateCollection } from "../../db/queries.ts";
|
|
26
|
+
import { findWorkspaceRoot } from "../../core/workspace/root.ts";
|
|
27
|
+
import { recordGeneratedFiles, inferApiName, autoGenHeader, type RecordInput } from "../../core/workspace/manifest.ts";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Walk up from outputDir looking for the API root — the first ancestor
|
|
31
|
+
* that already contains `.api-catalog.yaml` (= a directory `zond add api`
|
|
32
|
+
* has owned). Falls back to undefined when called from a non-conventional
|
|
33
|
+
* layout, in which case the caller writes `.env.yaml` next to outputDir.
|
|
34
|
+
*
|
|
35
|
+
* The walk stops at filesystem root (or HOME). The optional baseUrl is
|
|
36
|
+
* unused at the moment but kept on the signature so callers don't have
|
|
37
|
+
* to recompute the conditions for "should we even bother" — when no
|
|
38
|
+
* env vars are needed, the caller skips this entirely.
|
|
39
|
+
*/
|
|
40
|
+
function resolveApiRoot(outputDir: string, _baseUrl: string | undefined): string | undefined {
|
|
41
|
+
const abs = resolvePath(outputDir);
|
|
42
|
+
// 1) Walk up looking for an existing `.api-catalog.yaml` — strongest signal.
|
|
43
|
+
let dir = abs;
|
|
44
|
+
for (let i = 0; i < 8; i++) {
|
|
45
|
+
if (existsSync(join(dir, ".api-catalog.yaml"))) return dir;
|
|
46
|
+
const parent = resolvePath(dir, "..");
|
|
47
|
+
if (parent === dir) break;
|
|
48
|
+
dir = parent;
|
|
49
|
+
}
|
|
50
|
+
// 2) Fall back to the conventional layout: …/apis/<name>/[anything]/. The
|
|
51
|
+
// API root is the directory immediately under `apis/`. Picks up the
|
|
52
|
+
// case where the user runs `zond generate` before `zond add api`.
|
|
53
|
+
const norm = abs.replace(/\\/g, "/");
|
|
54
|
+
const m = norm.match(/^(.*?\/apis\/[^/]+)(?:\/|$)/);
|
|
55
|
+
return m?.[1];
|
|
56
|
+
}
|
|
23
57
|
|
|
24
58
|
export interface GenerateOptions {
|
|
25
59
|
specPath: string;
|
|
26
60
|
output: string;
|
|
27
61
|
tag?: string;
|
|
28
62
|
uncoveredOnly?: boolean;
|
|
63
|
+
/** When true, deprecated endpoints are included in generation. Default
|
|
64
|
+
* (false) filters them out and surfaces the count as a warning so users
|
|
65
|
+
* can distinguish "deprecated by design" from "accidentally dropped". */
|
|
66
|
+
includeDeprecated?: boolean;
|
|
67
|
+
/** TASK-139: dry-run that prints per-resource CRUD detection verdict and
|
|
68
|
+
* exits — no files written. Use to debug "why didn't generate emit a
|
|
69
|
+
* CRUD chain for resource X?" on real specs. */
|
|
70
|
+
explain?: boolean;
|
|
71
|
+
/** TASK-219: accepted but currently a no-op — `zond generate` already
|
|
72
|
+
* overwrites unconditionally. Kept on the CLI so agents passing
|
|
73
|
+
* `--force` / `--overwrite` don't see "unknown option" and bail. A
|
|
74
|
+
* future fix will gate sha-mismatched user edits behind this flag. */
|
|
75
|
+
force?: boolean;
|
|
29
76
|
json?: boolean;
|
|
77
|
+
/** ARV-9 unified filter: path:<regex> / method:<csv> / tag:<csv> /
|
|
78
|
+
* operation-id:<regex>. Multiple flags combine with OR; --exclude
|
|
79
|
+
* always removes. Stacks with --tag for back-compat. */
|
|
80
|
+
include?: string[];
|
|
81
|
+
exclude?: string[];
|
|
82
|
+
/** ARV-212 (R13/F16, R14): the explicit --api name. Lets generate read
|
|
83
|
+
* apis/<name>/.env.yaml directly even when --output points outside the
|
|
84
|
+
* apis/<name>/ tree (e.g. /tmp/<scratch>), where resolveApiRoot /
|
|
85
|
+
* inferApiName cannot recover the name from the path. */
|
|
86
|
+
apiName?: string;
|
|
87
|
+
/** ARV-212: explicit override for apis/<name>/ root. Caller pre-resolved
|
|
88
|
+
* it through the DB (base_dir column) for the case where the API was
|
|
89
|
+
* registered in a non-standard layout. */
|
|
90
|
+
apiDir?: string;
|
|
30
91
|
}
|
|
31
92
|
|
|
32
93
|
export async function generateCommand(options: GenerateOptions): Promise<number> {
|
|
@@ -35,9 +96,97 @@ export async function generateCommand(options: GenerateOptions): Promise<number>
|
|
|
35
96
|
const allEndpoints = extractEndpoints(doc);
|
|
36
97
|
let endpoints = allEndpoints;
|
|
37
98
|
const securitySchemes = extractSecuritySchemes(doc);
|
|
99
|
+
|
|
100
|
+
// --explain short-circuits: print the CRUD detection table and exit.
|
|
101
|
+
if (options.explain) {
|
|
102
|
+
let scope = endpoints;
|
|
103
|
+
if (options.tag) scope = filterByTag(scope, options.tag);
|
|
104
|
+
const { groups, diagnostics } = detectCrudGroupsWithDiagnostics(scope);
|
|
105
|
+
// Per-field body sources (TASK-269) — same scope as the table below.
|
|
106
|
+
const bodyFieldSources = scope
|
|
107
|
+
.filter(ep => ep.requestBodySchema && (ep.requestBodySchema as any).type === "object" &&
|
|
108
|
+
(ep.requestBodySchema as any).properties)
|
|
109
|
+
.map(ep => {
|
|
110
|
+
const props = (ep.requestBodySchema as any).properties as Record<string, any>;
|
|
111
|
+
const fields = Object.entries(props)
|
|
112
|
+
.filter(([k, s]) => !(s.readOnly === true) && k !== "id")
|
|
113
|
+
.map(([key, s]) => ({
|
|
114
|
+
field: key,
|
|
115
|
+
type: Array.isArray(s.type)
|
|
116
|
+
? (s.type as string[]).find(x => x !== "null") ?? "any"
|
|
117
|
+
: (s.type ?? "any"),
|
|
118
|
+
value: generateFromSchema(s, key),
|
|
119
|
+
source: classifyFieldSource(s, key),
|
|
120
|
+
}));
|
|
121
|
+
return { method: ep.method.toUpperCase(), path: ep.path, fields };
|
|
122
|
+
})
|
|
123
|
+
.filter(e => e.fields.length > 0);
|
|
124
|
+
|
|
125
|
+
if (options.json) {
|
|
126
|
+
printJson(jsonOk("generate", {
|
|
127
|
+
mode: "explain",
|
|
128
|
+
totalCandidates: diagnostics.length,
|
|
129
|
+
chains: groups.length,
|
|
130
|
+
diagnostics,
|
|
131
|
+
bodyFieldSources,
|
|
132
|
+
}));
|
|
133
|
+
} else {
|
|
134
|
+
if (diagnostics.length === 0) {
|
|
135
|
+
console.log("No POST endpoints in scope — nothing to evaluate.");
|
|
136
|
+
} else {
|
|
137
|
+
const chains = diagnostics.filter(d => d.verdict === "chain").length;
|
|
138
|
+
console.log(`CRUD detection: ${chains}/${diagnostics.length} POST endpoints became chain candidates.\n`);
|
|
139
|
+
const headers = ["resource", "post", "get/{id}", "put/patch", "delete", "list", "verdict", "reason"];
|
|
140
|
+
const rows = diagnostics.map(d => [
|
|
141
|
+
d.resource,
|
|
142
|
+
d.postPath,
|
|
143
|
+
d.hasGetById ? "✓" : "—",
|
|
144
|
+
d.hasUpdate ? "✓" : "—",
|
|
145
|
+
d.hasDelete ? "✓" : "—",
|
|
146
|
+
d.hasList ? "✓" : "—",
|
|
147
|
+
d.verdict,
|
|
148
|
+
d.reason,
|
|
149
|
+
]);
|
|
150
|
+
const widths = headers.map((h, i) =>
|
|
151
|
+
Math.max(h.length, ...rows.map(r => r[i]!.length)),
|
|
152
|
+
);
|
|
153
|
+
const fmt = (cells: string[]) =>
|
|
154
|
+
cells.map((c, i) => c.padEnd(widths[i]!)).join(" ");
|
|
155
|
+
console.log(fmt(headers));
|
|
156
|
+
console.log(widths.map(w => "─".repeat(w)).join(" "));
|
|
157
|
+
for (const row of rows) console.log(fmt(row));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// TASK-269: per-field provenance for endpoints carrying a request
|
|
161
|
+
// body. Helps debug "why did the API 400 on field X?" without
|
|
162
|
+
// re-running with --json and inspecting the generated suite.
|
|
163
|
+
if (bodyFieldSources.length > 0) {
|
|
164
|
+
console.log("");
|
|
165
|
+
console.log("Body field sources:");
|
|
166
|
+
for (const ep of bodyFieldSources) {
|
|
167
|
+
console.log(` ${ep.method} ${ep.path}`);
|
|
168
|
+
const fHeaders = ["field", "type", "value", "source"];
|
|
169
|
+
const rows2 = ep.fields.map(f => [
|
|
170
|
+
f.field,
|
|
171
|
+
String(f.type),
|
|
172
|
+
typeof f.value === "string" ? f.value : JSON.stringify(f.value),
|
|
173
|
+
`[${f.source}]`,
|
|
174
|
+
]);
|
|
175
|
+
const fAll = [fHeaders, ...rows2];
|
|
176
|
+
const fWidths = fHeaders.map((h, i) =>
|
|
177
|
+
Math.max(h.length, ...fAll.map(r => r[i]!.length)),
|
|
178
|
+
);
|
|
179
|
+
const ffmt = (cells: string[]) =>
|
|
180
|
+
cells.map((c, i) => c.padEnd(fWidths[i]!)).join(" ");
|
|
181
|
+
console.log(" " + ffmt(fHeaders));
|
|
182
|
+
console.log(" " + fWidths.map(w => "─".repeat(w)).join(" "));
|
|
183
|
+
for (const r of rows2) console.log(" " + ffmt(r));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return 0;
|
|
188
|
+
}
|
|
38
189
|
const baseUrl = ((doc as any).servers?.[0]?.url) as string | undefined;
|
|
39
|
-
const apiName = (doc as any).info?.title as string | undefined;
|
|
40
|
-
const apiVersion = (doc as any).info?.version as string | undefined;
|
|
41
190
|
const warnings: string[] = [];
|
|
42
191
|
|
|
43
192
|
// Filter to uncovered only
|
|
@@ -51,63 +200,197 @@ export async function generateCommand(options: GenerateOptions): Promise<number>
|
|
|
51
200
|
}
|
|
52
201
|
}
|
|
53
202
|
|
|
203
|
+
// ARV-9: unified --include/--exclude filter (applied before --tag so
|
|
204
|
+
// --tag stays a thin alias when both are passed; usually only one is).
|
|
205
|
+
if (options.include?.length || options.exclude?.length) {
|
|
206
|
+
const compiled = compileOperationFilter({ includes: options.include, excludes: options.exclude });
|
|
207
|
+
if (compiled.errors.length > 0) {
|
|
208
|
+
const message = compiled.errors.join("\n");
|
|
209
|
+
if (options.json) printJson(jsonOk("generate", { files: [], message }, compiled.errors));
|
|
210
|
+
else printError(message);
|
|
211
|
+
return 2;
|
|
212
|
+
}
|
|
213
|
+
endpoints = endpoints.filter(compiled.filter);
|
|
214
|
+
}
|
|
215
|
+
|
|
54
216
|
// Filter by tag
|
|
217
|
+
let tagDiagnostic: string | undefined;
|
|
55
218
|
if (options.tag) {
|
|
219
|
+
const beforeTag = endpoints;
|
|
56
220
|
endpoints = filterByTag(endpoints, options.tag);
|
|
221
|
+
// TASK-232: when --tag matches nothing, show the available tags so the
|
|
222
|
+
// user can tell "typo" apart from "spec really has no endpoints here".
|
|
223
|
+
// Cheap nearest-match: pick the first tag containing or contained-by the
|
|
224
|
+
// requested string (case-insensitive); covers "Members" → "Member".
|
|
225
|
+
if (endpoints.length === 0 && beforeTag.length > 0) {
|
|
226
|
+
const available = collectTags(beforeTag);
|
|
227
|
+
const wanted = options.tag.trim().toLowerCase();
|
|
228
|
+
const closest = available.find(t => {
|
|
229
|
+
const tl = t.toLowerCase();
|
|
230
|
+
return tl.includes(wanted) || wanted.includes(tl);
|
|
231
|
+
});
|
|
232
|
+
const hint = closest ? ` (did you mean ${closest}?)` : "";
|
|
233
|
+
const list = available.length > 0 ? available.join(", ") : "(none)";
|
|
234
|
+
tagDiagnostic = `No endpoints with tag '${options.tag}'${hint}. Available tags: ${list}`;
|
|
235
|
+
}
|
|
57
236
|
}
|
|
58
237
|
|
|
59
238
|
if (endpoints.length === 0) {
|
|
239
|
+
const message = tagDiagnostic ?? "No endpoints to generate tests for";
|
|
60
240
|
if (options.json) {
|
|
61
|
-
printJson(jsonOk("generate", { files: [], message
|
|
241
|
+
printJson(jsonOk("generate", { files: [], message }, warnings));
|
|
62
242
|
} else {
|
|
63
|
-
console.log(
|
|
243
|
+
console.log(`${message}.`);
|
|
64
244
|
}
|
|
65
245
|
return 0;
|
|
66
246
|
}
|
|
67
247
|
|
|
248
|
+
// Count deprecated endpoints before generateSuites filters them — we
|
|
249
|
+
// surface the count as a warning so deprecated-by-design and
|
|
250
|
+
// accidentally-dropped look different in stdout.
|
|
251
|
+
const deprecatedSkipped = options.includeDeprecated
|
|
252
|
+
? []
|
|
253
|
+
: endpoints.filter(ep => ep.deprecated).map(ep => `${ep.method} ${ep.path}`);
|
|
254
|
+
|
|
255
|
+
// ARV-212 (R13/F16, R14): peek at .env.yaml *before* generating suites so
|
|
256
|
+
// we can pass `defaultAuthVar` into generateSuites when the spec has no
|
|
257
|
+
// securitySchemes but the workspace is wired for Bearer auth (the
|
|
258
|
+
// ARV-201 seed in setup-api.ts). Without this, GitHub-style suites go
|
|
259
|
+
// unauth and brick on the first rate-limited 60 requests.
|
|
260
|
+
//
|
|
261
|
+
// R14 fix: do NOT rely on resolveApiRoot(options.output) — when the
|
|
262
|
+
// user passes --output to a scratch directory outside apis/<name>/
|
|
263
|
+
// (e.g. /tmp/foo), the resolver returns undefined and we fall back to
|
|
264
|
+
// the output dir, which has no .env.yaml. Prefer the explicit --api
|
|
265
|
+
// name to construct apis/<name>/ inside the workspace.
|
|
266
|
+
const envForWarnings: Record<string, unknown> = {};
|
|
267
|
+
try {
|
|
268
|
+
let envDir: string | undefined = options.apiDir;
|
|
269
|
+
if (!envDir && options.apiName) {
|
|
270
|
+
const ws = findWorkspaceRoot();
|
|
271
|
+
envDir = resolvePath(ws.root, "apis", options.apiName);
|
|
272
|
+
}
|
|
273
|
+
if (!envDir) envDir = resolveApiRoot(options.output, baseUrl) ?? options.output;
|
|
274
|
+
Object.assign(envForWarnings, await loadEnvironment(undefined, envDir));
|
|
275
|
+
} catch { /* env load failures stay silent — original behaviour for missing files */ }
|
|
276
|
+
|
|
277
|
+
let defaultAuthVar: string | undefined;
|
|
278
|
+
if (securitySchemes.length === 0 && "auth_token" in envForWarnings) {
|
|
279
|
+
// Presence-not-value: an empty .secrets.yaml.auth_token resolves to ""
|
|
280
|
+
// here, but the .env.yaml wiring is what matters. Once the user fills
|
|
281
|
+
// .secrets.yaml the generated suite picks up the Bearer header without
|
|
282
|
+
// a regenerate.
|
|
283
|
+
defaultAuthVar = "auth_token";
|
|
284
|
+
}
|
|
285
|
+
|
|
68
286
|
// Generate suites
|
|
69
|
-
const suites = generateSuites({
|
|
287
|
+
const suites = generateSuites({
|
|
288
|
+
endpoints,
|
|
289
|
+
securitySchemes,
|
|
290
|
+
specPath: options.specPath,
|
|
291
|
+
includeDeprecated: options.includeDeprecated,
|
|
292
|
+
defaultAuthVar,
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
const missingPathParams = new Set<string>();
|
|
296
|
+
let endpointsMissingPathExamples = 0;
|
|
297
|
+
for (const ep of endpoints) {
|
|
298
|
+
let epHadMiss = false;
|
|
299
|
+
for (const p of ep.parameters) {
|
|
300
|
+
if (p.in !== "path" || !p.required) continue;
|
|
301
|
+
const hasExample =
|
|
302
|
+
p.example !== undefined ||
|
|
303
|
+
(p.schema && (p.schema as any).example !== undefined) ||
|
|
304
|
+
(p.schema && (p.schema as any).default !== undefined);
|
|
305
|
+
const filledInEnv = (() => {
|
|
306
|
+
const v = envForWarnings[p.name];
|
|
307
|
+
return typeof v === "string" && v.length > 0 && !v.startsWith("{{");
|
|
308
|
+
})();
|
|
309
|
+
if (!hasExample && !filledInEnv) {
|
|
310
|
+
missingPathParams.add(p.name);
|
|
311
|
+
epHadMiss = true;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
if (epHadMiss) endpointsMissingPathExamples++;
|
|
315
|
+
}
|
|
316
|
+
if (missingPathParams.size > 0) {
|
|
317
|
+
const sample = [...missingPathParams].sort().slice(0, 3).join(", ");
|
|
318
|
+
const more = missingPathParams.size > 3 ? `, +${missingPathParams.size - 3} more` : "";
|
|
319
|
+
warnings.push(
|
|
320
|
+
`${missingPathParams.size} path param(s) have no examples (${sample}${more}) on ${endpointsMissingPathExamples} endpoint(s) — fill apis/<name>/.env.yaml to enable positive/smoke-positive suites`,
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (deprecatedSkipped.length > 0) {
|
|
325
|
+
const head = deprecatedSkipped.slice(0, 3).join(", ");
|
|
326
|
+
const more = deprecatedSkipped.length > 3 ? `, +${deprecatedSkipped.length - 3} more` : "";
|
|
327
|
+
warnings.push(
|
|
328
|
+
`Skipped ${deprecatedSkipped.length} deprecated endpoint(s): ${head}${more} — pass --include-deprecated to include`,
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// ARV-15: warn when in-scope endpoints will create/modify real resources.
|
|
333
|
+
// POST/PUT/PATCH/DELETE on a live API send real traffic — e.g. an
|
|
334
|
+
// email API's `POST /emails` literally sends mail; deleting a record
|
|
335
|
+
// is irreversible.
|
|
336
|
+
// Generation is harmless (just YAML), but `zond run` against the suite
|
|
337
|
+
// is not, so the warning fires here so the user sees it before they grep
|
|
338
|
+
// the output for what to run next.
|
|
339
|
+
const unsafeOps = endpoints.filter(
|
|
340
|
+
ep => ep.method !== "GET" && ep.method !== "HEAD" && ep.method !== "OPTIONS",
|
|
341
|
+
);
|
|
342
|
+
if (unsafeOps.length > 0) {
|
|
343
|
+
const byMethod = new Map<string, number>();
|
|
344
|
+
for (const ep of unsafeOps) {
|
|
345
|
+
byMethod.set(ep.method, (byMethod.get(ep.method) ?? 0) + 1);
|
|
346
|
+
}
|
|
347
|
+
const breakdown = [...byMethod.entries()]
|
|
348
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
349
|
+
.map(([m, n]) => `${n} ${m}`)
|
|
350
|
+
.join(", ");
|
|
351
|
+
warnings.push(
|
|
352
|
+
`${unsafeOps.length} write endpoint(s) in scope (${breakdown}) — \`zond run\` on the resulting *-unsafe.yaml / crud-*.yaml suites will hit the real API. Use --include 'method:GET' for read-only smoke first.`,
|
|
353
|
+
);
|
|
354
|
+
}
|
|
70
355
|
|
|
71
356
|
// Ensure output directory exists
|
|
72
357
|
await mkdir(options.output, { recursive: true });
|
|
73
358
|
|
|
74
359
|
// Write suite files
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
//
|
|
78
|
-
const
|
|
360
|
+
// ARV-15: tag each created file as safe/unsafe based on suite tags so the
|
|
361
|
+
// stdout summary can group them and the user can tell at a glance which
|
|
362
|
+
// suites send writes/deletes vs. read-only smoke.
|
|
363
|
+
const UNSAFE_TAGS = new Set(["unsafe", "crud", "system", "reset", "cleanup"]);
|
|
364
|
+
const isUnsafeSuite = (s: typeof suites[number]) =>
|
|
365
|
+
(s.tags ?? []).some(t => UNSAFE_TAGS.has(t));
|
|
366
|
+
const createdFiles: Array<{ file: string; suite: string; tests: number; safety: "safe" | "unsafe" }> = [];
|
|
367
|
+
const manifestEntries: RecordInput[] = [];
|
|
368
|
+
const inferredApi = inferApiName(options.output);
|
|
79
369
|
|
|
80
370
|
for (const suite of suites) {
|
|
81
371
|
const yaml = serializeSuite(suite);
|
|
82
372
|
const fileName = `${suite.fileStem ?? suite.name}.yaml`;
|
|
83
373
|
const filePath = join(options.output, fileName);
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
374
|
+
const header = autoGenHeader("zond generate", `zond generate --api <name> --output ${options.output}`);
|
|
375
|
+
await Bun.write(filePath, header + yaml);
|
|
376
|
+
createdFiles.push({
|
|
377
|
+
file: filePath,
|
|
378
|
+
suite: suite.name,
|
|
379
|
+
tests: suite.tests.length,
|
|
380
|
+
safety: isUnsafeSuite(suite) ? "unsafe" : "safe",
|
|
381
|
+
});
|
|
382
|
+
manifestEntries.push({
|
|
383
|
+
path: filePath,
|
|
384
|
+
by: "zond generate",
|
|
385
|
+
api: inferredApi,
|
|
386
|
+
category: "tests",
|
|
387
|
+
});
|
|
388
|
+
}
|
|
99
389
|
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
specSource: options.specPath,
|
|
105
|
-
specHash: hashSpec(specContent),
|
|
106
|
-
apiName,
|
|
107
|
-
apiVersion,
|
|
108
|
-
baseUrl,
|
|
109
|
-
});
|
|
110
|
-
await Bun.write(join(options.output, ".api-catalog.yaml"), serializeCatalog(catalog));
|
|
390
|
+
// TASK-157 (m-9 P1): generate no longer writes `.api-catalog.yaml` into
|
|
391
|
+
// options.output. The API-level catalog at `apis/<name>/.api-catalog.yaml`
|
|
392
|
+
// is the single source of truth — `zond add api` / `zond refresh-api`
|
|
393
|
+
// emit it.
|
|
111
394
|
|
|
112
395
|
// Sync DB collection spec reference if one is registered for this output directory
|
|
113
396
|
try {
|
|
@@ -121,8 +404,15 @@ export async function generateCommand(options: GenerateOptions): Promise<number>
|
|
|
121
404
|
// DB unavailable — not fatal
|
|
122
405
|
}
|
|
123
406
|
|
|
124
|
-
//
|
|
125
|
-
|
|
407
|
+
// TASK-158 (m-9 P2): the API-level `apis/<name>/.env.yaml` is the only
|
|
408
|
+
// source of truth for runtime variables. We never write a duplicate
|
|
409
|
+
// `tests/.env.yaml` — it would silently override the API-level file via
|
|
410
|
+
// deeper-scope precedence, wiping the user's auth_token / FK ids on
|
|
411
|
+
// every `zond generate`. If the API-level file is missing, we create it
|
|
412
|
+
// there; if it already exists, we leave it alone (re-running generate
|
|
413
|
+
// never clobbers values the user filled in).
|
|
414
|
+
const envTargetDir = resolveApiRoot(options.output, baseUrl) ?? options.output;
|
|
415
|
+
const envPath = join(envTargetDir, ".env.yaml");
|
|
126
416
|
const envFile = Bun.file(envPath);
|
|
127
417
|
if (!(await envFile.exists())) {
|
|
128
418
|
const unresolvedVars = new Set<string>();
|
|
@@ -135,11 +425,28 @@ export async function generateCommand(options: GenerateOptions): Promise<number>
|
|
|
135
425
|
lines.push(`${v}: "" # TODO: fill in`);
|
|
136
426
|
}
|
|
137
427
|
if (lines.length > 0) {
|
|
428
|
+
await mkdir(envTargetDir, { recursive: true });
|
|
138
429
|
await Bun.write(envPath, lines.join("\n") + "\n");
|
|
139
430
|
warnings.push(`Created ${envPath} with ${unresolvedVars.size} placeholder variable(s)`);
|
|
431
|
+
manifestEntries.push({
|
|
432
|
+
path: envPath,
|
|
433
|
+
by: "zond generate",
|
|
434
|
+
api: inferredApi,
|
|
435
|
+
category: "env",
|
|
436
|
+
});
|
|
140
437
|
}
|
|
141
438
|
}
|
|
142
439
|
|
|
440
|
+
// Record everything we wrote into .zond/manifest.json (TASK-156).
|
|
441
|
+
try {
|
|
442
|
+
const ws = findWorkspaceRoot();
|
|
443
|
+
if (!ws.fromFallback && manifestEntries.length > 0) {
|
|
444
|
+
recordGeneratedFiles(ws.root, manifestEntries);
|
|
445
|
+
}
|
|
446
|
+
} catch {
|
|
447
|
+
// Manifest is best-effort; never fail the generate command on it.
|
|
448
|
+
}
|
|
449
|
+
|
|
143
450
|
// Validate generated files
|
|
144
451
|
const validationErrors: string[] = [];
|
|
145
452
|
try {
|
|
@@ -164,14 +471,32 @@ export async function generateCommand(options: GenerateOptions): Promise<number>
|
|
|
164
471
|
}, warnings));
|
|
165
472
|
} else {
|
|
166
473
|
printSuccess(`Generated ${suites.length} suite(s) with ${totalTests} test(s) in ${options.output}`);
|
|
167
|
-
|
|
168
|
-
|
|
474
|
+
// ARV-15: split safe vs unsafe so the user can see at a glance which
|
|
475
|
+
// suites are read-only smoke and which will send writes/deletes.
|
|
476
|
+
const safeFiles = createdFiles.filter(f => f.safety === "safe");
|
|
477
|
+
const unsafeFiles = createdFiles.filter(f => f.safety === "unsafe");
|
|
478
|
+
if (safeFiles.length > 0 && unsafeFiles.length > 0) {
|
|
479
|
+
console.log(` Safe (read-only) — ${safeFiles.length} suite(s):`);
|
|
480
|
+
for (const f of safeFiles) console.log(` ${f.file} (${f.tests} tests)`);
|
|
481
|
+
console.log(` Unsafe (writes/deletes — hit live API) — ${unsafeFiles.length} suite(s):`);
|
|
482
|
+
for (const f of unsafeFiles) console.log(` ${f.file} (${f.tests} tests)`);
|
|
483
|
+
} else {
|
|
484
|
+
for (const f of createdFiles) {
|
|
485
|
+
console.log(` ${f.file} (${f.tests} tests)`);
|
|
486
|
+
}
|
|
169
487
|
}
|
|
170
488
|
if (warnings.length > 0) {
|
|
171
489
|
for (const w of warnings) {
|
|
172
490
|
console.log(` ⚠ ${w}`);
|
|
173
491
|
}
|
|
174
492
|
}
|
|
493
|
+
console.log("");
|
|
494
|
+
console.log("Next steps:");
|
|
495
|
+
console.log(" 1. Fill apis/<name>/.env.yaml with auth_token, real FK ids, verified emails, valid enums");
|
|
496
|
+
console.log(" (the fixture pack — without it, {{$randomString}} loses 5+ iterations to format-validation)");
|
|
497
|
+
console.log(" 2. zond run <output> --safe --report json # smoke (GET-only)");
|
|
498
|
+
console.log(` 3. zond run <output> --tag crud,setup --validate-schema --spec ${options.specPath} --report json`);
|
|
499
|
+
console.log(" (--validate-schema catches contract drift; recommended for every CRUD run)");
|
|
175
500
|
}
|
|
176
501
|
|
|
177
502
|
return 0;
|
|
@@ -185,3 +510,51 @@ export async function generateCommand(options: GenerateOptions): Promise<number>
|
|
|
185
510
|
return 2;
|
|
186
511
|
}
|
|
187
512
|
}
|
|
513
|
+
|
|
514
|
+
import type { Command } from "commander";
|
|
515
|
+
import { globalJson, resolveSpecArg } from "../resolve.ts";
|
|
516
|
+
import { getApi } from "../util/api-context.ts";
|
|
517
|
+
|
|
518
|
+
export function registerGenerate(program: Command): void {
|
|
519
|
+
program
|
|
520
|
+
.command("generate [spec]")
|
|
521
|
+
.description("Generate test suites from OpenAPI spec (overwrites existing suite files unconditionally — re-run is safe; user-edited tests are not preserved). Body fields are filled with `{{$random*}}` helpers (slug/email/url/uuid/…) — see `zond reference random-helpers` or docs/random-helpers.md for the full list (TASK-267).")
|
|
522
|
+
.option("--api <name>", "Use the registered API's spec (apis/<name>/spec.json)")
|
|
523
|
+
.option("--db <path>", "Path to SQLite database file")
|
|
524
|
+
.option("--output <dir>", "Output directory for generated test files (required unless --explain)")
|
|
525
|
+
.option("--tag <tag>", "Generate only for endpoints with this tag (accepts comma-separated list, e.g. --tag Releases,Events,Alerts — TASK-239)")
|
|
526
|
+
.option(
|
|
527
|
+
"--include <spec...>",
|
|
528
|
+
"ARV-9: keep only operations matching <selector>:<value>. Selectors: path:<regex>, method:<csv>, tag:<csv>, operation-id:<regex>. Repeat the flag for OR semantics.",
|
|
529
|
+
)
|
|
530
|
+
.option(
|
|
531
|
+
"--exclude <spec...>",
|
|
532
|
+
"ARV-9: drop operations matching <selector>:<value>. Same grammar as --include.",
|
|
533
|
+
)
|
|
534
|
+
.option("--uncovered-only", "Skip endpoints already covered by existing tests")
|
|
535
|
+
.option("--include-deprecated", "Generate suites for deprecated endpoints too (filtered out by default)")
|
|
536
|
+
.option("--explain", "Print the CRUD detection table (which resources became chain candidates and why) without writing files (TASK-139)")
|
|
537
|
+
.option("--force, --overwrite", "Accepted for compatibility — generate already overwrites by default (TASK-219). No-op today; will gate user-edited file overwrites in a future release.")
|
|
538
|
+
.action(async (specPos: string | undefined, opts, cmd: Command) => {
|
|
539
|
+
const resolved = resolveSpecArg(specPos, opts.api, opts.db);
|
|
540
|
+
if ("error" in resolved) { printError(resolved.error); process.exitCode = 2; return; }
|
|
541
|
+
if (!opts.explain && !opts.output) {
|
|
542
|
+
printError("--output <dir> is required (omit only when running with --explain).");
|
|
543
|
+
process.exitCode = 2;
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
process.exitCode = await generateCommand({
|
|
547
|
+
specPath: resolved.spec,
|
|
548
|
+
output: opts.output ?? "",
|
|
549
|
+
tag: opts.tag,
|
|
550
|
+
uncoveredOnly: opts.uncoveredOnly === true,
|
|
551
|
+
includeDeprecated: opts.includeDeprecated === true,
|
|
552
|
+
explain: opts.explain === true,
|
|
553
|
+
force: opts.force === true || opts.overwrite === true,
|
|
554
|
+
json: globalJson(cmd),
|
|
555
|
+
include: Array.isArray(opts.include) ? opts.include : undefined,
|
|
556
|
+
exclude: Array.isArray(opts.exclude) ? opts.exclude : undefined,
|
|
557
|
+
apiName: getApi(cmd, opts),
|
|
558
|
+
});
|
|
559
|
+
});
|
|
560
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
import agentsTemplate from "./templates/agents.md" with { type: "text" };
|
|
5
|
+
|
|
6
|
+
export const START_MARKER = "<!-- zond:start -->";
|
|
7
|
+
export const END_MARKER = "<!-- zond:end -->";
|
|
8
|
+
|
|
9
|
+
export interface AgentsBlockResult {
|
|
10
|
+
path: string;
|
|
11
|
+
action: "created" | "updated" | "noop";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function blockBody(): string {
|
|
15
|
+
return agentsTemplate.trim();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function wrap(body: string): string {
|
|
19
|
+
return `${START_MARKER}\n${body}\n${END_MARKER}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const BLOCK_RE = new RegExp(
|
|
23
|
+
`${escapeRe(START_MARKER)}[\\s\\S]*?${escapeRe(END_MARKER)}`,
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
function escapeRe(s: string): string {
|
|
27
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Idempotently inserts (or updates) the zond instruction block in `<cwd>/AGENTS.md`.
|
|
32
|
+
*
|
|
33
|
+
* - Missing file → create with just the block.
|
|
34
|
+
* - File without markers → append block at the end (preceded by `\n\n---\n\n`).
|
|
35
|
+
* - File with existing markers → replace the body between them.
|
|
36
|
+
* - File whose existing block already matches → noop.
|
|
37
|
+
*/
|
|
38
|
+
export function upsertAgentsBlock(cwd: string): AgentsBlockResult {
|
|
39
|
+
const path = join(cwd, "AGENTS.md");
|
|
40
|
+
const next = wrap(blockBody());
|
|
41
|
+
|
|
42
|
+
if (!existsSync(path)) {
|
|
43
|
+
writeFileSync(path, next + "\n", "utf-8");
|
|
44
|
+
return { path, action: "created" };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const current = readFileSync(path, "utf-8");
|
|
48
|
+
|
|
49
|
+
if (BLOCK_RE.test(current)) {
|
|
50
|
+
const updated = current.replace(BLOCK_RE, next);
|
|
51
|
+
if (updated === current) return { path, action: "noop" };
|
|
52
|
+
writeFileSync(path, updated, "utf-8");
|
|
53
|
+
return { path, action: "updated" };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Append with separator
|
|
57
|
+
const sep = current.endsWith("\n") ? "\n" : "\n\n";
|
|
58
|
+
const updated = current + sep + "---\n\n" + next + "\n";
|
|
59
|
+
writeFileSync(path, updated, "utf-8");
|
|
60
|
+
return { path, action: "updated" };
|
|
61
|
+
}
|