@kirrosh/zond 0.22.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 +648 -0
- package/README.md +58 -6
- package/package.json +9 -6
- 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 +43 -0
- package/src/cli/commands/clean.ts +212 -0
- package/src/cli/commands/cleanup.ts +262 -0
- package/src/cli/commands/completions.ts +16 -0
- package/src/cli/commands/coverage.ts +605 -132
- package/src/cli/commands/db.ts +178 -7
- 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 -46
- package/src/cli/commands/init/bootstrap.ts +30 -1
- package/src/cli/commands/{init.ts → init/index.ts} +99 -5
- package/src/cli/commands/init/skills.ts +56 -3
- package/src/cli/commands/init/templates/agents.md +65 -61
- 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 +592 -125
- package/src/cli/commands/init/templates/zond-config.yml +8 -9
- 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 +842 -53
- package/src/cli/commands/session.ts +244 -0
- package/src/cli/commands/use.ts +18 -1
- package/src/cli/index.ts +20 -3
- package/src/cli/json-envelope.ts +112 -3
- package/src/cli/json-schemas.ts +263 -0
- package/src/cli/program.ts +198 -635
- 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 +5 -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 +22 -6
- 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 +151 -11
- 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 +42 -16
- 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 +445 -19
- 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 +37 -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 +103 -13
- package/src/core/generator/suite-generator.ts +419 -111
- 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 +129 -4
- package/src/core/parser/types.ts +19 -1
- package/src/core/parser/variables.ts +0 -0
- package/src/core/parser/yaml-parser.ts +58 -12
- 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 +43 -76
- package/src/core/probe/method-shared.ts +69 -0
- package/src/core/probe/negative-probe.ts +183 -149
- 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 +41 -2
- package/src/core/reporter/index.ts +2 -3
- package/src/core/reporter/json.ts +11 -1
- 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 +58 -1
- 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 +264 -20
- package/src/core/runner/form-encode.ts +51 -0
- package/src/core/runner/http-client.ts +75 -2
- 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 +89 -17
- 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 +415 -16
- 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/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 +178 -50
- package/src/cli/commands/export.ts +0 -144
- package/src/cli/commands/guide.ts +0 -127
- package/src/cli/commands/init/templates/skills/scenarios.md +0 -97
- package/src/cli/commands/probe-methods.ts +0 -108
- package/src/cli/commands/probe-validation.ts +0 -124
- package/src/cli/commands/serve.ts +0 -114
- package/src/cli/commands/sync.ts +0 -268
- package/src/cli/commands/update.ts +0 -189
- package/src/cli/commands/validate.ts +0 -34
- package/src/core/diagnostics/render-md.ts +0 -112
- package/src/core/exporter/postman.ts +0 -963
- package/src/core/generator/guide-builder.ts +0 -253
- package/src/core/meta/types.ts +0 -19
- 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,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">▶ 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;">✓</span>`
|
|
326
|
-
: `<span style="color:var(--fail);font-weight:700;">✗</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;
|
package/src/web/routes/runs.ts
DELETED
|
@@ -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} ✓ ${run.failed} ✗ ${run.skipped} ○</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">← 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;
|
package/src/web/schemas.ts
DELETED
|
@@ -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
|
-
}
|