@kirrosh/zond 0.22.0 → 0.26.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.
Files changed (282) hide show
  1. package/CHANGELOG.md +811 -0
  2. package/README.md +59 -6
  3. package/package.json +9 -7
  4. package/src/CLAUDE.md +112 -0
  5. package/src/cli/argv.ts +122 -0
  6. package/src/cli/commands/add-api.ts +146 -0
  7. package/src/cli/commands/api/annotate/idempotency.ts +59 -0
  8. package/src/cli/commands/api/annotate/index.ts +880 -0
  9. package/src/cli/commands/api/annotate/lifecycle.ts +74 -0
  10. package/src/cli/commands/api/annotate/overlay.ts +206 -0
  11. package/src/cli/commands/api/annotate/pagination.ts +64 -0
  12. package/src/cli/commands/api/annotate/prompts.ts +220 -0
  13. package/src/cli/commands/api/annotate/readback.ts +58 -0
  14. package/src/cli/commands/api/annotate/resources.ts +91 -0
  15. package/src/cli/commands/api/annotate/seed-bodies.ts +61 -0
  16. package/src/cli/commands/audit.ts +786 -0
  17. package/src/cli/commands/catalog.ts +35 -0
  18. package/src/cli/commands/check.ts +361 -0
  19. package/src/cli/commands/checks.ts +1072 -0
  20. package/src/cli/commands/ci-init.ts +43 -0
  21. package/src/cli/commands/clean.ts +212 -0
  22. package/src/cli/commands/cleanup.ts +236 -0
  23. package/src/cli/commands/completions.ts +16 -0
  24. package/src/cli/commands/coverage.ts +823 -132
  25. package/src/cli/commands/db.ts +486 -12
  26. package/src/cli/commands/describe.ts +37 -2
  27. package/src/cli/commands/discover.ts +1356 -0
  28. package/src/cli/commands/doctor.ts +661 -0
  29. package/src/cli/commands/fixtures.ts +402 -0
  30. package/src/cli/commands/generate.ts +438 -47
  31. package/src/cli/commands/init/bootstrap.ts +34 -2
  32. package/src/cli/commands/{init.ts → init/index.ts} +99 -5
  33. package/src/cli/commands/init/skills.ts +99 -3
  34. package/src/cli/commands/init/templates/agents.md +77 -64
  35. package/src/cli/commands/init/templates/skills/warm-up-target.md +122 -0
  36. package/src/cli/commands/init/templates/skills/zond-checks.md +621 -0
  37. package/src/cli/commands/init/templates/skills/zond-seed.md +114 -0
  38. package/src/cli/commands/init/templates/skills/zond-triage.md +272 -0
  39. package/src/cli/commands/init/templates/skills/zond.md +802 -125
  40. package/src/cli/commands/init/templates/zond-config.yml +8 -9
  41. package/src/cli/commands/prepare-fixtures.ts +97 -0
  42. package/src/cli/commands/probe/_seed-bodies.ts +52 -0
  43. package/src/cli/commands/probe/mass-assignment.ts +594 -0
  44. package/src/cli/commands/probe/security.ts +537 -0
  45. package/src/cli/commands/probe/static.ts +255 -0
  46. package/src/cli/commands/probe/webhooks.ts +163 -0
  47. package/src/cli/commands/probe.ts +535 -0
  48. package/src/cli/commands/reference.ts +87 -0
  49. package/src/cli/commands/refresh-api.ts +227 -0
  50. package/src/cli/commands/remove-api.ts +150 -0
  51. package/src/cli/commands/report-bundle.ts +310 -0
  52. package/src/cli/commands/report.ts +241 -0
  53. package/src/cli/commands/request.ts +495 -4
  54. package/src/cli/commands/run.ts +870 -53
  55. package/src/cli/commands/schema-from-runs.ts +128 -0
  56. package/src/cli/commands/secrets.ts +133 -0
  57. package/src/cli/commands/session.ts +244 -0
  58. package/src/cli/commands/use.ts +18 -1
  59. package/src/cli/index.ts +20 -3
  60. package/src/cli/json-envelope.ts +92 -3
  61. package/src/cli/json-schemas.ts +314 -0
  62. package/src/cli/output.ts +17 -1
  63. package/src/cli/program.ts +199 -635
  64. package/src/cli/resolve.ts +105 -0
  65. package/src/cli/safe-live.ts +24 -0
  66. package/src/cli/status-filter.ts +114 -0
  67. package/src/cli/util/api-context.ts +85 -0
  68. package/src/cli/version.ts +5 -0
  69. package/src/core/audit/persist.ts +183 -0
  70. package/src/core/checks/budget.ts +59 -0
  71. package/src/core/checks/checks/_crud-helpers.ts +133 -0
  72. package/src/core/checks/checks/_negative_mutator.ts +133 -0
  73. package/src/core/checks/checks/_readback-helpers.ts +133 -0
  74. package/src/core/checks/checks/content_type_conformance.ts +39 -0
  75. package/src/core/checks/checks/cross_call_references.ts +147 -0
  76. package/src/core/checks/checks/cursor_boundary_fuzzing.ts +219 -0
  77. package/src/core/checks/checks/ensure_resource_availability.ts +62 -0
  78. package/src/core/checks/checks/idempotency_replay.ts +242 -0
  79. package/src/core/checks/checks/ignored_auth.ts +254 -0
  80. package/src/core/checks/checks/index.ts +68 -0
  81. package/src/core/checks/checks/lifecycle_transitions.ts +416 -0
  82. package/src/core/checks/checks/missing_required_header.ts +40 -0
  83. package/src/core/checks/checks/negative_data_rejection.ts +148 -0
  84. package/src/core/checks/checks/not_a_server_error.ts +35 -0
  85. package/src/core/checks/checks/open_cors_on_sensitive.ts +160 -0
  86. package/src/core/checks/checks/pagination_invariants.ts +419 -0
  87. package/src/core/checks/checks/positive_data_acceptance.ts +33 -0
  88. package/src/core/checks/checks/rate_limit_headers_absent.ts +77 -0
  89. package/src/core/checks/checks/response_headers_conformance.ts +74 -0
  90. package/src/core/checks/checks/response_schema_conformance.ts +30 -0
  91. package/src/core/checks/checks/status_code_conformance.ts +132 -0
  92. package/src/core/checks/checks/unsupported_method.ts +63 -0
  93. package/src/core/checks/checks/use_after_free.ts +78 -0
  94. package/src/core/checks/index.ts +30 -0
  95. package/src/core/checks/mode.ts +82 -0
  96. package/src/core/checks/recommended-action.ts +68 -0
  97. package/src/core/checks/registry.ts +78 -0
  98. package/src/core/checks/runner.ts +1461 -0
  99. package/src/core/checks/sarif.ts +230 -0
  100. package/src/core/checks/spec-findings.ts +308 -0
  101. package/src/core/checks/stateful.ts +121 -0
  102. package/src/core/checks/types.ts +305 -0
  103. package/src/core/checks/zond-extensions.ts +73 -0
  104. package/src/core/classifier/recommended-action.ts +251 -0
  105. package/src/core/context/current.ts +22 -6
  106. package/src/core/context/session.ts +78 -0
  107. package/src/core/coverage/loader.ts +216 -0
  108. package/src/core/coverage/reasons.ts +300 -0
  109. package/src/core/diagnostics/db-analysis.ts +293 -59
  110. package/src/core/diagnostics/failure-class.ts +140 -0
  111. package/src/core/diagnostics/failure-hints.ts +88 -89
  112. package/src/core/diagnostics/spec-pointer.ts +99 -0
  113. package/src/core/diagnostics/suggested-fixes.ts +155 -0
  114. package/src/core/exporter/case-study/index.ts +270 -0
  115. package/src/core/exporter/curl.ts +40 -0
  116. package/src/core/exporter/exporter.ts +48 -0
  117. package/src/core/exporter/html-report/escape.ts +24 -0
  118. package/src/core/exporter/html-report/index.ts +479 -0
  119. package/src/core/exporter/html-report/script.ts +100 -0
  120. package/src/core/exporter/html-report/styles.ts +408 -0
  121. package/src/core/generator/chunker.ts +38 -19
  122. package/src/core/generator/coverage-phase.ts +0 -0
  123. package/src/core/generator/data-factory.ts +586 -22
  124. package/src/core/generator/describe.ts +1 -1
  125. package/src/core/generator/fixtures-builder.ts +332 -0
  126. package/src/core/generator/index.ts +5 -5
  127. package/src/core/generator/openapi-reader.ts +135 -7
  128. package/src/core/generator/path-param-disambig.ts +140 -0
  129. package/src/core/generator/resources-builder.ts +898 -0
  130. package/src/core/generator/schema-utils.ts +33 -3
  131. package/src/core/generator/serializer.ts +103 -13
  132. package/src/core/generator/suite-generator.ts +583 -122
  133. package/src/core/generator/types.ts +14 -0
  134. package/src/core/identity/identity-file.ts +0 -0
  135. package/src/core/lint/affects.ts +28 -0
  136. package/src/core/lint/config.ts +96 -0
  137. package/src/core/lint/format.ts +42 -0
  138. package/src/core/lint/index.ts +94 -0
  139. package/src/core/lint/reporter.ts +128 -0
  140. package/src/core/lint/rules/consistency.ts +158 -0
  141. package/src/core/lint/rules/heuristics.ts +97 -0
  142. package/src/core/lint/rules/strictness.ts +109 -0
  143. package/src/core/lint/types.ts +96 -0
  144. package/src/core/lint/walker.ts +248 -0
  145. package/src/core/meta/meta-store.ts +6 -73
  146. package/src/core/output/README.md +73 -0
  147. package/src/core/output/index.ts +13 -0
  148. package/src/core/output/run.ts +91 -0
  149. package/src/core/output/types.ts +122 -0
  150. package/src/core/parser/dynamic-values.ts +160 -0
  151. package/src/core/parser/env-interpolation.ts +104 -0
  152. package/src/core/parser/filter.ts +57 -0
  153. package/src/core/parser/schema.ts +129 -4
  154. package/src/core/parser/types.ts +19 -1
  155. package/src/core/parser/variables.ts +0 -0
  156. package/src/core/parser/yaml-parser.ts +58 -12
  157. package/src/core/probe/bootstrap.ts +34 -0
  158. package/src/core/probe/dry-run-envelope.ts +61 -0
  159. package/src/core/probe/mass-assignment/classify.ts +175 -0
  160. package/src/core/probe/mass-assignment/cleanup.ts +52 -0
  161. package/src/core/probe/mass-assignment/digest.ts +114 -0
  162. package/src/core/probe/mass-assignment/orchestrator.ts +459 -0
  163. package/src/core/probe/mass-assignment/regression.ts +141 -0
  164. package/src/core/probe/mass-assignment/suspects.ts +92 -0
  165. package/src/core/probe/mass-assignment/types.ts +135 -0
  166. package/src/core/probe/mass-assignment-probe-class.ts +198 -0
  167. package/src/core/probe/mass-assignment-probe.ts +27 -0
  168. package/src/core/probe/mass-assignment-template.ts +240 -0
  169. package/src/core/probe/method-probe.ts +43 -76
  170. package/src/core/probe/method-shared.ts +69 -0
  171. package/src/core/probe/negative-probe.ts +183 -149
  172. package/src/core/probe/orphan-tracker.ts +188 -0
  173. package/src/core/probe/path-discovery.ts +439 -0
  174. package/src/core/probe/probe-harness.ts +119 -0
  175. package/src/core/probe/registry.ts +89 -0
  176. package/src/core/probe/runner.ts +136 -0
  177. package/src/core/probe/security/baseline.ts +174 -0
  178. package/src/core/probe/security/classify.ts +341 -0
  179. package/src/core/probe/security/cleanup.ts +125 -0
  180. package/src/core/probe/security/detectors.ts +71 -0
  181. package/src/core/probe/security/digest.ts +104 -0
  182. package/src/core/probe/security/orchestrator.ts +398 -0
  183. package/src/core/probe/security/regression.ts +103 -0
  184. package/src/core/probe/security/types.ts +151 -0
  185. package/src/core/probe/security-probe-class.ts +207 -0
  186. package/src/core/probe/security-probe.ts +32 -0
  187. package/src/core/probe/shared.ts +531 -0
  188. package/src/core/probe/static-probe-class.ts +125 -0
  189. package/src/core/probe/types.ts +165 -0
  190. package/src/core/probe/verdict-aggregator.ts +33 -0
  191. package/src/core/probe/webhooks-probe.ts +282 -0
  192. package/src/core/reporter/console.ts +41 -2
  193. package/src/core/reporter/index.ts +2 -3
  194. package/src/core/reporter/json.ts +11 -1
  195. package/src/core/reporter/junit.ts +27 -12
  196. package/src/core/reporter/ndjson.ts +37 -0
  197. package/src/core/reporter/types.ts +3 -0
  198. package/src/core/runner/assertions.ts +59 -2
  199. package/src/core/runner/async-pool.ts +108 -0
  200. package/src/core/runner/auth-path.ts +8 -0
  201. package/src/core/runner/ci-context.ts +72 -0
  202. package/src/core/runner/executor.ts +265 -36
  203. package/src/core/runner/form-encode.ts +41 -0
  204. package/src/core/runner/http-client.ts +112 -2
  205. package/src/core/runner/learn-drift.ts +293 -0
  206. package/src/core/runner/preflight-vars.ts +153 -0
  207. package/src/core/runner/progress-tracker.ts +73 -0
  208. package/src/core/runner/rate-limiter.ts +87 -33
  209. package/src/core/runner/run-kind.ts +45 -0
  210. package/src/core/runner/schema-validator.ts +308 -0
  211. package/src/core/runner/send-request.ts +158 -20
  212. package/src/core/runner/types.ts +44 -0
  213. package/src/core/secrets/registry.ts +164 -0
  214. package/src/core/secrets/secrets-file.ts +115 -0
  215. package/src/core/selectors/operation-filter.ts +144 -0
  216. package/src/core/setup-api.ts +457 -20
  217. package/src/core/severity/category.ts +94 -0
  218. package/src/core/severity/index.ts +58 -0
  219. package/src/core/spec/infer-schema.ts +102 -0
  220. package/src/core/spec/layers.ts +154 -0
  221. package/src/core/spec/merge-specs.ts +156 -0
  222. package/src/core/spec/schema-from-runs.ts +117 -0
  223. package/src/core/spec/schema-overlay.ts +130 -0
  224. package/src/core/util/ajv.ts +13 -0
  225. package/src/core/util/format-eta.ts +21 -0
  226. package/src/core/util/headers.ts +9 -0
  227. package/src/core/util/url.ts +24 -0
  228. package/src/core/utils.ts +5 -1
  229. package/src/core/workspace/config.ts +129 -0
  230. package/src/core/workspace/fixture-gap-report.ts +84 -0
  231. package/src/core/workspace/fixture-gaps.ts +71 -0
  232. package/src/core/workspace/manifest.ts +283 -0
  233. package/src/core/workspace/output-rotation.ts +62 -0
  234. package/src/core/workspace/root.ts +13 -11
  235. package/src/core/workspace/triage-path.ts +87 -0
  236. package/src/db/lint-runs.ts +47 -0
  237. package/src/db/migrate.ts +128 -0
  238. package/src/db/migrations/0001_run_kind.sql +25 -0
  239. package/src/db/migrations/0002_run_kind_request.sql +59 -0
  240. package/src/db/migrations/sql.d.ts +4 -0
  241. package/src/db/queries/collections.ts +133 -0
  242. package/src/db/queries/coverage.ts +9 -0
  243. package/src/db/queries/dashboard.ts +59 -0
  244. package/src/db/queries/results.ts +216 -0
  245. package/src/db/queries/runs.ts +289 -0
  246. package/src/db/queries/sessions.ts +42 -0
  247. package/src/db/queries/settings.ts +28 -0
  248. package/src/db/queries/types.ts +172 -0
  249. package/src/db/queries.ts +75 -802
  250. package/src/db/schema.ts +178 -50
  251. package/src/cli/commands/export.ts +0 -144
  252. package/src/cli/commands/guide.ts +0 -127
  253. package/src/cli/commands/init/templates/skills/scenarios.md +0 -97
  254. package/src/cli/commands/probe-methods.ts +0 -108
  255. package/src/cli/commands/probe-validation.ts +0 -124
  256. package/src/cli/commands/serve.ts +0 -114
  257. package/src/cli/commands/sync.ts +0 -268
  258. package/src/cli/commands/update.ts +0 -189
  259. package/src/cli/commands/validate.ts +0 -34
  260. package/src/core/diagnostics/render-md.ts +0 -112
  261. package/src/core/exporter/postman.ts +0 -963
  262. package/src/core/generator/guide-builder.ts +0 -253
  263. package/src/core/meta/types.ts +0 -19
  264. package/src/core/parser/index.ts +0 -21
  265. package/src/core/runner/execute-run.ts +0 -132
  266. package/src/core/runner/index.ts +0 -12
  267. package/src/core/sync/spec-differ.ts +0 -38
  268. package/src/web/data/collection-state.ts +0 -362
  269. package/src/web/routes/api.ts +0 -314
  270. package/src/web/routes/dashboard.ts +0 -350
  271. package/src/web/routes/runs.ts +0 -64
  272. package/src/web/schemas.ts +0 -121
  273. package/src/web/server.ts +0 -134
  274. package/src/web/static/htmx.min.cjs +0 -1
  275. package/src/web/static/style.css +0 -1148
  276. package/src/web/views/endpoints-tab.ts +0 -174
  277. package/src/web/views/explorer-tab.ts +0 -402
  278. package/src/web/views/health-strip.ts +0 -92
  279. package/src/web/views/layout.ts +0 -48
  280. package/src/web/views/results.ts +0 -210
  281. package/src/web/views/runs-tab.ts +0 -126
  282. package/src/web/views/suites-tab.ts +0 -181
@@ -25,6 +25,9 @@ permissions:
25
25
  contents: read
26
26
  checks: write
27
27
  pull-requests: write
28
+ # ARV-5: required for github/codeql-action/upload-sarif to surface
29
+ # zond-checks findings in the Code Scanning tab.
30
+ security-events: write
28
31
 
29
32
  jobs:
30
33
  test:
@@ -55,6 +58,21 @@ jobs:
55
58
  # Add --env-var "BASE_URL=\${{ secrets.STAGING_URL }}" for staging URL
56
59
  continue-on-error: true
57
60
 
61
+ - name: Run depth checks (SARIF for Code Scanning)
62
+ run: |
63
+ zond checks run --api myapi --report sarif --output test-results/zond-checks.sarif || true
64
+ # ARV-5: \`|| true\` keeps the workflow green even when HIGH/CRITICAL
65
+ # findings would otherwise exit 1 — Code Scanning will still get the
66
+ # SARIF and gate on the alerts via branch protection if desired.
67
+ continue-on-error: true
68
+
69
+ - name: Upload SARIF to GitHub Code Scanning
70
+ if: always()
71
+ uses: github/codeql-action/upload-sarif@v3
72
+ with:
73
+ sarif_file: test-results/zond-checks.sarif
74
+ category: zond-checks
75
+
58
76
  - name: Publish test results
59
77
  uses: EnricoMi/publish-unit-test-result-action@v2
60
78
  if: always()
@@ -178,3 +196,28 @@ export async function ciInitCommand(options: CiInitOptions): Promise<number> {
178
196
 
179
197
  return 0;
180
198
  }
199
+
200
+ import type { Command } from "commander";
201
+ import { globalJson } from "../resolve.ts";
202
+
203
+ export function registerCi(program: Command): void {
204
+ const ci = program.command("ci").description("CI/CD scaffolding");
205
+ ci
206
+ .command("init")
207
+ .description("Generate CI/CD workflow (GitHub Actions, GitLab CI)")
208
+ .option("--github", "Generate GitHub Actions workflow")
209
+ .option("--gitlab", "Generate GitLab CI config")
210
+ .option("--dir <path>", "Project root directory (default: current directory)")
211
+ .option("--force", "Overwrite existing CI config")
212
+ .action(async (opts, cmd: Command) => {
213
+ let platform: "github" | "gitlab" | undefined;
214
+ if (opts.github === true) platform = "github";
215
+ else if (opts.gitlab === true) platform = "gitlab";
216
+ process.exitCode = await ciInitCommand({
217
+ platform,
218
+ force: opts.force === true,
219
+ dir: opts.dir,
220
+ json: globalJson(cmd),
221
+ });
222
+ });
223
+ }
@@ -0,0 +1,212 @@
1
+ /**
2
+ * `zond clean` — remove auto-generated files tracked in `.zond/manifest.json`.
3
+ *
4
+ * Default mode is dry-run; `--force` is required to actually delete. Files
5
+ * whose sha256 no longer matches the manifest entry are treated as
6
+ * manually-edited and skipped (TASK-156, m-9).
7
+ */
8
+
9
+ import { rmSync, rmdirSync, readdirSync, existsSync } from "node:fs";
10
+ import { dirname, resolve } from "node:path";
11
+ import { findWorkspaceRoot } from "../../core/workspace/root.ts";
12
+ import {
13
+ hasManifest,
14
+ inspectEntries,
15
+ loadManifest,
16
+ removeManifestEntries,
17
+ selectEntriesEx,
18
+ type CleanItem,
19
+ type ManifestCategory,
20
+ } from "../../core/workspace/manifest.ts";
21
+ import type { Command } from "commander";
22
+ import { jsonOk, jsonError, printJson } from "../json-envelope.ts";
23
+ import { printError, printSuccess } from "../output.ts";
24
+ import { globalJson } from "../resolve.ts";
25
+ import { getApi } from "../util/api-context.ts";
26
+
27
+ export interface CleanOptions {
28
+ api?: string;
29
+ probes?: boolean;
30
+ all?: boolean;
31
+ force?: boolean;
32
+ json?: boolean;
33
+ }
34
+
35
+ export async function cleanCommand(opts: CleanOptions): Promise<number> {
36
+ const ws = findWorkspaceRoot();
37
+ if (ws.fromFallback) {
38
+ const m = "No workspace detected. Run `zond init` first.";
39
+ if (opts.json) printJson(jsonError("clean", [m])); else printError(m);
40
+ return 2;
41
+ }
42
+
43
+ if (!hasManifest(ws.root)) {
44
+ const msg = `No .zond/manifest.json — nothing tracked yet. Run \`zond add api\`, \`zond generate\`, or a probe-* --emit first.`;
45
+ if (opts.json) {
46
+ printJson(jsonOk("clean", { dryRun: true, deleted: [], modified: [], missing: [], message: msg }));
47
+ } else {
48
+ console.log(msg);
49
+ }
50
+ return 0;
51
+ }
52
+
53
+ if (!opts.api && !opts.probes && !opts.all) {
54
+ const m = "Specify a scope: --api <name>, --probes, or --all.";
55
+ if (opts.json) printJson(jsonError("clean", [m])); else printError(m);
56
+ return 2;
57
+ }
58
+
59
+ const manifest = loadManifest(ws.root);
60
+ const category: ManifestCategory | undefined = opts.probes ? "probes" : undefined;
61
+ const { selected: entries, probesPreserved } = selectEntriesEx(manifest, {
62
+ api: opts.api,
63
+ category,
64
+ all: opts.all && !opts.api && !opts.probes,
65
+ });
66
+
67
+ if (entries.length === 0 && probesPreserved.length === 0) {
68
+ const m = "No matching auto-generated files in manifest.";
69
+ if (opts.json) {
70
+ printJson(jsonOk("clean", { dryRun: true, deleted: [], modified: [], missing: [], message: m }));
71
+ } else {
72
+ console.log(m);
73
+ }
74
+ return 0;
75
+ }
76
+
77
+ const items = inspectEntries(ws.root, entries);
78
+ const toDelete = items.filter((i) => i.verdict === "delete");
79
+ const modified = items.filter((i) => i.verdict === "modified");
80
+ const missing = items.filter((i) => i.verdict === "missing");
81
+
82
+ const dryRun = !opts.force;
83
+
84
+ if (!dryRun) {
85
+ for (const item of toDelete) {
86
+ try {
87
+ rmSync(item.absPath, { force: true });
88
+ } catch {
89
+ // best-effort
90
+ }
91
+ }
92
+ pruneEmptyDirs(ws.root, toDelete);
93
+ const removedPaths = [...toDelete, ...missing].map((i) => i.entry.path);
94
+ removeManifestEntries(ws.root, removedPaths);
95
+ }
96
+
97
+ if (opts.json) {
98
+ printJson(jsonOk("clean", {
99
+ dryRun,
100
+ scope: { api: opts.api, probes: !!opts.probes, all: !!opts.all },
101
+ deleted: toDelete.map(itemSummary),
102
+ modified: modified.map(itemSummary),
103
+ missing: missing.map(itemSummary),
104
+ probesPreserved: probesPreserved.map((e) => ({ path: e.path, api: e.api })),
105
+ }));
106
+ return 0;
107
+ }
108
+
109
+ const verb = dryRun ? "Would delete" : "Deleted";
110
+ printSuccess(`${verb} ${toDelete.length} file(s); ${modified.length} skipped (manually edited); ${missing.length} already missing.`);
111
+ if (toDelete.length > 0) {
112
+ console.log("");
113
+ console.log(`${verb}:`);
114
+ for (const i of toDelete) console.log(` - ${i.entry.path}`);
115
+ }
116
+ if (modified.length > 0) {
117
+ console.log("");
118
+ console.log("Skipped (manually edited, sha256 mismatch):");
119
+ for (const i of modified) console.log(` ! ${i.entry.path}`);
120
+ }
121
+ // TASK-258: probes belong to a separate pipeline. Scoping by --api alone
122
+ // preserves them and surfaces the regen command so users don't lose
123
+ // 30s+ of probe-validation/-methods work to a typo.
124
+ if (probesPreserved.length > 0) {
125
+ console.log("");
126
+ console.log(`Preserved ${probesPreserved.length} probe-suite file(s) under apis/${opts.api ?? "<api>"}/probes/.`);
127
+ console.log("Pass --probes to also remove them, or regenerate via:");
128
+ console.log(` zond probe-validation --api ${opts.api} --output apis/${opts.api}/probes`);
129
+ console.log(` zond probe-methods --api ${opts.api} --output apis/${opts.api}/probes`);
130
+ }
131
+ if (dryRun) {
132
+ console.log("");
133
+ console.log("Re-run with --force to actually delete.");
134
+ }
135
+ return 0;
136
+ }
137
+
138
+ function itemSummary(i: CleanItem) {
139
+ return {
140
+ path: i.entry.path,
141
+ by: i.entry.by,
142
+ ts: i.entry.ts,
143
+ api: i.entry.api,
144
+ category: i.entry.category,
145
+ verdict: i.verdict,
146
+ };
147
+ }
148
+
149
+ /**
150
+ * After deleting tracked files, remove any directories that are now empty
151
+ * and live inside the workspace. Best-effort: stops at first non-empty dir.
152
+ */
153
+ function pruneEmptyDirs(workspaceRoot: string, items: CleanItem[]): void {
154
+ const dirs = new Set<string>();
155
+ for (const i of items) dirs.add(dirname(i.absPath));
156
+ // Process deepest first.
157
+ const sorted = [...dirs].sort((a, b) => b.length - a.length);
158
+ for (const d of sorted) {
159
+ let cur = d;
160
+ while (cur.startsWith(workspaceRoot) && cur !== workspaceRoot) {
161
+ if (!existsSync(cur)) {
162
+ cur = dirname(cur);
163
+ continue;
164
+ }
165
+ let entries: string[] = [];
166
+ try {
167
+ entries = readdirSync(cur);
168
+ } catch {
169
+ break;
170
+ }
171
+ if (entries.length > 0) break;
172
+ try {
173
+ rmdirSync(cur);
174
+ } catch {
175
+ break;
176
+ }
177
+ cur = dirname(cur);
178
+ }
179
+ }
180
+ // Keep `resolve` reachable for type-only imports.
181
+ void resolve;
182
+ }
183
+
184
+ export function registerClean(program: Command): void {
185
+ program
186
+ .command("clean")
187
+ .description("Remove auto-generated files tracked in .zond/manifest.json (TASK-156, m-9)")
188
+ .option("--api <name>", "Limit to a single API (apis/<name>/). Preserves probes/ unless --probes is also passed (TASK-258)")
189
+ .option(
190
+ "--probes",
191
+ "Scope deletion to probe-suite files only (apis/<api>/probes/). " +
192
+ "TASK-265: this is effectively `--probes-only` — `tests/`, `spec.json`, " +
193
+ "and `.api-catalog.yaml` are NEVER touched in this mode. Combine with " +
194
+ "--api <name> to limit to one API; alone, removes probes for every API.",
195
+ )
196
+ .option("--all", "Remove every tracked auto-generated file in the workspace (includes probes/)")
197
+ .option("--force", "Actually delete files (default is dry-run)")
198
+ .action(async (opts, cmd: Command) => {
199
+ // ARV-238 (R-03/F11/SD9): `--api` exists both at program level (TASK-290)
200
+ // and on this subcommand; commander absorbs the value into the program
201
+ // option, leaving `opts.api` undefined. Fall back via `getApi` so users
202
+ // who follow `zond clean --api <name>` (per skill / --help) actually
203
+ // scope the run instead of hitting "Specify a scope".
204
+ process.exitCode = await cleanCommand({
205
+ api: getApi(cmd, opts),
206
+ probes: opts.probes === true,
207
+ all: opts.all === true,
208
+ force: opts.force === true,
209
+ json: globalJson(cmd),
210
+ });
211
+ });
212
+ }
@@ -0,0 +1,236 @@
1
+ /**
2
+ * `zond cleanup` — retry housekeeping that probes couldn't finish.
3
+ *
4
+ * v1 ships only `--orphans`: read every record from
5
+ * `~/.zond/orphans/<api>/<run-id>.jsonl` and re-issue the DELETE for any
6
+ * resource that survived the probe's own cleanup attempts. 404 is treated
7
+ * as success — the goal is "resource is gone", and the API getting there
8
+ * before us is fine. (TASK-278.)
9
+ */
10
+ import { loadOrphans, markRemoved } from "../../core/probe/orphan-tracker.ts";
11
+ import type { OrphanRecord } from "../../core/probe/orphan-tracker.ts";
12
+ import { encodePathForRepro } from "../../core/probe/shared.ts";
13
+ import { executeRequest } from "../../core/runner/http-client.ts";
14
+ import { loadEnvironment, loadEnvMeta } from "../../core/parser/variables.ts";
15
+ import { resolveTimeoutMs } from "../../core/workspace/config.ts";
16
+ import { jsonOk, jsonError, printJson } from "../json-envelope.ts";
17
+ import { printError, printWarning, printSuccess } from "../output.ts";
18
+ import { readCurrentApi } from "../../core/context/current.ts";
19
+ import type { Command } from "commander";
20
+ import { globalJson } from "../resolve.ts";
21
+
22
+ export interface CleanupOptions {
23
+ orphans: boolean;
24
+ api?: string;
25
+ /** ARV-139: pass true to disable the default current-api scoping and look
26
+ * at orphans across every API in the tracker. */
27
+ allApis?: boolean;
28
+ runId?: string;
29
+ dryRun?: boolean;
30
+ json?: boolean;
31
+ /** Override base_url resolution. By default the env from cwd .env.yaml or
32
+ * apis/<api>/.env.yaml is used. Tests inject a known URL via this. */
33
+ baseUrl?: string;
34
+ /** Per-request timeout, ms. */
35
+ timeoutMs?: number;
36
+ }
37
+
38
+ /**
39
+ * ARV-244 (R-04/F15): orphan records store `deletePath` as it was used at
40
+ * probe time — typically built by concatenating literal id segments captured
41
+ * from the response. CRLF / open-redirect probes can poison those ids with
42
+ * raw `\r`, `\n`, spaces, etc. (e.g. label name `zond-safe\rX-Zond: yes`),
43
+ * which makes the DELETE URL malformed: the API gets a request line with an
44
+ * embedded CR and silently splits or routes elsewhere → 404 → record stays
45
+ * in the queue and the resource leaks. Encoding is shared with the
46
+ * security-probe digest via `core/probe/shared.ts#encodePathForRepro`.
47
+ */
48
+ export const encodeOrphanPath = encodePathForRepro;
49
+
50
+ export async function cleanupCommand(opts: CleanupOptions): Promise<number> {
51
+ if (!opts.orphans) {
52
+ const m = "Nothing to do — pass --orphans to retry leaked probe resources.";
53
+ if (opts.json) printJson(jsonError("cleanup", [m]));
54
+ else printError(m);
55
+ return 2;
56
+ }
57
+
58
+ // ARV-139: scope the orphan queue to the active API by default. The on-disk
59
+ // tracker at ~/.zond/orphans/<api>/... is shared across the workspace, so
60
+ // switching APIs (e.g. apis/resend → apis/sentry) would otherwise surface
61
+ // orphans from previous work on unrelated APIs — including DELETE attempts
62
+ // against endpoints that aren't even part of the active spec. Explicit
63
+ // `--api <name>` wins; `--all-apis` opts back into the pre-ARV-139 behaviour.
64
+ let scopedApi = opts.api;
65
+ if (!scopedApi && !opts.allApis) {
66
+ scopedApi = readCurrentApi() ?? undefined;
67
+ }
68
+
69
+ let records: OrphanRecord[];
70
+ try {
71
+ const filter: { api?: string; runId?: string } = {};
72
+ if (scopedApi) filter.api = scopedApi;
73
+ if (opts.runId) filter.runId = opts.runId;
74
+ records = await loadOrphans(filter);
75
+ } catch (err) {
76
+ const m = `Failed to load orphan tracker: ${(err as Error).message}`;
77
+ if (opts.json) printJson(jsonError("cleanup", [m]));
78
+ else printError(m);
79
+ return 2;
80
+ }
81
+
82
+ // ARV-102 (F7): split orphans into retriable (have a DELETE plan) and
83
+ // manual-only (probe knew the resource was created but couldn't derive
84
+ // a deletePath / id). Manual-only entries are surfaced separately —
85
+ // we can't auto-DELETE them, but the operator must know they exist.
86
+ const manualOnly = records.filter(r => r.requires_manual_cleanup === true || r.deletePath === "");
87
+ const retriable = records.filter(r => !(r.requires_manual_cleanup === true || r.deletePath === ""));
88
+
89
+ if (records.length === 0) {
90
+ if (opts.json) printJson(jsonOk("cleanup", { retried: 0, removed: 0, failed: 0, items: [], manual_cleanup_required: [] }));
91
+ else console.log("No orphan resources to retry.");
92
+ return 0;
93
+ }
94
+
95
+ // Group by api so we resolve env once per API instead of per record.
96
+ const baseUrlByApi = new Map<string, string>();
97
+ if (opts.baseUrl) {
98
+ for (const r of retriable) baseUrlByApi.set(r.api, opts.baseUrl);
99
+ }
100
+
101
+ if (opts.dryRun) {
102
+ if (opts.json) printJson(jsonOk("cleanup", { dryRun: true, items: retriable, manual_cleanup_required: manualOnly }));
103
+ else {
104
+ console.log(`Dry-run: ${retriable.length} orphan(s) would be retried:`);
105
+ for (const r of retriable) {
106
+ console.log(` ${r.method} ${r.path} (id=${r.id}); DELETE ${r.deletePath} — last status: ${r.lastCleanupStatus ?? "n/a"}`);
107
+ }
108
+ if (manualOnly.length > 0) {
109
+ console.log(`\nManual cleanup required: ${manualOnly.length} resource(s) (no DELETE plan):`);
110
+ for (const r of manualOnly) {
111
+ console.log(` ${r.method} ${r.path}${r.id ? ` (id=${r.id})` : ""} — ${r.lastCleanupError ?? "no DELETE counterpart"}`);
112
+ }
113
+ }
114
+ }
115
+ return 0;
116
+ }
117
+
118
+ // Per-API timeout: CLI flag → apis/<api>/.env.yaml `timeoutMs:` → workspace
119
+ // `defaults.timeout_ms` → 30000.
120
+ const timeoutByApi = new Map<string, number>();
121
+ async function timeoutFor(api: string): Promise<number> {
122
+ let t = timeoutByApi.get(api);
123
+ if (t !== undefined) return t;
124
+ let envTimeout: number | undefined;
125
+ try {
126
+ const meta = await loadEnvMeta(undefined, `apis/${api}`);
127
+ envTimeout = meta.timeoutMs;
128
+ } catch { /* meta is best-effort */ }
129
+ t = resolveTimeoutMs(opts.timeoutMs, envTimeout);
130
+ timeoutByApi.set(api, t);
131
+ return t;
132
+ }
133
+
134
+ const results: Array<{ record: OrphanRecord; status: number | null; ok: boolean; error?: string }> = [];
135
+ for (const r of retriable) {
136
+ let baseUrl = baseUrlByApi.get(r.api);
137
+ if (!baseUrl) {
138
+ try {
139
+ const env = await loadEnvironment(undefined, `apis/${r.api}`);
140
+ baseUrl = env["base_url"];
141
+ } catch { /* fall through */ }
142
+ }
143
+ if (!baseUrl) {
144
+ results.push({ record: r, status: null, ok: false, error: `base_url missing — set ZOND_BASE_URL or apis/${r.api}/.env.yaml` });
145
+ continue;
146
+ }
147
+ baseUrlByApi.set(r.api, baseUrl);
148
+
149
+ const url = `${baseUrl.replace(/\/+$/, "")}${encodeOrphanPath(r.deletePath)}`;
150
+ try {
151
+ const resp = await executeRequest(
152
+ { method: "DELETE", url, headers: {} },
153
+ { timeout: await timeoutFor(r.api), retries: 0 },
154
+ );
155
+ // 404 = already gone → success. 2xx = just deleted → success.
156
+ const ok = resp.status === 404 || (resp.status >= 200 && resp.status < 300);
157
+ results.push({ record: r, status: resp.status, ok });
158
+ if (ok) await markRemoved(r);
159
+ } catch (err) {
160
+ results.push({ record: r, status: null, ok: false, error: (err as Error).message });
161
+ }
162
+ }
163
+
164
+ const removed = results.filter(r => r.ok).length;
165
+ const failed = results.length - removed;
166
+
167
+ if (opts.json) {
168
+ printJson(jsonOk("cleanup", {
169
+ retried: results.length,
170
+ removed,
171
+ failed,
172
+ items: results.map(r => ({
173
+ api: r.record.api,
174
+ runId: r.record.runId,
175
+ method: r.record.method,
176
+ path: r.record.path,
177
+ id: r.record.id,
178
+ deletePath: r.record.deletePath,
179
+ status: r.status,
180
+ ok: r.ok,
181
+ error: r.error ?? null,
182
+ })),
183
+ manual_cleanup_required: manualOnly.map(r => ({
184
+ api: r.api,
185
+ runId: r.runId,
186
+ method: r.method,
187
+ path: r.path,
188
+ id: r.id,
189
+ reason: r.lastCleanupError ?? "no DELETE counterpart",
190
+ })),
191
+ }));
192
+ } else {
193
+ if (removed > 0) printSuccess(`${removed} orphan(s) cleaned up.`);
194
+ if (failed > 0) {
195
+ printWarning(`${failed} orphan(s) still alive:`);
196
+ for (const r of results) {
197
+ if (r.ok) continue;
198
+ const tail = r.status != null ? `→ ${r.status}` : (r.error ? `→ err: ${r.error}` : "");
199
+ process.stderr.write(` ${r.record.method} ${r.record.path} (id=${r.record.id}); DELETE ${r.record.deletePath} ${tail}\n`);
200
+ }
201
+ }
202
+ if (manualOnly.length > 0) {
203
+ printWarning(`${manualOnly.length} resource(s) need manual cleanup (no DELETE plan):`);
204
+ for (const r of manualOnly) {
205
+ process.stderr.write(` ${r.method} ${r.path}${r.id ? ` (id=${r.id})` : ""} — ${r.lastCleanupError ?? "no DELETE counterpart"}\n`);
206
+ }
207
+ }
208
+ }
209
+
210
+ // Manual-only orphans count as "still alive" for exit-code purposes —
211
+ // CI must fail loudly when probes leave un-deletable state behind.
212
+ return failed > 0 || manualOnly.length > 0 ? 1 : 0;
213
+ }
214
+
215
+ export function registerCleanup(program: Command): void {
216
+ program
217
+ .command("cleanup")
218
+ .description("Retry probe-leftover work. Currently only --orphans (TASK-278) — re-issues DELETE for resources captured in ~/.zond/orphans/.")
219
+ .option("--orphans", "Retry DELETE for resources in the orphan tracker")
220
+ .option("--api <name>", "Limit to a single API (matches the orphan-tracker subdirectory; defaults to the current API)")
221
+ .option("--all-apis", "Include orphans from every API in the tracker (disables the default current-api scoping)")
222
+ .option("--run <id>", "Limit to a single probe run id")
223
+ .option("--dry-run", "Print the plan without sending DELETEs")
224
+ .option("--timeout <ms>", "Per-request timeout in ms (overrides .env.yaml `timeoutMs` and zond.config.yml `defaults.timeout_ms`; default 30000)")
225
+ .action(async (opts, cmd: Command) => {
226
+ process.exitCode = await cleanupCommand({
227
+ orphans: opts.orphans === true,
228
+ api: typeof opts.api === "string" ? opts.api : undefined,
229
+ allApis: opts.allApis === true,
230
+ runId: typeof opts.run === "string" ? opts.run : undefined,
231
+ dryRun: opts.dryRun === true,
232
+ timeoutMs: typeof opts.timeout === "string" ? Number(opts.timeout) : undefined,
233
+ json: globalJson(cmd),
234
+ });
235
+ });
236
+ }
@@ -174,3 +174,19 @@ export function completionsCommand(options: CompletionsOptions): number {
174
174
  process.stdout.write(script);
175
175
  return 0;
176
176
  }
177
+
178
+ import { printError } from "../output.ts";
179
+
180
+ export function registerCompletions(program: Command): void {
181
+ program
182
+ .command("completions <shell>")
183
+ .description(`Generate shell completion script (${COMPLETION_SHELLS.join(", ")})`)
184
+ .action((shell: string) => {
185
+ if (!(COMPLETION_SHELLS as readonly string[]).includes(shell)) {
186
+ printError(`Unsupported shell: ${shell}. Supported: ${COMPLETION_SHELLS.join(", ")}`);
187
+ process.exitCode = 2;
188
+ return;
189
+ }
190
+ process.exitCode = completionsCommand({ shell: shell as CompletionShell, program });
191
+ });
192
+ }