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,126 @@
|
|
|
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.coverageCommand = coverageCommand;
|
|
7
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
+
const config_1 = require("../config");
|
|
9
|
+
/**
|
|
10
|
+
* `trickle coverage` — Type observation health report.
|
|
11
|
+
*
|
|
12
|
+
* Shows per-function type coverage, staleness, variant counts,
|
|
13
|
+
* error counts, and an overall health score. Useful for CI gates.
|
|
14
|
+
*/
|
|
15
|
+
async function coverageCommand(opts) {
|
|
16
|
+
const backendUrl = (0, config_1.getBackendUrl)();
|
|
17
|
+
// Fetch coverage data
|
|
18
|
+
const url = new URL("/api/coverage", backendUrl);
|
|
19
|
+
if (opts.env)
|
|
20
|
+
url.searchParams.set("env", opts.env);
|
|
21
|
+
if (opts.staleHours)
|
|
22
|
+
url.searchParams.set("stale_hours", opts.staleHours);
|
|
23
|
+
let data;
|
|
24
|
+
try {
|
|
25
|
+
const res = await fetch(url.toString());
|
|
26
|
+
if (!res.ok) {
|
|
27
|
+
const body = await res.text();
|
|
28
|
+
throw new Error(`HTTP ${res.status}: ${body}`);
|
|
29
|
+
}
|
|
30
|
+
data = (await res.json());
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
if (err instanceof Error && err.message.startsWith("HTTP ")) {
|
|
34
|
+
console.error(chalk_1.default.red(`\n Error: ${err.message}\n`));
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
console.error(chalk_1.default.red(`\n Cannot connect to trickle backend at ${chalk_1.default.bold(backendUrl)}.`));
|
|
38
|
+
console.error(chalk_1.default.red(" Is the backend running?\n"));
|
|
39
|
+
}
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
// JSON output mode
|
|
43
|
+
if (opts.json) {
|
|
44
|
+
console.log(JSON.stringify(data, null, 2));
|
|
45
|
+
checkThreshold(data.summary.health, opts.failUnder);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const { summary, entries } = data;
|
|
49
|
+
// Header
|
|
50
|
+
console.log("");
|
|
51
|
+
console.log(chalk_1.default.bold(" trickle coverage"));
|
|
52
|
+
console.log(chalk_1.default.gray(" " + "─".repeat(60)));
|
|
53
|
+
if (opts.env) {
|
|
54
|
+
console.log(chalk_1.default.gray(` Environment: ${opts.env}`));
|
|
55
|
+
}
|
|
56
|
+
console.log(chalk_1.default.gray(` Stale threshold: ${summary.staleThresholdHours}h`));
|
|
57
|
+
console.log(chalk_1.default.gray(" " + "─".repeat(60)));
|
|
58
|
+
console.log("");
|
|
59
|
+
if (entries.length === 0) {
|
|
60
|
+
console.log(chalk_1.default.yellow(" No functions observed yet."));
|
|
61
|
+
console.log(chalk_1.default.gray(" Instrument your app and make some requests first.\n"));
|
|
62
|
+
checkThreshold(0, opts.failUnder);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
// Health score bar
|
|
66
|
+
const healthColor = summary.health >= 80 ? chalk_1.default.green : summary.health >= 50 ? chalk_1.default.yellow : chalk_1.default.red;
|
|
67
|
+
const barFilled = Math.round(summary.health / 5);
|
|
68
|
+
const barEmpty = 20 - barFilled;
|
|
69
|
+
const healthBar = healthColor("█".repeat(barFilled)) + chalk_1.default.gray("░".repeat(barEmpty));
|
|
70
|
+
console.log(` Health: ${healthBar} ${healthColor(chalk_1.default.bold(`${summary.health}%`))}`);
|
|
71
|
+
console.log("");
|
|
72
|
+
// Summary stats
|
|
73
|
+
console.log(chalk_1.default.bold(" Summary"));
|
|
74
|
+
console.log(` ${chalk_1.default.cyan(String(summary.total))} functions observed`);
|
|
75
|
+
console.log(` ${chalk_1.default.green(String(summary.withTypes))} with types ${chalk_1.default.gray(`${summary.withoutTypes} without`)}`);
|
|
76
|
+
console.log(` ${chalk_1.default.green(String(summary.fresh))} fresh ${summary.stale > 0 ? chalk_1.default.yellow(`${summary.stale} stale`) : chalk_1.default.gray("0 stale")}`);
|
|
77
|
+
if (summary.withErrors > 0) {
|
|
78
|
+
console.log(` ${chalk_1.default.red(String(summary.withErrors))} with errors`);
|
|
79
|
+
}
|
|
80
|
+
if (summary.withMultipleVariants > 0) {
|
|
81
|
+
console.log(` ${chalk_1.default.yellow(String(summary.withMultipleVariants))} with multiple type variants`);
|
|
82
|
+
}
|
|
83
|
+
console.log("");
|
|
84
|
+
// Per-function table
|
|
85
|
+
console.log(chalk_1.default.bold(" Functions"));
|
|
86
|
+
console.log(chalk_1.default.gray(" " + "─".repeat(60)));
|
|
87
|
+
for (const entry of entries) {
|
|
88
|
+
const statusIcon = !entry.hasTypes
|
|
89
|
+
? chalk_1.default.red("✗")
|
|
90
|
+
: entry.isStale
|
|
91
|
+
? chalk_1.default.yellow("◦")
|
|
92
|
+
: chalk_1.default.green("✓");
|
|
93
|
+
const nameStr = chalk_1.default.bold(entry.functionName);
|
|
94
|
+
const moduleStr = entry.module !== "api" && entry.module !== "default" ? chalk_1.default.gray(` [${entry.module}]`) : "";
|
|
95
|
+
const badges = [];
|
|
96
|
+
if (entry.snapshots > 0)
|
|
97
|
+
badges.push(chalk_1.default.gray(`${entry.snapshots} snap`));
|
|
98
|
+
if (entry.variants > 1)
|
|
99
|
+
badges.push(chalk_1.default.yellow(`${entry.variants} variants`));
|
|
100
|
+
if (entry.errors > 0)
|
|
101
|
+
badges.push(chalk_1.default.red(`${entry.errors} err`));
|
|
102
|
+
if (entry.isStale)
|
|
103
|
+
badges.push(chalk_1.default.yellow("stale"));
|
|
104
|
+
const healthStr = entry.health >= 80
|
|
105
|
+
? chalk_1.default.green(`${entry.health}%`)
|
|
106
|
+
: entry.health >= 50
|
|
107
|
+
? chalk_1.default.yellow(`${entry.health}%`)
|
|
108
|
+
: chalk_1.default.red(`${entry.health}%`);
|
|
109
|
+
console.log(` ${statusIcon} ${nameStr}${moduleStr} ${badges.join(" ")} ${healthStr}`);
|
|
110
|
+
}
|
|
111
|
+
console.log(chalk_1.default.gray(" " + "─".repeat(60)));
|
|
112
|
+
console.log("");
|
|
113
|
+
checkThreshold(summary.health, opts.failUnder);
|
|
114
|
+
}
|
|
115
|
+
function checkThreshold(health, failUnder) {
|
|
116
|
+
if (!failUnder)
|
|
117
|
+
return;
|
|
118
|
+
const threshold = parseInt(failUnder, 10);
|
|
119
|
+
if (isNaN(threshold))
|
|
120
|
+
return;
|
|
121
|
+
if (health < threshold) {
|
|
122
|
+
console.error(chalk_1.default.red(` Coverage health ${health}% is below threshold ${threshold}%`));
|
|
123
|
+
console.error("");
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function dashboardCommand(): Promise<void>;
|
|
@@ -0,0 +1,83 @@
|
|
|
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.dashboardCommand = dashboardCommand;
|
|
40
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
41
|
+
const config_1 = require("../config");
|
|
42
|
+
async function dashboardCommand() {
|
|
43
|
+
const backendUrl = (0, config_1.getBackendUrl)();
|
|
44
|
+
const dashboardUrl = `${backendUrl}/dashboard`;
|
|
45
|
+
// Check backend is reachable
|
|
46
|
+
try {
|
|
47
|
+
const res = await fetch(`${backendUrl}/api/health`, { signal: AbortSignal.timeout(3000) });
|
|
48
|
+
if (!res.ok)
|
|
49
|
+
throw new Error("not ok");
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
console.error(chalk_1.default.red(`\n Cannot reach trickle backend at ${chalk_1.default.bold(backendUrl)}`));
|
|
53
|
+
console.error(chalk_1.default.gray(" Start the backend first: cd packages/backend && npm start\n"));
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
console.log("");
|
|
57
|
+
console.log(chalk_1.default.bold(" trickle dashboard"));
|
|
58
|
+
console.log(chalk_1.default.gray(" " + "─".repeat(50)));
|
|
59
|
+
console.log(chalk_1.default.gray(` Opening ${chalk_1.default.bold(dashboardUrl)}`));
|
|
60
|
+
console.log(chalk_1.default.gray(" " + "─".repeat(50)));
|
|
61
|
+
console.log("");
|
|
62
|
+
// Open in browser
|
|
63
|
+
const { exec } = await Promise.resolve().then(() => __importStar(require("child_process")));
|
|
64
|
+
const platform = process.platform;
|
|
65
|
+
let cmd;
|
|
66
|
+
if (platform === "darwin") {
|
|
67
|
+
cmd = `open "${dashboardUrl}"`;
|
|
68
|
+
}
|
|
69
|
+
else if (platform === "win32") {
|
|
70
|
+
cmd = `start "" "${dashboardUrl}"`;
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
cmd = `xdg-open "${dashboardUrl}"`;
|
|
74
|
+
}
|
|
75
|
+
exec(cmd, (err) => {
|
|
76
|
+
if (err) {
|
|
77
|
+
console.log(chalk_1.default.yellow(` Could not open browser automatically.`));
|
|
78
|
+
console.log(chalk_1.default.yellow(` Open this URL manually: ${dashboardUrl}\n`));
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
// Keep the process alive briefly so the user sees the message
|
|
82
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
83
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface DevOptions {
|
|
2
|
+
port?: string;
|
|
3
|
+
out?: string;
|
|
4
|
+
client?: boolean;
|
|
5
|
+
python?: boolean;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* `trickle dev` — All-in-one development command.
|
|
9
|
+
*
|
|
10
|
+
* Starts your app with auto-instrumentation and watches for type changes,
|
|
11
|
+
* regenerating type files as requests flow through. One command replaces
|
|
12
|
+
* the 2-terminal setup of `trickle:start` + `trickle:dev`.
|
|
13
|
+
*/
|
|
14
|
+
export declare function devCommand(command: string | undefined, opts: DevOptions): Promise<void>;
|
|
@@ -0,0 +1,319 @@
|
|
|
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.devCommand = devCommand;
|
|
40
|
+
const fs = __importStar(require("fs"));
|
|
41
|
+
const path = __importStar(require("path"));
|
|
42
|
+
const child_process_1 = require("child_process");
|
|
43
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
44
|
+
const config_1 = require("../config");
|
|
45
|
+
const api_client_1 = require("../api-client");
|
|
46
|
+
/**
|
|
47
|
+
* `trickle dev` — All-in-one development command.
|
|
48
|
+
*
|
|
49
|
+
* Starts your app with auto-instrumentation and watches for type changes,
|
|
50
|
+
* regenerating type files as requests flow through. One command replaces
|
|
51
|
+
* the 2-terminal setup of `trickle:start` + `trickle:dev`.
|
|
52
|
+
*/
|
|
53
|
+
async function devCommand(command, opts) {
|
|
54
|
+
const backendUrl = (0, config_1.getBackendUrl)();
|
|
55
|
+
// Resolve the app command to run
|
|
56
|
+
const appCommand = resolveAppCommand(command);
|
|
57
|
+
if (!appCommand) {
|
|
58
|
+
console.error(chalk_1.default.red("\n Could not determine app command to run."));
|
|
59
|
+
console.error(chalk_1.default.gray(" Provide a command: trickle dev \"node app.js\""));
|
|
60
|
+
console.error(chalk_1.default.gray(" Or ensure package.json has a start or dev script.\n"));
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
// Determine output paths
|
|
64
|
+
const isPython = opts.python === true;
|
|
65
|
+
const typesOut = opts.out || (isPython ? ".trickle/types.pyi" : ".trickle/types.d.ts");
|
|
66
|
+
const clientOut = opts.client ? ".trickle/api-client.ts" : undefined;
|
|
67
|
+
// Ensure .trickle directory exists
|
|
68
|
+
const trickleDir = path.resolve(".trickle");
|
|
69
|
+
if (!fs.existsSync(trickleDir)) {
|
|
70
|
+
fs.mkdirSync(trickleDir, { recursive: true });
|
|
71
|
+
}
|
|
72
|
+
// Check backend connectivity
|
|
73
|
+
const backendReachable = await checkBackend(backendUrl);
|
|
74
|
+
if (!backendReachable) {
|
|
75
|
+
console.error(chalk_1.default.red(`\n Cannot reach trickle backend at ${chalk_1.default.bold(backendUrl)}`));
|
|
76
|
+
console.error(chalk_1.default.gray(" Start the backend first: cd packages/backend && npm start"));
|
|
77
|
+
console.error(chalk_1.default.gray(" Or set TRICKLE_BACKEND_URL to point to a running backend.\n"));
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
// Print header
|
|
81
|
+
console.log("");
|
|
82
|
+
console.log(chalk_1.default.bold(" trickle dev"));
|
|
83
|
+
console.log(chalk_1.default.gray(" " + "─".repeat(50)));
|
|
84
|
+
console.log(chalk_1.default.gray(` App command: ${appCommand}`));
|
|
85
|
+
console.log(chalk_1.default.gray(` Backend: ${backendUrl}`));
|
|
86
|
+
console.log(chalk_1.default.gray(` Types output: ${typesOut}`));
|
|
87
|
+
if (clientOut) {
|
|
88
|
+
console.log(chalk_1.default.gray(` Client output: ${clientOut}`));
|
|
89
|
+
}
|
|
90
|
+
console.log(chalk_1.default.gray(" " + "─".repeat(50)));
|
|
91
|
+
console.log("");
|
|
92
|
+
// Inject -r trickle-observe/register into the command
|
|
93
|
+
const instrumentedCommand = injectRegister(appCommand);
|
|
94
|
+
// Start the app process
|
|
95
|
+
const appProc = startApp(instrumentedCommand, backendUrl);
|
|
96
|
+
// Start the codegen watcher
|
|
97
|
+
const stopWatcher = startCodegenWatcher(typesOut, clientOut, isPython);
|
|
98
|
+
// Handle cleanup
|
|
99
|
+
let shuttingDown = false;
|
|
100
|
+
function cleanup() {
|
|
101
|
+
if (shuttingDown)
|
|
102
|
+
return;
|
|
103
|
+
shuttingDown = true;
|
|
104
|
+
console.log(chalk_1.default.gray("\n Shutting down..."));
|
|
105
|
+
stopWatcher();
|
|
106
|
+
if (!appProc.killed) {
|
|
107
|
+
appProc.kill("SIGTERM");
|
|
108
|
+
}
|
|
109
|
+
// Give processes time to clean up
|
|
110
|
+
setTimeout(() => {
|
|
111
|
+
process.exit(0);
|
|
112
|
+
}, 500);
|
|
113
|
+
}
|
|
114
|
+
process.on("SIGINT", cleanup);
|
|
115
|
+
process.on("SIGTERM", cleanup);
|
|
116
|
+
appProc.on("exit", (code) => {
|
|
117
|
+
if (!shuttingDown) {
|
|
118
|
+
if (code !== null && code !== 0) {
|
|
119
|
+
console.log(chalk_1.default.red(`\n App exited with code ${code}`));
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
console.log(chalk_1.default.gray("\n App exited."));
|
|
123
|
+
}
|
|
124
|
+
cleanup();
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Resolve the command to run. Checks:
|
|
130
|
+
* 1. Explicit command argument
|
|
131
|
+
* 2. package.json scripts.start
|
|
132
|
+
* 3. package.json scripts.dev
|
|
133
|
+
*/
|
|
134
|
+
function resolveAppCommand(explicitCommand) {
|
|
135
|
+
if (explicitCommand) {
|
|
136
|
+
return explicitCommand;
|
|
137
|
+
}
|
|
138
|
+
// Try reading package.json
|
|
139
|
+
const pkgPath = path.resolve("package.json");
|
|
140
|
+
if (fs.existsSync(pkgPath)) {
|
|
141
|
+
try {
|
|
142
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
143
|
+
const scripts = pkg.scripts || {};
|
|
144
|
+
// Prefer trickle:start if it exists (already instrumented)
|
|
145
|
+
if (scripts["trickle:start"]) {
|
|
146
|
+
return scripts["trickle:start"];
|
|
147
|
+
}
|
|
148
|
+
if (scripts.start) {
|
|
149
|
+
return scripts.start;
|
|
150
|
+
}
|
|
151
|
+
if (scripts.dev) {
|
|
152
|
+
return scripts.dev;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
// ignore
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Inject `-r trickle-observe/register` into a node/ts-node/nodemon command.
|
|
163
|
+
* If the command already has it, return as-is.
|
|
164
|
+
*/
|
|
165
|
+
function injectRegister(command) {
|
|
166
|
+
// Already instrumented
|
|
167
|
+
if (command.includes("trickle-observe/register") || command.includes("trickle\\register")) {
|
|
168
|
+
return command;
|
|
169
|
+
}
|
|
170
|
+
// Inject -r flag
|
|
171
|
+
if (/\bnode\s/.test(command)) {
|
|
172
|
+
return command.replace(/\bnode\s/, "node -r trickle-observe/register ");
|
|
173
|
+
}
|
|
174
|
+
if (/\bts-node\s/.test(command)) {
|
|
175
|
+
return command.replace(/\bts-node\s/, "ts-node -r trickle-observe/register ");
|
|
176
|
+
}
|
|
177
|
+
if (/\bnodemon\s/.test(command)) {
|
|
178
|
+
return command.replace(/\bnodemon\s/, "nodemon -r trickle-observe/register ");
|
|
179
|
+
}
|
|
180
|
+
// Can't inject — run as-is with a warning
|
|
181
|
+
console.log(chalk_1.default.yellow(" Warning: Could not inject -r trickle-observe/register into command."));
|
|
182
|
+
console.log(chalk_1.default.yellow(" Auto-instrumentation may not work. Consider using:"));
|
|
183
|
+
console.log(chalk_1.default.yellow(` node -r trickle-observe/register ${command}\n`));
|
|
184
|
+
return command;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Start the app as a child process with environment variables set.
|
|
188
|
+
*/
|
|
189
|
+
function startApp(command, backendUrl) {
|
|
190
|
+
const parts = command.split(/\s+/);
|
|
191
|
+
const cmd = parts[0];
|
|
192
|
+
const args = parts.slice(1);
|
|
193
|
+
const prefix = chalk_1.default.cyan("[app]");
|
|
194
|
+
const proc = (0, child_process_1.spawn)(cmd, args, {
|
|
195
|
+
stdio: ["inherit", "pipe", "pipe"],
|
|
196
|
+
env: {
|
|
197
|
+
...process.env,
|
|
198
|
+
TRICKLE_BACKEND_URL: backendUrl,
|
|
199
|
+
},
|
|
200
|
+
shell: true,
|
|
201
|
+
});
|
|
202
|
+
proc.stdout?.on("data", (data) => {
|
|
203
|
+
const lines = data.toString().split("\n");
|
|
204
|
+
for (const line of lines) {
|
|
205
|
+
if (line.trim()) {
|
|
206
|
+
console.log(`${prefix} ${line}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
proc.stderr?.on("data", (data) => {
|
|
211
|
+
const lines = data.toString().split("\n");
|
|
212
|
+
for (const line of lines) {
|
|
213
|
+
if (line.trim()) {
|
|
214
|
+
console.error(`${prefix} ${chalk_1.default.red(line)}`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
return proc;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Start a codegen watcher that polls for type changes.
|
|
222
|
+
* Returns a stop function.
|
|
223
|
+
*/
|
|
224
|
+
function startCodegenWatcher(typesOut, clientOut, isPython) {
|
|
225
|
+
const prefix = chalk_1.default.magenta("[types]");
|
|
226
|
+
const language = isPython ? "python" : undefined;
|
|
227
|
+
let lastTypesContent = "";
|
|
228
|
+
let lastClientContent = "";
|
|
229
|
+
let stopped = false;
|
|
230
|
+
let firstRun = true;
|
|
231
|
+
// Wait a bit before first poll to let the app start
|
|
232
|
+
let initialDelay = true;
|
|
233
|
+
const poll = async () => {
|
|
234
|
+
if (stopped)
|
|
235
|
+
return;
|
|
236
|
+
try {
|
|
237
|
+
// Generate types
|
|
238
|
+
const result = await (0, api_client_1.fetchCodegen)({ language });
|
|
239
|
+
const types = result.types;
|
|
240
|
+
if (types !== lastTypesContent) {
|
|
241
|
+
lastTypesContent = types;
|
|
242
|
+
const resolvedPath = path.resolve(typesOut);
|
|
243
|
+
const dir = path.dirname(resolvedPath);
|
|
244
|
+
if (!fs.existsSync(dir)) {
|
|
245
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
246
|
+
}
|
|
247
|
+
fs.writeFileSync(resolvedPath, types, "utf-8");
|
|
248
|
+
if (!firstRun) {
|
|
249
|
+
const count = countTypes(types);
|
|
250
|
+
console.log(`${prefix} ${chalk_1.default.green("Updated")} ${chalk_1.default.bold(typesOut)} ${chalk_1.default.gray(`(${count} types)`)}`);
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
firstRun = false;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
// Generate client if requested
|
|
257
|
+
if (clientOut) {
|
|
258
|
+
const clientResult = await (0, api_client_1.fetchCodegen)({ format: "client" });
|
|
259
|
+
if (clientResult.types !== lastClientContent) {
|
|
260
|
+
lastClientContent = clientResult.types;
|
|
261
|
+
const resolvedPath = path.resolve(clientOut);
|
|
262
|
+
const dir = path.dirname(resolvedPath);
|
|
263
|
+
if (!fs.existsSync(dir)) {
|
|
264
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
265
|
+
}
|
|
266
|
+
fs.writeFileSync(resolvedPath, clientResult.types, "utf-8");
|
|
267
|
+
if (!firstRun) {
|
|
268
|
+
console.log(`${prefix} ${chalk_1.default.green("Updated")} ${chalk_1.default.bold(clientOut)}`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
// Backend might not be ready yet or app hasn't served requests — silently retry
|
|
275
|
+
if (!initialDelay) {
|
|
276
|
+
// Only log after initial startup period
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
initialDelay = false;
|
|
280
|
+
};
|
|
281
|
+
// First poll after 3s (give app time to start), then every 3s
|
|
282
|
+
const startTimeout = setTimeout(() => {
|
|
283
|
+
poll();
|
|
284
|
+
const interval = setInterval(poll, 3000);
|
|
285
|
+
// Store interval for cleanup
|
|
286
|
+
startTimeout.__interval = interval;
|
|
287
|
+
}, 3000);
|
|
288
|
+
let intervalRef = null;
|
|
289
|
+
// Use a different approach: start polling immediately with setInterval
|
|
290
|
+
const interval = setInterval(async () => {
|
|
291
|
+
if (stopped)
|
|
292
|
+
return;
|
|
293
|
+
await poll();
|
|
294
|
+
}, 3000);
|
|
295
|
+
intervalRef = interval;
|
|
296
|
+
// Initial poll after 2s delay
|
|
297
|
+
const initTimer = setTimeout(poll, 2000);
|
|
298
|
+
return () => {
|
|
299
|
+
stopped = true;
|
|
300
|
+
clearTimeout(startTimeout);
|
|
301
|
+
clearTimeout(initTimer);
|
|
302
|
+
if (intervalRef)
|
|
303
|
+
clearInterval(intervalRef);
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
function countTypes(code) {
|
|
307
|
+
const tsMatches = code.match(/export (interface|type) /g);
|
|
308
|
+
const pyMatches = code.match(/class \w+\(TypedDict\)/g);
|
|
309
|
+
return (tsMatches?.length ?? 0) + (pyMatches?.length ?? 0);
|
|
310
|
+
}
|
|
311
|
+
async function checkBackend(url) {
|
|
312
|
+
try {
|
|
313
|
+
const res = await fetch(`${url}/api/health`, { signal: AbortSignal.timeout(3000) });
|
|
314
|
+
return res.ok;
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
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.diffCommand = diffCommand;
|
|
7
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
+
const api_client_1 = require("../api-client");
|
|
9
|
+
const diff_formatter_1 = require("../formatters/diff-formatter");
|
|
10
|
+
const badges_1 = require("../ui/badges");
|
|
11
|
+
const helpers_1 = require("../ui/helpers");
|
|
12
|
+
async function diffCommand(opts) {
|
|
13
|
+
try {
|
|
14
|
+
// Parse --since into a datetime string for the backend
|
|
15
|
+
let sinceStr;
|
|
16
|
+
if (opts.since) {
|
|
17
|
+
sinceStr = (0, helpers_1.parseSince)(opts.since);
|
|
18
|
+
}
|
|
19
|
+
const result = await (0, api_client_1.fetchDiffReport)({
|
|
20
|
+
since: sinceStr,
|
|
21
|
+
env: opts.env,
|
|
22
|
+
env1: opts.env1,
|
|
23
|
+
env2: opts.env2,
|
|
24
|
+
});
|
|
25
|
+
console.log("");
|
|
26
|
+
if (result.mode === "cross-env") {
|
|
27
|
+
console.log(chalk_1.default.white.bold(" Type drift: ") +
|
|
28
|
+
(0, badges_1.envBadge)(result.env1) +
|
|
29
|
+
chalk_1.default.gray(" → ") +
|
|
30
|
+
(0, badges_1.envBadge)(result.env2));
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
const label = opts.since
|
|
34
|
+
? `changes in the last ${opts.since}`
|
|
35
|
+
: "all type changes";
|
|
36
|
+
console.log(chalk_1.default.white.bold(` Type drift: ${label}`));
|
|
37
|
+
if (opts.env) {
|
|
38
|
+
console.log(chalk_1.default.gray(` Environment: ${opts.env}`));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
console.log(chalk_1.default.gray(" " + "─".repeat(50)));
|
|
42
|
+
if (result.total === 0) {
|
|
43
|
+
console.log("");
|
|
44
|
+
console.log(chalk_1.default.green(" No type drift detected."));
|
|
45
|
+
if (opts.since) {
|
|
46
|
+
console.log(chalk_1.default.gray(` No functions had type changes in the last ${opts.since}.`));
|
|
47
|
+
}
|
|
48
|
+
console.log("");
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
console.log(chalk_1.default.gray(` ${result.total} function${result.total === 1 ? "" : "s"} with type changes`));
|
|
52
|
+
for (const entry of result.entries) {
|
|
53
|
+
console.log("");
|
|
54
|
+
console.log(chalk_1.default.white.bold(` ${entry.functionName}`) +
|
|
55
|
+
chalk_1.default.gray(` (${entry.module})`));
|
|
56
|
+
// Show from/to metadata
|
|
57
|
+
console.log(chalk_1.default.gray(" from: ") +
|
|
58
|
+
(0, badges_1.envBadge)(entry.from.env) +
|
|
59
|
+
chalk_1.default.gray(" " + (0, badges_1.timeBadge)(entry.from.observed_at)));
|
|
60
|
+
console.log(chalk_1.default.gray(" to: ") +
|
|
61
|
+
(0, badges_1.envBadge)(entry.to.env) +
|
|
62
|
+
chalk_1.default.gray(" " + (0, badges_1.timeBadge)(entry.to.observed_at)));
|
|
63
|
+
console.log("");
|
|
64
|
+
// Indent diffs by 2 extra spaces
|
|
65
|
+
const formattedDiffs = (0, diff_formatter_1.formatDiffs)(entry.diffs);
|
|
66
|
+
const indented = formattedDiffs
|
|
67
|
+
.split("\n")
|
|
68
|
+
.map((line) => " " + line)
|
|
69
|
+
.join("\n");
|
|
70
|
+
console.log(indented);
|
|
71
|
+
}
|
|
72
|
+
console.log("");
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
if (err instanceof Error) {
|
|
76
|
+
console.error(chalk_1.default.red(`\n Error: ${err.message}\n`));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface DocsOptions {
|
|
2
|
+
out?: string;
|
|
3
|
+
html?: boolean;
|
|
4
|
+
env?: string;
|
|
5
|
+
title?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* `trickle docs` — Generate API documentation from observed runtime types.
|
|
9
|
+
*
|
|
10
|
+
* Produces clean Markdown (or self-contained HTML) documenting every
|
|
11
|
+
* observed API route with request/response types and example payloads.
|
|
12
|
+
*/
|
|
13
|
+
export declare function docsCommand(opts: DocsOptions): Promise<void>;
|