@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.
Files changed (256) hide show
  1. package/CHANGELOG.md +758 -3
  2. package/README.md +78 -15
  3. package/package.json +17 -10
  4. package/src/cli/argv.ts +122 -0
  5. package/src/cli/commands/add-api.ts +134 -0
  6. package/src/cli/commands/api/annotate/idempotency.ts +59 -0
  7. package/src/cli/commands/api/annotate/index.ts +525 -0
  8. package/src/cli/commands/api/annotate/lifecycle.ts +74 -0
  9. package/src/cli/commands/api/annotate/overlay.ts +206 -0
  10. package/src/cli/commands/api/annotate/pagination.ts +60 -0
  11. package/src/cli/commands/api/annotate/prompts.ts +183 -0
  12. package/src/cli/commands/api/annotate/readback.ts +58 -0
  13. package/src/cli/commands/api/annotate/resources.ts +91 -0
  14. package/src/cli/commands/api/annotate/seed-bodies.ts +61 -0
  15. package/src/cli/commands/audit.ts +480 -0
  16. package/src/cli/commands/bootstrap.ts +710 -0
  17. package/src/cli/commands/catalog.ts +35 -0
  18. package/src/cli/commands/check.ts +348 -0
  19. package/src/cli/commands/checks.ts +756 -0
  20. package/src/cli/commands/ci-init.ts +55 -6
  21. package/src/cli/commands/clean.ts +212 -0
  22. package/src/cli/commands/cleanup.ts +262 -0
  23. package/src/cli/commands/completions.ts +192 -0
  24. package/src/cli/commands/coverage.ts +605 -132
  25. package/src/cli/commands/db.ts +180 -8
  26. package/src/cli/commands/describe.ts +37 -2
  27. package/src/cli/commands/discover.ts +1236 -0
  28. package/src/cli/commands/doctor.ts +607 -0
  29. package/src/cli/commands/fixtures.ts +402 -0
  30. package/src/cli/commands/generate.ts +420 -47
  31. package/src/cli/commands/init/agents-md.ts +61 -0
  32. package/src/cli/commands/init/bootstrap.ts +108 -0
  33. package/src/cli/commands/init/index.ts +244 -0
  34. package/src/cli/commands/init/skills.ts +98 -0
  35. package/src/cli/commands/init/templates/agents.md +77 -0
  36. package/src/cli/commands/init/templates/markdown.d.ts +4 -0
  37. package/src/cli/commands/init/templates/skills/zond-checks.md +397 -0
  38. package/src/cli/commands/init/templates/skills/zond-triage.md +210 -0
  39. package/src/cli/commands/init/templates/skills/zond.md +651 -0
  40. package/src/cli/commands/init/templates/zond-config.yml +14 -0
  41. package/src/cli/commands/prepare-fixtures.ts +135 -0
  42. package/src/cli/commands/probe/mass-assignment.ts +503 -0
  43. package/src/cli/commands/probe/security.ts +454 -0
  44. package/src/cli/commands/probe/static.ts +255 -0
  45. package/src/cli/commands/probe/webhooks.ts +161 -0
  46. package/src/cli/commands/probe.ts +459 -0
  47. package/src/cli/commands/reference.ts +87 -0
  48. package/src/cli/commands/refresh-api.ts +169 -0
  49. package/src/cli/commands/remove-api.ts +150 -0
  50. package/src/cli/commands/report-bundle.ts +318 -0
  51. package/src/cli/commands/report.ts +241 -0
  52. package/src/cli/commands/request.ts +379 -4
  53. package/src/cli/commands/run.ts +911 -33
  54. package/src/cli/commands/session.ts +244 -0
  55. package/src/cli/commands/use.ts +74 -0
  56. package/src/cli/index.ts +36 -607
  57. package/src/cli/json-envelope.ts +112 -3
  58. package/src/cli/json-schemas.ts +263 -0
  59. package/src/cli/program.ts +218 -0
  60. package/src/cli/resolve.ts +105 -0
  61. package/src/cli/status-filter.ts +124 -0
  62. package/src/cli/util/api-context.ts +85 -0
  63. package/src/cli/version.ts +8 -0
  64. package/src/core/anti-fp/bootstrap.ts +34 -0
  65. package/src/core/anti-fp/index.ts +33 -0
  66. package/src/core/anti-fp/registry.ts +44 -0
  67. package/src/core/anti-fp/rules/baseline-echo.ts +74 -0
  68. package/src/core/anti-fp/rules/schemathesis/body_negation_becomes_valid.ts +52 -0
  69. package/src/core/anti-fp/rules/schemathesis/coverage_phase_boundary_positive.ts +38 -0
  70. package/src/core/anti-fp/rules/schemathesis/has_unverifiable_mutations.ts +35 -0
  71. package/src/core/anti-fp/rules/schemathesis/index.ts +24 -0
  72. package/src/core/anti-fp/rules/schemathesis/string_type_mutation_becomes_valid.ts +53 -0
  73. package/src/core/anti-fp/rules/subscription-gated/index.ts +11 -0
  74. package/src/core/anti-fp/rules/subscription-gated/paid-plan-403.ts +75 -0
  75. package/src/core/anti-fp/types.ts +68 -0
  76. package/src/core/checks/checks/_crud-helpers.ts +133 -0
  77. package/src/core/checks/checks/_negative_mutator.ts +133 -0
  78. package/src/core/checks/checks/_readback-helpers.ts +133 -0
  79. package/src/core/checks/checks/content_type_conformance.ts +39 -0
  80. package/src/core/checks/checks/cross_call_references.ts +134 -0
  81. package/src/core/checks/checks/ensure_resource_availability.ts +62 -0
  82. package/src/core/checks/checks/idempotency_replay.ts +246 -0
  83. package/src/core/checks/checks/ignored_auth.ts +211 -0
  84. package/src/core/checks/checks/index.ts +65 -0
  85. package/src/core/checks/checks/lifecycle_transitions.ts +273 -0
  86. package/src/core/checks/checks/missing_required_header.ts +40 -0
  87. package/src/core/checks/checks/negative_data_rejection.ts +45 -0
  88. package/src/core/checks/checks/not_a_server_error.ts +27 -0
  89. package/src/core/checks/checks/open_cors_on_sensitive.ts +131 -0
  90. package/src/core/checks/checks/pagination_invariants.ts +238 -0
  91. package/src/core/checks/checks/positive_data_acceptance.ts +36 -0
  92. package/src/core/checks/checks/rate_limit_headers_absent.ts +77 -0
  93. package/src/core/checks/checks/response_headers_conformance.ts +74 -0
  94. package/src/core/checks/checks/response_schema_conformance.ts +30 -0
  95. package/src/core/checks/checks/status_code_conformance.ts +61 -0
  96. package/src/core/checks/checks/unsupported_method.ts +63 -0
  97. package/src/core/checks/checks/use_after_free.ts +78 -0
  98. package/src/core/checks/index.ts +30 -0
  99. package/src/core/checks/mode.ts +79 -0
  100. package/src/core/checks/recommended-action.ts +64 -0
  101. package/src/core/checks/registry.ts +78 -0
  102. package/src/core/checks/runner.ts +874 -0
  103. package/src/core/checks/sarif.ts +230 -0
  104. package/src/core/checks/stateful.ts +121 -0
  105. package/src/core/checks/types.ts +189 -0
  106. package/src/core/classifier/recommended-action.ts +222 -0
  107. package/src/core/context/current.ts +51 -0
  108. package/src/core/context/session.ts +78 -0
  109. package/src/core/coverage/loader.ts +185 -0
  110. package/src/core/coverage/reasons.ts +300 -0
  111. package/src/core/diagnostics/db-analysis.ts +161 -12
  112. package/src/core/diagnostics/failure-class.ts +120 -0
  113. package/src/core/diagnostics/failure-hints.ts +212 -9
  114. package/src/core/diagnostics/spec-pointer.ts +99 -0
  115. package/src/core/diagnostics/suggested-fixes.ts +156 -0
  116. package/src/core/exporter/case-study/index.ts +270 -0
  117. package/src/core/exporter/curl.ts +40 -0
  118. package/src/core/exporter/exporter.ts +48 -0
  119. package/src/core/exporter/html-report/escape.ts +24 -0
  120. package/src/core/exporter/html-report/index.ts +479 -0
  121. package/src/core/exporter/html-report/script.ts +100 -0
  122. package/src/core/exporter/html-report/styles.ts +408 -0
  123. package/src/core/generator/chunker.ts +53 -15
  124. package/src/core/generator/coverage-phase.ts +0 -0
  125. package/src/core/generator/create-body.ts +89 -0
  126. package/src/core/generator/data-factory.ts +490 -33
  127. package/src/core/generator/describe.ts +1 -1
  128. package/src/core/generator/fixtures-builder.ts +325 -0
  129. package/src/core/generator/index.ts +7 -5
  130. package/src/core/generator/openapi-reader.ts +55 -3
  131. package/src/core/generator/path-param-disambig.ts +114 -0
  132. package/src/core/generator/resources-builder.ts +648 -0
  133. package/src/core/generator/schema-utils.ts +11 -3
  134. package/src/core/generator/serializer.ts +114 -15
  135. package/src/core/generator/suite-generator.ts +484 -77
  136. package/src/core/generator/types.ts +8 -0
  137. package/src/core/identity/identity-file.ts +129 -0
  138. package/src/core/lint/affects.ts +28 -0
  139. package/src/core/lint/config.ts +96 -0
  140. package/src/core/lint/format.ts +42 -0
  141. package/src/core/lint/index.ts +94 -0
  142. package/src/core/lint/reporter.ts +128 -0
  143. package/src/core/lint/rules/consistency.ts +158 -0
  144. package/src/core/lint/rules/heuristics.ts +97 -0
  145. package/src/core/lint/rules/strictness.ts +109 -0
  146. package/src/core/lint/types.ts +96 -0
  147. package/src/core/lint/walker.ts +248 -0
  148. package/src/core/meta/meta-store.ts +6 -73
  149. package/src/core/output/README.md +91 -0
  150. package/src/core/output/index.ts +13 -0
  151. package/src/core/output/run.ts +126 -0
  152. package/src/core/output/types.ts +129 -0
  153. package/src/core/parser/env-interpolation.ts +104 -0
  154. package/src/core/parser/filter.ts +57 -0
  155. package/src/core/parser/schema.ts +132 -5
  156. package/src/core/parser/types.ts +29 -2
  157. package/src/core/parser/variables.ts +0 -0
  158. package/src/core/parser/yaml-parser.ts +108 -13
  159. package/src/core/probe/bootstrap.ts +34 -0
  160. package/src/core/probe/dry-run-envelope.ts +57 -0
  161. package/src/core/probe/mass-assignment-probe-class.ts +198 -0
  162. package/src/core/probe/mass-assignment-probe.ts +1122 -0
  163. package/src/core/probe/mass-assignment-template.ts +212 -0
  164. package/src/core/probe/method-probe.ts +164 -0
  165. package/src/core/probe/method-shared.ts +69 -0
  166. package/src/core/probe/negative-probe.ts +691 -0
  167. package/src/core/probe/orphan-tracker.ts +188 -0
  168. package/src/core/probe/path-discovery.ts +440 -0
  169. package/src/core/probe/probe-harness.ts +120 -0
  170. package/src/core/probe/registry.ts +89 -0
  171. package/src/core/probe/runner.ts +136 -0
  172. package/src/core/probe/security-probe-class.ts +201 -0
  173. package/src/core/probe/security-probe.ts +1453 -0
  174. package/src/core/probe/shared.ts +505 -0
  175. package/src/core/probe/static-probe-class.ts +125 -0
  176. package/src/core/probe/types.ts +165 -0
  177. package/src/core/probe/verdict-aggregator.ts +33 -0
  178. package/src/core/probe/webhooks-probe.ts +284 -0
  179. package/src/core/reporter/console.ts +69 -4
  180. package/src/core/reporter/index.ts +2 -3
  181. package/src/core/reporter/json.ts +15 -2
  182. package/src/core/reporter/junit.ts +27 -12
  183. package/src/core/reporter/ndjson.ts +37 -0
  184. package/src/core/reporter/types.ts +3 -0
  185. package/src/core/runner/assertions.ts +62 -2
  186. package/src/core/runner/async-pool.ts +108 -0
  187. package/src/core/runner/auth-path.ts +8 -0
  188. package/src/core/runner/ci-context.ts +72 -0
  189. package/src/core/runner/executor.ts +391 -52
  190. package/src/core/runner/form-encode.ts +51 -0
  191. package/src/core/runner/http-client.ts +115 -7
  192. package/src/core/runner/learn-drift.ts +293 -0
  193. package/src/core/runner/preflight-vars.ts +149 -0
  194. package/src/core/runner/progress-tracker.ts +73 -0
  195. package/src/core/runner/rate-limiter.ts +203 -0
  196. package/src/core/runner/run-kind.ts +39 -0
  197. package/src/core/runner/schema-validator.ts +312 -0
  198. package/src/core/runner/send-request.ts +153 -20
  199. package/src/core/runner/types.ts +38 -0
  200. package/src/core/secrets/registry.ts +164 -0
  201. package/src/core/secrets/secrets-file.ts +115 -0
  202. package/src/core/selectors/operation-filter.ts +144 -0
  203. package/src/core/setup-api.ts +419 -17
  204. package/src/core/severity/category.ts +94 -0
  205. package/src/core/severity/index.ts +121 -0
  206. package/src/core/spec/layers.ts +154 -0
  207. package/src/core/util/format-eta.ts +21 -0
  208. package/src/core/utils.ts +5 -1
  209. package/src/core/workspace/config.ts +129 -0
  210. package/src/core/workspace/manifest.ts +283 -0
  211. package/src/core/workspace/output-rotation.ts +62 -0
  212. package/src/core/workspace/root.ts +94 -0
  213. package/src/core/workspace/triage-path.ts +87 -0
  214. package/src/db/lint-runs.ts +47 -0
  215. package/src/db/migrate.ts +126 -0
  216. package/src/db/migrations/0001_run_kind.sql +25 -0
  217. package/src/db/migrations/sql.d.ts +4 -0
  218. package/src/db/queries/collections.ts +133 -0
  219. package/src/db/queries/coverage.ts +9 -0
  220. package/src/db/queries/dashboard.ts +59 -0
  221. package/src/db/queries/results.ts +128 -0
  222. package/src/db/queries/runs.ts +235 -0
  223. package/src/db/queries/sessions.ts +42 -0
  224. package/src/db/queries/settings.ts +28 -0
  225. package/src/db/queries/types.ts +172 -0
  226. package/src/db/queries.ts +72 -802
  227. package/src/db/schema.ts +179 -48
  228. package/src/cli/commands/export.ts +0 -144
  229. package/src/cli/commands/guide.ts +0 -127
  230. package/src/cli/commands/init.ts +0 -57
  231. package/src/cli/commands/serve.ts +0 -81
  232. package/src/cli/commands/sync.ts +0 -269
  233. package/src/cli/commands/update.ts +0 -189
  234. package/src/cli/commands/validate.ts +0 -34
  235. package/src/core/exporter/postman.ts +0 -963
  236. package/src/core/generator/guide-builder.ts +0 -253
  237. package/src/core/meta/types.ts +0 -21
  238. package/src/core/parser/index.ts +0 -21
  239. package/src/core/runner/execute-run.ts +0 -132
  240. package/src/core/runner/index.ts +0 -12
  241. package/src/core/sync/spec-differ.ts +0 -38
  242. package/src/web/data/collection-state.ts +0 -362
  243. package/src/web/routes/api.ts +0 -314
  244. package/src/web/routes/dashboard.ts +0 -350
  245. package/src/web/routes/runs.ts +0 -64
  246. package/src/web/schemas.ts +0 -121
  247. package/src/web/server.ts +0 -134
  248. package/src/web/static/htmx.min.cjs +0 -1
  249. package/src/web/static/style.css +0 -1148
  250. package/src/web/views/endpoints-tab.ts +0 -174
  251. package/src/web/views/explorer-tab.ts +0 -402
  252. package/src/web/views/health-strip.ts +0 -92
  253. package/src/web/views/layout.ts +0 -48
  254. package/src/web/views/results.ts +0 -210
  255. package/src/web/views/runs-tab.ts +0 -126
  256. package/src/web/views/suites-tab.ts +0 -181
@@ -1,350 +0,0 @@
1
- import { Hono } from "hono";
2
- import { layout, escapeHtml } from "../views/layout.ts";
3
- import { methodBadge } from "../views/results.ts";
4
- import { renderHealthStrip } from "../views/health-strip.ts";
5
- import { renderEndpointsTab } from "../views/endpoints-tab.ts";
6
- import { renderSuitesTab } from "../views/suites-tab.ts";
7
- import { renderRunsTab, renderRunDetail } from "../views/runs-tab.ts";
8
- import { renderExplorerTab } from "../views/explorer-tab.ts";
9
- import { buildCollectionState, invalidateCollectionCache } from "../data/collection-state.ts";
10
- import {
11
- listCollections,
12
- getCollectionById,
13
- countRunsByCollection,
14
- } from "../../db/queries.ts";
15
- import type { CollectionRecord, CollectionSummary } from "../../db/queries.ts";
16
- import { listEnvFiles } from "../../core/parser/variables.ts";
17
-
18
- const dashboard = new Hono();
19
-
20
- // ──────────────────────────────────────────────
21
- // GET / — Main dashboard
22
- // ──────────────────────────────────────────────
23
-
24
- dashboard.get("/", async (c) => {
25
- const collections = listCollections();
26
-
27
- let selectedId: number | null = null;
28
- const qId = c.req.query("collection");
29
- if (qId) {
30
- selectedId = parseInt(qId, 10) || null;
31
- } else if (collections.length === 1) {
32
- selectedId = collections[0]!.id;
33
- }
34
-
35
- const selected = selectedId ? collections.find(col => col.id === selectedId) ?? null : null;
36
- const selectedRecord = selected ? getCollectionById(selected.id) : null;
37
-
38
- const { content, navExtra } = await renderPage(collections, selectedId, selectedRecord);
39
- const isHtmx = c.req.header("HX-Request") === "true";
40
- if (isHtmx) return c.html(content);
41
- return c.html(layout("zond", content, navExtra));
42
- });
43
-
44
- // ──────────────────────────────────────────────
45
- // HTMX panel endpoints
46
- // ──────────────────────────────────────────────
47
-
48
- dashboard.get("/panels/content", async (c) => {
49
- const collectionId = parseInt(c.req.query("collection_id") ?? "", 10);
50
- if (isNaN(collectionId)) return c.html("");
51
-
52
- const collection = getCollectionById(collectionId);
53
- if (!collection) return c.html("<p>Collection not found</p>");
54
-
55
- return c.html(await renderCollectionContent(collection));
56
- });
57
-
58
- dashboard.get("/panels/health-strip", async (c) => {
59
- const collectionId = parseInt(c.req.query("collection_id") ?? "", 10);
60
- if (isNaN(collectionId)) return c.html("");
61
-
62
- const collection = getCollectionById(collectionId);
63
- if (!collection) return c.html("");
64
-
65
- invalidateCollectionCache(collectionId);
66
- const state = await buildCollectionState(collection);
67
- return c.html(renderHealthStrip(state));
68
- });
69
-
70
- dashboard.get("/panels/endpoints", async (c) => {
71
- const collectionId = parseInt(c.req.query("collection_id") ?? "", 10);
72
- if (isNaN(collectionId)) return c.html("");
73
-
74
- const collection = getCollectionById(collectionId);
75
- if (!collection) return c.html("");
76
-
77
- const state = await buildCollectionState(collection);
78
- const filters = {
79
- status: c.req.query("status") || undefined,
80
- method: c.req.query("method") || undefined,
81
- };
82
- return c.html(renderEndpointsTab(state, filters));
83
- });
84
-
85
- dashboard.get("/panels/explorer", async (c) => {
86
- const collectionId = parseInt(c.req.query("collection_id") ?? "", 10);
87
- if (isNaN(collectionId)) return c.html("");
88
-
89
- const collection = getCollectionById(collectionId);
90
- if (!collection) return c.html("");
91
-
92
- return c.html(await renderExplorerTab(collection));
93
- });
94
-
95
- dashboard.get("/panels/suites", async (c) => {
96
- const collectionId = parseInt(c.req.query("collection_id") ?? "", 10);
97
- if (isNaN(collectionId)) return c.html("");
98
-
99
- const collection = getCollectionById(collectionId);
100
- if (!collection) return c.html("");
101
-
102
- const state = await buildCollectionState(collection);
103
- return c.html(renderSuitesTab(state));
104
- });
105
-
106
- dashboard.get("/panels/runs-tab", (c) => {
107
- const collectionId = parseInt(c.req.query("collection_id") ?? "", 10);
108
- const page = Math.max(1, parseInt(c.req.query("page") ?? "1", 10));
109
- if (isNaN(collectionId)) return c.html("");
110
-
111
- return c.html(renderRunsTab(collectionId, page));
112
- });
113
-
114
- dashboard.get("/panels/run-detail", (c) => {
115
- const runId = parseInt(c.req.query("run_id") ?? "", 10);
116
- const collectionId = parseInt(c.req.query("collection_id") ?? "", 10);
117
- if (isNaN(runId)) return c.html("");
118
-
119
- return c.html(renderRunDetail(runId, collectionId));
120
- });
121
-
122
- // Legacy endpoints for backwards compat (runs.ts detail page uses /panels/results)
123
- dashboard.get("/panels/results", async (c) => {
124
- const collectionId = parseInt(c.req.query("collection_id") ?? "", 10);
125
- const runId = parseInt(c.req.query("run_id") ?? "", 10);
126
-
127
- if (!isNaN(runId)) {
128
- return c.html(renderRunDetail(runId, collectionId || 0));
129
- }
130
-
131
- if (!isNaN(collectionId)) {
132
- const { listRunsByCollection } = await import("../../db/queries.ts");
133
- const runs = listRunsByCollection(collectionId, 1, 0);
134
- if (runs.length === 0) return c.html(`<p style="color:var(--text-dim);">No runs yet.</p>`);
135
- return c.html(renderRunDetail(runs[0]!.id, collectionId));
136
- }
137
-
138
- return c.html("");
139
- });
140
-
141
- // Legacy coverage panel (kept for /runs/:id page)
142
- dashboard.get("/panels/coverage", async (c) => {
143
- const collectionId = parseInt(c.req.query("collection_id") ?? "", 10);
144
- if (isNaN(collectionId)) return c.html("");
145
-
146
- const collection = getCollectionById(collectionId);
147
- if (!collection?.openapi_spec) return c.html("");
148
-
149
- return c.html(await renderCoveragePanel(collection as CollectionRecord & { openapi_spec: string }));
150
- });
151
-
152
- // Legacy history panel (kept for /runs/:id page)
153
- dashboard.get("/panels/history", (c) => {
154
- const collectionId = parseInt(c.req.query("collection_id") ?? "", 10);
155
- const page = Math.max(1, parseInt(c.req.query("page") ?? "1", 10));
156
- if (isNaN(collectionId)) return c.html("");
157
-
158
- return c.html(renderRunsTab(collectionId, page));
159
- });
160
-
161
- // ──────────────────────────────────────────────
162
- // Rendering functions
163
- // ──────────────────────────────────────────────
164
-
165
- async function renderPage(
166
- collections: CollectionSummary[],
167
- selectedId: number | null,
168
- selectedRecord: CollectionRecord | null,
169
- ): Promise<{ content: string; navExtra: string }> {
170
- if (collections.length === 0) {
171
- return {
172
- navExtra: "",
173
- content: `
174
- <div style="text-align:center;padding:3rem 1rem;">
175
- <h1>zond</h1>
176
- <p style="color:var(--text-dim);margin:1rem 0;">No API collections registered yet.</p>
177
- <p style="color:var(--text-dim);">Use <code>setup_api</code> via CLI or MCP to register your first API.</p>
178
- </div>`,
179
- };
180
- }
181
-
182
- // Navbar: separator + collection selector + action bar
183
- const collectionOptions = collections.map(col =>
184
- `<option value="${col.id}"${col.id === selectedId ? " selected" : ""}>${escapeHtml(col.name)}${col.last_run_total > 0 ? ` (${col.pass_rate}%)` : ""}</option>`,
185
- ).join("");
186
-
187
- const selectorHtml = collections.length === 1
188
- ? `<span class="nav-separator"></span>
189
- <span class="collection-selector" style="border:none;background:none;">${escapeHtml(collections[0]!.name)}</span>
190
- <input type="hidden" id="collection-select" value="${collections[0]!.id}">`
191
- : `<span class="nav-separator"></span>
192
- <select id="collection-select" class="collection-selector"
193
- hx-get="/panels/content"
194
- hx-target="#main-content"
195
- hx-swap="innerHTML"
196
- name="collection_id">
197
- <option value="">Select an API...</option>
198
- ${collectionOptions}
199
- </select>`;
200
-
201
- // Action bar in navbar
202
- let actionHtml = "";
203
- if (selectedRecord) {
204
- const baseDir = selectedRecord.base_dir ?? selectedRecord.test_path;
205
- const envNames = await listEnvFiles(baseDir);
206
- const envSelect = envNames.length > 0
207
- ? `<select name="env" form="run-form" class="collection-selector" style="font-size:0.75rem;padding:0.25rem 0.5rem;">
208
- ${envNames.map(n => `<option value="${escapeHtml(n)}">${escapeHtml(n || "default")}</option>`).join("")}
209
- </select>`
210
- : "";
211
-
212
- actionHtml = `<div class="nav-actions">
213
- ${envSelect}
214
- <form id="run-form" style="display:contents;"
215
- hx-post="/run"
216
- hx-indicator="#run-spinner"
217
- hx-swap="none">
218
- <input type="hidden" name="path" value="${escapeHtml(selectedRecord.test_path)}">
219
- <button type="submit" class="btn btn-run" hx-disabled-elt="this">&#9654; Run Tests</button>
220
- <span id="run-spinner" class="htmx-indicator" style="color:var(--text-dim);font-size:0.85rem;">Running...</span>
221
- </form>
222
- </div>`;
223
- }
224
-
225
- const navExtra = `${selectorHtml}${actionHtml}`;
226
-
227
- const bodyContent = selectedRecord ? await renderCollectionContent(selectedRecord) : "";
228
-
229
- return {
230
- navExtra,
231
- content: `<div id="main-content">${bodyContent}</div>`,
232
- };
233
- }
234
-
235
- async function renderCollectionContent(collection: CollectionRecord): Promise<string> {
236
- const state = await buildCollectionState(collection);
237
-
238
- // Health strip
239
- const healthStrip = renderHealthStrip(state);
240
-
241
- // Tab bar with counts
242
- const runCount = countRunsByCollection(collection.id);
243
- const tabBar = `
244
- <div class="tab-bar" id="tab-bar">
245
- <button class="tab-btn tab-active" data-tab="endpoints"
246
- hx-get="/panels/endpoints?collection_id=${collection.id}"
247
- hx-target="#tab-content" hx-swap="innerHTML"
248
- onclick="activateTab(this)">Endpoints <span class="tab-count">${state.totalEndpoints}</span></button>
249
- <button class="tab-btn" data-tab="suites"
250
- hx-get="/panels/suites?collection_id=${collection.id}"
251
- hx-target="#tab-content" hx-swap="innerHTML"
252
- onclick="activateTab(this)">Suites <span class="tab-count">${state.suites.length}</span></button>
253
- <button class="tab-btn" data-tab="runs"
254
- hx-get="/panels/runs-tab?collection_id=${collection.id}"
255
- hx-target="#tab-content" hx-swap="innerHTML"
256
- onclick="activateTab(this)">Runs <span class="tab-count">${runCount}</span></button>
257
- <button class="tab-btn" data-tab="explorer"
258
- hx-get="/panels/explorer?collection_id=${collection.id}"
259
- hx-target="#tab-content" hx-swap="innerHTML"
260
- onclick="activateTab(this)">Explorer</button>
261
- </div>`;
262
-
263
- // Default tab content (endpoints)
264
- const defaultContent = renderEndpointsTab(state);
265
-
266
- const tabScript = `<script>
267
- function activateTab(el) {
268
- document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('tab-active'));
269
- el.classList.add('tab-active');
270
- }
271
- function switchToSuite(suiteName) {
272
- var suitesBtn = document.querySelector('[data-tab="suites"]');
273
- if (!suitesBtn) return;
274
- suitesBtn.click();
275
- document.addEventListener('htmx:afterSwap', function handler(e) {
276
- if (e.detail.target && e.detail.target.id === 'tab-content') {
277
- document.removeEventListener('htmx:afterSwap', handler);
278
- setTimeout(function() {
279
- var rows = document.querySelectorAll('.suite-row[data-suite-name]');
280
- for (var i = 0; i < rows.length; i++) {
281
- if (rows[i].dataset.suiteName === suiteName) {
282
- rows[i].scrollIntoView({ behavior: 'smooth', block: 'center' });
283
- rows[i].click();
284
- rows[i].classList.add('suite-highlight');
285
- setTimeout(function() { rows[i].classList.remove('suite-highlight'); }, 2000);
286
- break;
287
- }
288
- }
289
- }, 50);
290
- }
291
- });
292
- }
293
- </script>`;
294
-
295
- return `
296
- <div id="health-strip-panel">${healthStrip}</div>
297
- ${tabBar}
298
- <div id="tab-content">${defaultContent}</div>
299
- ${tabScript}`;
300
- }
301
-
302
- // ── Legacy helpers (kept for /runs/:id page) ──
303
-
304
- async function renderCoveragePanel(collection: CollectionRecord & { openapi_spec: string }): Promise<string> {
305
- try {
306
- const { readOpenApiSpec, extractEndpoints } = await import("../../core/generator/openapi-reader.ts");
307
- const { scanCoveredEndpoints, filterUncoveredEndpoints } = await import("../../core/generator/coverage-scanner.ts");
308
-
309
- const doc = await readOpenApiSpec(collection.openapi_spec);
310
- const allEndpoints = extractEndpoints(doc);
311
- const covered = await scanCoveredEndpoints(collection.test_path);
312
- const uncovered = filterUncoveredEndpoints(allEndpoints, covered);
313
-
314
- const totalEndpoints = allEndpoints.length;
315
- const coveredCount = totalEndpoints - uncovered.length;
316
- const pct = totalEndpoints > 0 ? Math.round((coveredCount / totalEndpoints) * 100) : 0;
317
-
318
- const badgeClass = pct >= 80 ? "badge-pass" : pct >= 50 ? "badge-skip" : "badge-fail";
319
-
320
- const uncoveredSet = new Set(uncovered.map(ep => `${ep.method} ${ep.path}`));
321
-
322
- const allItems = allEndpoints.map(ep => {
323
- const isCovered = !uncoveredSet.has(`${ep.method} ${ep.path}`);
324
- const icon = isCovered
325
- ? `<span style="color:var(--pass);font-weight:700;">&#10003;</span>`
326
- : `<span style="color:var(--fail);font-weight:700;">&#10007;</span>`;
327
- return `<div style="padding:0.2rem 0;font-size:0.85rem;font-family:monospace;display:flex;align-items:center;gap:0.5rem;">
328
- ${icon} ${methodBadge(ep.method)} ${escapeHtml(ep.path)}
329
- </div>`;
330
- }).join("");
331
-
332
- const endpointsHtml = totalEndpoints > 0
333
- ? `<details style="margin-top:0.5rem;">
334
- <summary style="cursor:pointer;font-size:0.85rem;color:var(--text-dim);">Show all ${totalEndpoints} endpoints</summary>
335
- <div style="margin-top:0.25rem;">${allItems}</div>
336
- </details>`
337
- : "";
338
-
339
- return `
340
- <div style="margin-bottom:1rem;">
341
- <span style="font-size:0.9rem;font-weight:600;">Coverage:</span>
342
- <span class="badge ${badgeClass}" style="margin-left:0.25rem;">${pct}% (${coveredCount}/${totalEndpoints})</span>
343
- ${endpointsHtml}
344
- </div>`;
345
- } catch {
346
- return "";
347
- }
348
- }
349
-
350
- export default dashboard;
@@ -1,64 +0,0 @@
1
- import { Hono } from "hono";
2
- import { layout, escapeHtml } from "../views/layout.ts";
3
- import { statusBadge, renderSuiteResults, failedFilterToggle, autoExpandFailedScript } from "../views/results.ts";
4
- import { getRunById, getResultsByRunId, getCollectionById } from "../../db/queries.ts";
5
- import { formatDuration } from "../../core/reporter/console.ts";
6
-
7
- const runs = new Hono();
8
-
9
- runs.get("/runs/:id", (c) => {
10
- const id = parseInt(c.req.param("id"), 10);
11
- if (isNaN(id)) return c.html(layout("Not Found", "<h1>Invalid run ID</h1>"), 400);
12
-
13
- const run = getRunById(id);
14
- if (!run) return c.html(layout("Not Found", "<h1>Run not found</h1>"), 404);
15
-
16
- const results = getResultsByRunId(id);
17
-
18
- // Resolve test_path for re-run button
19
- const collection = run.collection_id ? getCollectionById(run.collection_id) : null;
20
- const rerunBtnHtml = collection
21
- ? `<button class="btn btn-sm btn-run"
22
- hx-post="/run"
23
- hx-vals='${escapeHtml(JSON.stringify({ path: collection.test_path, ...(run.environment ? { env: run.environment } : {}) }))}'
24
- hx-disabled-elt="this"
25
- style="margin-left:0.5rem;">Re-run</button>`
26
- : "";
27
-
28
- const headerHtml = `
29
- <h1>Run #${run.id}</h1>
30
- <div class="cards">
31
- <div class="card">
32
- <div class="card-label">Date</div>
33
- <div class="card-value" style="font-size:1rem">${escapeHtml(run.started_at)}</div>
34
- </div>
35
- <div class="card">
36
- <div class="card-label">Environment</div>
37
- <div class="card-value" style="font-size:1rem">${run.environment ? escapeHtml(run.environment) : "-"}</div>
38
- </div>
39
- <div class="card">
40
- <div class="card-label">Duration</div>
41
- <div class="card-value">${run.duration_ms != null ? formatDuration(run.duration_ms) : "-"}</div>
42
- </div>
43
- <div class="card">
44
- <div class="card-label">Results</div>
45
- <div class="card-value" style="font-size:1rem">${run.passed} &#10003; ${run.failed} &#10007; ${run.skipped} &#9675;</div>
46
- </div>
47
- </div>
48
- <div style="margin:0.5rem 0 1rem;">
49
- <a href="/api/export/${run.id}/junit" download class="btn btn-sm btn-outline">Export JUnit XML</a>
50
- <a href="/api/export/${run.id}/json" download class="btn btn-sm btn-outline" style="margin-left:0.5rem;">Export JSON</a>
51
- ${rerunBtnHtml}
52
- </div>`;
53
-
54
- const suitesHtml = renderSuiteResults(results, id);
55
-
56
- const content = headerHtml + failedFilterToggle() + suitesHtml + autoExpandFailedScript()
57
- + `<div style="margin-top:1rem"><a href="/" class="btn btn-outline btn-sm">&larr; Back to Dashboard</a></div>`;
58
-
59
- const isHtmx = c.req.header("HX-Request") === "true";
60
- if (isHtmx) return c.html(content);
61
- return c.html(layout(`Run #${id}`, content));
62
- });
63
-
64
- export default runs;
@@ -1,121 +0,0 @@
1
- import { z } from "@hono/zod-openapi";
2
-
3
- // ──────────────────────────────────────────────
4
- // Common
5
- // ──────────────────────────────────────────────
6
-
7
- export const ErrorSchema = z.object({
8
- error: z.string(),
9
- }).openapi("Error");
10
-
11
- export const IdParamSchema = z.object({
12
- id: z.string().transform(Number).pipe(z.number().int().positive()).openapi({ type: "integer", example: 1 }),
13
- });
14
-
15
- // ──────────────────────────────────────────────
16
- // Environments
17
- // ──────────────────────────────────────────────
18
-
19
- export const EnvironmentSchema = z.object({
20
- id: z.number().int(),
21
- name: z.string(),
22
- variables: z.record(z.string(), z.string()),
23
- }).openapi("Environment");
24
-
25
- export const EnvironmentListSchema = z.array(EnvironmentSchema).openapi("EnvironmentList");
26
-
27
- export const CreateEnvironmentRequest = z.object({
28
- name: z.string().min(1),
29
- }).openapi("CreateEnvironmentRequest");
30
-
31
- export const CreateEnvironmentResponse = z.object({
32
- id: z.number().int(),
33
- name: z.string(),
34
- variables: z.record(z.string(), z.string()),
35
- }).openapi("CreateEnvironmentResponse");
36
-
37
- export const UpdateEnvironmentRequest = z.object({
38
- variables: z.record(z.string(), z.string()),
39
- }).openapi("UpdateEnvironmentRequest");
40
-
41
- // ──────────────────────────────────────────────
42
- // Collections
43
- // ──────────────────────────────────────────────
44
-
45
- export const CollectionSchema = z.object({
46
- id: z.number().int(),
47
- name: z.string(),
48
- test_path: z.string(),
49
- openapi_spec: z.string().nullable(),
50
- created_at: z.string(),
51
- }).openapi("Collection");
52
-
53
- export const CollectionListSchema = z.array(CollectionSchema).openapi("CollectionList");
54
-
55
- export const CreateCollectionRequest = z.object({
56
- name: z.string().min(1),
57
- test_path: z.string().min(1),
58
- openapi_spec: z.string().optional(),
59
- }).openapi("CreateCollectionRequest");
60
-
61
- export const CreateCollectionResponse = z.object({
62
- id: z.number().int(),
63
- name: z.string(),
64
- test_path: z.string(),
65
- openapi_spec: z.string().nullable(),
66
- }).openapi("CreateCollectionResponse");
67
-
68
- // ──────────────────────────────────────────────
69
- // Runs
70
- // ──────────────────────────────────────────────
71
-
72
- export const RunRequestSchema = z.object({
73
- path: z.string().min(1),
74
- env: z.string().optional(),
75
- }).openapi("RunRequest");
76
-
77
- export const RunResponseSchema = z.object({
78
- runId: z.number().int(),
79
- }).openapi("RunResponse");
80
-
81
- export const RunDetailSchema = z.object({
82
- suite_name: z.string(),
83
- started_at: z.string(),
84
- finished_at: z.string(),
85
- total: z.number().int(),
86
- passed: z.number().int(),
87
- failed: z.number().int(),
88
- skipped: z.number().int(),
89
- steps: z.array(z.object({
90
- name: z.string(),
91
- status: z.string(),
92
- duration_ms: z.number(),
93
- request: z.object({
94
- method: z.string(),
95
- url: z.string(),
96
- headers: z.record(z.string(), z.string()),
97
- }),
98
- response: z.object({
99
- status: z.number().int(),
100
- headers: z.record(z.string(), z.string()),
101
- body: z.string(),
102
- duration_ms: z.number(),
103
- }).optional(),
104
- assertions: z.array(z.object({
105
- field: z.string(),
106
- expected: z.string(),
107
- actual: z.string(),
108
- passed: z.boolean(),
109
- })),
110
- captures: z.record(z.string(), z.unknown()),
111
- error: z.string().optional(),
112
- })),
113
- }).openapi("RunDetail");
114
-
115
- // ──────────────────────────────────────────────
116
- // Export
117
- // ──────────────────────────────────────────────
118
-
119
- export const RunIdParam = z.object({
120
- runId: z.string().transform(Number).pipe(z.number().int().positive()).openapi({ type: "integer", example: 1 }),
121
- });
package/src/web/server.ts DELETED
@@ -1,134 +0,0 @@
1
- import { OpenAPIHono } from "@hono/zod-openapi";
2
- import { getDb } from "../db/schema.ts";
3
- import dashboard from "./routes/dashboard.ts";
4
- import runs from "./routes/runs.ts";
5
- import api from "./routes/api.ts";
6
- import styleCssPath from "./static/style.css" with { type: "file" };
7
- import htmxJsPath from "./static/htmx.min.cjs" with { type: "file" };
8
-
9
- export interface ServerOptions {
10
- port?: number;
11
- host?: string;
12
- dbPath?: string;
13
- dev?: boolean;
14
- }
15
-
16
- // SSE clients for dev hot reload
17
- let devClients: ReadableStreamDefaultController[] = [];
18
-
19
- export function notifyDevReload() {
20
- for (const ctrl of devClients) {
21
- try { ctrl.enqueue("data: reload\n\n"); } catch { /* client gone */ }
22
- }
23
- }
24
-
25
- export function createApp(options?: { dev?: boolean }) {
26
- const app = new OpenAPIHono();
27
-
28
- // Dev hot reload SSE endpoint
29
- if (options?.dev) {
30
- app.get("/dev/reload", (c) => {
31
- const stream = new ReadableStream({
32
- start(controller) {
33
- devClients.push(controller);
34
- controller.enqueue("data: connected\n\n");
35
- },
36
- cancel() {
37
- devClients = devClients.filter((c) => c !== arguments[0]);
38
- },
39
- });
40
- return new Response(stream, {
41
- headers: {
42
- "Content-Type": "text/event-stream",
43
- "Cache-Control": "no-cache",
44
- Connection: "keep-alive",
45
- },
46
- });
47
- });
48
- }
49
-
50
- // Static files
51
- app.get("/static/:file", async (c) => {
52
- const file = c.req.param("file");
53
- // Only serve known files, prevent path traversal
54
- if (file === "style.css") {
55
- const content = await Bun.file(styleCssPath).text();
56
- c.header("Content-Type", "text/css; charset=utf-8");
57
- c.header("Cache-Control", "public, max-age=3600");
58
- return c.body(content);
59
- }
60
- if (file === "htmx.min.js") {
61
- const content = await Bun.file(htmxJsPath as unknown as string).text();
62
- c.header("Content-Type", "application/javascript; charset=utf-8");
63
- c.header("Cache-Control", "public, max-age=86400");
64
- return c.body(content);
65
- }
66
- return c.notFound();
67
- });
68
-
69
- // Mount routes
70
- app.route("/", dashboard);
71
- app.route("/", runs);
72
- app.route("/", api);
73
-
74
- // OpenAPI spec endpoint — derive server URL from the incoming request
75
- app.doc("/api/openapi.json", (c) => ({
76
- openapi: "3.0.0",
77
- info: {
78
- title: "zond API",
79
- version: "0.1.0",
80
- description: "API testing platform — self-documented API",
81
- },
82
- servers: [
83
- {
84
- url: new URL(c.req.url).origin,
85
- description: "Current server",
86
- },
87
- ],
88
- }));
89
-
90
- return app;
91
- }
92
-
93
- export async function startServer(options: ServerOptions = {}): Promise<void> {
94
- const port = options.port ?? 8080;
95
- const host = options.host ?? "0.0.0.0";
96
-
97
- // Initialize DB
98
- getDb(options.dbPath);
99
-
100
- // Enable dev mode in layout
101
- if (options.dev) {
102
- const { setDevMode } = await import("./views/layout.ts");
103
- setDevMode(true);
104
- }
105
-
106
- const app = createApp({ dev: options.dev });
107
-
108
- const { getRuntimeInfo } = await import("../cli/runtime.ts");
109
- const devLabel = options.dev ? " [dev]" : "";
110
- console.log(`zond server (${getRuntimeInfo()}) running at http://${host === "0.0.0.0" ? "localhost" : host}:${port}${devLabel}`);
111
-
112
- Bun.serve({
113
- fetch: app.fetch,
114
- port,
115
- hostname: host,
116
- });
117
-
118
- // File watcher for dev hot reload
119
- if (options.dev) {
120
- const { watch } = await import("fs");
121
- const { dirname } = await import("path");
122
- const { fileURLToPath } = await import("url");
123
- const webDir = dirname(fileURLToPath(import.meta.url));
124
-
125
- console.log(`Watching ${webDir} for changes...`);
126
- watch(webDir, { recursive: true }, (_event, filename) => {
127
- if (!filename) return;
128
- const ext = filename.split(".").pop();
129
- if (!["ts", "css", "html", "js"].includes(ext ?? "")) return;
130
- console.log(`[dev] changed: ${filename}`);
131
- notifyDevReload();
132
- });
133
- }
134
- }