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,128 @@
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.typesCommand = typesCommand;
7
+ const chalk_1 = __importDefault(require("chalk"));
8
+ const api_client_1 = require("../api-client");
9
+ const type_formatter_1 = require("../formatters/type-formatter");
10
+ const diff_formatter_1 = require("../formatters/diff-formatter");
11
+ const badges_1 = require("../ui/badges");
12
+ async function typesCommand(functionName, opts) {
13
+ // Look up function by name (partial match)
14
+ const result = await (0, api_client_1.listFunctions)({ search: functionName });
15
+ const { functions } = result;
16
+ if (functions.length === 0) {
17
+ console.log(chalk_1.default.yellow(`\n No function found matching "${functionName}".\n`));
18
+ return;
19
+ }
20
+ // Prefer exact match, then first partial match
21
+ const exactMatch = functions.find((f) => f.function_name === functionName || f.function_name.toLowerCase() === functionName.toLowerCase());
22
+ const fn = exactMatch || functions[0];
23
+ if (!exactMatch && functions.length > 1) {
24
+ console.log(chalk_1.default.gray(`\n Multiple matches found, showing results for "${chalk_1.default.white(fn.function_name)}"`));
25
+ console.log(chalk_1.default.gray(` Other matches: ${functions
26
+ .slice(1, 5)
27
+ .map((f) => f.function_name)
28
+ .join(", ")}${functions.length > 5 ? "..." : ""}\n`));
29
+ }
30
+ // Diff mode
31
+ if (opts.diff) {
32
+ await showDiff(fn.id, fn.function_name, opts);
33
+ return;
34
+ }
35
+ // Normal mode: show type snapshots
36
+ const typesResult = await (0, api_client_1.listTypes)(fn.id, { env: opts.env });
37
+ const { snapshots } = typesResult;
38
+ if (snapshots.length === 0) {
39
+ console.log(chalk_1.default.yellow(`\n No type snapshots found for "${fn.function_name}".\n`));
40
+ return;
41
+ }
42
+ console.log("");
43
+ console.log(chalk_1.default.white.bold(` ${fn.function_name}`) + chalk_1.default.gray(` (${fn.module})`));
44
+ console.log(chalk_1.default.gray(" " + "─".repeat(50)));
45
+ for (const snapshot of snapshots) {
46
+ console.log("");
47
+ console.log(` ${(0, badges_1.envBadge)(snapshot.env)} ${(0, badges_1.timeBadge)(snapshot.observed_at)}`);
48
+ console.log("");
49
+ // Display function signature
50
+ console.log(chalk_1.default.gray(" args: ") + (0, type_formatter_1.formatType)(snapshot.args_type, 4));
51
+ console.log(chalk_1.default.gray(" returns: ") + (0, type_formatter_1.formatType)(snapshot.return_type, 4));
52
+ if (snapshot.sample_input !== undefined && snapshot.sample_input !== null) {
53
+ console.log("");
54
+ console.log(chalk_1.default.gray(" sample input:"));
55
+ console.log(chalk_1.default.gray(" ") + colorizeJson(snapshot.sample_input, 4));
56
+ }
57
+ if (snapshot.sample_output !== undefined && snapshot.sample_output !== null) {
58
+ console.log(chalk_1.default.gray(" sample output:"));
59
+ console.log(chalk_1.default.gray(" ") + colorizeJson(snapshot.sample_output, 4));
60
+ }
61
+ }
62
+ console.log("");
63
+ }
64
+ async function showDiff(functionId, functionName, opts) {
65
+ try {
66
+ let diffResult;
67
+ if (opts.env1 && opts.env2) {
68
+ diffResult = await (0, api_client_1.getTypeDiff)(functionId, {
69
+ fromEnv: opts.env1,
70
+ toEnv: opts.env2,
71
+ });
72
+ }
73
+ else {
74
+ // Get latest two snapshots and diff by their IDs
75
+ const typesResult = await (0, api_client_1.listTypes)(functionId, { limit: 2 });
76
+ const { snapshots } = typesResult;
77
+ if (snapshots.length < 2) {
78
+ console.log(chalk_1.default.yellow(`\n Not enough snapshots to diff for "${functionName}".`));
79
+ console.log(chalk_1.default.gray(" Need at least 2 snapshots. Try specifying --env1 and --env2.\n"));
80
+ return;
81
+ }
82
+ diffResult = await (0, api_client_1.getTypeDiff)(functionId, {
83
+ from: snapshots[1].id,
84
+ to: snapshots[0].id,
85
+ });
86
+ }
87
+ console.log("");
88
+ console.log(chalk_1.default.white.bold(` Type diff for ${functionName}`));
89
+ console.log(chalk_1.default.gray(" " + "─".repeat(50)));
90
+ console.log("");
91
+ console.log(chalk_1.default.gray(" from: ") +
92
+ (0, badges_1.envBadge)(diffResult.from.env) +
93
+ chalk_1.default.gray(" " + diffResult.from.observed_at));
94
+ console.log(chalk_1.default.gray(" to: ") +
95
+ (0, badges_1.envBadge)(diffResult.to.env) +
96
+ chalk_1.default.gray(" " + diffResult.to.observed_at));
97
+ console.log("");
98
+ if (diffResult.diffs.length === 0) {
99
+ console.log(chalk_1.default.green(" No type differences found.\n"));
100
+ }
101
+ else {
102
+ console.log((0, diff_formatter_1.formatDiffs)(diffResult.diffs));
103
+ console.log("");
104
+ }
105
+ }
106
+ catch (err) {
107
+ if (err instanceof Error) {
108
+ console.error(chalk_1.default.red(`\n Error: ${err.message}\n`));
109
+ }
110
+ }
111
+ }
112
+ function colorizeJson(value, indent = 0) {
113
+ const json = JSON.stringify(value, null, 2);
114
+ if (!json)
115
+ return chalk_1.default.gray("null");
116
+ return json
117
+ .split("\n")
118
+ .map((line, i) => {
119
+ const padded = i === 0 ? line : " ".repeat(indent) + line;
120
+ return padded
121
+ .replace(/"([^"]+)":/g, (_, key) => `${chalk_1.default.white(`"${key}"`)}:`)
122
+ .replace(/: "([^"]*)"/g, (_, val) => `: ${chalk_1.default.green(`"${val}"`)}`)
123
+ .replace(/: (\d+\.?\d*)/g, (_, val) => `: ${chalk_1.default.yellow(val)}`)
124
+ .replace(/: (true|false)/g, (_, val) => `: ${chalk_1.default.blue(val)}`)
125
+ .replace(/: (null)/g, (_, val) => `: ${chalk_1.default.gray(val)}`);
126
+ })
127
+ .join("\n");
128
+ }
@@ -0,0 +1,11 @@
1
+ export interface UnpackOptions {
2
+ env?: string;
3
+ dryRun?: boolean;
4
+ }
5
+ /**
6
+ * `trickle unpack <file>` — Import types from a packed bundle into the backend.
7
+ *
8
+ * Reads a .trickle.json bundle (created by `trickle pack`) and ingests
9
+ * all functions and their type snapshots into the backend.
10
+ */
11
+ export declare function unpackCommand(file: string, opts: UnpackOptions): Promise<void>;
@@ -0,0 +1,166 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.unpackCommand = unpackCommand;
40
+ const fs = __importStar(require("fs"));
41
+ const crypto = __importStar(require("crypto"));
42
+ const chalk_1 = __importDefault(require("chalk"));
43
+ const config_1 = require("../config");
44
+ /**
45
+ * `trickle unpack <file>` — Import types from a packed bundle into the backend.
46
+ *
47
+ * Reads a .trickle.json bundle (created by `trickle pack`) and ingests
48
+ * all functions and their type snapshots into the backend.
49
+ */
50
+ async function unpackCommand(file, opts) {
51
+ const backendUrl = (0, config_1.getBackendUrl)();
52
+ // Read and parse the bundle
53
+ if (!fs.existsSync(file)) {
54
+ console.error(chalk_1.default.red(`\n File not found: ${file}\n`));
55
+ process.exit(1);
56
+ }
57
+ let bundle;
58
+ try {
59
+ const content = fs.readFileSync(file, "utf-8");
60
+ bundle = JSON.parse(content);
61
+ }
62
+ catch {
63
+ console.error(chalk_1.default.red("\n Invalid bundle file — expected JSON.\n"));
64
+ process.exit(1);
65
+ }
66
+ // Validate bundle structure
67
+ if (!bundle.version || !bundle.functions || !Array.isArray(bundle.functions)) {
68
+ console.error(chalk_1.default.red("\n Invalid bundle format — missing required fields.\n"));
69
+ process.exit(1);
70
+ }
71
+ if (bundle.version !== 1) {
72
+ console.error(chalk_1.default.red(`\n Unsupported bundle version: ${bundle.version}\n`));
73
+ process.exit(1);
74
+ }
75
+ console.log("");
76
+ console.log(chalk_1.default.bold(" trickle unpack"));
77
+ console.log(chalk_1.default.gray(" " + "─".repeat(50)));
78
+ console.log(chalk_1.default.gray(` File: ${file}`));
79
+ console.log(chalk_1.default.gray(` Created: ${bundle.createdAt}`));
80
+ console.log(chalk_1.default.gray(` Source: ${bundle.source}`));
81
+ console.log(chalk_1.default.gray(` Contains: ${bundle.functions.length} functions, ${bundle.stats.totalSnapshots} snapshots`));
82
+ console.log(chalk_1.default.gray(" " + "─".repeat(50)));
83
+ if (opts.dryRun) {
84
+ console.log("");
85
+ console.log(chalk_1.default.yellow(" Dry run — listing contents without importing:"));
86
+ console.log("");
87
+ for (const fn of bundle.functions) {
88
+ console.log(chalk_1.default.white(` ${fn.functionName}`) +
89
+ chalk_1.default.gray(` (${fn.module}, ${fn.language}, ${fn.snapshots.length} snapshot${fn.snapshots.length === 1 ? "" : "s"})`));
90
+ }
91
+ console.log("");
92
+ console.log(chalk_1.default.gray(` ${bundle.functions.length} functions would be imported.`));
93
+ console.log(chalk_1.default.gray(" Remove --dry-run to import.\n"));
94
+ return;
95
+ }
96
+ // Check backend connectivity
97
+ try {
98
+ const res = await fetch(`${backendUrl}/api/health`, { signal: AbortSignal.timeout(3000) });
99
+ if (!res.ok)
100
+ throw new Error("not ok");
101
+ }
102
+ catch {
103
+ console.error(chalk_1.default.red(`\n Cannot reach trickle backend at ${chalk_1.default.bold(backendUrl)}\n`));
104
+ process.exit(1);
105
+ }
106
+ console.log("");
107
+ // Ingest each function's snapshots
108
+ let imported = 0;
109
+ let skipped = 0;
110
+ let errors = 0;
111
+ for (const fn of bundle.functions) {
112
+ // Use the latest snapshot for ingest
113
+ const snapshot = fn.snapshots[0];
114
+ if (!snapshot) {
115
+ skipped++;
116
+ continue;
117
+ }
118
+ const env = opts.env || snapshot.env || fn.environment;
119
+ const typeHash = snapshot.typeHash || computeTypeHash(snapshot.argsType, snapshot.returnType);
120
+ const payload = {
121
+ functionName: fn.functionName,
122
+ module: fn.module,
123
+ language: fn.language,
124
+ environment: env,
125
+ typeHash,
126
+ argsType: snapshot.argsType,
127
+ returnType: snapshot.returnType,
128
+ sampleInput: snapshot.sampleInput,
129
+ sampleOutput: snapshot.sampleOutput,
130
+ };
131
+ try {
132
+ const res = await fetch(`${backendUrl}/api/ingest`, {
133
+ method: "POST",
134
+ headers: { "Content-Type": "application/json" },
135
+ body: JSON.stringify(payload),
136
+ signal: AbortSignal.timeout(5000),
137
+ });
138
+ if (!res.ok)
139
+ throw new Error(`HTTP ${res.status}`);
140
+ imported++;
141
+ console.log(chalk_1.default.green(" ✓ ") + chalk_1.default.white(fn.functionName));
142
+ }
143
+ catch {
144
+ errors++;
145
+ console.log(chalk_1.default.red(" ✗ ") + chalk_1.default.white(fn.functionName) + chalk_1.default.gray(" (failed)"));
146
+ }
147
+ }
148
+ console.log("");
149
+ console.log(chalk_1.default.gray(" " + "─".repeat(50)));
150
+ if (imported > 0) {
151
+ console.log(chalk_1.default.green(` ${imported} functions imported successfully`));
152
+ }
153
+ if (skipped > 0) {
154
+ console.log(chalk_1.default.yellow(` ${skipped} skipped (no snapshots)`));
155
+ }
156
+ if (errors > 0) {
157
+ console.log(chalk_1.default.red(` ${errors} failed`));
158
+ }
159
+ console.log("");
160
+ console.log(chalk_1.default.gray(" Run ") + chalk_1.default.white("trickle overview") + chalk_1.default.gray(" to see imported types."));
161
+ console.log("");
162
+ }
163
+ function computeTypeHash(argsType, returnType) {
164
+ const data = JSON.stringify({ a: argsType, r: returnType });
165
+ return crypto.createHash("sha256").update(data).digest("hex").slice(0, 16);
166
+ }
@@ -0,0 +1,13 @@
1
+ export interface ValidateOptions {
2
+ header?: string[];
3
+ body?: string;
4
+ env?: string;
5
+ strict?: boolean;
6
+ }
7
+ /**
8
+ * `trickle validate <method> <url>` — Validate a live API response against observed types.
9
+ *
10
+ * Makes an HTTP request, infers types from the response, fetches the stored
11
+ * type for that route from the backend, and reports any mismatches.
12
+ */
13
+ export declare function validateCommand(method: string, url: string, opts: ValidateOptions): Promise<void>;
@@ -0,0 +1,310 @@
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.validateCommand = validateCommand;
7
+ const chalk_1 = __importDefault(require("chalk"));
8
+ const config_1 = require("../config");
9
+ /**
10
+ * `trickle validate <method> <url>` — Validate a live API response against observed types.
11
+ *
12
+ * Makes an HTTP request, infers types from the response, fetches the stored
13
+ * type for that route from the backend, and reports any mismatches.
14
+ */
15
+ async function validateCommand(method, url, opts) {
16
+ const backendUrl = (0, config_1.getBackendUrl)();
17
+ const httpMethod = method.toUpperCase();
18
+ const validMethods = ["GET", "POST", "PUT", "PATCH", "DELETE"];
19
+ if (!validMethods.includes(httpMethod)) {
20
+ console.error(chalk_1.default.red(`\n Invalid HTTP method: ${method}\n`));
21
+ process.exit(1);
22
+ }
23
+ let parsedUrl;
24
+ try {
25
+ parsedUrl = new URL(url);
26
+ }
27
+ catch {
28
+ console.error(chalk_1.default.red(`\n Invalid URL: ${url}\n`));
29
+ process.exit(1);
30
+ }
31
+ // Check backend
32
+ try {
33
+ const res = await fetch(`${backendUrl}/api/health`, { signal: AbortSignal.timeout(3000) });
34
+ if (!res.ok)
35
+ throw new Error("not ok");
36
+ }
37
+ catch {
38
+ console.error(chalk_1.default.red(`\n Cannot reach trickle backend at ${chalk_1.default.bold(backendUrl)}\n`));
39
+ process.exit(1);
40
+ }
41
+ // Build request headers
42
+ const headers = { Accept: "application/json" };
43
+ if (opts.header) {
44
+ for (const h of opts.header) {
45
+ const colonIdx = h.indexOf(":");
46
+ if (colonIdx === -1) {
47
+ console.error(chalk_1.default.red(`\n Invalid header: ${h}\n`));
48
+ process.exit(1);
49
+ }
50
+ headers[h.slice(0, colonIdx).trim()] = h.slice(colonIdx + 1).trim();
51
+ }
52
+ }
53
+ let reqBody;
54
+ if (opts.body) {
55
+ reqBody = opts.body;
56
+ headers["Content-Type"] = headers["Content-Type"] || "application/json";
57
+ }
58
+ console.log("");
59
+ console.log(chalk_1.default.bold(" trickle validate"));
60
+ console.log(chalk_1.default.gray(" " + "─".repeat(50)));
61
+ console.log(chalk_1.default.gray(` ${chalk_1.default.bold(httpMethod)} ${url}`));
62
+ // Make the request
63
+ let response;
64
+ try {
65
+ response = await fetch(url, {
66
+ method: httpMethod,
67
+ headers,
68
+ body: reqBody,
69
+ signal: AbortSignal.timeout(30000),
70
+ });
71
+ }
72
+ catch (err) {
73
+ const msg = err instanceof Error ? err.message : "Unknown error";
74
+ console.error(chalk_1.default.red(`\n Request failed: ${msg}\n`));
75
+ process.exit(1);
76
+ }
77
+ const status = response.status;
78
+ const statusColor = status < 400 ? chalk_1.default.green : chalk_1.default.red;
79
+ console.log(chalk_1.default.gray(` Status: `) + statusColor(`${status} ${response.statusText}`));
80
+ // Parse response
81
+ const resText = await response.text();
82
+ let resJson;
83
+ const contentType = response.headers.get("content-type") || "";
84
+ if (contentType.includes("json") && resText.length > 0) {
85
+ try {
86
+ resJson = JSON.parse(resText);
87
+ }
88
+ catch {
89
+ console.error(chalk_1.default.red("\n Response is not valid JSON.\n"));
90
+ process.exit(1);
91
+ }
92
+ }
93
+ else {
94
+ console.error(chalk_1.default.red("\n Response is not JSON.\n"));
95
+ process.exit(1);
96
+ }
97
+ // Normalize path and find stored types
98
+ const routePath = normalizePath(parsedUrl.pathname);
99
+ const functionName = `${httpMethod} ${routePath}`;
100
+ console.log(chalk_1.default.gray(` Route: `) + chalk_1.default.white(functionName));
101
+ // Fetch stored function from backend
102
+ const funcsRes = await fetch(`${backendUrl}/api/functions?q=${encodeURIComponent(functionName)}&limit=100`, { signal: AbortSignal.timeout(5000) });
103
+ const funcsData = await funcsRes.json();
104
+ const matchedFunc = funcsData.functions.find((f) => f.function_name === functionName);
105
+ if (!matchedFunc) {
106
+ console.log(chalk_1.default.gray(" " + "─".repeat(50)));
107
+ console.log(chalk_1.default.yellow(" No previously observed types for this route."));
108
+ console.log(chalk_1.default.gray(" Run ") + chalk_1.default.white("trickle capture") + chalk_1.default.gray(" first to establish a baseline.\n"));
109
+ process.exit(1);
110
+ }
111
+ // Fetch latest type snapshot
112
+ const envQuery = opts.env ? `&env=${encodeURIComponent(opts.env)}` : "";
113
+ const typesRes = await fetch(`${backendUrl}/api/types/${matchedFunc.id}?limit=1${envQuery}`, { signal: AbortSignal.timeout(5000) });
114
+ const typesData = await typesRes.json();
115
+ if (!typesData.snapshots || typesData.snapshots.length === 0) {
116
+ console.log(chalk_1.default.yellow(" No type snapshots found for this route.\n"));
117
+ process.exit(1);
118
+ }
119
+ const snapshot = typesData.snapshots[0];
120
+ let expectedType;
121
+ try {
122
+ expectedType = (typeof snapshot.return_type === 'string'
123
+ ? JSON.parse(snapshot.return_type)
124
+ : snapshot.return_type);
125
+ }
126
+ catch {
127
+ console.error(chalk_1.default.red(" Cannot parse stored type snapshot.\n"));
128
+ process.exit(1);
129
+ }
130
+ console.log(chalk_1.default.gray(` Baseline: `) + chalk_1.default.gray(`observed ${snapshot.observed_at}`));
131
+ console.log(chalk_1.default.gray(" " + "─".repeat(50)));
132
+ // Infer type from actual response
133
+ const actualType = jsonToTypeNode(resJson);
134
+ // Compare
135
+ const mismatches = [];
136
+ compareTypes(expectedType, actualType, "response", mismatches, opts.strict || false);
137
+ if (mismatches.length === 0) {
138
+ console.log(chalk_1.default.green("\n ✓ Response matches observed type shape\n"));
139
+ console.log(chalk_1.default.gray(" All fields present with expected types.\n"));
140
+ }
141
+ else {
142
+ console.log(chalk_1.default.red(`\n ✗ ${mismatches.length} mismatch${mismatches.length === 1 ? "" : "es"} found\n`));
143
+ for (const m of mismatches) {
144
+ switch (m.issue) {
145
+ case "missing":
146
+ console.log(chalk_1.default.red(" MISSING ") +
147
+ chalk_1.default.white(m.path) +
148
+ chalk_1.default.gray(` (expected ${m.expected})`));
149
+ break;
150
+ case "extra":
151
+ console.log(chalk_1.default.yellow(" EXTRA ") +
152
+ chalk_1.default.white(m.path) +
153
+ chalk_1.default.gray(` (${m.actual}, not in observed types)`));
154
+ break;
155
+ case "type_mismatch":
156
+ console.log(chalk_1.default.red(" TYPE ") +
157
+ chalk_1.default.white(m.path) +
158
+ chalk_1.default.gray(` (expected ${m.expected}, got ${m.actual})`));
159
+ break;
160
+ case "kind_mismatch":
161
+ console.log(chalk_1.default.red(" SHAPE ") +
162
+ chalk_1.default.white(m.path) +
163
+ chalk_1.default.gray(` (expected ${m.expected}, got ${m.actual})`));
164
+ break;
165
+ }
166
+ }
167
+ const errors = mismatches.filter((m) => m.issue !== "extra");
168
+ const warnings = mismatches.filter((m) => m.issue === "extra");
169
+ console.log("");
170
+ if (errors.length > 0) {
171
+ console.log(chalk_1.default.red(` ${errors.length} error${errors.length === 1 ? "" : "s"}`) +
172
+ (warnings.length > 0 ? chalk_1.default.yellow(`, ${warnings.length} warning${warnings.length === 1 ? "" : "s"}`) : ""));
173
+ }
174
+ else {
175
+ console.log(chalk_1.default.yellow(` ${warnings.length} warning${warnings.length === 1 ? "" : "s"} (extra fields only)`));
176
+ }
177
+ console.log("");
178
+ if (errors.length > 0) {
179
+ process.exitCode = 1;
180
+ }
181
+ }
182
+ }
183
+ /**
184
+ * Recursively compare expected vs actual TypeNode.
185
+ */
186
+ function compareTypes(expected, actual, path, mismatches, strict) {
187
+ // Kind mismatch
188
+ if (expected.kind !== actual.kind) {
189
+ // Special case: unknown expected matches anything
190
+ if (expected.kind === "unknown")
191
+ return;
192
+ // Special case: expected primitive null matches any actual (nullable)
193
+ if (expected.kind === "primitive" && expected.name === "null")
194
+ return;
195
+ mismatches.push({
196
+ path,
197
+ issue: "kind_mismatch",
198
+ expected: describeType(expected),
199
+ actual: describeType(actual),
200
+ });
201
+ return;
202
+ }
203
+ switch (expected.kind) {
204
+ case "primitive": {
205
+ const expectedName = expected.name;
206
+ const actualName = actual.name;
207
+ if (expectedName !== actualName) {
208
+ mismatches.push({
209
+ path,
210
+ issue: "type_mismatch",
211
+ expected: expectedName,
212
+ actual: actualName,
213
+ });
214
+ }
215
+ break;
216
+ }
217
+ case "object": {
218
+ const expectedProps = expected.properties;
219
+ const actualProps = actual.properties;
220
+ // Check for missing fields
221
+ for (const key of Object.keys(expectedProps)) {
222
+ if (!(key in actualProps)) {
223
+ mismatches.push({
224
+ path: `${path}.${key}`,
225
+ issue: "missing",
226
+ expected: describeType(expectedProps[key]),
227
+ });
228
+ }
229
+ else {
230
+ compareTypes(expectedProps[key], actualProps[key], `${path}.${key}`, mismatches, strict);
231
+ }
232
+ }
233
+ // Check for extra fields (warnings in non-strict, errors in strict)
234
+ if (strict) {
235
+ for (const key of Object.keys(actualProps)) {
236
+ if (!(key in expectedProps)) {
237
+ mismatches.push({
238
+ path: `${path}.${key}`,
239
+ issue: "extra",
240
+ actual: describeType(actualProps[key]),
241
+ });
242
+ }
243
+ }
244
+ }
245
+ break;
246
+ }
247
+ case "array": {
248
+ const expectedElement = expected.element;
249
+ const actualElement = actual.element;
250
+ if (expectedElement.kind !== "unknown" && actualElement.kind !== "unknown") {
251
+ compareTypes(expectedElement, actualElement, `${path}[]`, mismatches, strict);
252
+ }
253
+ break;
254
+ }
255
+ case "union": {
256
+ // For unions, check that actual matches at least one member
257
+ // Simplified: just skip deep comparison for unions
258
+ break;
259
+ }
260
+ }
261
+ }
262
+ function describeType(node) {
263
+ switch (node.kind) {
264
+ case "primitive":
265
+ return node.name;
266
+ case "object":
267
+ return "object";
268
+ case "array":
269
+ return `${describeType(node.element)}[]`;
270
+ case "union":
271
+ return node.members.map(describeType).join(" | ");
272
+ default:
273
+ return node.kind;
274
+ }
275
+ }
276
+ function jsonToTypeNode(value) {
277
+ if (value === null)
278
+ return { kind: "primitive", name: "null" };
279
+ if (value === undefined)
280
+ return { kind: "primitive", name: "undefined" };
281
+ switch (typeof value) {
282
+ case "string": return { kind: "primitive", name: "string" };
283
+ case "number": return { kind: "primitive", name: "number" };
284
+ case "boolean": return { kind: "primitive", name: "boolean" };
285
+ }
286
+ if (Array.isArray(value)) {
287
+ if (value.length === 0)
288
+ return { kind: "array", element: { kind: "unknown" } };
289
+ return { kind: "array", element: jsonToTypeNode(value[0]) };
290
+ }
291
+ const obj = value;
292
+ const properties = {};
293
+ for (const [key, val] of Object.entries(obj)) {
294
+ properties[key] = jsonToTypeNode(val);
295
+ }
296
+ return { kind: "object", properties };
297
+ }
298
+ function normalizePath(urlPath) {
299
+ return urlPath.split("/").map((part, i) => {
300
+ if (!part)
301
+ return part;
302
+ if (/^\d+$/.test(part))
303
+ return ":id";
304
+ if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(part))
305
+ return ":id";
306
+ if (/^[0-9a-f]{16,}$/i.test(part) && i > 1)
307
+ return ":id";
308
+ return part;
309
+ }).join("/");
310
+ }
@@ -0,0 +1,9 @@
1
+ export interface WatchOptions {
2
+ dir?: string;
3
+ env?: string;
4
+ interval?: string;
5
+ }
6
+ /**
7
+ * `trickle watch` — Watch for new type observations and auto-regenerate type files.
8
+ */
9
+ export declare function watchCommand(opts: WatchOptions): Promise<void>;