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.
- package/dist/api-client.d.ts +208 -0
- package/dist/api-client.js +237 -0
- package/dist/commands/annotate.d.ts +6 -0
- package/dist/commands/annotate.js +433 -0
- package/dist/commands/audit.d.ts +7 -0
- package/dist/commands/audit.js +82 -0
- package/dist/commands/auto.d.ts +8 -0
- package/dist/commands/auto.js +268 -0
- package/dist/commands/capture.d.ts +14 -0
- package/dist/commands/capture.js +271 -0
- package/dist/commands/check.d.ts +6 -0
- package/dist/commands/check.js +408 -0
- package/dist/commands/codegen.d.ts +21 -0
- package/dist/commands/codegen.js +129 -0
- package/dist/commands/coverage.d.ts +13 -0
- package/dist/commands/coverage.js +126 -0
- package/dist/commands/dashboard.d.ts +1 -0
- package/dist/commands/dashboard.js +83 -0
- package/dist/commands/dev.d.ts +14 -0
- package/dist/commands/dev.js +319 -0
- package/dist/commands/diff.d.ts +7 -0
- package/dist/commands/diff.js +79 -0
- package/dist/commands/docs.d.ts +13 -0
- package/dist/commands/docs.js +383 -0
- package/dist/commands/errors.d.ts +7 -0
- package/dist/commands/errors.js +180 -0
- package/dist/commands/export.d.ts +18 -0
- package/dist/commands/export.js +238 -0
- package/dist/commands/functions.d.ts +6 -0
- package/dist/commands/functions.js +71 -0
- package/dist/commands/infer.d.ts +14 -0
- package/dist/commands/infer.js +275 -0
- package/dist/commands/init.d.ts +5 -0
- package/dist/commands/init.js +395 -0
- package/dist/commands/mock.d.ts +5 -0
- package/dist/commands/mock.js +232 -0
- package/dist/commands/openapi.d.ts +8 -0
- package/dist/commands/openapi.js +82 -0
- package/dist/commands/overview.d.ts +11 -0
- package/dist/commands/overview.js +266 -0
- package/dist/commands/pack.d.ts +11 -0
- package/dist/commands/pack.js +133 -0
- package/dist/commands/proxy.d.ts +13 -0
- package/dist/commands/proxy.js +312 -0
- package/dist/commands/replay.d.ts +14 -0
- package/dist/commands/replay.js +289 -0
- package/dist/commands/run.d.ts +17 -0
- package/dist/commands/run.js +997 -0
- package/dist/commands/sample.d.ts +13 -0
- package/dist/commands/sample.js +260 -0
- package/dist/commands/search.d.ts +5 -0
- package/dist/commands/search.js +80 -0
- package/dist/commands/stubs.d.ts +6 -0
- package/dist/commands/stubs.js +187 -0
- package/dist/commands/tail.d.ts +4 -0
- package/dist/commands/tail.js +76 -0
- package/dist/commands/test-gen.d.ts +13 -0
- package/dist/commands/test-gen.js +237 -0
- package/dist/commands/trace.d.ts +14 -0
- package/dist/commands/trace.js +417 -0
- package/dist/commands/types.d.ts +7 -0
- package/dist/commands/types.js +128 -0
- package/dist/commands/unpack.d.ts +11 -0
- package/dist/commands/unpack.js +166 -0
- package/dist/commands/validate.d.ts +13 -0
- package/dist/commands/validate.js +310 -0
- package/dist/commands/watch.d.ts +9 -0
- package/dist/commands/watch.js +267 -0
- package/dist/config.d.ts +1 -0
- package/dist/config.js +66 -0
- package/dist/formatters/diff-formatter.d.ts +5 -0
- package/dist/formatters/diff-formatter.js +43 -0
- package/dist/formatters/type-formatter.d.ts +22 -0
- package/dist/formatters/type-formatter.js +135 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +419 -0
- package/dist/local-codegen.d.ts +22 -0
- package/dist/local-codegen.js +762 -0
- package/dist/ui/badges.d.ts +16 -0
- package/dist/ui/badges.js +71 -0
- package/dist/ui/helpers.d.ts +13 -0
- package/dist/ui/helpers.js +85 -0
- package/package.json +23 -0
- package/src/api-client.ts +407 -0
- package/src/commands/annotate.ts +450 -0
- package/src/commands/audit.ts +103 -0
- package/src/commands/auto.ts +268 -0
- package/src/commands/capture.ts +257 -0
- package/src/commands/check.ts +437 -0
- package/src/commands/codegen.ts +128 -0
- package/src/commands/coverage.ts +170 -0
- package/src/commands/dashboard.ts +46 -0
- package/src/commands/dev.ts +323 -0
- package/src/commands/diff.ts +99 -0
- package/src/commands/docs.ts +392 -0
- package/src/commands/errors.ts +205 -0
- package/src/commands/export.ts +287 -0
- package/src/commands/functions.ts +81 -0
- package/src/commands/infer.ts +260 -0
- package/src/commands/init.ts +419 -0
- package/src/commands/mock.ts +220 -0
- package/src/commands/openapi.ts +53 -0
- package/src/commands/overview.ts +310 -0
- package/src/commands/pack.ts +139 -0
- package/src/commands/proxy.ts +314 -0
- package/src/commands/replay.ts +356 -0
- package/src/commands/run.ts +1190 -0
- package/src/commands/sample.ts +259 -0
- package/src/commands/search.ts +107 -0
- package/src/commands/stubs.ts +211 -0
- package/src/commands/tail.ts +94 -0
- package/src/commands/test-gen.ts +236 -0
- package/src/commands/trace.ts +440 -0
- package/src/commands/types.ts +161 -0
- package/src/commands/unpack.ts +179 -0
- package/src/commands/validate.ts +368 -0
- package/src/commands/watch.ts +277 -0
- package/src/config.ts +38 -0
- package/src/formatters/diff-formatter.ts +51 -0
- package/src/formatters/type-formatter.ts +161 -0
- package/src/index.ts +454 -0
- package/src/local-codegen.ts +859 -0
- package/src/ui/badges.ts +66 -0
- package/src/ui/helpers.ts +80 -0
- 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
|
+
}
|