@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,411 @@
1
+ // @typokit/cli — Inspect Command Tests
2
+
3
+ import { describe, it, expect } from "@rstest/core";
4
+ import { parseArgs, createLogger } from "./index.js";
5
+ import type { InspectResult } from "./commands/inspect.js";
6
+
7
+ // ─── parseArgs for inspect ───────────────────────────────────
8
+
9
+ describe("parseArgs with inspect command", () => {
10
+ it("parses inspect routes", () => {
11
+ const result = parseArgs(["node", "typokit", "inspect", "routes"]);
12
+ expect(result.command).toBe("inspect");
13
+ expect(result.positional).toEqual(["routes"]);
14
+ });
15
+
16
+ it("parses inspect route with quoted key", () => {
17
+ const result = parseArgs([
18
+ "node",
19
+ "typokit",
20
+ "inspect",
21
+ "route",
22
+ "GET /users/:id",
23
+ ]);
24
+ expect(result.command).toBe("inspect");
25
+ expect(result.positional).toEqual(["route", "GET /users/:id"]);
26
+ });
27
+
28
+ it("parses inspect schema with type name", () => {
29
+ const result = parseArgs(["node", "typokit", "inspect", "schema", "User"]);
30
+ expect(result.command).toBe("inspect");
31
+ expect(result.positional).toEqual(["schema", "User"]);
32
+ });
33
+
34
+ it("parses --json flag", () => {
35
+ const result = parseArgs([
36
+ "node",
37
+ "typokit",
38
+ "inspect",
39
+ "routes",
40
+ "--json",
41
+ ]);
42
+ expect(result.flags["json"]).toBe(true);
43
+ });
44
+
45
+ it("parses --format json", () => {
46
+ const result = parseArgs([
47
+ "node",
48
+ "typokit",
49
+ "inspect",
50
+ "routes",
51
+ "--format",
52
+ "json",
53
+ ]);
54
+ expect(result.flags["format"]).toBe("json");
55
+ });
56
+
57
+ it("parses inspect errors --last 5", () => {
58
+ const result = parseArgs([
59
+ "node",
60
+ "typokit",
61
+ "inspect",
62
+ "errors",
63
+ "--last",
64
+ "5",
65
+ ]);
66
+ expect(result.command).toBe("inspect");
67
+ expect(result.positional).toEqual(["errors"]);
68
+ expect(result.flags["last"]).toBe("5");
69
+ });
70
+
71
+ it("parses inspect performance --route /users", () => {
72
+ const result = parseArgs([
73
+ "node",
74
+ "typokit",
75
+ "inspect",
76
+ "performance",
77
+ "--route",
78
+ "/users",
79
+ ]);
80
+ expect(result.command).toBe("inspect");
81
+ expect(result.positional).toEqual(["performance"]);
82
+ expect(result.flags["route"]).toBe("/users");
83
+ });
84
+
85
+ it("parses inspect build-pipeline", () => {
86
+ const result = parseArgs(["node", "typokit", "inspect", "build-pipeline"]);
87
+ expect(result.command).toBe("inspect");
88
+ expect(result.positional).toEqual(["build-pipeline"]);
89
+ });
90
+
91
+ it("parses inspect deps as alias for dependencies", () => {
92
+ const result = parseArgs(["node", "typokit", "inspect", "deps"]);
93
+ expect(result.command).toBe("inspect");
94
+ expect(result.positional).toEqual(["deps"]);
95
+ });
96
+ });
97
+
98
+ // ─── executeInspect ──────────────────────────────────────────
99
+
100
+ describe("executeInspect", () => {
101
+ const logger = createLogger({ verbose: false });
102
+ const baseConfig = {
103
+ typeFiles: ["src/**/*.types.ts"],
104
+ routeFiles: ["src/**/*.routes.ts"],
105
+ outputDir: ".typokit",
106
+ distDir: "dist",
107
+ compiler: "tsc" as const,
108
+ compilerArgs: [],
109
+ };
110
+
111
+ it("returns error for unknown subcommand", async () => {
112
+ const { executeInspect } = await import("./commands/inspect.js");
113
+ const result = await executeInspect({
114
+ rootDir: "/nonexistent",
115
+ config: baseConfig,
116
+ logger,
117
+ subcommand: "unknown-thing",
118
+ positional: [],
119
+ flags: {},
120
+ });
121
+ expect(result.success).toBe(false);
122
+ expect(result.error).toContain("Unknown inspect subcommand");
123
+ });
124
+
125
+ it("returns error for routes when no build output exists", async () => {
126
+ const { executeInspect } = await import("./commands/inspect.js");
127
+ const result = await executeInspect({
128
+ rootDir: "/nonexistent/path/no/build",
129
+ config: baseConfig,
130
+ logger,
131
+ subcommand: "routes",
132
+ positional: [],
133
+ flags: {},
134
+ });
135
+ expect(result.success).toBe(false);
136
+ expect(result.error).toContain("Run 'typokit build' first");
137
+ });
138
+
139
+ it("returns error for schema without type name", async () => {
140
+ const { executeInspect } = await import("./commands/inspect.js");
141
+ const result = await executeInspect({
142
+ rootDir: "/nonexistent",
143
+ config: baseConfig,
144
+ logger,
145
+ subcommand: "schema",
146
+ positional: [],
147
+ flags: {},
148
+ });
149
+ expect(result.success).toBe(false);
150
+ expect(result.error).toContain("Usage:");
151
+ });
152
+
153
+ it("returns error for route without key", async () => {
154
+ const { executeInspect } = await import("./commands/inspect.js");
155
+ const result = await executeInspect({
156
+ rootDir: "/nonexistent",
157
+ config: baseConfig,
158
+ logger,
159
+ subcommand: "route",
160
+ positional: [],
161
+ flags: {},
162
+ });
163
+ expect(result.success).toBe(false);
164
+ expect(result.error).toContain("Usage:");
165
+ });
166
+
167
+ it("returns build pipeline hooks", async () => {
168
+ const { executeInspect } = await import("./commands/inspect.js");
169
+ const result = await executeInspect({
170
+ rootDir: "/nonexistent",
171
+ config: baseConfig,
172
+ logger,
173
+ subcommand: "build-pipeline",
174
+ positional: [],
175
+ flags: {},
176
+ });
177
+ expect(result.success).toBe(true);
178
+ const data = result.data as {
179
+ hooks: Array<{ name: string; order: number; description: string }>;
180
+ lastBuildStatus: string;
181
+ };
182
+ expect(Array.isArray(data.hooks)).toBe(true);
183
+ expect(data.hooks.length).toBe(6);
184
+ expect(data.hooks[0].name).toBe("beforeTransform");
185
+ expect(data.hooks[5].name).toBe("done");
186
+ expect(data.lastBuildStatus).toBe("no build found");
187
+ });
188
+
189
+ it("returns dependency graph from monorepo root", async () => {
190
+ const { executeInspect } = await import("./commands/inspect.js");
191
+ const g = globalThis as Record<string, unknown>;
192
+ const proc = g["process"] as { cwd(): string } | undefined;
193
+ const cwd = proc?.cwd() ?? ".";
194
+ // rstest runs from the package root (packages/cli), go up to monorepo root
195
+ const { resolve } = await import("path");
196
+ const monorepoRoot = resolve(cwd, "../..");
197
+
198
+ const result = await executeInspect({
199
+ rootDir: monorepoRoot,
200
+ config: baseConfig,
201
+ logger,
202
+ subcommand: "dependencies",
203
+ positional: [],
204
+ flags: {},
205
+ });
206
+ expect(result.success).toBe(true);
207
+ const nodes = result.data as Array<{ name: string; dependsOn: string[] }>;
208
+ expect(Array.isArray(nodes)).toBe(true);
209
+ // Should find at least some @typokit packages
210
+ expect(nodes.length).toBeGreaterThan(0);
211
+ });
212
+
213
+ it("returns middleware info with built-in error middleware", async () => {
214
+ const { executeInspect } = await import("./commands/inspect.js");
215
+ const result = await executeInspect({
216
+ rootDir: "/nonexistent",
217
+ config: baseConfig,
218
+ logger,
219
+ subcommand: "middleware",
220
+ positional: [],
221
+ flags: {},
222
+ });
223
+ expect(result.success).toBe(true);
224
+ const mw = result.data as Array<{ name: string; type: string }>;
225
+ expect(Array.isArray(mw)).toBe(true);
226
+ expect(mw.some((m) => m.name === "errorMiddleware")).toBe(true);
227
+ });
228
+ });
229
+
230
+ // ─── Individual inspect functions ────────────────────────────
231
+
232
+ describe("inspectRoutes", () => {
233
+ it("returns empty routes when no compiled output", async () => {
234
+ const { inspectRoutes } = await import("./commands/inspect.js");
235
+ const result = await inspectRoutes("/nonexistent/path", {
236
+ typeFiles: [],
237
+ routeFiles: [],
238
+ outputDir: ".typokit",
239
+ distDir: "dist",
240
+ compiler: "tsc",
241
+ compilerArgs: [],
242
+ });
243
+ expect(result.success).toBe(false);
244
+ expect(result.error).toContain("No compiled routes");
245
+ });
246
+ });
247
+
248
+ describe("inspectSchema", () => {
249
+ it("returns error when no OpenAPI spec exists", async () => {
250
+ const { inspectSchema } = await import("./commands/inspect.js");
251
+ const result = await inspectSchema(
252
+ "/nonexistent/path",
253
+ {
254
+ typeFiles: [],
255
+ routeFiles: [],
256
+ outputDir: ".typokit",
257
+ distDir: "dist",
258
+ compiler: "tsc",
259
+ compilerArgs: [],
260
+ },
261
+ "User",
262
+ );
263
+ expect(result.success).toBe(false);
264
+ expect(result.error).toContain("No OpenAPI spec");
265
+ });
266
+ });
267
+
268
+ describe("inspectServer", () => {
269
+ it("returns not-running when no debug sidecar", async () => {
270
+ const { inspectServer } = await import("./commands/inspect.js");
271
+ // Use a port that won't have a server
272
+ const result = await inspectServer(19999);
273
+ expect(result.success).toBe(false);
274
+ const data = result.data as { status: string };
275
+ expect(data.status).toBe("not running");
276
+ expect(result.error).toContain("Could not connect");
277
+ });
278
+ });
279
+
280
+ describe("inspectErrors", () => {
281
+ it("returns error when no debug sidecar", async () => {
282
+ const { inspectErrors } = await import("./commands/inspect.js");
283
+ const result = await inspectErrors(19999, 5);
284
+ expect(result.success).toBe(false);
285
+ expect(result.error).toContain("Could not connect");
286
+ });
287
+ });
288
+
289
+ describe("inspectPerformance", () => {
290
+ it("returns error when no debug sidecar", async () => {
291
+ const { inspectPerformance } = await import("./commands/inspect.js");
292
+ const result = await inspectPerformance(19999, "/users");
293
+ expect(result.success).toBe(false);
294
+ expect(result.error).toContain("Could not connect");
295
+ });
296
+ });
297
+
298
+ describe("inspectBuildPipeline", () => {
299
+ it("returns standard hook order", async () => {
300
+ const { inspectBuildPipeline } = await import("./commands/inspect.js");
301
+ const result = await inspectBuildPipeline("/nonexistent", {
302
+ typeFiles: [],
303
+ routeFiles: [],
304
+ outputDir: ".typokit",
305
+ distDir: "dist",
306
+ compiler: "tsc",
307
+ compilerArgs: [],
308
+ });
309
+ expect(result.success).toBe(true);
310
+ const data = result.data as {
311
+ hooks: Array<{ name: string; order: number }>;
312
+ };
313
+ expect(data.hooks.length).toBe(6);
314
+ // Verify hook ordering
315
+ for (let i = 0; i < data.hooks.length - 1; i++) {
316
+ expect(data.hooks[i].order).toBeLessThan(data.hooks[i + 1].order);
317
+ }
318
+ });
319
+ });
320
+
321
+ describe("inspectDependencies", () => {
322
+ it("returns empty graph for nonexistent directory", async () => {
323
+ const { inspectDependencies } = await import("./commands/inspect.js");
324
+ const result = await inspectDependencies("/nonexistent/path/nowhere", {
325
+ typeFiles: [],
326
+ routeFiles: [],
327
+ outputDir: ".typokit",
328
+ distDir: "dist",
329
+ compiler: "tsc",
330
+ compilerArgs: [],
331
+ });
332
+ expect(result.success).toBe(true);
333
+ const nodes = result.data as Array<{ name: string }>;
334
+ expect(Array.isArray(nodes)).toBe(true);
335
+ // No package.json exists, so empty graph
336
+ expect(nodes.length).toBe(0);
337
+ });
338
+ });
339
+
340
+ // ─── InspectResult shape ─────────────────────────────────────
341
+
342
+ describe("InspectResult", () => {
343
+ it("has correct shape for success", () => {
344
+ const result: InspectResult = {
345
+ success: true,
346
+ data: { routes: [] },
347
+ };
348
+ expect(result.success).toBe(true);
349
+ expect(result.error).toBeUndefined();
350
+ });
351
+
352
+ it("has correct shape for failure", () => {
353
+ const result: InspectResult = {
354
+ success: false,
355
+ data: null,
356
+ error: "Something went wrong",
357
+ };
358
+ expect(result.success).toBe(false);
359
+ expect(result.error).toBe("Something went wrong");
360
+ });
361
+ });
362
+
363
+ // ─── JSON output validation ─────────────────────────────────
364
+
365
+ describe("JSON output", () => {
366
+ it("build-pipeline data is valid JSON", async () => {
367
+ const { inspectBuildPipeline } = await import("./commands/inspect.js");
368
+ const result = await inspectBuildPipeline("/nonexistent", {
369
+ typeFiles: [],
370
+ routeFiles: [],
371
+ outputDir: ".typokit",
372
+ distDir: "dist",
373
+ compiler: "tsc",
374
+ compilerArgs: [],
375
+ });
376
+ const jsonStr = JSON.stringify(result.data, null, 2);
377
+ const parsed = JSON.parse(jsonStr);
378
+ expect(parsed).not.toBeNull();
379
+ expect(typeof parsed).toBe("object");
380
+ });
381
+
382
+ it("dependency graph data is valid JSON", async () => {
383
+ const { inspectDependencies } = await import("./commands/inspect.js");
384
+ const result = await inspectDependencies("/nonexistent", {
385
+ typeFiles: [],
386
+ routeFiles: [],
387
+ outputDir: ".typokit",
388
+ distDir: "dist",
389
+ compiler: "tsc",
390
+ compilerArgs: [],
391
+ });
392
+ const jsonStr = JSON.stringify(result.data, null, 2);
393
+ const parsed = JSON.parse(jsonStr);
394
+ expect(Array.isArray(parsed)).toBe(true);
395
+ });
396
+
397
+ it("middleware data is valid JSON", async () => {
398
+ const { inspectMiddleware } = await import("./commands/inspect.js");
399
+ const result = await inspectMiddleware("/nonexistent", {
400
+ typeFiles: [],
401
+ routeFiles: [],
402
+ outputDir: ".typokit",
403
+ distDir: "dist",
404
+ compiler: "tsc",
405
+ compilerArgs: [],
406
+ });
407
+ const jsonStr = JSON.stringify(result.data, null, 2);
408
+ const parsed = JSON.parse(jsonStr);
409
+ expect(Array.isArray(parsed)).toBe(true);
410
+ });
411
+ });
package/src/logger.ts ADDED
@@ -0,0 +1,49 @@
1
+ // @typokit/cli — Structured CLI logger
2
+
3
+ export interface CliLogger {
4
+ info(message: string): void;
5
+ success(message: string): void;
6
+ warn(message: string): void;
7
+ error(message: string): void;
8
+ verbose(message: string): void;
9
+ step(label: string, message: string): void;
10
+ }
11
+
12
+ export function createLogger(options: { verbose?: boolean }): CliLogger {
13
+ const isVerbose = options.verbose ?? false;
14
+
15
+ const write = (prefix: string, message: string): void => {
16
+ const g = globalThis as Record<string, unknown>;
17
+ const proc = g["process"] as
18
+ | {
19
+ stderr: { write(s: string): void };
20
+ stdout: { write(s: string): void };
21
+ }
22
+ | undefined;
23
+ const out = proc?.stderr ?? { write: () => {} };
24
+ out.write(`${prefix} ${message}\n`);
25
+ };
26
+
27
+ return {
28
+ info(message: string) {
29
+ write("[info]", message);
30
+ },
31
+ success(message: string) {
32
+ write("[ok]", message);
33
+ },
34
+ warn(message: string) {
35
+ write("[warn]", message);
36
+ },
37
+ error(message: string) {
38
+ write("[error]", message);
39
+ },
40
+ verbose(message: string) {
41
+ if (isVerbose) {
42
+ write("[verbose]", message);
43
+ }
44
+ },
45
+ step(label: string, message: string) {
46
+ write(`[${label}]`, message);
47
+ },
48
+ };
49
+ }
@@ -0,0 +1,205 @@
1
+ // @typokit/cli — Migration Command Tests
2
+
3
+ import { describe, it, expect } from "@rstest/core";
4
+ import { createLogger } from "./index.js";
5
+ import type { MigrateCommandOptions } from "./commands/migrate.js";
6
+
7
+ // ─── Helper ─────────────────────────────────────────────────
8
+
9
+ function makeOptions(
10
+ overrides: Partial<MigrateCommandOptions> = {},
11
+ ): MigrateCommandOptions {
12
+ return {
13
+ rootDir: "/nonexistent",
14
+ config: {
15
+ typeFiles: [],
16
+ routeFiles: [],
17
+ outputDir: ".typokit",
18
+ distDir: "dist",
19
+ compiler: "tsc",
20
+ compilerArgs: [],
21
+ },
22
+ logger: createLogger({ verbose: false }),
23
+ subcommand: "generate",
24
+ flags: {},
25
+ verbose: false,
26
+ ...overrides,
27
+ };
28
+ }
29
+
30
+ // ─── executeMigrate dispatcher ──────────────────────────────
31
+
32
+ describe("executeMigrate", () => {
33
+ it("returns error for unknown subcommand", async () => {
34
+ const { executeMigrate } = await import("./commands/migrate.js");
35
+ const result = await executeMigrate(
36
+ makeOptions({ subcommand: "nonexistent" }),
37
+ );
38
+
39
+ expect(result.success).toBe(false);
40
+ expect(result.errors.length).toBeGreaterThan(0);
41
+ expect(result.errors[0]).toContain("Unknown migrate subcommand");
42
+ });
43
+
44
+ it("dispatches to migrate:generate with no type files", async () => {
45
+ const { executeMigrate } = await import("./commands/migrate.js");
46
+ const result = await executeMigrate(
47
+ makeOptions({ subcommand: "generate" }),
48
+ );
49
+
50
+ expect(result.success).toBe(true);
51
+ expect(result.filesWritten).toEqual([]);
52
+ expect(result.changes).toEqual([]);
53
+ });
54
+
55
+ it("dispatches to migrate:diff with no type files", async () => {
56
+ const { executeMigrate } = await import("./commands/migrate.js");
57
+ const result = await executeMigrate(makeOptions({ subcommand: "diff" }));
58
+
59
+ expect(result.success).toBe(true);
60
+ expect(result.filesWritten).toEqual([]);
61
+ expect(result.changes).toEqual([]);
62
+ });
63
+
64
+ it("dispatches to migrate:apply with no migrations dir", async () => {
65
+ const { executeMigrate } = await import("./commands/migrate.js");
66
+ const result = await executeMigrate(makeOptions({ subcommand: "apply" }));
67
+
68
+ expect(result.success).toBe(true);
69
+ expect(result.filesWritten).toEqual([]);
70
+ });
71
+ });
72
+
73
+ // ─── Utility functions ──────────────────────────────────────
74
+
75
+ describe("generateTimestamp", () => {
76
+ it("returns a 14-character timestamp string", async () => {
77
+ const { generateTimestamp } = await import("./commands/migrate.js");
78
+ const ts = generateTimestamp();
79
+ expect(ts.length).toBe(14);
80
+ expect(/^\d{14}$/.test(ts)).toBe(true);
81
+ });
82
+ });
83
+
84
+ describe("sanitizeName", () => {
85
+ it("converts to lowercase with underscores", async () => {
86
+ const { sanitizeName } = await import("./commands/migrate.js");
87
+ expect(sanitizeName("add-user-avatar")).toBe("add_user_avatar");
88
+ expect(sanitizeName("AddUserAvatar")).toBe("adduseravatar");
89
+ expect(sanitizeName(" spaces here ")).toBe("spaces_here");
90
+ });
91
+
92
+ it("removes leading and trailing underscores", async () => {
93
+ const { sanitizeName } = await import("./commands/migrate.js");
94
+ expect(sanitizeName("__test__")).toBe("test");
95
+ });
96
+ });
97
+
98
+ describe("isDestructiveChange", () => {
99
+ it("identifies remove changes as destructive", async () => {
100
+ const { isDestructiveChange } = await import("./commands/migrate.js");
101
+ expect(isDestructiveChange({ type: "remove", entity: "users" })).toBe(true);
102
+ expect(
103
+ isDestructiveChange({ type: "remove", entity: "users", field: "email" }),
104
+ ).toBe(true);
105
+ });
106
+
107
+ it("identifies type changes as destructive", async () => {
108
+ const { isDestructiveChange } = await import("./commands/migrate.js");
109
+ expect(
110
+ isDestructiveChange({
111
+ type: "modify",
112
+ entity: "users",
113
+ field: "age",
114
+ details: { oldType: "string", newType: "number" },
115
+ }),
116
+ ).toBe(true);
117
+ });
118
+
119
+ it("does not flag add changes as destructive", async () => {
120
+ const { isDestructiveChange } = await import("./commands/migrate.js");
121
+ expect(isDestructiveChange({ type: "add", entity: "users" })).toBe(false);
122
+ expect(
123
+ isDestructiveChange({ type: "add", entity: "users", field: "avatar" }),
124
+ ).toBe(false);
125
+ });
126
+
127
+ it("does not flag non-type modify as destructive", async () => {
128
+ const { isDestructiveChange } = await import("./commands/migrate.js");
129
+ expect(
130
+ isDestructiveChange({
131
+ type: "modify",
132
+ entity: "users",
133
+ field: "name",
134
+ details: { nullable: true },
135
+ }),
136
+ ).toBe(false);
137
+ });
138
+ });
139
+
140
+ describe("annotateSql", () => {
141
+ it("adds destructive comments to DROP statements", async () => {
142
+ const { annotateSql } = await import("./commands/migrate.js");
143
+ const sql = "DROP TABLE users;\nCREATE TABLE posts (id INTEGER);";
144
+ const changes = [{ type: "remove" as const, entity: "users" }];
145
+
146
+ const annotated = annotateSql(sql, changes);
147
+ expect(annotated).toContain("-- DESTRUCTIVE: requires review");
148
+ expect(annotated).toContain("DROP TABLE users;");
149
+ });
150
+
151
+ it("adds destructive comments to ALTER DROP statements", async () => {
152
+ const { annotateSql } = await import("./commands/migrate.js");
153
+ const sql = "ALTER TABLE users DROP COLUMN email;";
154
+ const changes = [
155
+ { type: "remove" as const, entity: "users", field: "email" },
156
+ ];
157
+
158
+ const annotated = annotateSql(sql, changes);
159
+ expect(annotated).toContain("-- DESTRUCTIVE: requires review");
160
+ });
161
+
162
+ it("does not annotate non-destructive SQL", async () => {
163
+ const { annotateSql } = await import("./commands/migrate.js");
164
+ const sql = "CREATE TABLE users (id INTEGER);";
165
+ const changes = [{ type: "add" as const, entity: "users" }];
166
+
167
+ const annotated = annotateSql(sql, changes);
168
+ expect(annotated).not.toContain("-- DESTRUCTIVE");
169
+ expect(annotated).toBe(sql);
170
+ });
171
+ });
172
+
173
+ // ─── Integration: parseArgs + run ───────────────────────────
174
+
175
+ describe("CLI migrate routing", () => {
176
+ it("parseArgs parses migrate:generate correctly", async () => {
177
+ const { parseArgs } = await import("./index.js");
178
+ const result = parseArgs([
179
+ "node",
180
+ "typokit",
181
+ "migrate:generate",
182
+ "--name",
183
+ "add-avatar",
184
+ ]);
185
+
186
+ expect(result.command).toBe("migrate:generate");
187
+ expect(result.flags["name"]).toBe("add-avatar");
188
+ });
189
+
190
+ it("parseArgs parses migrate:diff with --json", async () => {
191
+ const { parseArgs } = await import("./index.js");
192
+ const result = parseArgs(["node", "typokit", "migrate:diff", "--json"]);
193
+
194
+ expect(result.command).toBe("migrate:diff");
195
+ expect(result.flags["json"]).toBe(true);
196
+ });
197
+
198
+ it("parseArgs parses migrate:apply with --force", async () => {
199
+ const { parseArgs } = await import("./index.js");
200
+ const result = parseArgs(["node", "typokit", "migrate:apply", "--force"]);
201
+
202
+ expect(result.command).toBe("migrate:apply");
203
+ expect(result.flags["force"]).toBe(true);
204
+ });
205
+ });