@beignet/cli 0.0.31 → 0.0.33

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 (107) hide show
  1. package/CHANGELOG.md +107 -0
  2. package/README.md +70 -17
  3. package/dist/check.d.ts +2 -0
  4. package/dist/check.d.ts.map +1 -1
  5. package/dist/check.js +16 -0
  6. package/dist/check.js.map +1 -1
  7. package/dist/choices.d.ts +22 -2
  8. package/dist/choices.d.ts.map +1 -1
  9. package/dist/choices.js +62 -0
  10. package/dist/choices.js.map +1 -1
  11. package/dist/config.d.ts +2 -0
  12. package/dist/config.d.ts.map +1 -1
  13. package/dist/config.js +2 -0
  14. package/dist/config.js.map +1 -1
  15. package/dist/create-prompts.d.ts +6 -2
  16. package/dist/create-prompts.d.ts.map +1 -1
  17. package/dist/create-prompts.js +1 -1
  18. package/dist/create-prompts.js.map +1 -1
  19. package/dist/db.d.ts +5 -5
  20. package/dist/db.d.ts.map +1 -1
  21. package/dist/db.js +5 -5
  22. package/dist/db.js.map +1 -1
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +386 -27
  25. package/dist/index.js.map +1 -1
  26. package/dist/inspect.d.ts +5 -0
  27. package/dist/inspect.d.ts.map +1 -1
  28. package/dist/inspect.js +805 -62
  29. package/dist/inspect.js.map +1 -1
  30. package/dist/make/inbox.d.ts.map +1 -1
  31. package/dist/make/inbox.js +5 -3
  32. package/dist/make/inbox.js.map +1 -1
  33. package/dist/make/payments.d.ts.map +1 -1
  34. package/dist/make/payments.js +58 -29
  35. package/dist/make/payments.js.map +1 -1
  36. package/dist/make/shared.d.ts +12 -3
  37. package/dist/make/shared.d.ts.map +1 -1
  38. package/dist/make/shared.js +52 -11
  39. package/dist/make/shared.js.map +1 -1
  40. package/dist/make/tenancy.d.ts.map +1 -1
  41. package/dist/make/tenancy.js +6 -4
  42. package/dist/make/tenancy.js.map +1 -1
  43. package/dist/make.d.ts.map +1 -1
  44. package/dist/make.js +136 -65
  45. package/dist/make.js.map +1 -1
  46. package/dist/outbox.d.ts +114 -1
  47. package/dist/outbox.d.ts.map +1 -1
  48. package/dist/outbox.js +190 -0
  49. package/dist/outbox.js.map +1 -1
  50. package/dist/preflight.d.ts +90 -0
  51. package/dist/preflight.d.ts.map +1 -0
  52. package/dist/preflight.js +423 -0
  53. package/dist/preflight.js.map +1 -0
  54. package/dist/provider-add.d.ts +10 -1
  55. package/dist/provider-add.d.ts.map +1 -1
  56. package/dist/provider-add.js +255 -16
  57. package/dist/provider-add.js.map +1 -1
  58. package/dist/task.d.ts.map +1 -1
  59. package/dist/task.js +2 -3
  60. package/dist/task.js.map +1 -1
  61. package/dist/templates/agents.d.ts.map +1 -1
  62. package/dist/templates/agents.js +6 -3
  63. package/dist/templates/agents.js.map +1 -1
  64. package/dist/templates/base.d.ts.map +1 -1
  65. package/dist/templates/base.js +5 -11
  66. package/dist/templates/base.js.map +1 -1
  67. package/dist/templates/index.d.ts.map +1 -1
  68. package/dist/templates/index.js +2 -0
  69. package/dist/templates/index.js.map +1 -1
  70. package/dist/templates/server.d.ts.map +1 -1
  71. package/dist/templates/server.js +10 -5
  72. package/dist/templates/server.js.map +1 -1
  73. package/dist/templates/shared.d.ts +5 -1
  74. package/dist/templates/shared.d.ts.map +1 -1
  75. package/dist/templates/shared.js +13 -1
  76. package/dist/templates/shared.js.map +1 -1
  77. package/dist/templates/testing.d.ts +5 -0
  78. package/dist/templates/testing.d.ts.map +1 -0
  79. package/dist/templates/testing.js +542 -0
  80. package/dist/templates/testing.js.map +1 -0
  81. package/dist/templates/todos.js +2 -2
  82. package/package.json +3 -2
  83. package/skills/app-structure/SKILL.md +12 -4
  84. package/src/check.ts +23 -0
  85. package/src/choices.ts +84 -0
  86. package/src/config.ts +4 -0
  87. package/src/create-prompts.ts +8 -2
  88. package/src/db.ts +12 -12
  89. package/src/index.ts +592 -37
  90. package/src/inspect.ts +1253 -156
  91. package/src/make/inbox.ts +5 -3
  92. package/src/make/payments.ts +58 -29
  93. package/src/make/shared.ts +87 -13
  94. package/src/make/tenancy.ts +6 -4
  95. package/src/make.ts +179 -65
  96. package/src/outbox.ts +363 -0
  97. package/src/preflight.ts +596 -0
  98. package/src/provider-add.ts +270 -16
  99. package/src/task.ts +2 -3
  100. package/src/templates/agents.ts +6 -3
  101. package/src/templates/base.ts +5 -13
  102. package/src/templates/index.ts +2 -0
  103. package/src/templates/server.ts +10 -5
  104. package/src/templates/shared.ts +15 -1
  105. package/src/templates/testing.ts +545 -0
  106. package/src/templates/todos.ts +2 -2
  107. package/src/test-helpers/generated-app.ts +10 -6
@@ -0,0 +1,545 @@
1
+ import type { TemplateFile } from "./shared.js";
2
+
3
+ export const testHelperTemplateFile: TemplateFile = {
4
+ path: "lib/beignet-test.ts",
5
+ content: `import assert from "node:assert/strict";
6
+ import {
7
+ after,
8
+ afterEach,
9
+ before,
10
+ beforeEach,
11
+ describe as nodeDescribe,
12
+ it as nodeIt,
13
+ test as nodeTest,
14
+ } from "node:test";
15
+ import { inspect, isDeepStrictEqual } from "node:util";
16
+
17
+ export { after, afterEach, before, beforeEach };
18
+
19
+ type TestBody = () => void | Promise<void>;
20
+ type TestEach = {
21
+ <T>(cases: readonly T[]): (
22
+ name: string,
23
+ body: (value: T) => void | Promise<void>,
24
+ ) => void;
25
+ <T extends readonly unknown[]>(cases: readonly T[]): (
26
+ name: string,
27
+ body: (...values: T) => void | Promise<void>,
28
+ ) => void;
29
+ };
30
+ type TestRunner = ((name: string, body: TestBody) => unknown) & {
31
+ each: TestEach;
32
+ skip: (name: string, body: TestBody) => unknown;
33
+ skipIf: (condition: boolean) => TestRunner;
34
+ };
35
+ type NodeRunner = {
36
+ (name: string, body: TestBody): unknown;
37
+ (
38
+ name: string,
39
+ options: { skip?: boolean },
40
+ body: TestBody,
41
+ ): unknown;
42
+ };
43
+
44
+ type AsymmetricMatcher =
45
+ | { kind: "any"; expected: unknown }
46
+ | { kind: "arrayContaining"; expected: readonly unknown[] }
47
+ | { kind: "objectContaining"; expected: Record<string, unknown> };
48
+
49
+ type BaseMatchers = {
50
+ toBe(expected: unknown): void;
51
+ toEqual(expected: unknown): void;
52
+ toMatchObject(expected: unknown): void;
53
+ toContain(expected: unknown): void;
54
+ toContainEqual(expected: unknown): void;
55
+ toHaveLength(expected: number): void;
56
+ toBeDefined(): void;
57
+ toBeUndefined(): void;
58
+ toBeNull(): void;
59
+ toBeGreaterThan(expected: number): void;
60
+ toMatch(expected: string | RegExp): void;
61
+ toThrow(expected?: string | RegExp | (new (...args: unknown[]) => object)): void;
62
+ };
63
+
64
+ type AsyncMatchers = {
65
+ toBe(expected: unknown): Promise<void>;
66
+ toEqual(expected: unknown): Promise<void>;
67
+ toMatchObject(expected: unknown): Promise<void>;
68
+ toBeNull(): Promise<void>;
69
+ toBeUndefined(): Promise<void>;
70
+ toThrow(expected?: string | RegExp | (new (...args: unknown[]) => object)): Promise<void>;
71
+ };
72
+
73
+ type Matchers = BaseMatchers & {
74
+ not: BaseMatchers;
75
+ resolves: AsyncMatchers;
76
+ rejects: AsyncMatchers;
77
+ };
78
+
79
+ type Expect = ((actual: unknown) => Matchers) & {
80
+ any(expected: unknown): AsymmetricMatcher;
81
+ arrayContaining(expected: readonly unknown[]): AsymmetricMatcher;
82
+ objectContaining(expected: Record<string, unknown>): AsymmetricMatcher;
83
+ };
84
+
85
+ export const describe = createRunner(nodeDescribe as NodeRunner);
86
+ export const it = createRunner(nodeIt as NodeRunner);
87
+ export const test = createRunner(nodeTest as NodeRunner);
88
+
89
+ export const expect = Object.assign(
90
+ (actual: unknown) => createMatchers(actual),
91
+ {
92
+ any: (expected: unknown): AsymmetricMatcher => ({
93
+ kind: "any",
94
+ expected,
95
+ }),
96
+ arrayContaining: (expected: readonly unknown[]): AsymmetricMatcher => ({
97
+ kind: "arrayContaining",
98
+ expected,
99
+ }),
100
+ objectContaining: (
101
+ expected: Record<string, unknown>,
102
+ ): AsymmetricMatcher => ({ kind: "objectContaining", expected }),
103
+ },
104
+ ) satisfies Expect;
105
+
106
+ function createRunner(
107
+ base: NodeRunner,
108
+ options?: { skip?: boolean },
109
+ ): TestRunner {
110
+ const runner = ((name: string, body: TestBody) =>
111
+ runTest(base, name, body, options)) as TestRunner;
112
+ runner.each = ((cases: readonly unknown[]) => {
113
+ return (
114
+ name: string,
115
+ body: (...values: unknown[]) => void | Promise<void>,
116
+ ) => {
117
+ for (const testCase of cases) {
118
+ const values = (
119
+ Array.isArray(testCase) ? testCase : [testCase]
120
+ ) as unknown[];
121
+ runTest(base, formatEachName(name, values), () => body(...values), options);
122
+ }
123
+ };
124
+ }) as TestEach;
125
+ runner.skip = (name: string, body: TestBody) =>
126
+ runTest(base, name, body, { skip: true });
127
+ runner.skipIf = (condition: boolean) =>
128
+ condition ? createRunner(base, { skip: true }) : runner;
129
+ return runner;
130
+ }
131
+
132
+ function runTest(
133
+ base: NodeRunner,
134
+ name: string,
135
+ body: TestBody,
136
+ options?: { skip?: boolean },
137
+ ): unknown {
138
+ return options ? base(name, options, body) : base(name, body);
139
+ }
140
+
141
+ function formatEachName(name: string, values: readonly unknown[]): string {
142
+ let index = 0;
143
+ return name.replace(/%[sdifoO]/g, () => formatInline(values[index++]));
144
+ }
145
+
146
+ function createMatchers(actual: unknown): Matchers {
147
+ return {
148
+ ...createBaseMatchers(actual, false),
149
+ not: createBaseMatchers(actual, true),
150
+ resolves: createAsyncMatchers(actual, "resolves"),
151
+ rejects: createAsyncMatchers(actual, "rejects"),
152
+ };
153
+ }
154
+
155
+ function createBaseMatchers(actual: unknown, negated: boolean): BaseMatchers {
156
+ return {
157
+ toBe(expected) {
158
+ check(
159
+ Object.is(actual, expected),
160
+ negated,
161
+ "Expected values to be identical.",
162
+ );
163
+ },
164
+ toEqual(expected) {
165
+ check(matchesExpected(actual, expected), negated, "Expected values to be equal.");
166
+ },
167
+ toMatchObject(expected) {
168
+ check(
169
+ matchesObject(actual, expected),
170
+ negated,
171
+ "Expected value to match object.",
172
+ );
173
+ },
174
+ toContain(expected) {
175
+ const matches =
176
+ typeof actual === "string"
177
+ ? actual.includes(String(expected))
178
+ : Array.isArray(actual) && actual.includes(expected);
179
+ check(matches, negated, "Expected value to contain item.");
180
+ },
181
+ toContainEqual(expected) {
182
+ check(
183
+ Array.isArray(actual) &&
184
+ actual.some((item) => matchesExpected(item, expected)),
185
+ negated,
186
+ "Expected array to contain equal item.",
187
+ );
188
+ },
189
+ toHaveLength(expected) {
190
+ const length =
191
+ actual !== null && typeof actual === "object"
192
+ ? (actual as { length?: unknown }).length
193
+ : undefined;
194
+ check(length === expected, negated, "Expected value to have length.");
195
+ },
196
+ toBeDefined() {
197
+ check(actual !== undefined, negated, "Expected value to be defined.");
198
+ },
199
+ toBeUndefined() {
200
+ check(actual === undefined, negated, "Expected value to be undefined.");
201
+ },
202
+ toBeNull() {
203
+ check(actual === null, negated, "Expected value to be null.");
204
+ },
205
+ toBeGreaterThan(expected) {
206
+ check(
207
+ typeof actual === "number" && actual > expected,
208
+ negated,
209
+ "Expected value to be greater than " + expected + ".",
210
+ );
211
+ },
212
+ toMatch(expected) {
213
+ const text = String(actual);
214
+ const matches =
215
+ typeof expected === "string" ? text.includes(expected) : expected.test(text);
216
+ check(matches, negated, "Expected value to match pattern.");
217
+ },
218
+ toThrow(expected) {
219
+ assert.equal(typeof actual, "function", "toThrow expects a function.");
220
+ let didThrow = false;
221
+ let thrown: unknown;
222
+ try {
223
+ (actual as () => unknown)();
224
+ } catch (error) {
225
+ didThrow = true;
226
+ thrown = error;
227
+ }
228
+ check(
229
+ didThrow && thrownMatches(thrown, expected),
230
+ negated,
231
+ "Expected function to throw.",
232
+ );
233
+ },
234
+ };
235
+ }
236
+
237
+ function createAsyncMatchers(
238
+ actual: unknown,
239
+ kind: "resolves" | "rejects",
240
+ ): AsyncMatchers {
241
+ const resolveActual = async () =>
242
+ kind === "resolves" ? await Promise.resolve(actual) : await rejected(actual);
243
+
244
+ return {
245
+ async toBe(expected) {
246
+ createBaseMatchers(await resolveActual(), false).toBe(expected);
247
+ },
248
+ async toEqual(expected) {
249
+ createBaseMatchers(await resolveActual(), false).toEqual(expected);
250
+ },
251
+ async toMatchObject(expected) {
252
+ createBaseMatchers(await resolveActual(), false).toMatchObject(expected);
253
+ },
254
+ async toBeNull() {
255
+ createBaseMatchers(await resolveActual(), false).toBeNull();
256
+ },
257
+ async toBeUndefined() {
258
+ createBaseMatchers(await resolveActual(), false).toBeUndefined();
259
+ },
260
+ async toThrow(expected) {
261
+ const thrown =
262
+ kind === "rejects" ? await rejected(actual) : await resolveActual();
263
+ check(
264
+ thrownMatches(thrown, expected),
265
+ false,
266
+ "Expected promise to reject with matching error.",
267
+ );
268
+ },
269
+ };
270
+ }
271
+
272
+ async function rejected(actual: unknown): Promise<unknown> {
273
+ try {
274
+ await Promise.resolve(actual);
275
+ } catch (error) {
276
+ return error;
277
+ }
278
+ assert.fail("Expected promise to reject.");
279
+ }
280
+
281
+ function check(condition: boolean, negated: boolean, message: string): void {
282
+ assert.ok(
283
+ negated ? !condition : condition,
284
+ negated ? "Negated assertion failed: " + message : message,
285
+ );
286
+ }
287
+
288
+ function matchesExpected(actual: unknown, expected: unknown): boolean {
289
+ if (!containsAsymmetric(expected)) {
290
+ return isDeepStrictEqual(actual, expected);
291
+ }
292
+
293
+ if (isAsymmetricMatcher(expected)) {
294
+ switch (expected.kind) {
295
+ case "any":
296
+ return matchesAny(actual, expected.expected);
297
+ case "arrayContaining":
298
+ return (
299
+ Array.isArray(actual) &&
300
+ expected.expected.every((expectedItem) =>
301
+ actual.some((actualItem) => matchesExpected(actualItem, expectedItem)),
302
+ )
303
+ );
304
+ case "objectContaining":
305
+ return matchesObject(actual, expected.expected);
306
+ }
307
+ }
308
+
309
+ if (Array.isArray(expected)) {
310
+ return (
311
+ Array.isArray(actual) &&
312
+ actual.length === expected.length &&
313
+ expected.every((item, index) => matchesExpected(actual[index], item))
314
+ );
315
+ }
316
+
317
+ if (isObjectRecord(expected)) {
318
+ if (!isObjectRecord(actual)) return false;
319
+ const actualKeys = Object.keys(actual);
320
+ const expectedKeys = Object.keys(expected);
321
+ return (
322
+ actualKeys.length === expectedKeys.length &&
323
+ expectedKeys.every(
324
+ (key) =>
325
+ Object.hasOwn(actual, key) &&
326
+ matchesExpected(actual[key], expected[key]),
327
+ )
328
+ );
329
+ }
330
+
331
+ return Object.is(actual, expected);
332
+ }
333
+
334
+ function matchesObject(actual: unknown, expected: unknown): boolean {
335
+ if (isAsymmetricMatcher(expected)) return matchesExpected(actual, expected);
336
+ if (Array.isArray(expected)) {
337
+ return (
338
+ Array.isArray(actual) &&
339
+ actual.length >= expected.length &&
340
+ expected.every((item, index) => matchesObject(actual[index], item))
341
+ );
342
+ }
343
+ if (!isObjectRecord(expected)) return matchesExpected(actual, expected);
344
+ if (!isObjectRecord(actual)) return false;
345
+ return Object.entries(expected).every(([key, expectedValue]) =>
346
+ matchesObject(actual[key], expectedValue),
347
+ );
348
+ }
349
+
350
+ function containsAsymmetric(value: unknown): boolean {
351
+ if (isAsymmetricMatcher(value)) return true;
352
+ if (Array.isArray(value)) return value.some(containsAsymmetric);
353
+ if (!isObjectRecord(value)) return false;
354
+ return Object.values(value).some(containsAsymmetric);
355
+ }
356
+
357
+ function isAsymmetricMatcher(value: unknown): value is AsymmetricMatcher {
358
+ return (
359
+ isObjectRecord(value) &&
360
+ (value.kind === "any" ||
361
+ value.kind === "arrayContaining" ||
362
+ value.kind === "objectContaining")
363
+ );
364
+ }
365
+
366
+ function matchesAny(actual: unknown, expected: unknown): boolean {
367
+ if (expected === String) return typeof actual === "string";
368
+ if (expected === Number) return typeof actual === "number";
369
+ if (expected === Boolean) return typeof actual === "boolean";
370
+ if (expected === Array) return Array.isArray(actual);
371
+ if (expected === Object) return isObjectRecord(actual);
372
+ if (expected instanceof Function) {
373
+ return actual instanceof (expected as new (...args: unknown[]) => object);
374
+ }
375
+ return actual !== undefined && actual !== null;
376
+ }
377
+
378
+ function thrownMatches(
379
+ error: unknown,
380
+ expected: string | RegExp | (new (...args: unknown[]) => object) | undefined,
381
+ ): boolean {
382
+ if (expected === undefined) return true;
383
+ if (typeof expected === "string") return errorMessage(error).includes(expected);
384
+ if (expected instanceof RegExp) return expected.test(errorMessage(error));
385
+ return error instanceof expected;
386
+ }
387
+
388
+ function errorMessage(error: unknown): string {
389
+ return error instanceof Error ? error.message : String(error);
390
+ }
391
+
392
+ function isObjectRecord(value: unknown): value is Record<string, unknown> {
393
+ return value !== null && typeof value === "object";
394
+ }
395
+
396
+ function formatInline(value: unknown): string {
397
+ return typeof value === "string"
398
+ ? value
399
+ : inspect(value, { breakLength: Number.POSITIVE_INFINITY, depth: 2 });
400
+ }
401
+ `,
402
+ };
403
+
404
+ export const testRunnerTemplateFile: TemplateFile = {
405
+ path: "lib/beignet-test-runner.ts",
406
+ content: `import { spawnSync } from "node:child_process";
407
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
408
+ import path from "node:path";
409
+
410
+ const defaultTestRoots = [
411
+ "app",
412
+ "features",
413
+ "infra",
414
+ "lib",
415
+ "ports",
416
+ "server",
417
+ "tests",
418
+ "src/tests",
419
+ ];
420
+ const ignoredDirectories = new Set([
421
+ ".beignet-local",
422
+ ".git",
423
+ ".next",
424
+ ".turbo",
425
+ "coverage",
426
+ "dist",
427
+ "node_modules",
428
+ ]);
429
+ const testFilePattern = /\\.(test|spec)\\.[cm]?[tj]sx?$/;
430
+
431
+ const explicitArgs = process.argv.slice(2);
432
+ const testArgs =
433
+ explicitArgs.length > 0
434
+ ? resolveTestArgs(process.cwd(), explicitArgs)
435
+ : discoverTestFiles(process.cwd());
436
+
437
+ if (testArgs.length === 0) {
438
+ console.log("No test files found.");
439
+ process.exit(0);
440
+ }
441
+
442
+ const result = spawnSync(
443
+ process.execPath,
444
+ ["--import", "tsx", "--test", ...testArgs],
445
+ {
446
+ cwd: process.cwd(),
447
+ env: process.env,
448
+ stdio: "inherit",
449
+ },
450
+ );
451
+
452
+ if (result.error) {
453
+ console.error(result.error.message);
454
+ process.exit(1);
455
+ }
456
+
457
+ process.exit(result.status ?? 1);
458
+
459
+ function discoverTestFiles(cwd: string): string[] {
460
+ const files: string[] = [];
461
+ for (const root of testRoots(cwd)) {
462
+ const rootPath = path.join(cwd, root);
463
+ if (existsSync(rootPath)) {
464
+ walk(rootPath, cwd, files);
465
+ }
466
+ }
467
+ return files.sort();
468
+ }
469
+
470
+ function resolveTestArgs(cwd: string, args: string[]): string[] {
471
+ const resolved: string[] = [];
472
+ for (const arg of args) {
473
+ if (arg.startsWith("-")) {
474
+ resolved.push(arg);
475
+ continue;
476
+ }
477
+
478
+ const argPath = path.resolve(cwd, arg);
479
+ if (!existsSync(argPath)) {
480
+ resolved.push(arg);
481
+ continue;
482
+ }
483
+
484
+ const stats = statSync(argPath);
485
+ if (stats.isDirectory()) {
486
+ const files: string[] = [];
487
+ walk(argPath, cwd, files);
488
+ resolved.push(...files.sort());
489
+ continue;
490
+ }
491
+
492
+ resolved.push(path.relative(cwd, argPath).split(path.sep).join("/"));
493
+ }
494
+ return Array.from(new Set(resolved));
495
+ }
496
+
497
+ function testRoots(cwd: string): string[] {
498
+ return Array.from(new Set([...defaultTestRoots, ...configuredRoots(cwd)]));
499
+ }
500
+
501
+ function configuredRoots(cwd: string): string[] {
502
+ const configPath = path.join(cwd, "beignet.config.json");
503
+ if (!existsSync(configPath)) return [];
504
+ try {
505
+ const config = JSON.parse(readFileSync(configPath, "utf8")) as {
506
+ paths?: Record<string, string>;
507
+ };
508
+ const paths = config.paths ?? {};
509
+ return [
510
+ paths.tests,
511
+ paths.features,
512
+ paths.routes ? path.dirname(paths.routes) : undefined,
513
+ paths.server ? path.dirname(paths.server) : undefined,
514
+ paths.infrastructurePorts
515
+ ? path.dirname(paths.infrastructurePorts)
516
+ : undefined,
517
+ paths.ports ? path.dirname(paths.ports) : undefined,
518
+ paths.useCaseBuilder ? path.dirname(paths.useCaseBuilder) : undefined,
519
+ ].filter((root): root is string => Boolean(root) && root !== ".");
520
+ } catch {
521
+ return [];
522
+ }
523
+ }
524
+
525
+ function walk(directory: string, cwd: string, files: string[]): void {
526
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
527
+ const entryPath = path.join(directory, entry.name);
528
+ if (entry.isDirectory()) {
529
+ if (!ignoredDirectories.has(entry.name)) {
530
+ walk(entryPath, cwd, files);
531
+ }
532
+ continue;
533
+ }
534
+ if (entry.isFile() && testFilePattern.test(entry.name)) {
535
+ files.push(path.relative(cwd, entryPath).split(path.sep).join("/"));
536
+ }
537
+ }
538
+ }
539
+ `,
540
+ };
541
+
542
+ export const testSupportTemplateFiles: TemplateFile[] = [
543
+ testHelperTemplateFile,
544
+ testRunnerTemplateFile,
545
+ ];
@@ -359,7 +359,7 @@ export function createTestTodoRepository(): TodoRepository {
359
359
  };
360
360
  }
361
361
  `,
362
- useCaseTest: `import { describe, expect, it } from "bun:test";
362
+ useCaseTest: `import { describe, expect, it } from "@/lib/beignet-test";
363
363
  import { createUseCaseTester } from "@beignet/core/application";
364
364
  import { createTestUserActor } from "@beignet/core/ports/testing";
365
365
  import {
@@ -476,7 +476,7 @@ describe("todos use cases", () => {
476
476
  });
477
477
  });
478
478
  `,
479
- routesTest: `import { describe, expect, it } from "bun:test";
479
+ routesTest: `import { describe, expect, it } from "@/lib/beignet-test";
480
480
  import { createStaticAuth } from "@beignet/core/ports";
481
481
  import { createIdempotencyHooks, defineRoutes } from "@beignet/core/server";
482
482
  import { createTestPorts } from "@beignet/core/testing";
@@ -12,7 +12,8 @@ const repoRoot = path.resolve(import.meta.dir, "../../../..");
12
12
 
13
13
  /**
14
14
  * Symlink the workspace and vendored dependencies a generated app needs for a
15
- * real `tsc --noEmit` and `bun test` run, without a package-manager install.
15
+ * real `tsc --noEmit` and generated test script run, without a package-manager
16
+ * install.
16
17
  * Pass `shell: true` for full-stack apps so the shadcn shell dependencies
17
18
  * (icons, radix, theming) resolve too.
18
19
  */
@@ -56,7 +57,6 @@ export async function linkSmokeDependencies(
56
57
  "@libsql/client",
57
58
  "@tanstack/react-query",
58
59
  "better-auth",
59
- "@types/bun",
60
60
  "@types/node",
61
61
  "@types/pg",
62
62
  "@types/react",
@@ -71,6 +71,7 @@ export async function linkSmokeDependencies(
71
71
  "react-hook-form",
72
72
  "resend",
73
73
  "typescript",
74
+ "tsx",
74
75
  "zod",
75
76
  ];
76
77
  if (options.shell) {
@@ -114,10 +115,8 @@ export async function linkSmokeDependencies(
114
115
  repoRoot,
115
116
  "packages/provider-mail-resend/node_modules/resend",
116
117
  ),
117
- "next-themes": path.join(
118
- repoRoot,
119
- "apps/example-ehr/node_modules/next-themes",
120
- ),
118
+ "next-themes": path.join(repoRoot, "packages/cli/node_modules/next-themes"),
119
+ tsx: path.join(repoRoot, "node_modules/tsx"),
121
120
  };
122
121
 
123
122
  for (const packageName of externalPackages) {
@@ -128,6 +127,11 @@ export async function linkSmokeDependencies(
128
127
  );
129
128
  }
130
129
 
130
+ await linkDependency(
131
+ path.join(nodeModules, "tsx/dist/cli.mjs"),
132
+ path.join(nodeModules, ".bin/tsx"),
133
+ );
134
+
131
135
  await linkDependency(
132
136
  path.join(
133
137
  repoRoot,