@beignet/cli 0.0.32 → 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 (89) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +26 -16
  3. package/dist/choices.d.ts +3 -3
  4. package/dist/choices.d.ts.map +1 -1
  5. package/dist/choices.js +2 -0
  6. package/dist/choices.js.map +1 -1
  7. package/dist/config.d.ts +1 -0
  8. package/dist/config.d.ts.map +1 -1
  9. package/dist/config.js +1 -0
  10. package/dist/config.js.map +1 -1
  11. package/dist/db.js +1 -1
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +279 -1
  14. package/dist/index.js.map +1 -1
  15. package/dist/inspect.d.ts.map +1 -1
  16. package/dist/inspect.js +797 -58
  17. package/dist/inspect.js.map +1 -1
  18. package/dist/make/inbox.d.ts.map +1 -1
  19. package/dist/make/inbox.js +5 -3
  20. package/dist/make/inbox.js.map +1 -1
  21. package/dist/make/payments.d.ts.map +1 -1
  22. package/dist/make/payments.js +58 -29
  23. package/dist/make/payments.js.map +1 -1
  24. package/dist/make/shared.d.ts +11 -2
  25. package/dist/make/shared.d.ts.map +1 -1
  26. package/dist/make/shared.js +44 -10
  27. package/dist/make/shared.js.map +1 -1
  28. package/dist/make/tenancy.d.ts.map +1 -1
  29. package/dist/make/tenancy.js +6 -4
  30. package/dist/make/tenancy.js.map +1 -1
  31. package/dist/make.d.ts.map +1 -1
  32. package/dist/make.js +129 -55
  33. package/dist/make.js.map +1 -1
  34. package/dist/outbox.d.ts +114 -1
  35. package/dist/outbox.d.ts.map +1 -1
  36. package/dist/outbox.js +190 -0
  37. package/dist/outbox.js.map +1 -1
  38. package/dist/preflight.js +1 -1
  39. package/dist/preflight.js.map +1 -1
  40. package/dist/provider-add.d.ts.map +1 -1
  41. package/dist/provider-add.js +46 -0
  42. package/dist/provider-add.js.map +1 -1
  43. package/dist/task.d.ts.map +1 -1
  44. package/dist/task.js +2 -3
  45. package/dist/task.js.map +1 -1
  46. package/dist/templates/agents.d.ts.map +1 -1
  47. package/dist/templates/agents.js +6 -3
  48. package/dist/templates/agents.js.map +1 -1
  49. package/dist/templates/base.js +3 -3
  50. package/dist/templates/base.js.map +1 -1
  51. package/dist/templates/index.d.ts.map +1 -1
  52. package/dist/templates/index.js +2 -0
  53. package/dist/templates/index.js.map +1 -1
  54. package/dist/templates/server.d.ts.map +1 -1
  55. package/dist/templates/server.js +6 -1
  56. package/dist/templates/server.js.map +1 -1
  57. package/dist/templates/shared.d.ts +3 -1
  58. package/dist/templates/shared.d.ts.map +1 -1
  59. package/dist/templates/shared.js +11 -1
  60. package/dist/templates/shared.js.map +1 -1
  61. package/dist/templates/testing.d.ts +5 -0
  62. package/dist/templates/testing.d.ts.map +1 -0
  63. package/dist/templates/testing.js +542 -0
  64. package/dist/templates/testing.js.map +1 -0
  65. package/dist/templates/todos.js +2 -2
  66. package/package.json +3 -2
  67. package/skills/app-structure/SKILL.md +12 -4
  68. package/src/choices.ts +3 -0
  69. package/src/config.ts +2 -0
  70. package/src/db.ts +1 -1
  71. package/src/index.ts +437 -1
  72. package/src/inspect.ts +1247 -154
  73. package/src/make/inbox.ts +5 -3
  74. package/src/make/payments.ts +58 -29
  75. package/src/make/shared.ts +78 -11
  76. package/src/make/tenancy.ts +6 -4
  77. package/src/make.ts +172 -54
  78. package/src/outbox.ts +363 -0
  79. package/src/preflight.ts +1 -1
  80. package/src/provider-add.ts +47 -0
  81. package/src/task.ts +2 -3
  82. package/src/templates/agents.ts +6 -3
  83. package/src/templates/base.ts +3 -3
  84. package/src/templates/index.ts +2 -0
  85. package/src/templates/server.ts +6 -1
  86. package/src/templates/shared.ts +13 -1
  87. package/src/templates/testing.ts +545 -0
  88. package/src/templates/todos.ts +2 -2
  89. package/src/test-helpers/generated-app.ts +10 -6
@@ -0,0 +1,542 @@
1
+ export const testHelperTemplateFile = {
2
+ path: "lib/beignet-test.ts",
3
+ content: `import assert from "node:assert/strict";
4
+ import {
5
+ after,
6
+ afterEach,
7
+ before,
8
+ beforeEach,
9
+ describe as nodeDescribe,
10
+ it as nodeIt,
11
+ test as nodeTest,
12
+ } from "node:test";
13
+ import { inspect, isDeepStrictEqual } from "node:util";
14
+
15
+ export { after, afterEach, before, beforeEach };
16
+
17
+ type TestBody = () => void | Promise<void>;
18
+ type TestEach = {
19
+ <T>(cases: readonly T[]): (
20
+ name: string,
21
+ body: (value: T) => void | Promise<void>,
22
+ ) => void;
23
+ <T extends readonly unknown[]>(cases: readonly T[]): (
24
+ name: string,
25
+ body: (...values: T) => void | Promise<void>,
26
+ ) => void;
27
+ };
28
+ type TestRunner = ((name: string, body: TestBody) => unknown) & {
29
+ each: TestEach;
30
+ skip: (name: string, body: TestBody) => unknown;
31
+ skipIf: (condition: boolean) => TestRunner;
32
+ };
33
+ type NodeRunner = {
34
+ (name: string, body: TestBody): unknown;
35
+ (
36
+ name: string,
37
+ options: { skip?: boolean },
38
+ body: TestBody,
39
+ ): unknown;
40
+ };
41
+
42
+ type AsymmetricMatcher =
43
+ | { kind: "any"; expected: unknown }
44
+ | { kind: "arrayContaining"; expected: readonly unknown[] }
45
+ | { kind: "objectContaining"; expected: Record<string, unknown> };
46
+
47
+ type BaseMatchers = {
48
+ toBe(expected: unknown): void;
49
+ toEqual(expected: unknown): void;
50
+ toMatchObject(expected: unknown): void;
51
+ toContain(expected: unknown): void;
52
+ toContainEqual(expected: unknown): void;
53
+ toHaveLength(expected: number): void;
54
+ toBeDefined(): void;
55
+ toBeUndefined(): void;
56
+ toBeNull(): void;
57
+ toBeGreaterThan(expected: number): void;
58
+ toMatch(expected: string | RegExp): void;
59
+ toThrow(expected?: string | RegExp | (new (...args: unknown[]) => object)): void;
60
+ };
61
+
62
+ type AsyncMatchers = {
63
+ toBe(expected: unknown): Promise<void>;
64
+ toEqual(expected: unknown): Promise<void>;
65
+ toMatchObject(expected: unknown): Promise<void>;
66
+ toBeNull(): Promise<void>;
67
+ toBeUndefined(): Promise<void>;
68
+ toThrow(expected?: string | RegExp | (new (...args: unknown[]) => object)): Promise<void>;
69
+ };
70
+
71
+ type Matchers = BaseMatchers & {
72
+ not: BaseMatchers;
73
+ resolves: AsyncMatchers;
74
+ rejects: AsyncMatchers;
75
+ };
76
+
77
+ type Expect = ((actual: unknown) => Matchers) & {
78
+ any(expected: unknown): AsymmetricMatcher;
79
+ arrayContaining(expected: readonly unknown[]): AsymmetricMatcher;
80
+ objectContaining(expected: Record<string, unknown>): AsymmetricMatcher;
81
+ };
82
+
83
+ export const describe = createRunner(nodeDescribe as NodeRunner);
84
+ export const it = createRunner(nodeIt as NodeRunner);
85
+ export const test = createRunner(nodeTest as NodeRunner);
86
+
87
+ export const expect = Object.assign(
88
+ (actual: unknown) => createMatchers(actual),
89
+ {
90
+ any: (expected: unknown): AsymmetricMatcher => ({
91
+ kind: "any",
92
+ expected,
93
+ }),
94
+ arrayContaining: (expected: readonly unknown[]): AsymmetricMatcher => ({
95
+ kind: "arrayContaining",
96
+ expected,
97
+ }),
98
+ objectContaining: (
99
+ expected: Record<string, unknown>,
100
+ ): AsymmetricMatcher => ({ kind: "objectContaining", expected }),
101
+ },
102
+ ) satisfies Expect;
103
+
104
+ function createRunner(
105
+ base: NodeRunner,
106
+ options?: { skip?: boolean },
107
+ ): TestRunner {
108
+ const runner = ((name: string, body: TestBody) =>
109
+ runTest(base, name, body, options)) as TestRunner;
110
+ runner.each = ((cases: readonly unknown[]) => {
111
+ return (
112
+ name: string,
113
+ body: (...values: unknown[]) => void | Promise<void>,
114
+ ) => {
115
+ for (const testCase of cases) {
116
+ const values = (
117
+ Array.isArray(testCase) ? testCase : [testCase]
118
+ ) as unknown[];
119
+ runTest(base, formatEachName(name, values), () => body(...values), options);
120
+ }
121
+ };
122
+ }) as TestEach;
123
+ runner.skip = (name: string, body: TestBody) =>
124
+ runTest(base, name, body, { skip: true });
125
+ runner.skipIf = (condition: boolean) =>
126
+ condition ? createRunner(base, { skip: true }) : runner;
127
+ return runner;
128
+ }
129
+
130
+ function runTest(
131
+ base: NodeRunner,
132
+ name: string,
133
+ body: TestBody,
134
+ options?: { skip?: boolean },
135
+ ): unknown {
136
+ return options ? base(name, options, body) : base(name, body);
137
+ }
138
+
139
+ function formatEachName(name: string, values: readonly unknown[]): string {
140
+ let index = 0;
141
+ return name.replace(/%[sdifoO]/g, () => formatInline(values[index++]));
142
+ }
143
+
144
+ function createMatchers(actual: unknown): Matchers {
145
+ return {
146
+ ...createBaseMatchers(actual, false),
147
+ not: createBaseMatchers(actual, true),
148
+ resolves: createAsyncMatchers(actual, "resolves"),
149
+ rejects: createAsyncMatchers(actual, "rejects"),
150
+ };
151
+ }
152
+
153
+ function createBaseMatchers(actual: unknown, negated: boolean): BaseMatchers {
154
+ return {
155
+ toBe(expected) {
156
+ check(
157
+ Object.is(actual, expected),
158
+ negated,
159
+ "Expected values to be identical.",
160
+ );
161
+ },
162
+ toEqual(expected) {
163
+ check(matchesExpected(actual, expected), negated, "Expected values to be equal.");
164
+ },
165
+ toMatchObject(expected) {
166
+ check(
167
+ matchesObject(actual, expected),
168
+ negated,
169
+ "Expected value to match object.",
170
+ );
171
+ },
172
+ toContain(expected) {
173
+ const matches =
174
+ typeof actual === "string"
175
+ ? actual.includes(String(expected))
176
+ : Array.isArray(actual) && actual.includes(expected);
177
+ check(matches, negated, "Expected value to contain item.");
178
+ },
179
+ toContainEqual(expected) {
180
+ check(
181
+ Array.isArray(actual) &&
182
+ actual.some((item) => matchesExpected(item, expected)),
183
+ negated,
184
+ "Expected array to contain equal item.",
185
+ );
186
+ },
187
+ toHaveLength(expected) {
188
+ const length =
189
+ actual !== null && typeof actual === "object"
190
+ ? (actual as { length?: unknown }).length
191
+ : undefined;
192
+ check(length === expected, negated, "Expected value to have length.");
193
+ },
194
+ toBeDefined() {
195
+ check(actual !== undefined, negated, "Expected value to be defined.");
196
+ },
197
+ toBeUndefined() {
198
+ check(actual === undefined, negated, "Expected value to be undefined.");
199
+ },
200
+ toBeNull() {
201
+ check(actual === null, negated, "Expected value to be null.");
202
+ },
203
+ toBeGreaterThan(expected) {
204
+ check(
205
+ typeof actual === "number" && actual > expected,
206
+ negated,
207
+ "Expected value to be greater than " + expected + ".",
208
+ );
209
+ },
210
+ toMatch(expected) {
211
+ const text = String(actual);
212
+ const matches =
213
+ typeof expected === "string" ? text.includes(expected) : expected.test(text);
214
+ check(matches, negated, "Expected value to match pattern.");
215
+ },
216
+ toThrow(expected) {
217
+ assert.equal(typeof actual, "function", "toThrow expects a function.");
218
+ let didThrow = false;
219
+ let thrown: unknown;
220
+ try {
221
+ (actual as () => unknown)();
222
+ } catch (error) {
223
+ didThrow = true;
224
+ thrown = error;
225
+ }
226
+ check(
227
+ didThrow && thrownMatches(thrown, expected),
228
+ negated,
229
+ "Expected function to throw.",
230
+ );
231
+ },
232
+ };
233
+ }
234
+
235
+ function createAsyncMatchers(
236
+ actual: unknown,
237
+ kind: "resolves" | "rejects",
238
+ ): AsyncMatchers {
239
+ const resolveActual = async () =>
240
+ kind === "resolves" ? await Promise.resolve(actual) : await rejected(actual);
241
+
242
+ return {
243
+ async toBe(expected) {
244
+ createBaseMatchers(await resolveActual(), false).toBe(expected);
245
+ },
246
+ async toEqual(expected) {
247
+ createBaseMatchers(await resolveActual(), false).toEqual(expected);
248
+ },
249
+ async toMatchObject(expected) {
250
+ createBaseMatchers(await resolveActual(), false).toMatchObject(expected);
251
+ },
252
+ async toBeNull() {
253
+ createBaseMatchers(await resolveActual(), false).toBeNull();
254
+ },
255
+ async toBeUndefined() {
256
+ createBaseMatchers(await resolveActual(), false).toBeUndefined();
257
+ },
258
+ async toThrow(expected) {
259
+ const thrown =
260
+ kind === "rejects" ? await rejected(actual) : await resolveActual();
261
+ check(
262
+ thrownMatches(thrown, expected),
263
+ false,
264
+ "Expected promise to reject with matching error.",
265
+ );
266
+ },
267
+ };
268
+ }
269
+
270
+ async function rejected(actual: unknown): Promise<unknown> {
271
+ try {
272
+ await Promise.resolve(actual);
273
+ } catch (error) {
274
+ return error;
275
+ }
276
+ assert.fail("Expected promise to reject.");
277
+ }
278
+
279
+ function check(condition: boolean, negated: boolean, message: string): void {
280
+ assert.ok(
281
+ negated ? !condition : condition,
282
+ negated ? "Negated assertion failed: " + message : message,
283
+ );
284
+ }
285
+
286
+ function matchesExpected(actual: unknown, expected: unknown): boolean {
287
+ if (!containsAsymmetric(expected)) {
288
+ return isDeepStrictEqual(actual, expected);
289
+ }
290
+
291
+ if (isAsymmetricMatcher(expected)) {
292
+ switch (expected.kind) {
293
+ case "any":
294
+ return matchesAny(actual, expected.expected);
295
+ case "arrayContaining":
296
+ return (
297
+ Array.isArray(actual) &&
298
+ expected.expected.every((expectedItem) =>
299
+ actual.some((actualItem) => matchesExpected(actualItem, expectedItem)),
300
+ )
301
+ );
302
+ case "objectContaining":
303
+ return matchesObject(actual, expected.expected);
304
+ }
305
+ }
306
+
307
+ if (Array.isArray(expected)) {
308
+ return (
309
+ Array.isArray(actual) &&
310
+ actual.length === expected.length &&
311
+ expected.every((item, index) => matchesExpected(actual[index], item))
312
+ );
313
+ }
314
+
315
+ if (isObjectRecord(expected)) {
316
+ if (!isObjectRecord(actual)) return false;
317
+ const actualKeys = Object.keys(actual);
318
+ const expectedKeys = Object.keys(expected);
319
+ return (
320
+ actualKeys.length === expectedKeys.length &&
321
+ expectedKeys.every(
322
+ (key) =>
323
+ Object.hasOwn(actual, key) &&
324
+ matchesExpected(actual[key], expected[key]),
325
+ )
326
+ );
327
+ }
328
+
329
+ return Object.is(actual, expected);
330
+ }
331
+
332
+ function matchesObject(actual: unknown, expected: unknown): boolean {
333
+ if (isAsymmetricMatcher(expected)) return matchesExpected(actual, expected);
334
+ if (Array.isArray(expected)) {
335
+ return (
336
+ Array.isArray(actual) &&
337
+ actual.length >= expected.length &&
338
+ expected.every((item, index) => matchesObject(actual[index], item))
339
+ );
340
+ }
341
+ if (!isObjectRecord(expected)) return matchesExpected(actual, expected);
342
+ if (!isObjectRecord(actual)) return false;
343
+ return Object.entries(expected).every(([key, expectedValue]) =>
344
+ matchesObject(actual[key], expectedValue),
345
+ );
346
+ }
347
+
348
+ function containsAsymmetric(value: unknown): boolean {
349
+ if (isAsymmetricMatcher(value)) return true;
350
+ if (Array.isArray(value)) return value.some(containsAsymmetric);
351
+ if (!isObjectRecord(value)) return false;
352
+ return Object.values(value).some(containsAsymmetric);
353
+ }
354
+
355
+ function isAsymmetricMatcher(value: unknown): value is AsymmetricMatcher {
356
+ return (
357
+ isObjectRecord(value) &&
358
+ (value.kind === "any" ||
359
+ value.kind === "arrayContaining" ||
360
+ value.kind === "objectContaining")
361
+ );
362
+ }
363
+
364
+ function matchesAny(actual: unknown, expected: unknown): boolean {
365
+ if (expected === String) return typeof actual === "string";
366
+ if (expected === Number) return typeof actual === "number";
367
+ if (expected === Boolean) return typeof actual === "boolean";
368
+ if (expected === Array) return Array.isArray(actual);
369
+ if (expected === Object) return isObjectRecord(actual);
370
+ if (expected instanceof Function) {
371
+ return actual instanceof (expected as new (...args: unknown[]) => object);
372
+ }
373
+ return actual !== undefined && actual !== null;
374
+ }
375
+
376
+ function thrownMatches(
377
+ error: unknown,
378
+ expected: string | RegExp | (new (...args: unknown[]) => object) | undefined,
379
+ ): boolean {
380
+ if (expected === undefined) return true;
381
+ if (typeof expected === "string") return errorMessage(error).includes(expected);
382
+ if (expected instanceof RegExp) return expected.test(errorMessage(error));
383
+ return error instanceof expected;
384
+ }
385
+
386
+ function errorMessage(error: unknown): string {
387
+ return error instanceof Error ? error.message : String(error);
388
+ }
389
+
390
+ function isObjectRecord(value: unknown): value is Record<string, unknown> {
391
+ return value !== null && typeof value === "object";
392
+ }
393
+
394
+ function formatInline(value: unknown): string {
395
+ return typeof value === "string"
396
+ ? value
397
+ : inspect(value, { breakLength: Number.POSITIVE_INFINITY, depth: 2 });
398
+ }
399
+ `,
400
+ };
401
+ export const testRunnerTemplateFile = {
402
+ path: "lib/beignet-test-runner.ts",
403
+ content: `import { spawnSync } from "node:child_process";
404
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
405
+ import path from "node:path";
406
+
407
+ const defaultTestRoots = [
408
+ "app",
409
+ "features",
410
+ "infra",
411
+ "lib",
412
+ "ports",
413
+ "server",
414
+ "tests",
415
+ "src/tests",
416
+ ];
417
+ const ignoredDirectories = new Set([
418
+ ".beignet-local",
419
+ ".git",
420
+ ".next",
421
+ ".turbo",
422
+ "coverage",
423
+ "dist",
424
+ "node_modules",
425
+ ]);
426
+ const testFilePattern = /\\.(test|spec)\\.[cm]?[tj]sx?$/;
427
+
428
+ const explicitArgs = process.argv.slice(2);
429
+ const testArgs =
430
+ explicitArgs.length > 0
431
+ ? resolveTestArgs(process.cwd(), explicitArgs)
432
+ : discoverTestFiles(process.cwd());
433
+
434
+ if (testArgs.length === 0) {
435
+ console.log("No test files found.");
436
+ process.exit(0);
437
+ }
438
+
439
+ const result = spawnSync(
440
+ process.execPath,
441
+ ["--import", "tsx", "--test", ...testArgs],
442
+ {
443
+ cwd: process.cwd(),
444
+ env: process.env,
445
+ stdio: "inherit",
446
+ },
447
+ );
448
+
449
+ if (result.error) {
450
+ console.error(result.error.message);
451
+ process.exit(1);
452
+ }
453
+
454
+ process.exit(result.status ?? 1);
455
+
456
+ function discoverTestFiles(cwd: string): string[] {
457
+ const files: string[] = [];
458
+ for (const root of testRoots(cwd)) {
459
+ const rootPath = path.join(cwd, root);
460
+ if (existsSync(rootPath)) {
461
+ walk(rootPath, cwd, files);
462
+ }
463
+ }
464
+ return files.sort();
465
+ }
466
+
467
+ function resolveTestArgs(cwd: string, args: string[]): string[] {
468
+ const resolved: string[] = [];
469
+ for (const arg of args) {
470
+ if (arg.startsWith("-")) {
471
+ resolved.push(arg);
472
+ continue;
473
+ }
474
+
475
+ const argPath = path.resolve(cwd, arg);
476
+ if (!existsSync(argPath)) {
477
+ resolved.push(arg);
478
+ continue;
479
+ }
480
+
481
+ const stats = statSync(argPath);
482
+ if (stats.isDirectory()) {
483
+ const files: string[] = [];
484
+ walk(argPath, cwd, files);
485
+ resolved.push(...files.sort());
486
+ continue;
487
+ }
488
+
489
+ resolved.push(path.relative(cwd, argPath).split(path.sep).join("/"));
490
+ }
491
+ return Array.from(new Set(resolved));
492
+ }
493
+
494
+ function testRoots(cwd: string): string[] {
495
+ return Array.from(new Set([...defaultTestRoots, ...configuredRoots(cwd)]));
496
+ }
497
+
498
+ function configuredRoots(cwd: string): string[] {
499
+ const configPath = path.join(cwd, "beignet.config.json");
500
+ if (!existsSync(configPath)) return [];
501
+ try {
502
+ const config = JSON.parse(readFileSync(configPath, "utf8")) as {
503
+ paths?: Record<string, string>;
504
+ };
505
+ const paths = config.paths ?? {};
506
+ return [
507
+ paths.tests,
508
+ paths.features,
509
+ paths.routes ? path.dirname(paths.routes) : undefined,
510
+ paths.server ? path.dirname(paths.server) : undefined,
511
+ paths.infrastructurePorts
512
+ ? path.dirname(paths.infrastructurePorts)
513
+ : undefined,
514
+ paths.ports ? path.dirname(paths.ports) : undefined,
515
+ paths.useCaseBuilder ? path.dirname(paths.useCaseBuilder) : undefined,
516
+ ].filter((root): root is string => Boolean(root) && root !== ".");
517
+ } catch {
518
+ return [];
519
+ }
520
+ }
521
+
522
+ function walk(directory: string, cwd: string, files: string[]): void {
523
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
524
+ const entryPath = path.join(directory, entry.name);
525
+ if (entry.isDirectory()) {
526
+ if (!ignoredDirectories.has(entry.name)) {
527
+ walk(entryPath, cwd, files);
528
+ }
529
+ continue;
530
+ }
531
+ if (entry.isFile() && testFilePattern.test(entry.name)) {
532
+ files.push(path.relative(cwd, entryPath).split(path.sep).join("/"));
533
+ }
534
+ }
535
+ }
536
+ `,
537
+ };
538
+ export const testSupportTemplateFiles = [
539
+ testHelperTemplateFile,
540
+ testRunnerTemplateFile,
541
+ ];
542
+ //# sourceMappingURL=testing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"testing.js","sourceRoot":"","sources":["../../src/templates/testing.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,sBAAsB,GAAiB;IAClD,IAAI,EAAE,qBAAqB;IAC3B,OAAO,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4YV;CACA,CAAC;AAEF,MAAM,CAAC,MAAM,sBAAsB,GAAiB;IAClD,IAAI,EAAE,4BAA4B;IAClC,OAAO,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqIV;CACA,CAAC;AAEF,MAAM,CAAC,MAAM,wBAAwB,GAAmB;IACtD,sBAAsB;IACtB,sBAAsB;CACvB,CAAC"}
@@ -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";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beignet/cli",
3
- "version": "0.0.32",
3
+ "version": "0.0.33",
4
4
  "type": "module",
5
5
  "description": "CLI for creating and maintaining Beignet apps.",
6
6
  "main": "./dist/lib.js",
@@ -65,10 +65,11 @@
65
65
  "devDependencies": {
66
66
  "@types/bun": "^1.3.13",
67
67
  "@types/node": "^20.10.0",
68
+ "next-themes": "^0.4.6",
68
69
  "typescript": "^5.3.0"
69
70
  },
70
71
  "dependencies": {
71
- "@beignet/core": "^0.0.32",
72
+ "@beignet/core": "^0.0.33",
72
73
  "@clack/prompts": "^1.5.1",
73
74
  "@modelcontextprotocol/sdk": "^1.29.0",
74
75
  "@stricli/core": "^1.2.7",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: app-structure
3
- description: "Maintain Beignet app structure with @beignet/cli: create starter output, make generators, full-slice feature recipes, db schema sync, beignet.config paths, routes inspection, lint dependency boundaries, doctor drift checks and fixes, MCP tools, generated AGENTS.md guidance, Intent package skills, and generated-app validation. Use when scaffolding, generating, fixing, or reviewing a Beignet app's structure."
3
+ description: "Maintain Beignet app structure with @beignet/cli: create starter output, make generators, full-slice feature recipes, db schema sync, TenantScope resources, workflow registries, runtime-integrity manifests, beignet.config paths, routes inspection, lint dependency boundaries, doctor drift checks and fixes, MCP tools, generated AGENTS.md guidance, Intent package skills, and generated-app validation. Use when scaffolding, generating, fixing, or reviewing a Beignet app's structure."
4
4
  ---
5
5
 
6
6
  # Beignet CLI App Structure
@@ -63,12 +63,16 @@ Hand-written files do not run until registered:
63
63
  - schedules -> `server/schedules.ts`
64
64
  - tasks -> `server/tasks.ts`
65
65
  - outbox events/jobs -> `server/outbox.ts`
66
- - listeners -> `registerListeners(...)` in provider or infra wiring
66
+ - listeners -> `server/listeners.ts` plus `registerListeners(...)` in server provider wiring
67
67
  - seeds -> the app-owned seed entrypoint, usually `server/seed.ts`
68
68
 
69
+ When an app opts into runtime integrity, keep `server/runtime-integrity.ts`
70
+ aligned with those central registries. The boot check is pure and
71
+ serverless-safe: it compares declared listeners, schedules, tasks, and outbox
72
+ artifacts against runtime registries before providers start.
73
+
69
74
  Run `beignet doctor --strict` after hand edits. Use `beignet doctor --fix` for
70
- low-risk route-group, schedule, task, and outbox registry repair; listener
71
- drift remains manual.
75
+ low-risk route-group, schedule, task, outbox, and listener registry repair.
72
76
 
73
77
  ## Path Config
74
78
 
@@ -113,6 +117,10 @@ root at `src/client/index.ts`.
113
117
  - route-owned error catalog drift
114
118
  - misplaced workflow artifacts
115
119
  - unregistered schedules, tasks, outbox events/jobs, and listeners
120
+ - runtime manifest and registry drift for apps that opt into boot integrity
121
+ - tenant-scoped Drizzle repository drift, including generated `TenantScope`
122
+ boundaries, raw `tenantId` app-facing repository methods, and scoped
123
+ `tenantScopeId(scope)` predicates for tenant/workspace tables
116
124
 
117
125
  Fix structure or config instead of suppressing diagnostics.
118
126
 
package/src/choices.ts CHANGED
@@ -38,6 +38,7 @@ export type ProviderPresetName =
38
38
  | "sentry"
39
39
  | "upstash-rate-limit"
40
40
  | "redis-cache"
41
+ | "event-bus-redis"
41
42
  | "redis-locks"
42
43
  | "s3-storage"
43
44
  | "vercel-blob-storage";
@@ -91,6 +92,7 @@ export const providerPresetChoices = [
91
92
  "sentry",
92
93
  "upstash-rate-limit",
93
94
  "redis-cache",
95
+ "event-bus-redis",
94
96
  "redis-locks",
95
97
  "s3-storage",
96
98
  "vercel-blob-storage",
@@ -117,6 +119,7 @@ export const createIntegrationChoices = [
117
119
  "mail-smtp",
118
120
  "payments-stripe",
119
121
  "redis-cache",
122
+ "event-bus-redis",
120
123
  "redis-locks",
121
124
  "s3-storage",
122
125
  "search-meilisearch",
package/src/config.ts CHANGED
@@ -16,6 +16,7 @@ export type BeignetPaths = {
16
16
  contracts: string;
17
17
  routes: string;
18
18
  server: string;
19
+ listeners: string;
19
20
  openapiRoute: string;
20
21
  tasks: string;
21
22
  outbox: string;
@@ -85,6 +86,7 @@ export const defaultBeignetConfig: ResolvedBeignetConfig = {
85
86
  contracts: "features",
86
87
  routes: "app/api",
87
88
  server: "server/index.ts",
89
+ listeners: "server/listeners.ts",
88
90
  openapiRoute: "app/api/openapi/route.ts",
89
91
  tasks: "server/tasks.ts",
90
92
  outbox: "server/outbox.ts",
package/src/db.ts CHANGED
@@ -464,7 +464,7 @@ function missingDatabaseScriptMessage(
464
464
 
465
465
  const entrypoint =
466
466
  command === "seed" ? "server/seed.ts" : "infra/db/reset.ts";
467
- return `Missing package.json script "${script}". Standard Drizzle apps use "${script}": "bun ${entrypoint}" so beignet db ${command} can run the app-owned ${command} entrypoint.`;
467
+ return `Missing package.json script "${script}". Standard Drizzle apps use "${script}": "tsx ${entrypoint}" so beignet db ${command} can run the app-owned ${command} entrypoint.`;
468
468
  }
469
469
 
470
470
  async function readPackageJson(cwd: string): Promise<PackageJson> {