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,259 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import { fetchMockConfig, MockRoute } from "../api-client";
|
|
5
|
+
|
|
6
|
+
export interface SampleOptions {
|
|
7
|
+
format?: string;
|
|
8
|
+
out?: string;
|
|
9
|
+
route?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* `trickle sample` — Generate test fixtures from observed runtime data.
|
|
14
|
+
*
|
|
15
|
+
* Produces JSON, TypeScript constants, or factory functions from the
|
|
16
|
+
* actual sample inputs and outputs captured by trickle. Great for tests,
|
|
17
|
+
* seed scripts, and Storybook data.
|
|
18
|
+
*/
|
|
19
|
+
export async function sampleCommand(routeFilter: string | undefined, opts: SampleOptions): Promise<void> {
|
|
20
|
+
let routes: MockRoute[];
|
|
21
|
+
try {
|
|
22
|
+
const config = await fetchMockConfig();
|
|
23
|
+
routes = config.routes;
|
|
24
|
+
} catch {
|
|
25
|
+
console.error(chalk.red("\n Cannot connect to trickle backend."));
|
|
26
|
+
console.error(chalk.gray(" Is the backend running?\n"));
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Filter routes
|
|
31
|
+
if (routeFilter) {
|
|
32
|
+
const filter = routeFilter.toLowerCase();
|
|
33
|
+
routes = routes.filter((r) =>
|
|
34
|
+
r.functionName.toLowerCase().includes(filter) ||
|
|
35
|
+
r.path.toLowerCase().includes(filter),
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Only include routes with sample data
|
|
40
|
+
routes = routes.filter((r) => r.sampleOutput !== null && r.sampleOutput !== undefined);
|
|
41
|
+
|
|
42
|
+
if (routes.length === 0) {
|
|
43
|
+
console.error(chalk.yellow("\n No sample data found."));
|
|
44
|
+
if (routeFilter) {
|
|
45
|
+
console.error(chalk.gray(` No routes matching "${routeFilter}" with sample data.\n`));
|
|
46
|
+
} else {
|
|
47
|
+
console.error(chalk.gray(" Instrument your app and make some requests first.\n"));
|
|
48
|
+
}
|
|
49
|
+
process.exit(0);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const format = (opts.format || "json").toLowerCase();
|
|
53
|
+
let output: string;
|
|
54
|
+
|
|
55
|
+
switch (format) {
|
|
56
|
+
case "json":
|
|
57
|
+
output = generateJson(routes);
|
|
58
|
+
break;
|
|
59
|
+
case "ts":
|
|
60
|
+
case "typescript":
|
|
61
|
+
output = generateTypeScript(routes);
|
|
62
|
+
break;
|
|
63
|
+
case "factory":
|
|
64
|
+
output = generateFactories(routes);
|
|
65
|
+
break;
|
|
66
|
+
default:
|
|
67
|
+
console.error(chalk.red(`\n Unknown format: ${format}`));
|
|
68
|
+
console.error(chalk.gray(" Supported: json, ts, factory\n"));
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (opts.out) {
|
|
73
|
+
const resolvedPath = path.resolve(opts.out);
|
|
74
|
+
const dir = path.dirname(resolvedPath);
|
|
75
|
+
if (!fs.existsSync(dir)) {
|
|
76
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
77
|
+
}
|
|
78
|
+
fs.writeFileSync(resolvedPath, output, "utf-8");
|
|
79
|
+
console.log(chalk.green(`\n Fixtures written to ${chalk.bold(opts.out)}`));
|
|
80
|
+
console.log(chalk.gray(` ${routes.length} routes, format: ${format}\n`));
|
|
81
|
+
} else {
|
|
82
|
+
console.log(output);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function toPascalCase(name: string): string {
|
|
87
|
+
return name
|
|
88
|
+
.replace(/[^a-zA-Z0-9]+/g, " ")
|
|
89
|
+
.split(/\s+/)
|
|
90
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
|
|
91
|
+
.join("");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function toCamelCase(name: string): string {
|
|
95
|
+
const pascal = toPascalCase(name);
|
|
96
|
+
return pascal.charAt(0).toLowerCase() + pascal.slice(1);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ── JSON format ──
|
|
100
|
+
|
|
101
|
+
function generateJson(routes: MockRoute[]): string {
|
|
102
|
+
const samples: Record<string, { request?: unknown; response: unknown }> = {};
|
|
103
|
+
|
|
104
|
+
for (const route of routes) {
|
|
105
|
+
const key = `${route.method} ${route.path}`;
|
|
106
|
+
const entry: { request?: unknown; response: unknown } = {
|
|
107
|
+
response: route.sampleOutput,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
if (route.sampleInput) {
|
|
111
|
+
const input = route.sampleInput as Record<string, unknown>;
|
|
112
|
+
const body = input.body || input;
|
|
113
|
+
if (body && typeof body === "object" && Object.keys(body as object).length > 0) {
|
|
114
|
+
entry.request = body;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
samples[key] = entry;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return JSON.stringify(samples, null, 2) + "\n";
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ── TypeScript constants ──
|
|
125
|
+
|
|
126
|
+
function generateTypeScript(routes: MockRoute[]): string {
|
|
127
|
+
const lines: string[] = [];
|
|
128
|
+
lines.push("// Auto-generated test fixtures by trickle");
|
|
129
|
+
lines.push(`// Generated at ${new Date().toISOString()}`);
|
|
130
|
+
lines.push("// Do not edit manually — re-run `trickle sample --format ts` to update");
|
|
131
|
+
lines.push("");
|
|
132
|
+
|
|
133
|
+
for (const route of routes) {
|
|
134
|
+
const varName = toCamelCase(route.functionName);
|
|
135
|
+
|
|
136
|
+
// Response sample
|
|
137
|
+
lines.push(`/** Sample response for ${route.method} ${route.path} */`);
|
|
138
|
+
lines.push(`export const ${varName}Response = ${formatValue(route.sampleOutput, 0)} as const;`);
|
|
139
|
+
lines.push("");
|
|
140
|
+
|
|
141
|
+
// Request body sample (for POST/PUT/PATCH)
|
|
142
|
+
if (["POST", "PUT", "PATCH"].includes(route.method) && route.sampleInput) {
|
|
143
|
+
const input = route.sampleInput as Record<string, unknown>;
|
|
144
|
+
const body = input.body || input;
|
|
145
|
+
if (body && typeof body === "object" && Object.keys(body as object).length > 0) {
|
|
146
|
+
lines.push(`/** Sample request body for ${route.method} ${route.path} */`);
|
|
147
|
+
lines.push(`export const ${varName}Request = ${formatValue(body, 0)} as const;`);
|
|
148
|
+
lines.push("");
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return lines.join("\n").trimEnd() + "\n";
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ── Factory functions ──
|
|
157
|
+
|
|
158
|
+
function generateFactories(routes: MockRoute[]): string {
|
|
159
|
+
const lines: string[] = [];
|
|
160
|
+
lines.push("// Auto-generated test fixture factories by trickle");
|
|
161
|
+
lines.push(`// Generated at ${new Date().toISOString()}`);
|
|
162
|
+
lines.push("// Do not edit manually — re-run `trickle sample --format factory` to update");
|
|
163
|
+
lines.push("");
|
|
164
|
+
|
|
165
|
+
// First emit the base samples as constants
|
|
166
|
+
for (const route of routes) {
|
|
167
|
+
const varName = toCamelCase(route.functionName);
|
|
168
|
+
|
|
169
|
+
lines.push(`const _${varName}Response = ${formatValue(route.sampleOutput, 0)};`);
|
|
170
|
+
lines.push("");
|
|
171
|
+
|
|
172
|
+
if (["POST", "PUT", "PATCH"].includes(route.method) && route.sampleInput) {
|
|
173
|
+
const input = route.sampleInput as Record<string, unknown>;
|
|
174
|
+
const body = input.body || input;
|
|
175
|
+
if (body && typeof body === "object" && Object.keys(body as object).length > 0) {
|
|
176
|
+
lines.push(`const _${varName}Request = ${formatValue(body, 0)};`);
|
|
177
|
+
lines.push("");
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Then emit factory functions
|
|
183
|
+
for (const route of routes) {
|
|
184
|
+
const varName = toCamelCase(route.functionName);
|
|
185
|
+
const typeName = toPascalCase(route.functionName);
|
|
186
|
+
|
|
187
|
+
// Response factory
|
|
188
|
+
if (route.sampleOutput && typeof route.sampleOutput === "object" && !Array.isArray(route.sampleOutput)) {
|
|
189
|
+
lines.push(`/** Create a test fixture for ${route.method} ${route.path} response */`);
|
|
190
|
+
lines.push(`export function create${typeName}Response(overrides?: Partial<typeof _${varName}Response>): typeof _${varName}Response {`);
|
|
191
|
+
lines.push(` return { ..._${varName}Response, ...overrides };`);
|
|
192
|
+
lines.push(`}`);
|
|
193
|
+
lines.push("");
|
|
194
|
+
} else {
|
|
195
|
+
// For non-object responses (arrays, primitives), just export the constant
|
|
196
|
+
lines.push(`/** Sample response for ${route.method} ${route.path} */`);
|
|
197
|
+
lines.push(`export const ${varName}Response = _${varName}Response;`);
|
|
198
|
+
lines.push("");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Request body factory
|
|
202
|
+
if (["POST", "PUT", "PATCH"].includes(route.method) && route.sampleInput) {
|
|
203
|
+
const input = route.sampleInput as Record<string, unknown>;
|
|
204
|
+
const body = input.body || input;
|
|
205
|
+
if (body && typeof body === "object" && !Array.isArray(body) && Object.keys(body as object).length > 0) {
|
|
206
|
+
lines.push(`/** Create a test fixture for ${route.method} ${route.path} request body */`);
|
|
207
|
+
lines.push(`export function create${typeName}Request(overrides?: Partial<typeof _${varName}Request>): typeof _${varName}Request {`);
|
|
208
|
+
lines.push(` return { ..._${varName}Request, ...overrides };`);
|
|
209
|
+
lines.push(`}`);
|
|
210
|
+
lines.push("");
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return lines.join("\n").trimEnd() + "\n";
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ── Value formatting ──
|
|
219
|
+
|
|
220
|
+
function formatValue(value: unknown, indent: number): string {
|
|
221
|
+
const pad = " ".repeat(indent);
|
|
222
|
+
const innerPad = " ".repeat(indent + 1);
|
|
223
|
+
|
|
224
|
+
if (value === null) return "null";
|
|
225
|
+
if (value === undefined) return "undefined";
|
|
226
|
+
|
|
227
|
+
switch (typeof value) {
|
|
228
|
+
case "string":
|
|
229
|
+
return JSON.stringify(value);
|
|
230
|
+
case "number":
|
|
231
|
+
case "boolean":
|
|
232
|
+
return String(value);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (Array.isArray(value)) {
|
|
236
|
+
if (value.length === 0) return "[]";
|
|
237
|
+
if (value.length === 1 && typeof value[0] !== "object") {
|
|
238
|
+
return `[${formatValue(value[0], 0)}]`;
|
|
239
|
+
}
|
|
240
|
+
const items = value.map((v) => `${innerPad}${formatValue(v, indent + 1)}`);
|
|
241
|
+
return `[\n${items.join(",\n")}\n${pad}]`;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (typeof value === "object") {
|
|
245
|
+
const obj = value as Record<string, unknown>;
|
|
246
|
+
const keys = Object.keys(obj);
|
|
247
|
+
if (keys.length === 0) return "{}";
|
|
248
|
+
|
|
249
|
+
const entries = keys.map((key) => {
|
|
250
|
+
const formattedVal = formatValue(obj[key], indent + 1);
|
|
251
|
+
// Use identifier-safe keys without quotes, quoted otherwise
|
|
252
|
+
const safeKey = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key) ? key : JSON.stringify(key);
|
|
253
|
+
return `${innerPad}${safeKey}: ${formattedVal}`;
|
|
254
|
+
});
|
|
255
|
+
return `{\n${entries.join(",\n")}\n${pad}}`;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return JSON.stringify(value);
|
|
259
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { getBackendUrl } from "../config";
|
|
3
|
+
|
|
4
|
+
export interface SearchOptions {
|
|
5
|
+
env?: string;
|
|
6
|
+
json?: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface FieldMatch {
|
|
10
|
+
path: string;
|
|
11
|
+
kind: string;
|
|
12
|
+
typeName?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface SearchResult {
|
|
16
|
+
functionName: string;
|
|
17
|
+
module: string;
|
|
18
|
+
environment: string;
|
|
19
|
+
lastSeen: string;
|
|
20
|
+
matches: FieldMatch[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface SearchResponse {
|
|
24
|
+
query: string;
|
|
25
|
+
total: number;
|
|
26
|
+
results: SearchResult[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function searchCommand(
|
|
30
|
+
query: string,
|
|
31
|
+
opts: SearchOptions,
|
|
32
|
+
): Promise<void> {
|
|
33
|
+
const backendUrl = getBackendUrl();
|
|
34
|
+
const url = new URL("/api/search", backendUrl);
|
|
35
|
+
url.searchParams.set("q", query);
|
|
36
|
+
if (opts.env) {
|
|
37
|
+
url.searchParams.set("env", opts.env);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let data: SearchResponse;
|
|
41
|
+
try {
|
|
42
|
+
const res = await fetch(url.toString());
|
|
43
|
+
if (!res.ok) {
|
|
44
|
+
const body = await res.text();
|
|
45
|
+
throw new Error(`HTTP ${res.status}: ${body}`);
|
|
46
|
+
}
|
|
47
|
+
data = await res.json() as SearchResponse;
|
|
48
|
+
} catch (err: unknown) {
|
|
49
|
+
if (err instanceof Error && err.message.startsWith("HTTP ")) {
|
|
50
|
+
console.error(chalk.red(`\n Error: ${err.message}\n`));
|
|
51
|
+
} else {
|
|
52
|
+
console.error(chalk.red(`\n Cannot connect to trickle backend at ${chalk.bold(backendUrl)}.`));
|
|
53
|
+
console.error(chalk.red(" Is the backend running?\n"));
|
|
54
|
+
}
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (opts.json) {
|
|
59
|
+
console.log(JSON.stringify(data, null, 2));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
console.log("");
|
|
64
|
+
console.log(chalk.bold(` Search: "${query}"`));
|
|
65
|
+
console.log(chalk.gray(" " + "─".repeat(50)));
|
|
66
|
+
|
|
67
|
+
if (data.total === 0) {
|
|
68
|
+
console.log(chalk.gray(" No matches found.\n"));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
console.log(chalk.gray(` ${data.total} function${data.total === 1 ? "" : "s"} matched\n`));
|
|
73
|
+
|
|
74
|
+
for (const result of data.results) {
|
|
75
|
+
// Function name with method coloring
|
|
76
|
+
const fnName = result.functionName;
|
|
77
|
+
const methodMatch = fnName.match(/^(GET|POST|PUT|PATCH|DELETE)\s/);
|
|
78
|
+
if (methodMatch) {
|
|
79
|
+
const method = methodMatch[1];
|
|
80
|
+
const rest = fnName.slice(method.length);
|
|
81
|
+
const methodColors: Record<string, (s: string) => string> = {
|
|
82
|
+
GET: chalk.green,
|
|
83
|
+
POST: chalk.yellow,
|
|
84
|
+
PUT: chalk.blue,
|
|
85
|
+
PATCH: chalk.cyan,
|
|
86
|
+
DELETE: chalk.red,
|
|
87
|
+
};
|
|
88
|
+
const colorFn = methodColors[method] || chalk.white;
|
|
89
|
+
console.log(` ${colorFn(chalk.bold(method))}${chalk.white(rest)}`);
|
|
90
|
+
} else {
|
|
91
|
+
console.log(` ${chalk.white(chalk.bold(fnName))}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
console.log(chalk.gray(` module: ${result.module} env: ${result.environment}`));
|
|
95
|
+
|
|
96
|
+
// Show matching fields
|
|
97
|
+
for (const match of result.matches) {
|
|
98
|
+
const typeStr = match.typeName ? chalk.cyan(match.typeName) : chalk.gray(match.kind);
|
|
99
|
+
if (match.kind === "name") {
|
|
100
|
+
console.log(chalk.gray(" → ") + chalk.yellow("function name match"));
|
|
101
|
+
} else {
|
|
102
|
+
console.log(chalk.gray(" → ") + chalk.white(match.path) + chalk.gray(": ") + typeStr);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
console.log("");
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import { fetchStubs } from "../api-client";
|
|
5
|
+
|
|
6
|
+
export interface StubsOptions {
|
|
7
|
+
env?: string;
|
|
8
|
+
dryRun?: boolean;
|
|
9
|
+
silent?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Map of file extensions to their stub extension.
|
|
14
|
+
*/
|
|
15
|
+
const JS_EXTS = new Set([".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx"]);
|
|
16
|
+
const PY_EXTS = new Set([".py"]);
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Normalize a module name for matching against file stems.
|
|
20
|
+
* Module names from trickle may use dashes or underscores.
|
|
21
|
+
* File stems use whatever the OS has.
|
|
22
|
+
*/
|
|
23
|
+
function normalizeForMatch(name: string): string {
|
|
24
|
+
return name.replace(/[-_]/g, "").toLowerCase();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Recursively find all source files in a directory.
|
|
29
|
+
*/
|
|
30
|
+
function findSourceFiles(dir: string): string[] {
|
|
31
|
+
const results: string[] = [];
|
|
32
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
33
|
+
|
|
34
|
+
for (const entry of entries) {
|
|
35
|
+
const fullPath = path.join(dir, entry.name);
|
|
36
|
+
|
|
37
|
+
if (entry.isDirectory()) {
|
|
38
|
+
// Skip node_modules, __pycache__, .git, etc.
|
|
39
|
+
if (
|
|
40
|
+
entry.name === "node_modules" ||
|
|
41
|
+
entry.name === "__pycache__" ||
|
|
42
|
+
entry.name === ".git" ||
|
|
43
|
+
entry.name === "dist" ||
|
|
44
|
+
entry.name === "build" ||
|
|
45
|
+
entry.name === ".trickle"
|
|
46
|
+
) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
results.push(...findSourceFiles(fullPath));
|
|
50
|
+
} else if (entry.isFile()) {
|
|
51
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
52
|
+
if (JS_EXTS.has(ext) || PY_EXTS.has(ext)) {
|
|
53
|
+
results.push(fullPath);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return results;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function stubsCommand(
|
|
62
|
+
dir: string,
|
|
63
|
+
opts: StubsOptions,
|
|
64
|
+
): Promise<void> {
|
|
65
|
+
const targetDir = path.resolve(dir);
|
|
66
|
+
|
|
67
|
+
if (!fs.existsSync(targetDir)) {
|
|
68
|
+
console.error(chalk.red(`\n Directory not found: ${targetDir}\n`));
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!fs.statSync(targetDir).isDirectory()) {
|
|
73
|
+
console.error(chalk.red(`\n Not a directory: ${targetDir}\n`));
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Fetch per-module stubs from backend
|
|
78
|
+
const { stubs } = await fetchStubs({ env: opts.env });
|
|
79
|
+
|
|
80
|
+
if (!stubs || Object.keys(stubs).length === 0) {
|
|
81
|
+
if (!opts.silent) {
|
|
82
|
+
console.log(
|
|
83
|
+
chalk.yellow(
|
|
84
|
+
"\n No observed types found. Run your code with trickle first.\n",
|
|
85
|
+
),
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Find all source files in the target directory
|
|
92
|
+
const sourceFiles = findSourceFiles(targetDir);
|
|
93
|
+
|
|
94
|
+
if (sourceFiles.length === 0) {
|
|
95
|
+
if (!opts.silent) {
|
|
96
|
+
console.log(
|
|
97
|
+
chalk.yellow(`\n No source files found in ${targetDir}\n`),
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Build a map: normalized stem → source file path
|
|
104
|
+
const fileMap: Map<string, string[]> = new Map();
|
|
105
|
+
for (const filePath of sourceFiles) {
|
|
106
|
+
const ext = path.extname(filePath);
|
|
107
|
+
const stem = path.basename(filePath, ext);
|
|
108
|
+
const key = normalizeForMatch(stem);
|
|
109
|
+
if (!fileMap.has(key)) fileMap.set(key, []);
|
|
110
|
+
fileMap.get(key)!.push(filePath);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const written: string[] = [];
|
|
114
|
+
const writtenPaths = new Set<string>();
|
|
115
|
+
const skipped: string[] = [];
|
|
116
|
+
|
|
117
|
+
for (const [moduleName, moduleStubs] of Object.entries(stubs)) {
|
|
118
|
+
const normalizedModule = normalizeForMatch(moduleName);
|
|
119
|
+
|
|
120
|
+
// Find matching source files
|
|
121
|
+
const matchingFiles = fileMap.get(normalizedModule);
|
|
122
|
+
|
|
123
|
+
if (!matchingFiles || matchingFiles.length === 0) {
|
|
124
|
+
skipped.push(moduleName);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
for (const sourceFile of matchingFiles) {
|
|
129
|
+
const ext = path.extname(sourceFile).toLowerCase();
|
|
130
|
+
const isPython = PY_EXTS.has(ext);
|
|
131
|
+
const stubContent = isPython ? moduleStubs.python : moduleStubs.ts;
|
|
132
|
+
const stubExt = isPython ? ".pyi" : ".d.ts";
|
|
133
|
+
|
|
134
|
+
// Generate stub file path next to source file
|
|
135
|
+
const sourceDir = path.dirname(sourceFile);
|
|
136
|
+
const sourceStem = path.basename(sourceFile, ext);
|
|
137
|
+
const stubPath = path.join(sourceDir, `${sourceStem}${stubExt}`);
|
|
138
|
+
|
|
139
|
+
// Skip if already written (multiple modules may normalize to same name)
|
|
140
|
+
if (writtenPaths.has(stubPath)) continue;
|
|
141
|
+
writtenPaths.add(stubPath);
|
|
142
|
+
|
|
143
|
+
if (opts.dryRun) {
|
|
144
|
+
const relPath = path.relative(process.cwd(), stubPath);
|
|
145
|
+
console.log(chalk.cyan(` Would create: ${relPath}`));
|
|
146
|
+
console.log(
|
|
147
|
+
chalk.gray(
|
|
148
|
+
` (from module "${moduleName}" → ${path.basename(sourceFile)})`,
|
|
149
|
+
),
|
|
150
|
+
);
|
|
151
|
+
written.push(relPath);
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Write stub file
|
|
156
|
+
fs.writeFileSync(stubPath, stubContent, "utf-8");
|
|
157
|
+
const relPath = path.relative(process.cwd(), stubPath);
|
|
158
|
+
written.push(relPath);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Output summary
|
|
163
|
+
if (opts.silent) return;
|
|
164
|
+
console.log();
|
|
165
|
+
if (opts.dryRun) {
|
|
166
|
+
console.log(chalk.cyan(" Dry run — no files written.\n"));
|
|
167
|
+
if (written.length > 0) {
|
|
168
|
+
console.log(chalk.gray(` ${written.length} stub file(s) would be created.\n`));
|
|
169
|
+
}
|
|
170
|
+
} else if (written.length > 0) {
|
|
171
|
+
console.log(chalk.green(` Generated ${written.length} type stub file(s):\n`));
|
|
172
|
+
for (const f of written) {
|
|
173
|
+
console.log(chalk.gray(` ${f}`));
|
|
174
|
+
}
|
|
175
|
+
console.log();
|
|
176
|
+
console.log(
|
|
177
|
+
chalk.gray(
|
|
178
|
+
" Your IDE should now pick up these types automatically.",
|
|
179
|
+
),
|
|
180
|
+
);
|
|
181
|
+
console.log(
|
|
182
|
+
chalk.gray(
|
|
183
|
+
" Add *.d.ts / *.pyi to .gitignore if you don't want to commit them.",
|
|
184
|
+
),
|
|
185
|
+
);
|
|
186
|
+
console.log();
|
|
187
|
+
} else {
|
|
188
|
+
console.log(
|
|
189
|
+
chalk.yellow(
|
|
190
|
+
" No matching source files found for observed modules.\n",
|
|
191
|
+
),
|
|
192
|
+
);
|
|
193
|
+
if (skipped.length > 0) {
|
|
194
|
+
console.log(
|
|
195
|
+
chalk.gray(
|
|
196
|
+
` Observed modules: ${skipped.join(", ")}`,
|
|
197
|
+
),
|
|
198
|
+
);
|
|
199
|
+
console.log(
|
|
200
|
+
chalk.gray(
|
|
201
|
+
` Source files in: ${path.relative(process.cwd(), targetDir) || "."}`,
|
|
202
|
+
),
|
|
203
|
+
);
|
|
204
|
+
console.log(
|
|
205
|
+
chalk.gray(
|
|
206
|
+
" Make sure module names match file names (e.g., module 'helpers' → helpers.js)\n",
|
|
207
|
+
),
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { tailEvents, TailEvent } from "../api-client";
|
|
3
|
+
import { envBadge, timeBadge } from "../ui/badges";
|
|
4
|
+
import { getBackendUrl } from "../config";
|
|
5
|
+
|
|
6
|
+
export interface TailOptions {
|
|
7
|
+
filter?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function eventBadge(event: string): string {
|
|
11
|
+
const lower = event.toLowerCase();
|
|
12
|
+
if (lower === "error" || lower.includes("error")) {
|
|
13
|
+
return chalk.bgRed.white.bold(" ERROR ");
|
|
14
|
+
}
|
|
15
|
+
if (lower === "new_type" || lower.includes("new")) {
|
|
16
|
+
return chalk.bgGreen.black.bold(" NEW_TYPE ");
|
|
17
|
+
}
|
|
18
|
+
if (lower === "type_changed" || lower.includes("changed")) {
|
|
19
|
+
return chalk.bgYellow.black.bold(" TYPE_CHANGED ");
|
|
20
|
+
}
|
|
21
|
+
if (lower === "ingest" || lower.includes("ingest")) {
|
|
22
|
+
return chalk.bgBlue.white.bold(" INGEST ");
|
|
23
|
+
}
|
|
24
|
+
return chalk.bgGray.white.bold(` ${event.toUpperCase()} `);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function formatEventDetail(event: TailEvent): string {
|
|
28
|
+
const data = event.data;
|
|
29
|
+
const parts: string[] = [];
|
|
30
|
+
|
|
31
|
+
if (data.functionName) {
|
|
32
|
+
parts.push(chalk.white.bold(String(data.functionName)));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (data.module) {
|
|
36
|
+
parts.push(chalk.gray(`(${data.module})`));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (data.env) {
|
|
40
|
+
parts.push(envBadge(String(data.env)));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (data.error && typeof data.error === "object") {
|
|
44
|
+
const err = data.error as Record<string, unknown>;
|
|
45
|
+
if (err.message) {
|
|
46
|
+
parts.push(chalk.red(String(err.message)));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (data.error_message) {
|
|
51
|
+
parts.push(chalk.red(String(data.error_message)));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return parts.join(" ");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function tailCommand(opts: TailOptions): Promise<void> {
|
|
58
|
+
const url = getBackendUrl();
|
|
59
|
+
console.log(chalk.gray(`\n Connecting to trickle at ${url}...`));
|
|
60
|
+
|
|
61
|
+
const cleanup = tailEvents(
|
|
62
|
+
(event: TailEvent) => {
|
|
63
|
+
const now = new Date().toISOString();
|
|
64
|
+
const timeStr = chalk.gray(
|
|
65
|
+
now.replace(/T/, " ").replace(/\..+/, "")
|
|
66
|
+
);
|
|
67
|
+
const badge = eventBadge(event.event);
|
|
68
|
+
const detail = formatEventDetail(event);
|
|
69
|
+
|
|
70
|
+
console.log(` ${timeStr} ${badge} ${detail}`);
|
|
71
|
+
},
|
|
72
|
+
opts.filter
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
// Give it a moment to connect, then show the listening message
|
|
76
|
+
setTimeout(() => {
|
|
77
|
+
console.log(chalk.green(" Listening for events...") + chalk.gray(" (Ctrl+C to stop)\n"));
|
|
78
|
+
}, 500);
|
|
79
|
+
|
|
80
|
+
// Keep the process alive and handle graceful shutdown
|
|
81
|
+
const onSignal = () => {
|
|
82
|
+
console.log(chalk.gray("\n Disconnected.\n"));
|
|
83
|
+
cleanup();
|
|
84
|
+
process.exit(0);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
process.on("SIGINT", onSignal);
|
|
88
|
+
process.on("SIGTERM", onSignal);
|
|
89
|
+
|
|
90
|
+
// Keep the event loop alive
|
|
91
|
+
await new Promise<void>(() => {
|
|
92
|
+
// Never resolves — the process stays alive until killed
|
|
93
|
+
});
|
|
94
|
+
}
|