@typokit/cli 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/dist/bin.d.ts +3 -0
  2. package/dist/bin.d.ts.map +1 -0
  3. package/dist/bin.js +13 -0
  4. package/dist/bin.js.map +1 -0
  5. package/dist/commands/build.d.ts +42 -0
  6. package/dist/commands/build.d.ts.map +1 -0
  7. package/dist/commands/build.js +302 -0
  8. package/dist/commands/build.js.map +1 -0
  9. package/dist/commands/dev.d.ts +106 -0
  10. package/dist/commands/dev.d.ts.map +1 -0
  11. package/dist/commands/dev.js +536 -0
  12. package/dist/commands/dev.js.map +1 -0
  13. package/dist/commands/generate.d.ts +65 -0
  14. package/dist/commands/generate.d.ts.map +1 -0
  15. package/dist/commands/generate.js +430 -0
  16. package/dist/commands/generate.js.map +1 -0
  17. package/dist/commands/inspect.d.ts +26 -0
  18. package/dist/commands/inspect.d.ts.map +1 -0
  19. package/dist/commands/inspect.js +579 -0
  20. package/dist/commands/inspect.js.map +1 -0
  21. package/dist/commands/migrate.d.ts +70 -0
  22. package/dist/commands/migrate.d.ts.map +1 -0
  23. package/dist/commands/migrate.js +570 -0
  24. package/dist/commands/migrate.js.map +1 -0
  25. package/dist/commands/scaffold.d.ts +70 -0
  26. package/dist/commands/scaffold.d.ts.map +1 -0
  27. package/dist/commands/scaffold.js +483 -0
  28. package/dist/commands/scaffold.js.map +1 -0
  29. package/dist/commands/test.d.ts +56 -0
  30. package/dist/commands/test.d.ts.map +1 -0
  31. package/dist/commands/test.js +248 -0
  32. package/dist/commands/test.js.map +1 -0
  33. package/dist/config.d.ts +20 -0
  34. package/dist/config.d.ts.map +1 -0
  35. package/dist/config.js +69 -0
  36. package/dist/config.js.map +1 -0
  37. package/dist/index.d.ts +30 -0
  38. package/dist/index.d.ts.map +1 -0
  39. package/dist/index.js +245 -0
  40. package/dist/index.js.map +1 -0
  41. package/dist/logger.d.ts +12 -0
  42. package/dist/logger.d.ts.map +1 -0
  43. package/dist/logger.js +33 -0
  44. package/dist/logger.js.map +1 -0
  45. package/package.json +33 -0
  46. package/src/bin.ts +22 -0
  47. package/src/commands/build.ts +433 -0
  48. package/src/commands/dev.ts +822 -0
  49. package/src/commands/generate.ts +640 -0
  50. package/src/commands/inspect.ts +885 -0
  51. package/src/commands/migrate.ts +800 -0
  52. package/src/commands/scaffold.ts +627 -0
  53. package/src/commands/test.ts +353 -0
  54. package/src/config.ts +93 -0
  55. package/src/dev.test.ts +285 -0
  56. package/src/env.d.ts +86 -0
  57. package/src/generate.test.ts +304 -0
  58. package/src/index.test.ts +217 -0
  59. package/src/index.ts +397 -0
  60. package/src/inspect.test.ts +411 -0
  61. package/src/logger.ts +49 -0
  62. package/src/migrate.test.ts +205 -0
  63. package/src/scaffold.test.ts +256 -0
  64. package/src/test.test.ts +230 -0
@@ -0,0 +1,885 @@
1
+ // @typokit/cli — Inspect Commands
2
+ // Subcommands for querying framework internal state as structured JSON
3
+
4
+ import type { CliLogger } from "../logger.js";
5
+ import type { TypoKitConfig } from "../config.js";
6
+
7
+ // ─── Types ───────────────────────────────────────────────────
8
+
9
+ export interface InspectOptions {
10
+ rootDir: string;
11
+ config: Required<TypoKitConfig>;
12
+ logger: CliLogger;
13
+ subcommand: string;
14
+ positional: string[];
15
+ flags: Record<string, string | boolean>;
16
+ }
17
+
18
+ export interface InspectResult {
19
+ success: boolean;
20
+ data: unknown;
21
+ error?: string;
22
+ }
23
+
24
+ interface RouteInfo {
25
+ method: string;
26
+ path: string;
27
+ params?: string[];
28
+ query?: Record<string, string>;
29
+ body?: string;
30
+ response?: string;
31
+ middleware?: string[];
32
+ handler?: string;
33
+ }
34
+
35
+ interface MiddlewareInfo {
36
+ name: string;
37
+ priority: number;
38
+ type: string;
39
+ }
40
+
41
+ interface DependencyNode {
42
+ name: string;
43
+ dependsOn: string[];
44
+ }
45
+
46
+ interface SchemaInfo {
47
+ name: string;
48
+ properties: Record<
49
+ string,
50
+ { type: string; optional?: boolean; tags?: Record<string, string> }
51
+ >;
52
+ usedIn: string[];
53
+ }
54
+
55
+ interface BuildHookInfo {
56
+ name: string;
57
+ order: number;
58
+ description: string;
59
+ }
60
+
61
+ interface ServerInfo {
62
+ adapter: string;
63
+ platform: string;
64
+ status: string;
65
+ port?: number;
66
+ host?: string;
67
+ }
68
+
69
+ interface ErrorEntry {
70
+ timestamp: string;
71
+ traceId: string;
72
+ code: string;
73
+ message: string;
74
+ route?: string;
75
+ phase?: string;
76
+ }
77
+
78
+ interface PerformanceInfo {
79
+ route: string;
80
+ p50: number;
81
+ p95: number;
82
+ p99: number;
83
+ count: number;
84
+ avgMs: number;
85
+ }
86
+
87
+ // ─── Output Formatting ──────────────────────────────────────
88
+
89
+ function isJsonOutput(flags: Record<string, string | boolean>): boolean {
90
+ return flags["json"] === true || flags["format"] === "json";
91
+ }
92
+
93
+ function writeOutput(data: unknown, json: boolean, logger: CliLogger): void {
94
+ const g = globalThis as Record<string, unknown>;
95
+ const proc = g["process"] as
96
+ | { stdout: { write(s: string): void } }
97
+ | undefined;
98
+ const stdout = proc?.stdout ?? { write: () => {} };
99
+
100
+ if (json) {
101
+ stdout.write(JSON.stringify(data, null, 2) + "\n");
102
+ } else {
103
+ formatHumanReadable(data, logger);
104
+ }
105
+ }
106
+
107
+ function formatHumanReadable(data: unknown, logger: CliLogger): void {
108
+ if (Array.isArray(data)) {
109
+ for (const item of data) {
110
+ if (typeof item === "object" && item !== null) {
111
+ const obj = item as Record<string, unknown>;
112
+ if ("method" in obj && "path" in obj) {
113
+ // Route info
114
+ logger.info(` ${String(obj["method"])} ${String(obj["path"])}`);
115
+ if (obj["params"])
116
+ logger.info(` params: ${JSON.stringify(obj["params"])}`);
117
+ if (obj["handler"])
118
+ logger.info(` handler: ${String(obj["handler"])}`);
119
+ } else if ("name" in obj && "priority" in obj) {
120
+ // Middleware info
121
+ logger.info(
122
+ ` [${String(obj["priority"])}] ${String(obj["name"])} (${String(obj["type"])})`,
123
+ );
124
+ } else if ("name" in obj && "dependsOn" in obj) {
125
+ // Dependency node
126
+ const deps = obj["dependsOn"] as string[];
127
+ logger.info(
128
+ ` ${String(obj["name"])} → ${deps.length > 0 ? deps.join(", ") : "(none)"}`,
129
+ );
130
+ } else if ("name" in obj && "order" in obj) {
131
+ // Build hook
132
+ logger.info(
133
+ ` ${String(obj["order"])}. ${String(obj["name"])}: ${String(obj["description"])}`,
134
+ );
135
+ } else if ("timestamp" in obj && "code" in obj) {
136
+ // Error entry
137
+ logger.info(
138
+ ` [${String(obj["timestamp"])}] ${String(obj["code"])}: ${String(obj["message"])}`,
139
+ );
140
+ if (obj["route"]) logger.info(` route: ${String(obj["route"])}`);
141
+ } else {
142
+ logger.info(` ${JSON.stringify(obj)}`);
143
+ }
144
+ } else {
145
+ logger.info(` ${String(item)}`);
146
+ }
147
+ }
148
+ } else if (typeof data === "object" && data !== null) {
149
+ const obj = data as Record<string, unknown>;
150
+ for (const [key, value] of Object.entries(obj)) {
151
+ if (typeof value === "object" && value !== null) {
152
+ logger.info(`${key}:`);
153
+ formatHumanReadable(value, logger);
154
+ } else {
155
+ logger.info(` ${key}: ${String(value)}`);
156
+ }
157
+ }
158
+ } else {
159
+ logger.info(String(data));
160
+ }
161
+ }
162
+
163
+ // ─── Subcommand Implementations ─────────────────────────────
164
+
165
+ async function readGeneratedFile(
166
+ rootDir: string,
167
+ config: Required<TypoKitConfig>,
168
+ relativePath: string,
169
+ ): Promise<string | null> {
170
+ const { join } = (await import(/* @vite-ignore */ "path")) as {
171
+ join: (...args: string[]) => string;
172
+ };
173
+ const { existsSync, readFileSync } = (await import(
174
+ /* @vite-ignore */ "fs"
175
+ )) as {
176
+ existsSync: (p: string) => boolean;
177
+ readFileSync: (p: string, encoding: string) => string;
178
+ };
179
+
180
+ const filePath = join(rootDir, config.outputDir, relativePath);
181
+ if (!existsSync(filePath)) {
182
+ return null;
183
+ }
184
+ return readFileSync(filePath, "utf-8");
185
+ }
186
+
187
+ /** Parse route entries from compiled router TypeScript output */
188
+ function parseRoutesFromCompiled(content: string): RouteInfo[] {
189
+ const routes: RouteInfo[] = [];
190
+ // The compiled router contains route definitions as a radix tree structure
191
+ // Parse the route entries from comment blocks or structured data
192
+ const routePattern =
193
+ /\/\/ Route: (GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS) ([^\n]+)/g;
194
+ let match: RegExpExecArray | null;
195
+ while ((match = routePattern.exec(content)) !== null) {
196
+ routes.push({
197
+ method: match[1],
198
+ path: match[2].trim(),
199
+ });
200
+ }
201
+
202
+ // Also try to extract from the TypeScript type annotations
203
+ // Pattern: "METHOD /path" in route table keys
204
+ const keyPattern = /"(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS) (\/[^"]+)"/g;
205
+ while ((match = keyPattern.exec(content)) !== null) {
206
+ const method = match[1];
207
+ const path = match[2];
208
+ // Avoid duplicates
209
+ if (!routes.some((r) => r.method === method && r.path === path)) {
210
+ const params: string[] = [];
211
+ const paramPattern = /:(\w+)/g;
212
+ let paramMatch: RegExpExecArray | null;
213
+ while ((paramMatch = paramPattern.exec(path)) !== null) {
214
+ params.push(paramMatch[1]);
215
+ }
216
+ routes.push({
217
+ method,
218
+ path,
219
+ params: params.length > 0 ? params : undefined,
220
+ });
221
+ }
222
+ }
223
+
224
+ return routes;
225
+ }
226
+
227
+ /** Parse schema types from OpenAPI or type metadata files */
228
+ function parseSchemaFromOpenApi(
229
+ content: string,
230
+ typeName?: string,
231
+ ): SchemaInfo[] {
232
+ try {
233
+ const spec = JSON.parse(content) as {
234
+ components?: {
235
+ schemas?: Record<
236
+ string,
237
+ {
238
+ type?: string;
239
+ properties?: Record<string, { type?: string; format?: string }>;
240
+ required?: string[];
241
+ }
242
+ >;
243
+ };
244
+ paths?: Record<
245
+ string,
246
+ Record<
247
+ string,
248
+ {
249
+ parameters?: Array<{ schema?: { $ref?: string } }>;
250
+ requestBody?: {
251
+ content?: Record<string, { schema?: { $ref?: string } }>;
252
+ };
253
+ responses?: Record<
254
+ string,
255
+ { content?: Record<string, { schema?: { $ref?: string } }> }
256
+ >;
257
+ }
258
+ >
259
+ >;
260
+ };
261
+
262
+ const schemas: SchemaInfo[] = [];
263
+ const componentSchemas = spec.components?.schemas ?? {};
264
+
265
+ for (const [name, schema] of Object.entries(componentSchemas)) {
266
+ if (typeName && name !== typeName) continue;
267
+
268
+ const properties: SchemaInfo["properties"] = {};
269
+ const required = schema.required ?? [];
270
+
271
+ if (schema.properties) {
272
+ for (const [propName, propDef] of Object.entries(schema.properties)) {
273
+ properties[propName] = {
274
+ type: propDef.type ?? "unknown",
275
+ optional: !required.includes(propName) ? true : undefined,
276
+ };
277
+ }
278
+ }
279
+
280
+ // Find where this type is used in paths
281
+ const usedIn: string[] = [];
282
+ if (spec.paths) {
283
+ for (const [pathKey, methods] of Object.entries(spec.paths)) {
284
+ for (const [method, operation] of Object.entries(methods)) {
285
+ const refPattern = `#/components/schemas/${name}`;
286
+ const opStr = JSON.stringify(operation);
287
+ if (opStr.includes(refPattern)) {
288
+ usedIn.push(`${method.toUpperCase()} ${pathKey}`);
289
+ }
290
+ }
291
+ }
292
+ }
293
+
294
+ schemas.push({ name, properties, usedIn });
295
+ }
296
+
297
+ return schemas;
298
+ } catch {
299
+ return [];
300
+ }
301
+ }
302
+
303
+ export async function inspectRoutes(
304
+ rootDir: string,
305
+ config: Required<TypoKitConfig>,
306
+ ): Promise<InspectResult> {
307
+ const content = await readGeneratedFile(
308
+ rootDir,
309
+ config,
310
+ "routes/compiled-router.ts",
311
+ );
312
+ if (!content) {
313
+ return {
314
+ success: false,
315
+ data: [],
316
+ error: "No compiled routes found. Run 'typokit build' first.",
317
+ };
318
+ }
319
+
320
+ const routes = parseRoutesFromCompiled(content);
321
+
322
+ // Also try to enrich from OpenAPI spec
323
+ const openApiContent = await readGeneratedFile(
324
+ rootDir,
325
+ config,
326
+ "schemas/openapi.json",
327
+ );
328
+ if (openApiContent) {
329
+ try {
330
+ const spec = JSON.parse(openApiContent) as {
331
+ paths?: Record<
332
+ string,
333
+ Record<
334
+ string,
335
+ {
336
+ summary?: string;
337
+ parameters?: Array<{
338
+ name: string;
339
+ in: string;
340
+ schema?: { type?: string };
341
+ }>;
342
+ requestBody?: {
343
+ content?: Record<
344
+ string,
345
+ { schema?: { $ref?: string; type?: string } }
346
+ >;
347
+ };
348
+ responses?: Record<
349
+ string,
350
+ {
351
+ description?: string;
352
+ content?: Record<
353
+ string,
354
+ { schema?: { $ref?: string; type?: string } }
355
+ >;
356
+ }
357
+ >;
358
+ }
359
+ >
360
+ >;
361
+ };
362
+
363
+ if (spec.paths) {
364
+ for (const route of routes) {
365
+ const pathEntry = spec.paths[route.path];
366
+ if (!pathEntry) continue;
367
+ const methodEntry = pathEntry[route.method.toLowerCase()];
368
+ if (!methodEntry) continue;
369
+
370
+ // Extract query params
371
+ if (methodEntry.parameters) {
372
+ const queryParams: Record<string, string> = {};
373
+ for (const param of methodEntry.parameters) {
374
+ if (param.in === "query") {
375
+ queryParams[param.name] = param.schema?.type ?? "string";
376
+ }
377
+ }
378
+ if (Object.keys(queryParams).length > 0) {
379
+ route.query = queryParams;
380
+ }
381
+ }
382
+
383
+ // Extract body ref
384
+ const bodyContent = methodEntry.requestBody?.content;
385
+ if (bodyContent) {
386
+ const jsonBody = bodyContent["application/json"];
387
+ if (jsonBody?.schema) {
388
+ route.body =
389
+ jsonBody.schema["$ref"]?.replace("#/components/schemas/", "") ??
390
+ jsonBody.schema.type;
391
+ }
392
+ }
393
+
394
+ // Extract response ref
395
+ const resp200 = methodEntry.responses?.["200"];
396
+ if (resp200?.content) {
397
+ const jsonResp = resp200.content["application/json"];
398
+ if (jsonResp?.schema) {
399
+ route.response =
400
+ jsonResp.schema["$ref"]?.replace("#/components/schemas/", "") ??
401
+ jsonResp.schema.type;
402
+ }
403
+ }
404
+ }
405
+ }
406
+ } catch {
407
+ // OpenAPI enrichment failed, continue with basic routes
408
+ }
409
+ }
410
+
411
+ return { success: true, data: routes };
412
+ }
413
+
414
+ export async function inspectRoute(
415
+ rootDir: string,
416
+ config: Required<TypoKitConfig>,
417
+ routeKey: string,
418
+ ): Promise<InspectResult> {
419
+ const result = await inspectRoutes(rootDir, config);
420
+ if (!result.success) return result;
421
+
422
+ const routes = result.data as RouteInfo[];
423
+ // Match "GET /users/:id" format
424
+ const parts = routeKey.split(" ", 2);
425
+ const method = parts[0]?.toUpperCase() ?? "";
426
+ const path = parts[1] ?? "";
427
+
428
+ const found = routes.find((r) => r.method === method && r.path === path);
429
+ if (!found) {
430
+ return {
431
+ success: false,
432
+ data: null,
433
+ error: `Route not found: ${routeKey}`,
434
+ };
435
+ }
436
+
437
+ return { success: true, data: found };
438
+ }
439
+
440
+ export async function inspectMiddleware(
441
+ rootDir: string,
442
+ config: Required<TypoKitConfig>,
443
+ ): Promise<InspectResult> {
444
+ // Middleware info is not persisted to disk during build.
445
+ // Return what we can infer from the build output or provide empty result.
446
+ const content = await readGeneratedFile(
447
+ rootDir,
448
+ config,
449
+ "routes/compiled-router.ts",
450
+ );
451
+ const middleware: MiddlewareInfo[] = [];
452
+
453
+ if (content) {
454
+ // Check for middleware references in compiled output
455
+ const mwPattern = /middleware:\s*\[([^\]]*)\]/g;
456
+ let match: RegExpExecArray | null;
457
+ let order = 0;
458
+ while ((match = mwPattern.exec(content)) !== null) {
459
+ const refs = match[1]
460
+ .split(",")
461
+ .map((s) => s.trim().replace(/['"]/g, ""))
462
+ .filter(Boolean);
463
+ for (const ref of refs) {
464
+ if (!middleware.some((m) => m.name === ref)) {
465
+ middleware.push({
466
+ name: ref,
467
+ priority: order++,
468
+ type: "registered",
469
+ });
470
+ }
471
+ }
472
+ }
473
+ }
474
+
475
+ // Always include the built-in error middleware
476
+ middleware.push({
477
+ name: "errorMiddleware",
478
+ priority: middleware.length,
479
+ type: "built-in",
480
+ });
481
+
482
+ return { success: true, data: middleware };
483
+ }
484
+
485
+ export async function inspectDependencies(
486
+ rootDir: string,
487
+ _config: Required<TypoKitConfig>,
488
+ ): Promise<InspectResult> {
489
+ const { join } = (await import(/* @vite-ignore */ "path")) as {
490
+ join: (...args: string[]) => string;
491
+ };
492
+ const { existsSync, readFileSync } = (await import(
493
+ /* @vite-ignore */ "fs"
494
+ )) as {
495
+ existsSync: (p: string) => boolean;
496
+ readFileSync: (p: string, encoding: string) => string;
497
+ };
498
+
499
+ // Build dependency graph from package.json files in the project
500
+ const nodes: DependencyNode[] = [];
501
+ const pkgPath = join(rootDir, "package.json");
502
+
503
+ if (existsSync(pkgPath)) {
504
+ try {
505
+ const pkgContent = readFileSync(pkgPath, "utf-8");
506
+ const pkg = JSON.parse(pkgContent) as {
507
+ name?: string;
508
+ dependencies?: Record<string, string>;
509
+ devDependencies?: Record<string, string>;
510
+ };
511
+
512
+ const deps = Object.keys(pkg.dependencies ?? {}).filter((d) =>
513
+ d.startsWith("@typokit/"),
514
+ );
515
+ nodes.push({
516
+ name: pkg.name ?? "root",
517
+ dependsOn: deps,
518
+ });
519
+ } catch {
520
+ // Skip unparseable
521
+ }
522
+ }
523
+
524
+ // Also check for workspace packages if we're in a monorepo
525
+ const packagesDir = join(rootDir, "packages");
526
+ const { readdirSync } = (await import(/* @vite-ignore */ "fs")) as {
527
+ readdirSync: (p: string) => string[];
528
+ };
529
+
530
+ if (existsSync(packagesDir)) {
531
+ try {
532
+ const packages = readdirSync(packagesDir);
533
+ for (const pkg of packages) {
534
+ const subPkgPath = join(packagesDir, pkg, "package.json");
535
+ if (existsSync(subPkgPath)) {
536
+ try {
537
+ const content = readFileSync(subPkgPath, "utf-8");
538
+ const subPkg = JSON.parse(content) as {
539
+ name?: string;
540
+ dependencies?: Record<string, string>;
541
+ };
542
+ const deps = Object.keys(subPkg.dependencies ?? {}).filter((d) =>
543
+ d.startsWith("@typokit/"),
544
+ );
545
+ nodes.push({
546
+ name: subPkg.name ?? `@typokit/${pkg}`,
547
+ dependsOn: deps,
548
+ });
549
+ } catch {
550
+ // Skip
551
+ }
552
+ }
553
+ }
554
+ } catch {
555
+ // Not a monorepo root
556
+ }
557
+ }
558
+
559
+ return { success: true, data: nodes };
560
+ }
561
+
562
+ export async function inspectSchema(
563
+ rootDir: string,
564
+ config: Required<TypoKitConfig>,
565
+ typeName: string,
566
+ ): Promise<InspectResult> {
567
+ const openApiContent = await readGeneratedFile(
568
+ rootDir,
569
+ config,
570
+ "schemas/openapi.json",
571
+ );
572
+ if (!openApiContent) {
573
+ return {
574
+ success: false,
575
+ data: null,
576
+ error: "No OpenAPI spec found. Run 'typokit build' first.",
577
+ };
578
+ }
579
+
580
+ const schemas = parseSchemaFromOpenApi(openApiContent, typeName);
581
+ if (schemas.length === 0) {
582
+ return {
583
+ success: false,
584
+ data: null,
585
+ error: `Schema not found: ${typeName}`,
586
+ };
587
+ }
588
+
589
+ return { success: true, data: schemas[0] };
590
+ }
591
+
592
+ export async function inspectErrors(
593
+ debugPort: number,
594
+ lastN: number,
595
+ ): Promise<InspectResult> {
596
+ // Errors require a running server with debug sidecar
597
+ // Attempt to fetch from the debug sidecar endpoint
598
+ try {
599
+ const url = `http://localhost:${debugPort}/_debug/errors?last=${lastN}`;
600
+ const response = await fetchDebugEndpoint(url);
601
+ return { success: true, data: response };
602
+ } catch {
603
+ return {
604
+ success: false,
605
+ data: [] as ErrorEntry[],
606
+ error: `Could not connect to debug sidecar on port ${debugPort}. Is the server running with debug enabled?`,
607
+ };
608
+ }
609
+ }
610
+
611
+ export async function inspectPerformance(
612
+ debugPort: number,
613
+ routePath: string,
614
+ ): Promise<InspectResult> {
615
+ try {
616
+ const url = `http://localhost:${debugPort}/_debug/performance?route=${encodeURIComponent(routePath)}`;
617
+ const response = await fetchDebugEndpoint(url);
618
+ return { success: true, data: response };
619
+ } catch {
620
+ return {
621
+ success: false,
622
+ data: null as unknown as PerformanceInfo,
623
+ error: `Could not connect to debug sidecar on port ${debugPort}. Is the server running with debug enabled?`,
624
+ };
625
+ }
626
+ }
627
+
628
+ export async function inspectServer(debugPort: number): Promise<InspectResult> {
629
+ try {
630
+ const url = `http://localhost:${debugPort}/_debug/health`;
631
+ const response = await fetchDebugEndpoint(url);
632
+ return { success: true, data: response };
633
+ } catch {
634
+ return {
635
+ success: false,
636
+ data: {
637
+ adapter: "unknown",
638
+ platform: "unknown",
639
+ status: "not running",
640
+ } as ServerInfo,
641
+ error: `Could not connect to debug sidecar on port ${debugPort}. Is the server running with debug enabled?`,
642
+ };
643
+ }
644
+ }
645
+
646
+ export async function inspectBuildPipeline(
647
+ rootDir: string,
648
+ config: Required<TypoKitConfig>,
649
+ ): Promise<InspectResult> {
650
+ // Build pipeline hooks are defined in the plugin system
651
+ // Return the standard hook order from the TypoKit build pipeline
652
+ const hooks: BuildHookInfo[] = [
653
+ {
654
+ name: "beforeTransform",
655
+ order: 1,
656
+ description: "Register additional type sources before parsing",
657
+ },
658
+ {
659
+ name: "afterTypeParse",
660
+ order: 2,
661
+ description: "Inspect or modify the schema type map after parsing",
662
+ },
663
+ {
664
+ name: "afterValidators",
665
+ order: 3,
666
+ description: "Add custom validators after Typia generation",
667
+ },
668
+ {
669
+ name: "afterRouteTable",
670
+ order: 4,
671
+ description: "Post-process the compiled route table",
672
+ },
673
+ { name: "emit", order: 5, description: "Plugins emit their own artifacts" },
674
+ {
675
+ name: "done",
676
+ order: 6,
677
+ description: "Cleanup, reporting, and finalization",
678
+ },
679
+ ];
680
+
681
+ // Try to detect registered plugins from config or build output
682
+ const { join } = (await import(/* @vite-ignore */ "path")) as {
683
+ join: (...args: string[]) => string;
684
+ };
685
+ const { existsSync } = (await import(/* @vite-ignore */ "fs")) as {
686
+ existsSync: (p: string) => boolean;
687
+ };
688
+
689
+ const cacheHashPath = join(rootDir, config.outputDir, ".cache-hash");
690
+ const lastBuild = existsSync(cacheHashPath) ? "cached" : "no build found";
691
+
692
+ // Try to load plugins and show their registered taps
693
+ let registeredTaps: Array<{
694
+ hookName: string;
695
+ tapName: string;
696
+ order: number;
697
+ }> = [];
698
+ try {
699
+ const { createBuildPipeline, getPipelineTaps } = (await import(
700
+ /* @vite-ignore */ "@typokit/core"
701
+ )) as {
702
+ createBuildPipeline: () => {
703
+ hooks: Record<
704
+ string,
705
+ { tap(name: string, fn: (...args: unknown[]) => void): void }
706
+ >;
707
+ };
708
+ getPipelineTaps: (
709
+ pipeline: unknown,
710
+ ) => Array<{ hookName: string; tapName: string; order: number }>;
711
+ };
712
+ const pipeline = createBuildPipeline();
713
+ registeredTaps = getPipelineTaps(pipeline);
714
+ } catch {
715
+ // Core not available — skip tap introspection
716
+ }
717
+
718
+ return {
719
+ success: true,
720
+ data: {
721
+ hooks,
722
+ registeredTaps,
723
+ lastBuildStatus: lastBuild,
724
+ outputDir: config.outputDir,
725
+ },
726
+ };
727
+ }
728
+
729
+ /** Helper to fetch from the debug sidecar HTTP endpoint */
730
+ async function fetchDebugEndpoint(url: string): Promise<unknown> {
731
+ // Use dynamic import for http module (no @types/node)
732
+ const http = (await import(/* @vite-ignore */ "http")) as {
733
+ get: (
734
+ url: string,
735
+ cb: (res: {
736
+ statusCode?: number;
737
+ on(event: string, cb: (data?: unknown) => void): void;
738
+ setEncoding(enc: string): void;
739
+ }) => void,
740
+ ) => { on(event: string, cb: (err: Error) => void): void };
741
+ };
742
+
743
+ return new Promise((resolve, reject) => {
744
+ const req = http.get(url, (res) => {
745
+ let body = "";
746
+ res.setEncoding("utf-8");
747
+ res.on("data", (chunk: unknown) => {
748
+ body += String(chunk);
749
+ });
750
+ res.on("end", () => {
751
+ try {
752
+ resolve(JSON.parse(body));
753
+ } catch {
754
+ resolve(body);
755
+ }
756
+ });
757
+ });
758
+ req.on("error", (err: Error) => {
759
+ reject(err);
760
+ });
761
+ });
762
+ }
763
+
764
+ // ─── Main Dispatcher ────────────────────────────────────────
765
+
766
+ export async function executeInspect(
767
+ options: InspectOptions,
768
+ ): Promise<InspectResult> {
769
+ const { rootDir, config, logger, subcommand, positional, flags } = options;
770
+ const json = isJsonOutput(flags);
771
+ const debugPort =
772
+ typeof flags["debug-port"] === "string"
773
+ ? parseInt(flags["debug-port"], 10)
774
+ : 9800;
775
+
776
+ let result: InspectResult;
777
+
778
+ switch (subcommand) {
779
+ case "routes": {
780
+ logger.step("inspect", "Listing all routes...");
781
+ result = await inspectRoutes(rootDir, config);
782
+ break;
783
+ }
784
+
785
+ case "route": {
786
+ const routeKey = positional[0];
787
+ if (!routeKey) {
788
+ return {
789
+ success: false,
790
+ data: null,
791
+ error: "Usage: typokit inspect route 'GET /users/:id'",
792
+ };
793
+ }
794
+ logger.step("inspect", `Looking up route: ${routeKey}`);
795
+ result = await inspectRoute(rootDir, config, routeKey);
796
+ break;
797
+ }
798
+
799
+ case "middleware": {
800
+ logger.step("inspect", "Listing middleware chain...");
801
+ result = await inspectMiddleware(rootDir, config);
802
+ break;
803
+ }
804
+
805
+ case "dependencies":
806
+ case "deps": {
807
+ logger.step("inspect", "Building dependency graph...");
808
+ result = await inspectDependencies(rootDir, config);
809
+ break;
810
+ }
811
+
812
+ case "schema": {
813
+ const typeName = positional[0];
814
+ if (!typeName) {
815
+ return {
816
+ success: false,
817
+ data: null,
818
+ error: "Usage: typokit inspect schema <TypeName>",
819
+ };
820
+ }
821
+ logger.step("inspect", `Looking up schema: ${typeName}`);
822
+ result = await inspectSchema(rootDir, config, typeName);
823
+ break;
824
+ }
825
+
826
+ case "errors": {
827
+ const lastN =
828
+ typeof flags["last"] === "string" ? parseInt(flags["last"], 10) : 10;
829
+ logger.step("inspect", `Fetching last ${lastN} errors...`);
830
+ result = await inspectErrors(debugPort, lastN);
831
+ break;
832
+ }
833
+
834
+ case "performance": {
835
+ const routePath =
836
+ typeof flags["route"] === "string" ? flags["route"] : "/";
837
+ logger.step("inspect", `Fetching performance for: ${routePath}`);
838
+ result = await inspectPerformance(debugPort, routePath);
839
+ break;
840
+ }
841
+
842
+ case "server": {
843
+ logger.step("inspect", "Querying server info...");
844
+ result = await inspectServer(debugPort);
845
+ break;
846
+ }
847
+
848
+ case "build-pipeline": {
849
+ logger.step("inspect", "Listing build pipeline hooks...");
850
+ result = await inspectBuildPipeline(rootDir, config);
851
+ break;
852
+ }
853
+
854
+ default: {
855
+ logger.error(`Unknown inspect subcommand: ${subcommand}`);
856
+ logger.info("Available subcommands:");
857
+ logger.info(" routes List all registered routes");
858
+ logger.info(" route <key> Detailed single route info");
859
+ logger.info(" middleware Full middleware chain");
860
+ logger.info(" dependencies Service dependency graph");
861
+ logger.info(" schema <TypeName> Type details and usage");
862
+ logger.info(
863
+ " errors --last <N> Recent errors (requires running server)",
864
+ );
865
+ logger.info(
866
+ " performance --route <path> Latency percentiles (requires running server)",
867
+ );
868
+ logger.info(" server Active server adapter info");
869
+ logger.info(" build-pipeline Registered build hooks and order");
870
+ return {
871
+ success: false,
872
+ data: null,
873
+ error: `Unknown inspect subcommand: ${subcommand}`,
874
+ };
875
+ }
876
+ }
877
+
878
+ if (result.success) {
879
+ writeOutput(result.data, json, logger);
880
+ } else if (result.error) {
881
+ logger.error(result.error);
882
+ }
883
+
884
+ return result;
885
+ }