trickle-cli 0.1.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 (125) hide show
  1. package/dist/api-client.d.ts +208 -0
  2. package/dist/api-client.js +237 -0
  3. package/dist/commands/annotate.d.ts +6 -0
  4. package/dist/commands/annotate.js +433 -0
  5. package/dist/commands/audit.d.ts +7 -0
  6. package/dist/commands/audit.js +82 -0
  7. package/dist/commands/auto.d.ts +8 -0
  8. package/dist/commands/auto.js +268 -0
  9. package/dist/commands/capture.d.ts +14 -0
  10. package/dist/commands/capture.js +271 -0
  11. package/dist/commands/check.d.ts +6 -0
  12. package/dist/commands/check.js +408 -0
  13. package/dist/commands/codegen.d.ts +21 -0
  14. package/dist/commands/codegen.js +129 -0
  15. package/dist/commands/coverage.d.ts +13 -0
  16. package/dist/commands/coverage.js +126 -0
  17. package/dist/commands/dashboard.d.ts +1 -0
  18. package/dist/commands/dashboard.js +83 -0
  19. package/dist/commands/dev.d.ts +14 -0
  20. package/dist/commands/dev.js +319 -0
  21. package/dist/commands/diff.d.ts +7 -0
  22. package/dist/commands/diff.js +79 -0
  23. package/dist/commands/docs.d.ts +13 -0
  24. package/dist/commands/docs.js +383 -0
  25. package/dist/commands/errors.d.ts +7 -0
  26. package/dist/commands/errors.js +180 -0
  27. package/dist/commands/export.d.ts +18 -0
  28. package/dist/commands/export.js +238 -0
  29. package/dist/commands/functions.d.ts +6 -0
  30. package/dist/commands/functions.js +71 -0
  31. package/dist/commands/infer.d.ts +14 -0
  32. package/dist/commands/infer.js +275 -0
  33. package/dist/commands/init.d.ts +5 -0
  34. package/dist/commands/init.js +395 -0
  35. package/dist/commands/mock.d.ts +5 -0
  36. package/dist/commands/mock.js +232 -0
  37. package/dist/commands/openapi.d.ts +8 -0
  38. package/dist/commands/openapi.js +82 -0
  39. package/dist/commands/overview.d.ts +11 -0
  40. package/dist/commands/overview.js +266 -0
  41. package/dist/commands/pack.d.ts +11 -0
  42. package/dist/commands/pack.js +133 -0
  43. package/dist/commands/proxy.d.ts +13 -0
  44. package/dist/commands/proxy.js +312 -0
  45. package/dist/commands/replay.d.ts +14 -0
  46. package/dist/commands/replay.js +289 -0
  47. package/dist/commands/run.d.ts +17 -0
  48. package/dist/commands/run.js +997 -0
  49. package/dist/commands/sample.d.ts +13 -0
  50. package/dist/commands/sample.js +260 -0
  51. package/dist/commands/search.d.ts +5 -0
  52. package/dist/commands/search.js +80 -0
  53. package/dist/commands/stubs.d.ts +6 -0
  54. package/dist/commands/stubs.js +187 -0
  55. package/dist/commands/tail.d.ts +4 -0
  56. package/dist/commands/tail.js +76 -0
  57. package/dist/commands/test-gen.d.ts +13 -0
  58. package/dist/commands/test-gen.js +237 -0
  59. package/dist/commands/trace.d.ts +14 -0
  60. package/dist/commands/trace.js +417 -0
  61. package/dist/commands/types.d.ts +7 -0
  62. package/dist/commands/types.js +128 -0
  63. package/dist/commands/unpack.d.ts +11 -0
  64. package/dist/commands/unpack.js +166 -0
  65. package/dist/commands/validate.d.ts +13 -0
  66. package/dist/commands/validate.js +310 -0
  67. package/dist/commands/watch.d.ts +9 -0
  68. package/dist/commands/watch.js +267 -0
  69. package/dist/config.d.ts +1 -0
  70. package/dist/config.js +66 -0
  71. package/dist/formatters/diff-formatter.d.ts +5 -0
  72. package/dist/formatters/diff-formatter.js +43 -0
  73. package/dist/formatters/type-formatter.d.ts +22 -0
  74. package/dist/formatters/type-formatter.js +135 -0
  75. package/dist/index.d.ts +2 -0
  76. package/dist/index.js +419 -0
  77. package/dist/local-codegen.d.ts +22 -0
  78. package/dist/local-codegen.js +762 -0
  79. package/dist/ui/badges.d.ts +16 -0
  80. package/dist/ui/badges.js +71 -0
  81. package/dist/ui/helpers.d.ts +13 -0
  82. package/dist/ui/helpers.js +85 -0
  83. package/package.json +23 -0
  84. package/src/api-client.ts +407 -0
  85. package/src/commands/annotate.ts +450 -0
  86. package/src/commands/audit.ts +103 -0
  87. package/src/commands/auto.ts +268 -0
  88. package/src/commands/capture.ts +257 -0
  89. package/src/commands/check.ts +437 -0
  90. package/src/commands/codegen.ts +128 -0
  91. package/src/commands/coverage.ts +170 -0
  92. package/src/commands/dashboard.ts +46 -0
  93. package/src/commands/dev.ts +323 -0
  94. package/src/commands/diff.ts +99 -0
  95. package/src/commands/docs.ts +392 -0
  96. package/src/commands/errors.ts +205 -0
  97. package/src/commands/export.ts +287 -0
  98. package/src/commands/functions.ts +81 -0
  99. package/src/commands/infer.ts +260 -0
  100. package/src/commands/init.ts +419 -0
  101. package/src/commands/mock.ts +220 -0
  102. package/src/commands/openapi.ts +53 -0
  103. package/src/commands/overview.ts +310 -0
  104. package/src/commands/pack.ts +139 -0
  105. package/src/commands/proxy.ts +314 -0
  106. package/src/commands/replay.ts +356 -0
  107. package/src/commands/run.ts +1190 -0
  108. package/src/commands/sample.ts +259 -0
  109. package/src/commands/search.ts +107 -0
  110. package/src/commands/stubs.ts +211 -0
  111. package/src/commands/tail.ts +94 -0
  112. package/src/commands/test-gen.ts +236 -0
  113. package/src/commands/trace.ts +440 -0
  114. package/src/commands/types.ts +161 -0
  115. package/src/commands/unpack.ts +179 -0
  116. package/src/commands/validate.ts +368 -0
  117. package/src/commands/watch.ts +277 -0
  118. package/src/config.ts +38 -0
  119. package/src/formatters/diff-formatter.ts +51 -0
  120. package/src/formatters/type-formatter.ts +161 -0
  121. package/src/index.ts +454 -0
  122. package/src/local-codegen.ts +859 -0
  123. package/src/ui/badges.ts +66 -0
  124. package/src/ui/helpers.ts +80 -0
  125. package/tsconfig.json +8 -0
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Colored badge for environment names.
3
+ */
4
+ export declare function envBadge(env: string): string;
5
+ /**
6
+ * Colored badge for language.
7
+ */
8
+ export declare function langBadge(lang: string): string;
9
+ /**
10
+ * Colored badge for error type.
11
+ */
12
+ export declare function errorTypeBadge(type: string): string;
13
+ /**
14
+ * Format a timestamp as a relative time badge.
15
+ */
16
+ export declare function timeBadge(timestamp: string): string;
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.envBadge = envBadge;
7
+ exports.langBadge = langBadge;
8
+ exports.errorTypeBadge = errorTypeBadge;
9
+ exports.timeBadge = timeBadge;
10
+ const chalk_1 = __importDefault(require("chalk"));
11
+ const helpers_1 = require("./helpers");
12
+ /**
13
+ * Colored badge for environment names.
14
+ */
15
+ function envBadge(env) {
16
+ const lower = env.toLowerCase();
17
+ if (lower === "prod" || lower === "production") {
18
+ return chalk_1.default.bgRed.white.bold(` ${env} `);
19
+ }
20
+ if (lower === "staging" || lower === "stage") {
21
+ return chalk_1.default.bgYellow.black.bold(` ${env} `);
22
+ }
23
+ if (lower === "local" || lower === "dev" || lower === "development") {
24
+ return chalk_1.default.bgGreen.black.bold(` ${env} `);
25
+ }
26
+ if (lower === "test" || lower === "ci") {
27
+ return chalk_1.default.bgCyan.black.bold(` ${env} `);
28
+ }
29
+ return chalk_1.default.bgGray.white.bold(` ${env} `);
30
+ }
31
+ /**
32
+ * Colored badge for language.
33
+ */
34
+ function langBadge(lang) {
35
+ const lower = lang.toLowerCase();
36
+ if (lower === "js" || lower === "javascript" || lower === "typescript" || lower === "ts") {
37
+ return chalk_1.default.bgYellow.black(` ${lang} `);
38
+ }
39
+ if (lower === "python" || lower === "py") {
40
+ return chalk_1.default.bgBlue.white(` ${lang} `);
41
+ }
42
+ if (lower === "go" || lower === "golang") {
43
+ return chalk_1.default.bgCyan.black(` ${lang} `);
44
+ }
45
+ return chalk_1.default.bgGray.white(` ${lang} `);
46
+ }
47
+ /**
48
+ * Colored badge for error type.
49
+ */
50
+ function errorTypeBadge(type) {
51
+ const lower = type.toLowerCase();
52
+ if (lower.includes("type")) {
53
+ return chalk_1.default.bgMagenta.white.bold(` ${type} `);
54
+ }
55
+ if (lower.includes("reference") || lower.includes("undefined")) {
56
+ return chalk_1.default.bgRed.white.bold(` ${type} `);
57
+ }
58
+ if (lower.includes("syntax")) {
59
+ return chalk_1.default.bgYellow.black.bold(` ${type} `);
60
+ }
61
+ if (lower.includes("range") || lower.includes("overflow")) {
62
+ return chalk_1.default.bgCyan.black.bold(` ${type} `);
63
+ }
64
+ return chalk_1.default.bgRed.white.bold(` ${type} `);
65
+ }
66
+ /**
67
+ * Format a timestamp as a relative time badge.
68
+ */
69
+ function timeBadge(timestamp) {
70
+ return chalk_1.default.gray((0, helpers_1.relativeTime)(timestamp));
71
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Parse human-readable time strings into ISO date strings.
3
+ * Supports: "30s", "5m", "5 min", "2h", "3d", "1w"
4
+ */
5
+ export declare function parseSince(since: string): string;
6
+ /**
7
+ * Truncate a string with ellipsis.
8
+ */
9
+ export declare function truncate(str: string, maxLen: number): string;
10
+ /**
11
+ * Convert an ISO date string to a relative time string like "2m ago".
12
+ */
13
+ export declare function relativeTime(isoDate: string): string;
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseSince = parseSince;
4
+ exports.truncate = truncate;
5
+ exports.relativeTime = relativeTime;
6
+ /**
7
+ * Parse human-readable time strings into ISO date strings.
8
+ * Supports: "30s", "5m", "5 min", "2h", "3d", "1w"
9
+ */
10
+ function parseSince(since) {
11
+ const now = Date.now();
12
+ const cleaned = since.trim().toLowerCase();
13
+ const match = cleaned.match(/^(\d+)\s*(s|sec|seconds?|m|min|minutes?|h|hrs?|hours?|d|days?|w|weeks?)$/);
14
+ if (!match) {
15
+ // Try to parse as ISO date directly
16
+ const d = new Date(since);
17
+ if (!isNaN(d.getTime())) {
18
+ return d.toISOString();
19
+ }
20
+ throw new Error(`Cannot parse time string: "${since}". Use formats like "30s", "5m", "2h", "3d", "1w".`);
21
+ }
22
+ const amount = parseInt(match[1], 10);
23
+ const unit = match[2];
24
+ let ms;
25
+ if (unit.startsWith("s")) {
26
+ ms = amount * 1000;
27
+ }
28
+ else if (unit.startsWith("m") && !unit.startsWith("mo")) {
29
+ ms = amount * 60 * 1000;
30
+ }
31
+ else if (unit.startsWith("h")) {
32
+ ms = amount * 60 * 60 * 1000;
33
+ }
34
+ else if (unit.startsWith("d")) {
35
+ ms = amount * 24 * 60 * 60 * 1000;
36
+ }
37
+ else if (unit.startsWith("w")) {
38
+ ms = amount * 7 * 24 * 60 * 60 * 1000;
39
+ }
40
+ else {
41
+ ms = amount * 60 * 1000; // default to minutes
42
+ }
43
+ // Output in SQLite datetime format to match backend storage
44
+ return new Date(now - ms).toISOString().replace('T', ' ').replace(/\.\d+Z$/, '');
45
+ }
46
+ /**
47
+ * Truncate a string with ellipsis.
48
+ */
49
+ function truncate(str, maxLen) {
50
+ if (str.length <= maxLen)
51
+ return str;
52
+ return str.slice(0, maxLen - 1) + "\u2026";
53
+ }
54
+ /**
55
+ * Convert an ISO date string to a relative time string like "2m ago".
56
+ */
57
+ function relativeTime(isoDate) {
58
+ const now = Date.now();
59
+ const then = new Date(isoDate).getTime();
60
+ const diff = now - then;
61
+ if (isNaN(then))
62
+ return isoDate;
63
+ const seconds = Math.floor(diff / 1000);
64
+ if (seconds < 5)
65
+ return "just now";
66
+ if (seconds < 60)
67
+ return `${seconds}s ago`;
68
+ const minutes = Math.floor(seconds / 60);
69
+ if (minutes < 60)
70
+ return `${minutes}m ago`;
71
+ const hours = Math.floor(minutes / 60);
72
+ if (hours < 24)
73
+ return `${hours}h ago`;
74
+ const days = Math.floor(hours / 24);
75
+ if (days < 7)
76
+ return `${days}d ago`;
77
+ const weeks = Math.floor(days / 7);
78
+ if (weeks < 4)
79
+ return `${weeks}w ago`;
80
+ const months = Math.floor(days / 30);
81
+ if (months < 12)
82
+ return `${months}mo ago`;
83
+ const years = Math.floor(days / 365);
84
+ return `${years}y ago`;
85
+ }
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "trickle-cli",
3
+ "version": "0.1.0",
4
+ "description": "CLI for trickle runtime type observability",
5
+ "bin": {
6
+ "trickle": "dist/index.js"
7
+ },
8
+ "scripts": {
9
+ "build": "tsc",
10
+ "dev": "ts-node src/index.ts"
11
+ },
12
+ "dependencies": {
13
+ "chalk": "^4.1.2",
14
+ "cli-table3": "^0.6.5",
15
+ "commander": "^12.0.0",
16
+ "trickle-observe": "^0.1.0",
17
+ "trickle-backend": "^0.1.0"
18
+ },
19
+ "devDependencies": {
20
+ "ts-node": "^10.9.0",
21
+ "typescript": "^5.4.0"
22
+ }
23
+ }
@@ -0,0 +1,407 @@
1
+ import * as http from "http";
2
+ import * as https from "https";
3
+ import { URL } from "url";
4
+ import chalk from "chalk";
5
+ import { getBackendUrl } from "./config";
6
+
7
+ // ── Types matching backend responses ──
8
+
9
+ export interface FunctionRow {
10
+ id: number;
11
+ function_name: string;
12
+ module: string;
13
+ language: string;
14
+ environment: string;
15
+ first_seen_at: string;
16
+ last_seen_at: string;
17
+ }
18
+
19
+ export interface FunctionDetail {
20
+ function: FunctionRow;
21
+ latestSnapshots: Record<string, TypeSnapshot>;
22
+ }
23
+
24
+ export interface TypeSnapshot {
25
+ id: number;
26
+ function_id: number;
27
+ type_hash: string;
28
+ env: string;
29
+ args_type: unknown;
30
+ return_type: unknown;
31
+ observed_at: string;
32
+ sample_input?: unknown;
33
+ sample_output?: unknown;
34
+ }
35
+
36
+ export interface ErrorRow {
37
+ id: number;
38
+ function_id: number;
39
+ function_name: string;
40
+ module: string;
41
+ language: string;
42
+ env: string;
43
+ error_type: string;
44
+ error_message: string;
45
+ stack_trace?: string;
46
+ type_hash?: string;
47
+ args_type?: unknown;
48
+ return_type?: unknown;
49
+ variables_type?: unknown;
50
+ args_snapshot?: unknown;
51
+ occurred_at: string;
52
+ }
53
+
54
+ export interface ErrorDetail {
55
+ error: ErrorRow;
56
+ snapshot: TypeSnapshot | null;
57
+ }
58
+
59
+ export interface TypeDiff {
60
+ kind: "added" | "removed" | "changed";
61
+ path: string;
62
+ from?: unknown;
63
+ to?: unknown;
64
+ type?: unknown;
65
+ }
66
+
67
+ export interface DiffResponse {
68
+ from: { id: number; env: string; observed_at: string };
69
+ to: { id: number; env: string; observed_at: string };
70
+ diffs: TypeDiff[];
71
+ }
72
+
73
+ export interface TailEvent {
74
+ event: string;
75
+ data: Record<string, unknown>;
76
+ }
77
+
78
+ // ── Helpers ──
79
+
80
+ function backendUrl(): string {
81
+ return getBackendUrl();
82
+ }
83
+
84
+ function connectionError(): never {
85
+ const url = backendUrl();
86
+ console.error(
87
+ chalk.red(`\nCannot connect to trickle backend at ${chalk.bold(url)}.`)
88
+ );
89
+ console.error(chalk.red("Is the backend running?\n"));
90
+ process.exit(1);
91
+ }
92
+
93
+ async function fetchJson<T>(path: string, query?: Record<string, string | undefined>): Promise<T> {
94
+ const base = backendUrl();
95
+ const url = new URL(path, base);
96
+
97
+ if (query) {
98
+ for (const [key, value] of Object.entries(query)) {
99
+ if (value !== undefined && value !== "") {
100
+ url.searchParams.set(key, value);
101
+ }
102
+ }
103
+ }
104
+
105
+ try {
106
+ const res = await fetch(url.toString());
107
+ if (!res.ok) {
108
+ const body = await res.text();
109
+ throw new Error(`HTTP ${res.status}: ${body}`);
110
+ }
111
+ return (await res.json()) as T;
112
+ } catch (err: unknown) {
113
+ if (err instanceof Error && err.message.startsWith("HTTP ")) {
114
+ throw err;
115
+ }
116
+ connectionError();
117
+ }
118
+ }
119
+
120
+ // ── API Functions ──
121
+
122
+ export interface ListFunctionsOpts {
123
+ env?: string;
124
+ language?: string;
125
+ search?: string;
126
+ limit?: number;
127
+ offset?: number;
128
+ }
129
+
130
+ export async function listFunctions(opts?: ListFunctionsOpts): Promise<{ functions: FunctionRow[]; total: number }> {
131
+ return fetchJson("/api/functions", {
132
+ env: opts?.env,
133
+ language: opts?.language,
134
+ q: opts?.search,
135
+ limit: opts?.limit?.toString(),
136
+ offset: opts?.offset?.toString(),
137
+ });
138
+ }
139
+
140
+ export async function getFunction(id: number): Promise<FunctionDetail> {
141
+ return fetchJson(`/api/functions/${id}`);
142
+ }
143
+
144
+ export interface ListErrorsOpts {
145
+ env?: string;
146
+ functionName?: string;
147
+ since?: string;
148
+ limit?: number;
149
+ offset?: number;
150
+ }
151
+
152
+ export async function listErrors(opts?: ListErrorsOpts): Promise<{ errors: ErrorRow[]; total: number }> {
153
+ return fetchJson("/api/errors", {
154
+ env: opts?.env,
155
+ functionName: opts?.functionName,
156
+ since: opts?.since,
157
+ limit: opts?.limit?.toString(),
158
+ offset: opts?.offset?.toString(),
159
+ });
160
+ }
161
+
162
+ export async function getError(id: number): Promise<ErrorDetail> {
163
+ return fetchJson(`/api/errors/${id}`);
164
+ }
165
+
166
+ export interface ListTypesOpts {
167
+ env?: string;
168
+ limit?: number;
169
+ }
170
+
171
+ export async function listTypes(functionId: number, opts?: ListTypesOpts): Promise<{ snapshots: TypeSnapshot[] }> {
172
+ return fetchJson(`/api/types/${functionId}`, {
173
+ env: opts?.env,
174
+ limit: opts?.limit?.toString(),
175
+ });
176
+ }
177
+
178
+ export interface GetTypeDiffOpts {
179
+ from?: number;
180
+ to?: number;
181
+ fromEnv?: string;
182
+ toEnv?: string;
183
+ }
184
+
185
+ export async function getTypeDiff(functionId: number, opts?: GetTypeDiffOpts): Promise<DiffResponse> {
186
+ return fetchJson(`/api/types/${functionId}/diff`, {
187
+ from: opts?.from?.toString(),
188
+ to: opts?.to?.toString(),
189
+ fromEnv: opts?.fromEnv,
190
+ toEnv: opts?.toEnv,
191
+ });
192
+ }
193
+
194
+ export interface CodegenOpts {
195
+ functionName?: string;
196
+ env?: string;
197
+ language?: string;
198
+ format?: string;
199
+ }
200
+
201
+ export async function fetchCodegen(opts?: CodegenOpts): Promise<{ types: string }> {
202
+ const pathStr = opts?.functionName
203
+ ? `/api/codegen/${encodeURIComponent(opts.functionName)}`
204
+ : "/api/codegen";
205
+
206
+ return fetchJson(pathStr, {
207
+ env: opts?.env,
208
+ language: opts?.language,
209
+ format: opts?.format,
210
+ });
211
+ }
212
+
213
+ export interface AnnotationParam {
214
+ name: string;
215
+ type: string;
216
+ }
217
+
218
+ export interface AnnotationEntry {
219
+ params: AnnotationParam[];
220
+ returnType: string;
221
+ }
222
+
223
+ export async function fetchAnnotations(opts?: {
224
+ env?: string;
225
+ language?: string;
226
+ }): Promise<{ annotations: Record<string, AnnotationEntry> }> {
227
+ return fetchJson("/api/codegen", {
228
+ env: opts?.env,
229
+ language: opts?.language,
230
+ format: "annotate",
231
+ });
232
+ }
233
+
234
+ export interface StubsResponse {
235
+ stubs: Record<string, { ts: string; python: string }>;
236
+ }
237
+
238
+ export async function fetchStubs(opts?: {
239
+ env?: string;
240
+ }): Promise<StubsResponse> {
241
+ return fetchJson("/api/codegen", {
242
+ env: opts?.env,
243
+ format: "stubs",
244
+ });
245
+ }
246
+
247
+ export interface MockRoute {
248
+ method: string;
249
+ path: string;
250
+ functionName: string;
251
+ module: string;
252
+ sampleInput: unknown;
253
+ sampleOutput: unknown;
254
+ observedAt: string;
255
+ }
256
+
257
+ export async function fetchMockConfig(): Promise<{ routes: MockRoute[] }> {
258
+ return fetchJson("/api/mock-config");
259
+ }
260
+
261
+ // ── Snapshot (for trickle check) ──
262
+
263
+ export interface SnapshotFunction {
264
+ name: string;
265
+ module: string;
266
+ env?: string;
267
+ argsType: unknown;
268
+ returnType: unknown;
269
+ }
270
+
271
+ export interface CheckSnapshot {
272
+ version: number;
273
+ createdAt: string;
274
+ functions: SnapshotFunction[];
275
+ }
276
+
277
+ export async function fetchSnapshot(opts?: { env?: string }): Promise<CheckSnapshot> {
278
+ return fetchJson("/api/codegen", {
279
+ format: "snapshot",
280
+ env: opts?.env,
281
+ });
282
+ }
283
+
284
+ // ── OpenAPI ──
285
+
286
+ export interface OpenApiOpts {
287
+ env?: string;
288
+ title?: string;
289
+ version?: string;
290
+ serverUrl?: string;
291
+ }
292
+
293
+ export async function fetchOpenApiSpec(opts?: OpenApiOpts): Promise<object> {
294
+ return fetchJson("/api/codegen", {
295
+ format: "openapi",
296
+ env: opts?.env,
297
+ title: opts?.title,
298
+ version: opts?.version,
299
+ serverUrl: opts?.serverUrl,
300
+ });
301
+ }
302
+
303
+ // ── Diff Report ──
304
+
305
+ export interface DiffReportEntry {
306
+ functionName: string;
307
+ module: string;
308
+ language: string;
309
+ from: { id: number; env: string; observed_at: string; type_hash: string };
310
+ to: { id: number; env: string; observed_at: string; type_hash: string };
311
+ diffs: TypeDiff[];
312
+ }
313
+
314
+ export interface DiffReportResponse {
315
+ mode: "temporal" | "cross-env";
316
+ entries: DiffReportEntry[];
317
+ total: number;
318
+ since?: string | null;
319
+ env?: string | null;
320
+ env1?: string;
321
+ env2?: string;
322
+ }
323
+
324
+ export interface DiffReportOpts {
325
+ since?: string;
326
+ env?: string;
327
+ env1?: string;
328
+ env2?: string;
329
+ }
330
+
331
+ export async function fetchDiffReport(opts?: DiffReportOpts): Promise<DiffReportResponse> {
332
+ return fetchJson("/api/diff", {
333
+ since: opts?.since,
334
+ env: opts?.env,
335
+ env1: opts?.env1,
336
+ env2: opts?.env2,
337
+ });
338
+ }
339
+
340
+ export function tailEvents(
341
+ onEvent: (event: TailEvent) => void,
342
+ filter?: string
343
+ ): () => void {
344
+ const base = backendUrl();
345
+ const url = new URL("/api/tail", base);
346
+ if (filter) {
347
+ url.searchParams.set("filter", filter);
348
+ }
349
+
350
+ const mod = url.protocol === "https:" ? https : http;
351
+ let destroyed = false;
352
+ let currentReq: http.ClientRequest | null = null;
353
+
354
+ function connect() {
355
+ if (destroyed) return;
356
+
357
+ const req = mod.get(url.toString(), (res) => {
358
+ let buffer = "";
359
+
360
+ res.on("data", (chunk: Buffer) => {
361
+ buffer += chunk.toString();
362
+ const lines = buffer.split("\n");
363
+ buffer = lines.pop() || "";
364
+
365
+ let currentEvent = "";
366
+ for (const line of lines) {
367
+ if (line.startsWith("event: ")) {
368
+ currentEvent = line.slice(7).trim();
369
+ } else if (line.startsWith("data: ")) {
370
+ const dataStr = line.slice(6).trim();
371
+ try {
372
+ const data = JSON.parse(dataStr);
373
+ onEvent({ event: currentEvent, data });
374
+ } catch {
375
+ // Ignore invalid JSON
376
+ }
377
+ currentEvent = "";
378
+ }
379
+ }
380
+ });
381
+
382
+ res.on("end", () => {
383
+ if (!destroyed) {
384
+ // Reconnect after a delay
385
+ setTimeout(connect, 2000);
386
+ }
387
+ });
388
+ });
389
+
390
+ req.on("error", () => {
391
+ if (!destroyed) {
392
+ setTimeout(connect, 3000);
393
+ }
394
+ });
395
+
396
+ currentReq = req;
397
+ }
398
+
399
+ connect();
400
+
401
+ return () => {
402
+ destroyed = true;
403
+ if (currentReq) {
404
+ currentReq.destroy();
405
+ }
406
+ };
407
+ }