@apisurf/wireui 0.1.2

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 (38) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +157 -0
  3. package/bin/wireui.mjs +233 -0
  4. package/dist/nitro.json +17 -0
  5. package/dist/public/assets/collections-CLHcfRfi.js +1 -0
  6. package/dist/public/assets/globals-CZiNFGaK.css +1 -0
  7. package/dist/public/assets/index-D5VFd_3H.js +14 -0
  8. package/dist/public/assets/kv-table-oB5gcq0i.js +2 -0
  9. package/dist/public/assets/query-BKAhHNIi.js +19 -0
  10. package/dist/public/assets/requests._requestId-Bu7E2eoI.js +1 -0
  11. package/dist/public/assets/routes-DDY2CfzA.js +1 -0
  12. package/dist/public/assets/runs._runId-CzwtVjYq.js +1 -0
  13. package/dist/public/assets/scoped-DB8raIWx.js +1 -0
  14. package/dist/public/assets/split-Br1RX2I4.js +1 -0
  15. package/dist/public/favicon.svg +1 -0
  16. package/dist/server/_chunks/ssr-renderer.mjs +26 -0
  17. package/dist/server/_libs/@tanstack/react-router+[...].mjs +15246 -0
  18. package/dist/server/_libs/h3+rou3+srvx.mjs +2086 -0
  19. package/dist/server/_libs/hookable.mjs +41 -0
  20. package/dist/server/_libs/tanstack__history.mjs +369 -0
  21. package/dist/server/_libs/ufo.mjs +64 -0
  22. package/dist/server/_runtime.mjs +35 -0
  23. package/dist/server/_ssr/api-Dw1Zb_PK.mjs +931 -0
  24. package/dist/server/_ssr/collections-CqidTQpw.mjs +495 -0
  25. package/dist/server/_ssr/empty-plugin-adapters-CCuiUfKt.mjs +5 -0
  26. package/dist/server/_ssr/kv-table-DQUkoItO.mjs +458 -0
  27. package/dist/server/_ssr/query-D4so9xiY.mjs +416 -0
  28. package/dist/server/_ssr/requests._requestId-BFukRwqL.mjs +942 -0
  29. package/dist/server/_ssr/router-CPcgc_kh.mjs +3176 -0
  30. package/dist/server/_ssr/routes-DaV9vGHn.mjs +375 -0
  31. package/dist/server/_ssr/runs._runId-Df_LmPHa.mjs +836 -0
  32. package/dist/server/_ssr/scoped-CnQ56K8h.mjs +42 -0
  33. package/dist/server/_ssr/split-DWrMIs7K.mjs +115 -0
  34. package/dist/server/_ssr/ssr.mjs +6827 -0
  35. package/dist/server/_ssr/start-zk_ZfrWl.mjs +4 -0
  36. package/dist/server/_tanstack-start-manifest_v-CARKjHfs.mjs +50 -0
  37. package/dist/server/index.mjs +368 -0
  38. package/package.json +71 -0
@@ -0,0 +1,931 @@
1
+ import { TSS_SERVER_FUNCTION, createServerFn } from "./ssr.mjs";
2
+ import { existsSync, statSync } from "node:fs";
3
+ import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
4
+ import Database from "better-sqlite3";
5
+ import { homedir } from "node:os";
6
+ //#region node_modules/.nitro/vite/services/ssr/assets/api-Dw1Zb_PK.js
7
+ var createServerRpc = (serverFnMeta, splitImportFn) => {
8
+ const url = "/_serverFn/" + serverFnMeta.id;
9
+ return Object.assign(splitImportFn, {
10
+ url,
11
+ serverFnMeta,
12
+ [TSS_SERVER_FUNCTION]: true
13
+ });
14
+ };
15
+ /** Schema version this build writes. Equals the number of migrations. */
16
+ var SCHEMA_VERSION = 4;
17
+ /** Default database filename, created in the working directory. */
18
+ var DEFAULT_DB_FILENAME = "wire.sqlite";
19
+ function resolveDbPath(options = {}) {
20
+ const cwd = options.cwd ?? process.cwd();
21
+ const raw = options.path ?? DEFAULT_DB_FILENAME;
22
+ return isAbsolute(raw) ? raw : resolve(cwd, raw);
23
+ }
24
+ /**
25
+ * Types for view columns SQLite cannot type on its own.
26
+ *
27
+ * `pragma_table_info` on a view resolves each column back to its origin
28
+ * column's declared type — `v_requests.status` reports INTEGER — but returns an
29
+ * empty string for columns that are expressions, because there is no origin
30
+ * column to inherit from. That is a dozen columns across the views, all of
31
+ * them either a correlated subquery counter or a CASE/concatenation.
32
+ * better-sqlite3's `statement.columns()` reports null for exactly the same set
33
+ * and for the same reason, so there is nothing to fall back to but this map.
34
+ *
35
+ * Keep it in step with the views in ./schema.ts. {@link untypedColumns} below
36
+ * turns a drift into a test failure rather than a silently untyped column.
37
+ */
38
+ var COMPUTED_TYPES = {
39
+ "v_runs.request_count": "INTEGER",
40
+ "v_runs.error_count": "INTEGER",
41
+ "v_runs.connection_count": "INTEGER",
42
+ "v_runs.message_count": "INTEGER",
43
+ "v_runs.check_count": "INTEGER",
44
+ "v_runs.failed_check_count": "INTEGER",
45
+ "v_runs.total_duration_ms": "REAL",
46
+ "v_runs.total_bytes": "INTEGER",
47
+ "v_requests.message_count": "INTEGER",
48
+ "v_messages.endpoint": "TEXT",
49
+ "v_messages.direction": "TEXT",
50
+ "v_messages.text": "TEXT",
51
+ "v_headers.kind": "TEXT",
52
+ "v_bodies.text": "TEXT"
53
+ };
54
+ /** One line per view, for `wire schema` with no argument. */
55
+ var RELATION_DESCRIPTIONS = {
56
+ v_runs: "One row per execute, counts computed on read.",
57
+ v_requests: "One HTTP request, or one WebSocket/SSE connection.",
58
+ v_headers: "Request and response headers, in wire order.",
59
+ v_bodies: "Deduped payloads, content-addressed by SHA-256.",
60
+ v_messages: "WebSocket frames and SSE events, with payloads.",
61
+ v_checks: "Assertions a run made, from check() and assert().",
62
+ tags: "Per-request key/value tags, from execute --tag.",
63
+ body_fts: "FTS5 index over text payloads. Join on body_id."
64
+ };
65
+ /**
66
+ * Every relation worth querying.
67
+ *
68
+ * Views only by default, because that is the documented surface and the raw
69
+ * tables store interned ids rather than the strings anyone wants to read.
70
+ * `all` adds the base tables — minus FTS5's shadow tables, which are storage
71
+ * for `body_fts` and not something to query directly.
72
+ */
73
+ function listRelations(db, options = {}) {
74
+ const types = options.all ? ["view", "table"] : ["view"];
75
+ return db.prepare(`SELECT name, type FROM sqlite_master
76
+ WHERE type IN (${types.map(() => "?").join(", ")})
77
+ AND name NOT LIKE 'sqlite_%'
78
+ AND name NOT LIKE 'body_fts_%'
79
+ ORDER BY type DESC, name`).all(...types).map((r) => ({
80
+ name: r.name,
81
+ type: r.type,
82
+ description: RELATION_DESCRIPTIONS[r.name] ?? null
83
+ }));
84
+ }
85
+ /**
86
+ * Columns of a view or table, with types.
87
+ *
88
+ * Uses the `pragma_table_info` table-valued function rather than the `PRAGMA`
89
+ * statement so the relation name is a bound parameter and never string
90
+ * interpolation.
91
+ */
92
+ function describeRelation(db, name) {
93
+ return db.prepare(`SELECT name, type FROM pragma_table_info(?) ORDER BY cid`).all(name).map((r) => {
94
+ const declared = r.type.trim();
95
+ if (declared) return {
96
+ name: r.name,
97
+ type: declared,
98
+ computed: false
99
+ };
100
+ return {
101
+ name: r.name,
102
+ type: COMPUTED_TYPES[`${name}.${r.name}`] ?? "",
103
+ computed: true
104
+ };
105
+ });
106
+ }
107
+ /** Runs, newest first. */
108
+ function listRuns(db, limit = 200) {
109
+ return db.prepare(`SELECT * FROM v_runs ORDER BY started_at DESC LIMIT ?`).all(limit);
110
+ }
111
+ function getRun$1(db, id) {
112
+ return db.prepare(`SELECT * FROM v_runs WHERE id = ?`).get(id) ?? null;
113
+ }
114
+ /**
115
+ * Every script that has been run, one row each, most recently run first.
116
+ *
117
+ * Grouping is on `entry` — the absolute path of the executed file — because
118
+ * that is the only stable identity a script has. `--label` is per run and the
119
+ * default label is a basename, which collides the moment two folders hold a
120
+ * `sync.ts`.
121
+ *
122
+ * `label`, `id` and `exit_status` are bare columns under a GROUP BY, which
123
+ * SQLite answers from the row that produced the query's single MAX() — so they
124
+ * describe the latest run, not an arbitrary one. That is a documented SQLite
125
+ * guarantee (one min/max aggregate, bare columns come from that row), not an
126
+ * accident of the query plan.
127
+ */
128
+ function listScripts(db) {
129
+ return db.prepare(`SELECT entry,
130
+ cwd,
131
+ label,
132
+ id AS last_run_id,
133
+ MAX(started_at) AS last_started_at,
134
+ exit_status AS last_exit_status,
135
+ COUNT(*) AS run_count,
136
+ SUM(exit_status = 'error') AS failed_run_count,
137
+ SUM(request_count) AS request_count,
138
+ SUM(connection_count) AS connection_count,
139
+ SUM(message_count) AS message_count,
140
+ SUM(error_count) AS error_count,
141
+ SUM(check_count) AS check_count,
142
+ SUM(failed_check_count) AS failed_check_count,
143
+ SUM(total_bytes) AS total_bytes
144
+ FROM v_runs
145
+ GROUP BY entry
146
+ ORDER BY last_started_at DESC`).all();
147
+ }
148
+ /** Runs of one script, newest first — the sessions behind a script row. */
149
+ function listRunsForEntry(db, entry, limit = 200) {
150
+ return db.prepare(`SELECT * FROM v_runs WHERE entry = ? ORDER BY started_at DESC LIMIT ?`).all(entry, limit);
151
+ }
152
+ /** Requests for a run, in capture order. */
153
+ function listRequests(db, runId) {
154
+ return db.prepare(`SELECT id, seq, ts, kind, message_count, method, host, path, status, ok, error_name,
155
+ duration_ms, request_bytes, response_bytes
156
+ FROM v_requests WHERE run_id = ? ORDER BY seq ASC`).all(runId);
157
+ }
158
+ function getRequestDetail(db, id) {
159
+ const request = db.prepare(`SELECT * FROM v_requests WHERE id = ?`).get(id);
160
+ if (!request) return null;
161
+ const headers = db.prepare(`SELECT kind, name, value FROM v_headers WHERE request_id = ? ORDER BY kind, ord`).all(id);
162
+ const tags = db.prepare(`SELECT key, value FROM tags WHERE request_id = ? ORDER BY key`).all(id);
163
+ const split = (kind) => headers.filter((h) => h.kind === kind).map((h) => [h.name, h.value]);
164
+ return {
165
+ request,
166
+ requestHeaders: split("request"),
167
+ responseHeaders: split("response"),
168
+ requestBody: loadBody(db, request.request_body_id, request.request_content_type),
169
+ responseBody: loadBody(db, request.response_body_id, request.response_content_type),
170
+ tags,
171
+ messages: request.kind === "http" ? [] : listMessages(db, id)
172
+ };
173
+ }
174
+ /**
175
+ * Every tag on a run's requests, once each.
176
+ *
177
+ * `execute --tag` attaches the same pairs to every capture in the run, so read
178
+ * back per request they are the same two or three facts repeated a hundred
179
+ * times — they describe the run, not the call. The DISTINCT is what makes this
180
+ * honest for the one case that is not uniform: a library caller can configure
181
+ * two recorders differently inside one run, and then the union is the answer.
182
+ */
183
+ function listRunTags(db, runId) {
184
+ return db.prepare(`SELECT DISTINCT t.key, t.value
185
+ FROM tags t JOIN requests r ON r.id = t.request_id
186
+ WHERE r.run_id = ?
187
+ ORDER BY t.key, t.value`).all(runId);
188
+ }
189
+ /** Every assertion a run made, in the order the script made them. */
190
+ function listChecks(db, runId) {
191
+ return db.prepare(`SELECT id, run_id, seq, ts, name, mode, status, message
192
+ FROM v_checks WHERE run_id = ? ORDER BY seq ASC`).all(runId);
193
+ }
194
+ /** Every frame or event on one connection, in the order it happened. */
195
+ function listMessages(db, requestId) {
196
+ return db.prepare(`SELECT id, request_id, seq, ts, direction, name, code, event_id,
197
+ bytes, body_id, truncated, text
198
+ FROM v_messages WHERE request_id = ? ORDER BY seq ASC`).all(requestId);
199
+ }
200
+ /**
201
+ * Image types worth inlining into the page.
202
+ *
203
+ * An allow-list rather than a `image/*` prefix test: the content type recorded
204
+ * off the wire ends up verbatim in a `data:` URL, and only these are types a
205
+ * browser will draw.
206
+ */
207
+ var IMAGE_MIME = /* @__PURE__ */ new Set([
208
+ "image/apng",
209
+ "image/avif",
210
+ "image/bmp",
211
+ "image/gif",
212
+ "image/jpeg",
213
+ "image/png",
214
+ "image/svg+xml",
215
+ "image/webp",
216
+ "image/x-icon",
217
+ "image/vnd.microsoft.icon"
218
+ ]);
219
+ /**
220
+ * Largest image inlined as a `data:` URL.
221
+ *
222
+ * Base64 costs a third on top, and the encoded string travels inside the page's
223
+ * loader payload — past a couple of megabytes the picture costs more than it is
224
+ * worth. The capture cap (1 MiB by default) usually bites first.
225
+ */
226
+ var MAX_INLINE_IMAGE_BYTES = 2e6;
227
+ /** `image/png; charset=x` → `image/png`, or null when it is not a drawable image. */
228
+ function imageMime(contentType) {
229
+ if (!contentType) return null;
230
+ const mime = contentType.split(";")[0].trim().toLowerCase();
231
+ return IMAGE_MIME.has(mime) ? mime : null;
232
+ }
233
+ /**
234
+ * Fetch a payload and shape it for display: decode text, parse JSON, inline an
235
+ * image, and say plainly why there is nothing to show when there isn't.
236
+ *
237
+ * `kind` is the whole answer — the UI switches on it and never re-sniffs the
238
+ * content type.
239
+ */
240
+ function loadBody(db, bodyId, contentType) {
241
+ const base = {
242
+ present: false,
243
+ text: null,
244
+ json: null,
245
+ dataUrl: null,
246
+ contentType,
247
+ size: null,
248
+ truncated: false
249
+ };
250
+ const empty = (note) => ({
251
+ ...base,
252
+ kind: "empty",
253
+ note
254
+ });
255
+ if (bodyId == null) return empty("No body captured (absent or redacted).");
256
+ const row = db.prepare(`SELECT size, stored_size, truncated, is_text, content FROM bodies WHERE id = ?`).get(bodyId);
257
+ if (!row) return empty("Body reference is set but the payload row is missing.");
258
+ const truncated = row.truncated === 1;
259
+ const stored = {
260
+ ...base,
261
+ present: true,
262
+ size: row.size,
263
+ truncated
264
+ };
265
+ const binary = (note) => ({
266
+ ...stored,
267
+ kind: "binary",
268
+ note
269
+ });
270
+ const mime = imageMime(contentType);
271
+ if (mime) {
272
+ if (truncated) return binary(`Image truncated at ${row.stored_size} of ${row.size} bytes — too incomplete to draw.`);
273
+ if (row.stored_size > MAX_INLINE_IMAGE_BYTES) return binary(`Image is ${row.stored_size} bytes — too large to inline.`);
274
+ return {
275
+ ...stored,
276
+ kind: "image",
277
+ text: row.is_text === 1 ? row.content.toString("utf8") : null,
278
+ dataUrl: `data:${mime};base64,${row.content.toString("base64")}`,
279
+ note: null
280
+ };
281
+ }
282
+ if (row.is_text !== 1) return binary(`Binary body — ${row.stored_size} bytes stored, not shown.`);
283
+ const text = row.content.toString("utf8");
284
+ if (contentType && /json/i.test(contentType)) try {
285
+ const json = JSON.parse(text);
286
+ if (json !== null && typeof json === "object") return {
287
+ ...stored,
288
+ kind: "json",
289
+ json,
290
+ note: null
291
+ };
292
+ } catch {
293
+ return {
294
+ ...stored,
295
+ kind: "text",
296
+ text,
297
+ note: "Content-Type is JSON but the body did not parse; shown as-is."
298
+ };
299
+ }
300
+ return {
301
+ ...stored,
302
+ kind: "text",
303
+ text,
304
+ note: null
305
+ };
306
+ }
307
+ /**
308
+ * Per-run aggregates.
309
+ *
310
+ * SQLite has no `quantile()`, so percentiles use the nearest-rank definition:
311
+ * order the durations, take the row at ceil(q * n). One ordered CTE serves all
312
+ * three, and with `idx_requests_run_seq` narrowing to the run first the sort is
313
+ * over that run's rows only.
314
+ *
315
+ * Latency figures cover HTTP requests only. A connection's `duration_ms` is how
316
+ * long it stayed open, so letting one WebSocket sit in the same distribution
317
+ * would make "p95" mean nothing. `latencySamples` says how many rows they are
318
+ * computed over; the status mix and totals still count everything.
319
+ */
320
+ function getRunAnalytics(db, runId) {
321
+ const quantiles = db.prepare(`WITH d AS (
322
+ SELECT duration_ms, ROW_NUMBER() OVER (ORDER BY duration_ms) AS rn
323
+ FROM requests WHERE run_id = @run AND kind = 'http'
324
+ ), c AS (SELECT COUNT(*) AS n FROM d)
325
+ SELECT
326
+ (SELECT COUNT(*) FROM requests WHERE run_id = @run) AS total,
327
+ (SELECT n FROM c) AS latency_samples,
328
+ (SELECT duration_ms FROM d
329
+ WHERE rn = MAX(1, CAST((SELECT n FROM c) * 0.50 AS INTEGER))) AS p50,
330
+ (SELECT duration_ms FROM d
331
+ WHERE rn = MAX(1, CAST((SELECT n FROM c) * 0.95 AS INTEGER))) AS p95,
332
+ (SELECT duration_ms FROM d
333
+ WHERE rn = MAX(1, CAST((SELECT n FROM c) * 0.99 AS INTEGER))) AS p99`).get({ run: runId });
334
+ const statusMix = db.prepare(`SELECT CASE
335
+ WHEN status IS NULL THEN 'error'
336
+ ELSE CAST(status / 100 AS TEXT) || 'xx'
337
+ END AS status_class,
338
+ COUNT(*) AS count
339
+ FROM requests WHERE run_id = ?
340
+ GROUP BY status_class ORDER BY status_class`).all(runId);
341
+ const slowest = db.prepare(`SELECT id, method, host, path, duration_ms FROM v_requests
342
+ WHERE run_id = ? AND kind = 'http' ORDER BY duration_ms DESC LIMIT 5`).all(runId);
343
+ const byHost = db.prepare(`WITH d AS (
344
+ SELECT host_id, duration_ms, status, error_name,
345
+ ROW_NUMBER() OVER (PARTITION BY host_id ORDER BY duration_ms) AS rn,
346
+ COUNT(*) OVER (PARTITION BY host_id) AS n
347
+ FROM requests WHERE run_id = @run AND kind = 'http'
348
+ )
349
+ SELECT h.host AS host,
350
+ MAX(d.n) AS count,
351
+ MAX(CASE WHEN d.rn = MAX(1, CAST(d.n * 0.95 AS INTEGER))
352
+ THEN d.duration_ms END) AS p95,
353
+ SUM(CASE WHEN d.error_name IS NOT NULL OR d.status >= 400
354
+ THEN 1 ELSE 0 END) AS errors
355
+ FROM d JOIN hosts h ON h.id = d.host_id
356
+ GROUP BY d.host_id ORDER BY count DESC`).all({ run: runId });
357
+ return {
358
+ total: quantiles.total,
359
+ latencySamples: quantiles.latency_samples,
360
+ statusMix,
361
+ p50: quantiles.latency_samples > 0 ? quantiles.p50 : null,
362
+ p95: quantiles.latency_samples > 0 ? quantiles.p95 : null,
363
+ p99: quantiles.latency_samples > 0 ? quantiles.p99 : null,
364
+ slowest,
365
+ byHost
366
+ };
367
+ }
368
+ var cached = null;
369
+ /** Absolute path of the database this server is showing. */
370
+ function dbPath() {
371
+ const raw = process.env.WIRE_DB;
372
+ return resolveDbPath({
373
+ ...raw ? { path: raw } : {},
374
+ cwd: process.cwd()
375
+ });
376
+ }
377
+ /**
378
+ * The open database, or null when the file does not exist yet.
379
+ *
380
+ * Null is a normal state, not an error: someone can start the UI before their
381
+ * first run. Pages render an empty state for it rather than a 500. A missing
382
+ * file is re-checked on every call, so the first `wire execute` after start-up
383
+ * is picked up without a restart.
384
+ *
385
+ * The connection is opened `readonly` and additionally `query_only`, which is
386
+ * what makes the SQL console safe to offer: a write fails in SQLite itself, not
387
+ * in a check this module could get wrong. Migrations are never applied — a
388
+ * viewer must not change the file it is showing.
389
+ */
390
+ function getDb() {
391
+ if (cached) return cached;
392
+ const file = dbPath();
393
+ if (!existsSync(file)) return null;
394
+ const db = new Database(file, {
395
+ readonly: true,
396
+ fileMustExist: true
397
+ });
398
+ db.pragma("query_only = ON");
399
+ db.pragma("temp_store = MEMORY");
400
+ cached = db;
401
+ return cached;
402
+ }
403
+ /** The schema version of the file being read, or 0 when there is nothing to read. */
404
+ function fileSchemaVersion(db) {
405
+ if (!db) return 0;
406
+ return db.pragma("user_version", { simple: true });
407
+ }
408
+ function schemaState(db) {
409
+ const version = fileSchemaVersion(db);
410
+ if (!db || version === 0) return "missing";
411
+ if (version < SCHEMA_VERSION) return "stale";
412
+ return version > SCHEMA_VERSION ? "newer" : "current";
413
+ }
414
+ var EXPECTED_SCHEMA_VERSION = SCHEMA_VERSION;
415
+ /** The database's size on disk, WAL included — what a recording costs. */
416
+ function fileBytes() {
417
+ const file = dbPath();
418
+ let total = 0;
419
+ for (const path of [file, `${file}-wal`]) try {
420
+ total += statSync(path).size;
421
+ } catch {}
422
+ return total;
423
+ }
424
+ /**
425
+ * How far above the root a script may sit and still be shown relative to it.
426
+ *
427
+ * One or two `../` reads fine and stays searchable. Six means the database and
428
+ * the script only share a home directory, and `~/work/api/scripts/sync.ts` is
429
+ * the more honest rendering of that.
430
+ */
431
+ var MAX_ASCENT = 2;
432
+ /** The folder paths are shown relative to: the one holding the database. */
433
+ function pathRoot(dbPath) {
434
+ return dirname(dbPath);
435
+ }
436
+ /** Describe one absolute entry path against a root. */
437
+ function describeEntry(root, entry) {
438
+ return split(shorten(root, entry));
439
+ }
440
+ function shorten(root, entry) {
441
+ if (!isAbsolute(entry)) return entry;
442
+ const rel = toPosix(relative(root, entry));
443
+ if (rel === "" || isAbsolute(rel)) return home(entry);
444
+ return rel.split("/").filter((segment) => segment === "..").length > MAX_ASCENT ? home(entry) : rel;
445
+ }
446
+ /** `/Users/you/work/sync.ts` → `~/work/sync.ts`. */
447
+ function home(absolute) {
448
+ const dir = toPosix(homedir());
449
+ const path = toPosix(absolute);
450
+ return path === dir || path.startsWith(`${dir}/`) ? `~${path.slice(dir.length)}` : path;
451
+ }
452
+ function split(path) {
453
+ const cut = path.lastIndexOf("/");
454
+ return {
455
+ rel: path,
456
+ dir: cut < 0 ? "" : path.slice(0, cut + 1),
457
+ file: cut < 0 ? path : path.slice(cut + 1)
458
+ };
459
+ }
460
+ function toPosix(path) {
461
+ return sep === "/" ? path : path.split(sep).join("/");
462
+ }
463
+ function workspaceCounts(db) {
464
+ return db.prepare(`SELECT
465
+ (SELECT COUNT(DISTINCT entry) FROM runs) AS scripts,
466
+ (SELECT COUNT(*) FROM runs) AS runs,
467
+ (SELECT COUNT(*) FROM requests WHERE kind = 'http') AS requests,
468
+ (SELECT COUNT(*) FROM requests WHERE kind <> 'http') AS connections,
469
+ (SELECT COUNT(*) FROM messages) AS messages,
470
+ (SELECT COUNT(*) FROM checks) AS checks,
471
+ (SELECT COUNT(*) FROM checks WHERE status = 'fail') AS failedChecks,
472
+ (SELECT COUNT(*) FROM requests
473
+ WHERE error_name IS NOT NULL OR status >= 400) AS failedRequests,
474
+ (SELECT COUNT(*) FROM bodies) AS bodies,
475
+ (SELECT COALESCE(SUM(stored_size), 0) FROM bodies) AS storedBodyBytes,
476
+ (SELECT COALESCE(SUM(request_bytes + response_bytes), 0)
477
+ FROM requests) AS trafficBytes`).get();
478
+ }
479
+ function envClause(env, column = "env") {
480
+ if (env === void 0) return {
481
+ sql: "",
482
+ params: []
483
+ };
484
+ if (env === null) return {
485
+ sql: ` AND ${column} IS NULL`,
486
+ params: []
487
+ };
488
+ return {
489
+ sql: ` AND ${column} = ?`,
490
+ params: [env]
491
+ };
492
+ }
493
+ /** Every environment the recording has runs for, most recently used first. */
494
+ function listEnvs(db) {
495
+ return db.prepare(`SELECT env,
496
+ COUNT(*) AS run_count,
497
+ COUNT(DISTINCT entry) AS script_count,
498
+ MAX(started_at) AS last_started_at,
499
+ SUM(exit_status = 'error') AS failed_run_count
500
+ FROM runs GROUP BY env ORDER BY last_started_at DESC`).all();
501
+ }
502
+ /**
503
+ * An FTS5 MATCH expression for free text.
504
+ *
505
+ * Every term is quoted, so what someone types is always searched for as words
506
+ * and never parsed as FTS syntax — `a-b` or a stray `"` would otherwise be a
507
+ * query error. Terms are ANDed, which is what a search box means.
508
+ */
509
+ function ftsExpression(text) {
510
+ const terms = text.split(/\s+/).map((t) => t.trim()).filter(Boolean);
511
+ if (terms.length === 0) return null;
512
+ return terms.map((t) => `"${t.replace(/"/g, "\"\"")}"`).join(" ");
513
+ }
514
+ /** Recent requests across every run, newest first, with the run they belong to. */
515
+ function listHistory(db, filter) {
516
+ const where = [];
517
+ const params = [];
518
+ const q = filter.q?.trim() ?? "";
519
+ if (q) {
520
+ const text = [`q.url LIKE ?`, `q.method = ?`];
521
+ params.push(`%${q}%`, q.toUpperCase());
522
+ if (/^\d{3}$/.test(q)) {
523
+ text.push(`q.status = ?`);
524
+ params.push(Number(q));
525
+ }
526
+ const match = filter.bodies ? ftsExpression(q) : null;
527
+ if (match) {
528
+ const hits = `(SELECT CAST(body_id AS INTEGER) FROM body_fts WHERE body_fts MATCH ?)`;
529
+ text.push(`q.request_body_id IN ${hits}`, `q.response_body_id IN ${hits}`, `q.id IN (SELECT request_id FROM messages WHERE body_id IN ${hits})`);
530
+ params.push(match, match, match);
531
+ }
532
+ where.push(`(${text.join(" OR ")})`);
533
+ }
534
+ if (filter.kind === "http") where.push(`q.kind = 'http'`);
535
+ if (filter.kind === "stream") where.push(`q.kind <> 'http'`);
536
+ if (filter.failed) where.push(`(q.error_name IS NOT NULL OR q.status >= 400)`);
537
+ const env = envClause(filter.env, "r.env");
538
+ const limit = Math.min(Math.max(filter.limit ?? 300, 1), 2e3);
539
+ const sql = `SELECT q.id, q.run_id, q.seq, q.ts, q.kind, q.method, q.host, q.path, q.query,
540
+ q.status, q.error_name, q.duration_ms, q.response_bytes, q.message_count,
541
+ r.label AS run_label, r.env, r.entry
542
+ FROM v_requests q JOIN runs r ON r.id = q.run_id
543
+ WHERE 1 = 1 ${where.length ? `AND ${where.join(" AND ")}` : ""}${env.sql}
544
+ ORDER BY q.ts DESC, q.id DESC
545
+ LIMIT ${limit}`;
546
+ return db.prepare(sql).all(...params, ...env.params);
547
+ }
548
+ function recentFailedChecks(db, env, limit = 12) {
549
+ const e = envClause(env, "r.env");
550
+ return db.prepare(`SELECT c.id, c.run_id, c.ts, c.name, c.mode, c.message,
551
+ r.label AS run_label, r.entry, r.env
552
+ FROM checks c JOIN runs r ON r.id = c.run_id
553
+ WHERE c.status = 'fail'${e.sql}
554
+ ORDER BY c.ts DESC LIMIT ?`).all(...e.params, limit);
555
+ }
556
+ /**
557
+ * The endpoints that cost the most, across every run.
558
+ *
559
+ * Grouped on method + host + path — the query string is left out on purpose,
560
+ * because `/products?skip=0` and `/products?skip=30` are one endpoint to anyone
561
+ * asking what is slow. HTTP only: a connection's duration is its lifetime.
562
+ */
563
+ function slowestEndpoints(db, env, limit = 8) {
564
+ const e = envClause(env, "r.env");
565
+ return db.prepare(`SELECT m.method, h.host, q.path,
566
+ COUNT(*) AS count,
567
+ AVG(q.duration_ms) AS avg_ms,
568
+ MAX(q.duration_ms) AS max_ms,
569
+ SUM(q.error_name IS NOT NULL OR q.status >= 400) AS errors,
570
+ -- Bare column under the one MAX(): SQLite takes it from the
571
+ -- slowest row, which is the call worth opening.
572
+ q.id AS slowest_request_id
573
+ FROM requests q
574
+ JOIN runs r ON r.id = q.run_id
575
+ JOIN methods m ON m.id = q.method_id
576
+ JOIN hosts h ON h.id = q.host_id
577
+ WHERE q.kind = 'http'${e.sql}
578
+ GROUP BY q.method_id, q.host_id, q.path
579
+ ORDER BY avg_ms DESC LIMIT ?`).all(...e.params, limit);
580
+ }
581
+ /**
582
+ * The requests a script makes, once each — what Postman calls a collection.
583
+ *
584
+ * A script has no saved request definitions; its collection is whatever it
585
+ * actually called. Ordered by where each endpoint first appears in a run, which
586
+ * reads as the script's own order. The `last_*` columns come from joining the
587
+ * newest exchange back in: SQLite only takes bare columns from the MAX() row
588
+ * when a query has a single min/max aggregate, and this one needs two.
589
+ */
590
+ function listEndpoints(db, entry, env) {
591
+ const e = envClause(env, "r.env");
592
+ return db.prepare(`WITH g AS (
593
+ SELECT q.kind, q.method_id, q.host_id, q.path,
594
+ COUNT(*) AS count,
595
+ SUM(q.error_name IS NOT NULL OR q.status >= 400) AS errors,
596
+ AVG(q.duration_ms) AS avg_ms,
597
+ MAX(q.id) AS last_id,
598
+ MIN(q.seq) AS first_seq
599
+ FROM requests q JOIN runs r ON r.id = q.run_id
600
+ WHERE r.entry = ?${e.sql}
601
+ GROUP BY q.kind, q.method_id, q.host_id, q.path
602
+ )
603
+ SELECT g.kind, m.method, h.host, g.path, g.count, g.errors, g.avg_ms,
604
+ g.last_id AS last_request_id,
605
+ l.status AS last_status,
606
+ l.error_name AS last_error_name,
607
+ l.ts AS last_ts
608
+ FROM g
609
+ JOIN requests l ON l.id = g.last_id
610
+ JOIN methods m ON m.id = g.method_id
611
+ JOIN hosts h ON h.id = g.host_id
612
+ ORDER BY g.first_seq, g.count DESC`).all(entry, ...e.params);
613
+ }
614
+ /**
615
+ * Every check a script makes, with its record across runs: has it always
616
+ * passed? The newest outcome is joined back in, for the reason given on
617
+ * {@link listEndpoints}.
618
+ */
619
+ function listCheckHistory(db, entry, env) {
620
+ const e = envClause(env, "r.env");
621
+ return db.prepare(`WITH g AS (
622
+ SELECT c.name,
623
+ COUNT(*) AS total,
624
+ SUM(c.status = 'fail') AS failures,
625
+ MAX(c.id) AS last_id,
626
+ MIN(c.seq) AS first_seq
627
+ FROM checks c JOIN runs r ON r.id = c.run_id
628
+ WHERE r.entry = ?${e.sql}
629
+ GROUP BY c.name
630
+ )
631
+ SELECT g.name, g.total, g.failures,
632
+ g.last_id AS last_check_id,
633
+ l.run_id AS last_run_id,
634
+ l.status AS last_status,
635
+ l.message AS last_message
636
+ FROM g JOIN checks l ON l.id = g.last_id
637
+ ORDER BY g.first_seq`).all(entry, ...e.params);
638
+ }
639
+ function requestNeighbours(db, runId, seq) {
640
+ return db.prepare(`SELECT
641
+ (SELECT id FROM requests WHERE run_id = @run AND seq < @seq ORDER BY seq DESC LIMIT 1) AS prev,
642
+ (SELECT id FROM requests WHERE run_id = @run AND seq > @seq ORDER BY seq ASC LIMIT 1) AS next`).get({
643
+ run: runId,
644
+ seq
645
+ });
646
+ }
647
+ /** Older and newer runs of the same script. */
648
+ function runNeighbours(db, entry, startedAt, id) {
649
+ return db.prepare(`SELECT
650
+ (SELECT id FROM runs WHERE entry = @entry AND (started_at, id) < (@at, @id)
651
+ ORDER BY started_at DESC, id DESC LIMIT 1) AS prev,
652
+ (SELECT id FROM runs WHERE entry = @entry AND (started_at, id) > (@at, @id)
653
+ ORDER BY started_at ASC, id ASC LIMIT 1) AS next`).get({
654
+ entry,
655
+ at: startedAt,
656
+ id
657
+ });
658
+ }
659
+ /**
660
+ * Run one read statement and return at most {@link MAX_QUERY_ROWS} rows.
661
+ *
662
+ * Safety is not this function's job — the connection is read-only and
663
+ * `query_only`, so SQLite itself refuses a write. What this adds is a clear
664
+ * message for a statement that returns nothing to show, and a row cap enforced
665
+ * by stepping the cursor rather than by rewriting the SQL with a LIMIT.
666
+ */
667
+ function runQuery(db, sql) {
668
+ const started = performance.now();
669
+ try {
670
+ const statement = db.prepare(sql);
671
+ if (!statement.reader) return {
672
+ ok: false,
673
+ error: "Only statements that return rows run here — SELECT, WITH, PRAGMA, EXPLAIN. The database is open read-only."
674
+ };
675
+ const columns = statement.columns().map((c) => c.name);
676
+ const rows = [];
677
+ let truncated = false;
678
+ for (const row of statement.raw(true).iterate()) {
679
+ if (rows.length === 1e3) {
680
+ truncated = true;
681
+ break;
682
+ }
683
+ rows.push(row.map(cell));
684
+ }
685
+ return {
686
+ ok: true,
687
+ columns,
688
+ rows,
689
+ truncated,
690
+ ms: performance.now() - started
691
+ };
692
+ } catch (err) {
693
+ return {
694
+ ok: false,
695
+ error: err instanceof Error ? err.message : String(err)
696
+ };
697
+ }
698
+ }
699
+ /** A value that survives the trip to the browser and says what it is. */
700
+ function cell(value) {
701
+ if (value === null || value === void 0) return null;
702
+ if (typeof value === "number" || typeof value === "string") return value;
703
+ if (typeof value === "bigint") return value.toString();
704
+ if (Buffer.isBuffer(value)) return `<blob · ${value.length} bytes>`;
705
+ return String(value);
706
+ }
707
+ function record(input) {
708
+ return input && typeof input === "object" ? input : {};
709
+ }
710
+ function id(value, name) {
711
+ const n = typeof value === "string" ? Number(value) : value;
712
+ if (typeof n !== "number" || !Number.isInteger(n) || n <= 0) throw new Error(`${name} must be a positive integer`);
713
+ return n;
714
+ }
715
+ function optionalString(value) {
716
+ return typeof value === "string" ? value : void 0;
717
+ }
718
+ /** `undefined` → every environment, `null` → runs recorded without one. */
719
+ function env(value) {
720
+ if (value === null) return null;
721
+ return typeof value === "string" ? value : void 0;
722
+ }
723
+ /** The open database when its schema is one these queries can read, else null. */
724
+ function readable() {
725
+ const db = getDb();
726
+ const state = schemaState(db);
727
+ return state === "current" || state === "newer" ? db : null;
728
+ }
729
+ function describe(row) {
730
+ return {
731
+ ...row,
732
+ ...describeEntry(pathRoot(dbPath()), row.entry)
733
+ };
734
+ }
735
+ /** Runs the sidebar holds. Older ones stay reachable from their script's page. */
736
+ var RUN_WINDOW = 500;
737
+ var getWorkspace_createServerFn_handler = createServerRpc({
738
+ id: "65bc418eb60581304ebde68d2b8b775b58aa758789bcdd2e616a2bdd5657ef20",
739
+ name: "getWorkspace",
740
+ filename: "src/server/api.ts"
741
+ }, (opts) => getWorkspace.__executeServer(opts));
742
+ var getWorkspace = createServerFn({ method: "GET" }).handler(getWorkspace_createServerFn_handler, async () => {
743
+ const path = dbPath();
744
+ const handle = getDb();
745
+ const schema = {
746
+ state: schemaState(handle),
747
+ file: fileSchemaVersion(handle),
748
+ expected: EXPECTED_SCHEMA_VERSION
749
+ };
750
+ const base = {
751
+ path,
752
+ root: pathRoot(path),
753
+ schema,
754
+ now: Date.now()
755
+ };
756
+ const db = readable();
757
+ if (!db) return {
758
+ ...base,
759
+ bytes: handle ? fileBytes() : 0,
760
+ counts: null,
761
+ envs: [],
762
+ scripts: [],
763
+ runs: []
764
+ };
765
+ return {
766
+ ...base,
767
+ bytes: fileBytes(),
768
+ counts: workspaceCounts(db),
769
+ envs: listEnvs(db),
770
+ scripts: listScripts(db).map(describe),
771
+ runs: listRuns(db, RUN_WINDOW).map(describe)
772
+ };
773
+ });
774
+ var getOverview_createServerFn_handler = createServerRpc({
775
+ id: "edb0cb28f3af0347a499a4c076ecc5f8247f0ace0fb524957f40f6a706d7224d",
776
+ name: "getOverview",
777
+ filename: "src/server/api.ts"
778
+ }, (opts) => getOverview.__executeServer(opts));
779
+ var getOverview = createServerFn({ method: "GET" }).validator((input) => ({ env: env(record(input).env) })).handler(getOverview_createServerFn_handler, async ({ data }) => {
780
+ const db = readable();
781
+ if (!db) return {
782
+ runs: [],
783
+ failedRequests: [],
784
+ failedChecks: [],
785
+ slowest: []
786
+ };
787
+ return {
788
+ runs: listRuns(db, 200).filter((r) => data.env === void 0 || r.env === data.env).slice(0, 12).map(describe),
789
+ failedRequests: listHistory(db, {
790
+ env: data.env,
791
+ failed: true,
792
+ limit: 12
793
+ }).map(describe),
794
+ failedChecks: recentFailedChecks(db, data.env).map(describe),
795
+ slowest: slowestEndpoints(db, data.env)
796
+ };
797
+ });
798
+ var getHistory_createServerFn_handler = createServerRpc({
799
+ id: "c1e4e973ed505d1515bcb9192482c65b1680975a5daa6973befb704515a618e3",
800
+ name: "getHistory",
801
+ filename: "src/server/api.ts"
802
+ }, (opts) => getHistory.__executeServer(opts));
803
+ var getHistory = createServerFn({ method: "GET" }).validator((input) => {
804
+ const r = record(input);
805
+ const kind = r.kind === "http" || r.kind === "stream" ? r.kind : void 0;
806
+ return {
807
+ q: optionalString(r.q),
808
+ env: env(r.env),
809
+ kind,
810
+ failed: r.failed === true,
811
+ bodies: r.bodies === true,
812
+ limit: typeof r.limit === "number" ? r.limit : void 0
813
+ };
814
+ }).handler(getHistory_createServerFn_handler, async ({ data }) => {
815
+ const db = readable();
816
+ if (!db) return [];
817
+ try {
818
+ return listHistory(db, data).map(describe);
819
+ } catch (err) {
820
+ if (data.bodies) return listHistory(db, {
821
+ ...data,
822
+ bodies: false
823
+ }).map(describe);
824
+ throw err;
825
+ }
826
+ });
827
+ var getScript_createServerFn_handler = createServerRpc({
828
+ id: "5eff62884e56f81742c81b44dc7dcd1afc2e67323a30591a8b94b8895eda0dc9",
829
+ name: "getScript",
830
+ filename: "src/server/api.ts"
831
+ }, (opts) => getScript.__executeServer(opts));
832
+ var getScript = createServerFn({ method: "GET" }).validator((input) => {
833
+ const r = record(input);
834
+ const entry = optionalString(r.entry);
835
+ if (!entry) throw new Error("entry is required");
836
+ return {
837
+ entry,
838
+ env: env(r.env)
839
+ };
840
+ }).handler(getScript_createServerFn_handler, async ({ data }) => {
841
+ const db = readable();
842
+ if (!db) return null;
843
+ const script = listScripts(db).find((s) => s.entry === data.entry);
844
+ if (!script) return null;
845
+ const runs = listRunsForEntry(db, data.entry, 1e3).filter((r) => data.env === void 0 || r.env === data.env);
846
+ return {
847
+ script: describe(script),
848
+ runs,
849
+ endpoints: listEndpoints(db, data.entry, data.env),
850
+ checks: listCheckHistory(db, data.entry, data.env)
851
+ };
852
+ });
853
+ var getRunRequests_createServerFn_handler = createServerRpc({
854
+ id: "3828e982aebdd19568d199b4820f9fb3ca9fab2c60ce502ee35e3f6075d422c9",
855
+ name: "getRunRequests",
856
+ filename: "src/server/api.ts"
857
+ }, (opts) => getRunRequests.__executeServer(opts));
858
+ var getRunRequests = createServerFn({ method: "GET" }).validator((input) => ({ runId: id(record(input).runId, "runId") })).handler(getRunRequests_createServerFn_handler, async ({ data }) => {
859
+ const db = readable();
860
+ return db ? listRequests(db, data.runId) : [];
861
+ });
862
+ var getRun_createServerFn_handler = createServerRpc({
863
+ id: "56c165307cd91159eb72bfa882932f9552f05a5bbd6b5725f0fd5f4ad6b8295d",
864
+ name: "getRun",
865
+ filename: "src/server/api.ts"
866
+ }, (opts) => getRun.__executeServer(opts));
867
+ var getRun = createServerFn({ method: "GET" }).validator((input) => ({ runId: id(record(input).runId, "runId") })).handler(getRun_createServerFn_handler, async ({ data }) => {
868
+ const db = readable();
869
+ if (!db) return null;
870
+ const run = getRun$1(db, data.runId);
871
+ if (!run) return null;
872
+ return {
873
+ run: describe(run),
874
+ tags: listRunTags(db, run.id),
875
+ requests: listRequests(db, run.id),
876
+ checks: listChecks(db, run.id),
877
+ analytics: getRunAnalytics(db, run.id),
878
+ neighbours: runNeighbours(db, run.entry, run.started_at, run.id)
879
+ };
880
+ });
881
+ var getRequest_createServerFn_handler = createServerRpc({
882
+ id: "3772b5ea3e53d85a63e4df11f128b1c907e508d82cca6e71e6ee48dba1664133",
883
+ name: "getRequest",
884
+ filename: "src/server/api.ts"
885
+ }, (opts) => getRequest.__executeServer(opts));
886
+ var getRequest = createServerFn({ method: "GET" }).validator((input) => ({ requestId: id(record(input).requestId, "requestId") })).handler(getRequest_createServerFn_handler, async ({ data }) => {
887
+ const db = readable();
888
+ if (!db) return null;
889
+ const detail = getRequestDetail(db, data.requestId);
890
+ if (!detail) return null;
891
+ const run = getRun$1(db, detail.request.run_id);
892
+ if (!run) return null;
893
+ return {
894
+ detail,
895
+ run: describe(run),
896
+ neighbours: requestNeighbours(db, run.id, detail.request.seq),
897
+ runRequestCount: run.request_count
898
+ };
899
+ });
900
+ var getSchema_createServerFn_handler = createServerRpc({
901
+ id: "06ff3639e7164884f56665ea621dc233dbfc78fa1557b1cb20105ff2fa90521e",
902
+ name: "getSchema",
903
+ filename: "src/server/api.ts"
904
+ }, (opts) => getSchema.__executeServer(opts));
905
+ var getSchema = createServerFn({ method: "GET" }).handler(getSchema_createServerFn_handler, async () => {
906
+ const db = getDb();
907
+ if (!db) return [];
908
+ return listRelations(db, { all: true }).map((relation) => ({
909
+ ...relation,
910
+ columns: describeRelation(db, relation.name)
911
+ }));
912
+ });
913
+ var runSql_createServerFn_handler = createServerRpc({
914
+ id: "0f45629470ab3fbe618e9693d8685920d6ce48b2384512e5d9eb88f6b46ec2d9",
915
+ name: "runSql",
916
+ filename: "src/server/api.ts"
917
+ }, (opts) => runSql.__executeServer(opts));
918
+ var runSql = createServerFn({ method: "POST" }).validator((input) => {
919
+ const sql = optionalString(record(input).sql)?.trim();
920
+ if (!sql) throw new Error("sql is required");
921
+ return { sql };
922
+ }).handler(runSql_createServerFn_handler, async ({ data }) => {
923
+ const db = getDb();
924
+ if (!db) return {
925
+ ok: false,
926
+ error: `No database at ${dbPath()} yet.`
927
+ };
928
+ return runQuery(db, data.sql);
929
+ });
930
+ //#endregion
931
+ export { getHistory_createServerFn_handler, getOverview_createServerFn_handler, getRequest_createServerFn_handler, getRunRequests_createServerFn_handler, getRun_createServerFn_handler, getSchema_createServerFn_handler, getScript_createServerFn_handler, getWorkspace_createServerFn_handler, runSql_createServerFn_handler };