@beignet/cli 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/make.ts ADDED
@@ -0,0 +1,3931 @@
1
+ import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import {
4
+ type BeignetConfig,
5
+ directoryPath,
6
+ loadBeignetConfig,
7
+ normalizePath,
8
+ type ResolvedBeignetConfig,
9
+ resolveConfig,
10
+ } from "./config.js";
11
+
12
+ type MakeResourceOptions = {
13
+ name: string;
14
+ cwd?: string;
15
+ force?: boolean;
16
+ dryRun?: boolean;
17
+ config?: BeignetConfig;
18
+ };
19
+
20
+ type MakeContractOptions = {
21
+ name: string;
22
+ cwd?: string;
23
+ force?: boolean;
24
+ dryRun?: boolean;
25
+ config?: BeignetConfig;
26
+ };
27
+
28
+ type MakeUseCaseOptions = {
29
+ name: string;
30
+ cwd?: string;
31
+ force?: boolean;
32
+ dryRun?: boolean;
33
+ config?: BeignetConfig;
34
+ };
35
+
36
+ type MakeTestOptions = {
37
+ name: string;
38
+ cwd?: string;
39
+ force?: boolean;
40
+ dryRun?: boolean;
41
+ config?: BeignetConfig;
42
+ };
43
+
44
+ type MakePortOptions = {
45
+ name: string;
46
+ cwd?: string;
47
+ force?: boolean;
48
+ dryRun?: boolean;
49
+ config?: BeignetConfig;
50
+ };
51
+
52
+ type MakeAdapterOptions = {
53
+ name: string;
54
+ cwd?: string;
55
+ force?: boolean;
56
+ dryRun?: boolean;
57
+ config?: BeignetConfig;
58
+ };
59
+
60
+ type MakePolicyOptions = {
61
+ name: string;
62
+ cwd?: string;
63
+ force?: boolean;
64
+ dryRun?: boolean;
65
+ config?: BeignetConfig;
66
+ };
67
+
68
+ type MakeEventOptions = {
69
+ name: string;
70
+ cwd?: string;
71
+ force?: boolean;
72
+ dryRun?: boolean;
73
+ config?: BeignetConfig;
74
+ };
75
+
76
+ type MakeJobOptions = {
77
+ name: string;
78
+ cwd?: string;
79
+ force?: boolean;
80
+ dryRun?: boolean;
81
+ config?: BeignetConfig;
82
+ };
83
+
84
+ type MakeListenerOptions = {
85
+ name: string;
86
+ event: string;
87
+ cwd?: string;
88
+ force?: boolean;
89
+ dryRun?: boolean;
90
+ config?: BeignetConfig;
91
+ };
92
+
93
+ type MakeScheduleOptions = {
94
+ name: string;
95
+ cwd?: string;
96
+ force?: boolean;
97
+ dryRun?: boolean;
98
+ config?: BeignetConfig;
99
+ cron?: string;
100
+ timezone?: string;
101
+ route?: boolean;
102
+ };
103
+
104
+ type MakeResult = {
105
+ name: string;
106
+ targetDir: string;
107
+ dryRun: boolean;
108
+ files: string[];
109
+ createdFiles: string[];
110
+ updatedFiles: string[];
111
+ skippedFiles: string[];
112
+ };
113
+
114
+ export type MakeResourceResult = MakeResult;
115
+ export type MakeContractResult = MakeResult;
116
+ export type MakeUseCaseResult = MakeResult;
117
+ export type MakeTestResult = MakeResult;
118
+ export type MakePortResult = MakeResult;
119
+ export type MakeAdapterResult = MakeResult;
120
+ export type MakePolicyResult = MakeResult;
121
+ export type MakeEventResult = MakeResult;
122
+ export type MakeJobResult = MakeResult;
123
+ export type MakeListenerResult = MakeResult;
124
+ export type MakeScheduleResult = MakeResult;
125
+
126
+ type ResourceNames = {
127
+ input: string;
128
+ pluralKebab: string;
129
+ singularKebab: string;
130
+ pluralCamel: string;
131
+ singularCamel: string;
132
+ singularPascal: string;
133
+ pluralPascal: string;
134
+ };
135
+
136
+ type GeneratedFile = {
137
+ path: string;
138
+ content: string;
139
+ };
140
+
141
+ type ResourcePersistence = "memory" | "drizzle";
142
+
143
+ type IdentifierNames = {
144
+ input: string;
145
+ kebab: string;
146
+ camel: string;
147
+ pascal: string;
148
+ };
149
+
150
+ type UseCaseNames = {
151
+ input: string;
152
+ feature: IdentifierNames;
153
+ action: IdentifierNames;
154
+ exportName: string;
155
+ operationName: string;
156
+ kind: "command" | "query";
157
+ };
158
+
159
+ type PortNames = IdentifierNames & {
160
+ interfaceName: string;
161
+ inputName: string;
162
+ adapterFactoryName: string;
163
+ };
164
+
165
+ type PolicyNames = IdentifierNames & {
166
+ policyName: string;
167
+ };
168
+
169
+ type FeatureArtifactNames = {
170
+ input: string;
171
+ feature: IdentifierNames;
172
+ featureSingularKebab: string;
173
+ featureSingularCamel: string;
174
+ featureSingularPascal: string;
175
+ artifact: IdentifierNames;
176
+ stableName: string;
177
+ };
178
+
179
+ type EventNames = FeatureArtifactNames & {
180
+ eventName: string;
181
+ eventExportName: string;
182
+ payloadSchemaName: string;
183
+ payloadTypeName: string;
184
+ };
185
+
186
+ type JobNames = FeatureArtifactNames & {
187
+ jobName: string;
188
+ jobExportName: string;
189
+ payloadSchemaName: string;
190
+ payloadTypeName: string;
191
+ };
192
+
193
+ type ListenerNames = FeatureArtifactNames & {
194
+ listenerName: string;
195
+ listenerExportName: string;
196
+ event: EventNames;
197
+ };
198
+
199
+ type ScheduleNames = FeatureArtifactNames & {
200
+ scheduleName: string;
201
+ scheduleExportName: string;
202
+ payloadSchemaName: string;
203
+ payloadTypeName: string;
204
+ cron: string;
205
+ timezone?: string;
206
+ };
207
+
208
+ export async function makeResource(
209
+ options: MakeResourceOptions,
210
+ ): Promise<MakeResourceResult> {
211
+ const targetDir = path.resolve(options.cwd ?? process.cwd());
212
+ const names = resourceNames(options.name);
213
+ const config = options.config
214
+ ? resolveConfig(options.config)
215
+ : await loadBeignetConfig(targetDir);
216
+
217
+ await assertStandardApp(targetDir, config);
218
+
219
+ const persistence = await detectResourcePersistence(targetDir, config);
220
+ const generatedFiles = resourceFiles(names, config, persistence);
221
+ const plannedFiles = await planGeneratedFiles(targetDir, generatedFiles, {
222
+ force: Boolean(options.force),
223
+ });
224
+ const plannedWiringUpdates = await updateResourceWiring(
225
+ targetDir,
226
+ names,
227
+ config,
228
+ persistence,
229
+ {
230
+ dryRun: true,
231
+ },
232
+ );
233
+ const createdFiles = plannedFiles
234
+ .filter((file) => file.result === "created")
235
+ .map((file) => file.path);
236
+ const updatedFiles = plannedFiles
237
+ .filter((file) => file.result === "updated")
238
+ .map((file) => file.path);
239
+ const skippedFiles = plannedFiles
240
+ .filter((file) => file.result === "skipped")
241
+ .map((file) => file.path);
242
+
243
+ if (options.dryRun) {
244
+ updatedFiles.push(...plannedWiringUpdates);
245
+ } else {
246
+ for (const file of plannedFiles) {
247
+ await writePlannedGeneratedFile(targetDir, file);
248
+ }
249
+
250
+ updatedFiles.push(
251
+ ...(await updateResourceWiring(targetDir, names, config, {
252
+ persistence,
253
+ dryRun: false,
254
+ })),
255
+ );
256
+ }
257
+
258
+ return {
259
+ name: names.pluralKebab,
260
+ targetDir,
261
+ dryRun: Boolean(options.dryRun),
262
+ files: generatedFiles.map((file) => file.path),
263
+ createdFiles,
264
+ updatedFiles,
265
+ skippedFiles,
266
+ };
267
+ }
268
+
269
+ export async function makeContract(
270
+ options: MakeContractOptions,
271
+ ): Promise<MakeContractResult> {
272
+ const targetDir = path.resolve(options.cwd ?? process.cwd());
273
+ const names = resourceNames(options.name);
274
+ const config = options.config
275
+ ? resolveConfig(options.config)
276
+ : await loadBeignetConfig(targetDir);
277
+ const generatedFiles = contractFiles(names, config);
278
+ const createdFiles: string[] = [];
279
+ const updatedFiles: string[] = [];
280
+ const skippedFiles: string[] = [];
281
+
282
+ for (const file of generatedFiles) {
283
+ const result = await writeGeneratedFile(targetDir, file, {
284
+ force: Boolean(options.force),
285
+ dryRun: Boolean(options.dryRun),
286
+ });
287
+ if (result === "created") createdFiles.push(file.path);
288
+ if (result === "updated") updatedFiles.push(file.path);
289
+ if (result === "skipped") skippedFiles.push(file.path);
290
+ }
291
+
292
+ return {
293
+ name: names.pluralKebab,
294
+ targetDir,
295
+ dryRun: Boolean(options.dryRun),
296
+ files: generatedFiles.map((file) => file.path),
297
+ createdFiles,
298
+ updatedFiles,
299
+ skippedFiles,
300
+ };
301
+ }
302
+
303
+ export async function makeUseCase(
304
+ options: MakeUseCaseOptions,
305
+ ): Promise<MakeUseCaseResult> {
306
+ const targetDir = path.resolve(options.cwd ?? process.cwd());
307
+ const names = useCaseNames(options.name);
308
+ const config = options.config
309
+ ? resolveConfig(options.config)
310
+ : await loadBeignetConfig(targetDir);
311
+
312
+ await assertUseCaseApp(targetDir, config);
313
+
314
+ const generatedFiles = useCaseFiles(names, config);
315
+ const createdFiles: string[] = [];
316
+ const updatedFiles: string[] = [];
317
+ const skippedFiles: string[] = [];
318
+
319
+ for (const file of generatedFiles) {
320
+ const result = await writeGeneratedFile(targetDir, file, {
321
+ force: Boolean(options.force),
322
+ dryRun: Boolean(options.dryRun),
323
+ });
324
+ if (result === "created") createdFiles.push(file.path);
325
+ if (result === "updated") updatedFiles.push(file.path);
326
+ if (result === "skipped") skippedFiles.push(file.path);
327
+ }
328
+
329
+ const indexFile = useCaseIndexFile(names, config);
330
+ const indexResult = await updateUseCaseIndex(targetDir, indexFile, {
331
+ dryRun: Boolean(options.dryRun),
332
+ });
333
+ if (indexResult === "created") createdFiles.push(indexFile.path);
334
+ if (indexResult === "updated") updatedFiles.push(indexFile.path);
335
+ if (indexResult === "skipped") skippedFiles.push(indexFile.path);
336
+
337
+ return {
338
+ name: `${names.feature.kebab}/${names.action.kebab}`,
339
+ targetDir,
340
+ dryRun: Boolean(options.dryRun),
341
+ files: [...generatedFiles.map((file) => file.path), indexFile.path],
342
+ createdFiles,
343
+ updatedFiles,
344
+ skippedFiles,
345
+ };
346
+ }
347
+
348
+ export async function makeTest(
349
+ options: MakeTestOptions,
350
+ ): Promise<MakeTestResult> {
351
+ const targetDir = path.resolve(options.cwd ?? process.cwd());
352
+ const names = useCaseNames(options.name);
353
+ const config = options.config
354
+ ? resolveConfig(options.config)
355
+ : await loadBeignetConfig(targetDir);
356
+
357
+ await assertTestApp(targetDir, names, config);
358
+
359
+ const generatedFiles = testFiles(names, config);
360
+ const createdFiles: string[] = [];
361
+ const updatedFiles: string[] = [];
362
+ const skippedFiles: string[] = [];
363
+
364
+ for (const file of generatedFiles) {
365
+ const result = await writeGeneratedFile(targetDir, file, {
366
+ force: Boolean(options.force),
367
+ dryRun: Boolean(options.dryRun),
368
+ });
369
+ if (result === "created") createdFiles.push(file.path);
370
+ if (result === "updated") updatedFiles.push(file.path);
371
+ if (result === "skipped") skippedFiles.push(file.path);
372
+ }
373
+
374
+ if (await updatePackageJson(targetDir, { dryRun: Boolean(options.dryRun) })) {
375
+ updatedFiles.push("package.json");
376
+ }
377
+
378
+ return {
379
+ name: `${names.feature.kebab}/${names.action.kebab}`,
380
+ targetDir,
381
+ dryRun: Boolean(options.dryRun),
382
+ files: generatedFiles.map((file) => file.path),
383
+ createdFiles,
384
+ updatedFiles,
385
+ skippedFiles,
386
+ };
387
+ }
388
+
389
+ export async function makePort(
390
+ options: MakePortOptions,
391
+ ): Promise<MakePortResult> {
392
+ const targetDir = path.resolve(options.cwd ?? process.cwd());
393
+ const names = portNames(options.name);
394
+ const config = options.config
395
+ ? resolveConfig(options.config)
396
+ : await loadBeignetConfig(targetDir);
397
+
398
+ await assertPortApp(targetDir, config);
399
+
400
+ const generatedFiles = portFiles(names, config);
401
+ const plannedFiles = await planGeneratedFiles(targetDir, generatedFiles, {
402
+ force: Boolean(options.force),
403
+ });
404
+ const createdFiles: string[] = [];
405
+ const updatedFiles: string[] = [];
406
+ const skippedFiles: string[] = [];
407
+
408
+ for (const file of plannedFiles) {
409
+ if (file.result === "created") createdFiles.push(file.path);
410
+ if (file.result === "updated") updatedFiles.push(file.path);
411
+ if (file.result === "skipped") skippedFiles.push(file.path);
412
+ }
413
+
414
+ await updatePortIndex(targetDir, names, config, { dryRun: true });
415
+ await updateInfrastructurePortStub(targetDir, names, config, {
416
+ dryRun: true,
417
+ });
418
+
419
+ if (!options.dryRun) {
420
+ for (const file of plannedFiles) {
421
+ await writePlannedGeneratedFile(targetDir, file);
422
+ }
423
+ }
424
+
425
+ if (
426
+ await updatePortIndex(targetDir, names, config, {
427
+ dryRun: Boolean(options.dryRun),
428
+ })
429
+ ) {
430
+ updatedFiles.push(config.paths.ports);
431
+ }
432
+ if (
433
+ await updateInfrastructurePortStub(targetDir, names, config, {
434
+ dryRun: Boolean(options.dryRun),
435
+ })
436
+ ) {
437
+ updatedFiles.push(config.paths.infrastructurePorts);
438
+ }
439
+
440
+ return {
441
+ name: names.kebab,
442
+ targetDir,
443
+ dryRun: Boolean(options.dryRun),
444
+ files: generatedFiles.map((file) => file.path),
445
+ createdFiles,
446
+ updatedFiles,
447
+ skippedFiles,
448
+ };
449
+ }
450
+
451
+ export async function makeAdapter(
452
+ options: MakeAdapterOptions,
453
+ ): Promise<MakeAdapterResult> {
454
+ const targetDir = path.resolve(options.cwd ?? process.cwd());
455
+ const names = portNames(options.name);
456
+ const config = options.config
457
+ ? resolveConfig(options.config)
458
+ : await loadBeignetConfig(targetDir);
459
+
460
+ await assertAdapterApp(targetDir, names, config);
461
+
462
+ const generatedFiles = adapterFiles(names, config);
463
+ const createdFiles: string[] = [];
464
+ const updatedFiles: string[] = [];
465
+ const skippedFiles: string[] = [];
466
+
467
+ for (const file of generatedFiles) {
468
+ const result = await writeGeneratedFile(targetDir, file, {
469
+ force: Boolean(options.force),
470
+ dryRun: Boolean(options.dryRun),
471
+ });
472
+ if (result === "created") createdFiles.push(file.path);
473
+ if (result === "updated") updatedFiles.push(file.path);
474
+ if (result === "skipped") skippedFiles.push(file.path);
475
+ }
476
+
477
+ if (
478
+ await updateInfrastructureAdapterWiring(targetDir, names, config, {
479
+ dryRun: Boolean(options.dryRun),
480
+ })
481
+ ) {
482
+ updatedFiles.push(config.paths.infrastructurePorts);
483
+ }
484
+
485
+ return {
486
+ name: names.kebab,
487
+ targetDir,
488
+ dryRun: Boolean(options.dryRun),
489
+ files: generatedFiles.map((file) => file.path),
490
+ createdFiles,
491
+ updatedFiles,
492
+ skippedFiles,
493
+ };
494
+ }
495
+
496
+ export async function makePolicy(
497
+ options: MakePolicyOptions,
498
+ ): Promise<MakePolicyResult> {
499
+ const targetDir = path.resolve(options.cwd ?? process.cwd());
500
+ const names = policyNames(options.name);
501
+ const config = options.config
502
+ ? resolveConfig(options.config)
503
+ : await loadBeignetConfig(targetDir);
504
+
505
+ await assertPolicyApp(targetDir, config);
506
+
507
+ const generatedFiles = policyFiles(names, config);
508
+ const createdFiles: string[] = [];
509
+ const updatedFiles: string[] = [];
510
+ const skippedFiles: string[] = [];
511
+
512
+ for (const file of generatedFiles) {
513
+ const result = await writeGeneratedFile(targetDir, file, {
514
+ force: Boolean(options.force),
515
+ dryRun: Boolean(options.dryRun),
516
+ });
517
+ if (result === "created") createdFiles.push(file.path);
518
+ if (result === "updated") updatedFiles.push(file.path);
519
+ if (result === "skipped") skippedFiles.push(file.path);
520
+ }
521
+
522
+ return {
523
+ name: names.kebab,
524
+ targetDir,
525
+ dryRun: Boolean(options.dryRun),
526
+ files: generatedFiles.map((file) => file.path),
527
+ createdFiles,
528
+ updatedFiles,
529
+ skippedFiles,
530
+ };
531
+ }
532
+
533
+ export async function makeEvent(
534
+ options: MakeEventOptions,
535
+ ): Promise<MakeEventResult> {
536
+ const targetDir = path.resolve(options.cwd ?? process.cwd());
537
+ const names = eventNames(options.name);
538
+ const config = options.config
539
+ ? resolveConfig(options.config)
540
+ : await loadBeignetConfig(targetDir);
541
+
542
+ await assertFeatureArtifactApp(targetDir, config, "event");
543
+
544
+ return makeFeatureArtifact({
545
+ targetDir,
546
+ config,
547
+ force: Boolean(options.force),
548
+ dryRun: Boolean(options.dryRun),
549
+ name: `${names.feature.kebab}/${names.artifact.kebab}`,
550
+ files: eventFiles(names, config),
551
+ index: featureArtifactIndexFile("event", names, config),
552
+ dependencies: {
553
+ "@beignet/core": beignetDependencyVersion,
554
+ },
555
+ });
556
+ }
557
+
558
+ export async function makeJob(options: MakeJobOptions): Promise<MakeJobResult> {
559
+ const targetDir = path.resolve(options.cwd ?? process.cwd());
560
+ const names = jobNames(options.name);
561
+ const config = options.config
562
+ ? resolveConfig(options.config)
563
+ : await loadBeignetConfig(targetDir);
564
+
565
+ await assertFeatureArtifactApp(targetDir, config, "job");
566
+
567
+ return makeFeatureArtifact({
568
+ targetDir,
569
+ config,
570
+ force: Boolean(options.force),
571
+ dryRun: Boolean(options.dryRun),
572
+ name: `${names.feature.kebab}/${names.artifact.kebab}`,
573
+ files: jobFiles(names, config),
574
+ index: featureArtifactIndexFile("job", names, config),
575
+ dependencies: {
576
+ "@beignet/core": beignetDependencyVersion,
577
+ },
578
+ });
579
+ }
580
+
581
+ export async function makeListener(
582
+ options: MakeListenerOptions,
583
+ ): Promise<MakeListenerResult> {
584
+ const targetDir = path.resolve(options.cwd ?? process.cwd());
585
+ const names = listenerNames(options.name, options.event);
586
+ const config = options.config
587
+ ? resolveConfig(options.config)
588
+ : await loadBeignetConfig(targetDir);
589
+
590
+ await assertFeatureArtifactApp(targetDir, config, "listener");
591
+ await assertListenerEventExists(targetDir, names.event, config);
592
+
593
+ return makeFeatureArtifact({
594
+ targetDir,
595
+ config,
596
+ force: Boolean(options.force),
597
+ dryRun: Boolean(options.dryRun),
598
+ name: `${names.feature.kebab}/${names.artifact.kebab}`,
599
+ files: listenerFiles(names, config),
600
+ index: featureArtifactIndexFile("listener", names, config),
601
+ dependencies: {
602
+ "@beignet/core": beignetDependencyVersion,
603
+ },
604
+ });
605
+ }
606
+
607
+ async function assertListenerEventExists(
608
+ targetDir: string,
609
+ names: EventNames,
610
+ config: ResolvedBeignetConfig,
611
+ ): Promise<void> {
612
+ const file = eventFilePath(names, config);
613
+
614
+ try {
615
+ await stat(path.join(targetDir, file));
616
+ } catch {
617
+ throw new Error(
618
+ `beignet make listener expected event ${names.eventName} at ${file}. Run beignet make event ${names.feature.kebab}/${names.artifact.kebab} first, or pass an event that exists.`,
619
+ );
620
+ }
621
+ }
622
+
623
+ export async function makeSchedule(
624
+ options: MakeScheduleOptions,
625
+ ): Promise<MakeScheduleResult> {
626
+ const targetDir = path.resolve(options.cwd ?? process.cwd());
627
+ const names = scheduleNames(options.name, {
628
+ cron: options.cron,
629
+ timezone: options.timezone,
630
+ });
631
+ const config = options.config
632
+ ? resolveConfig(options.config)
633
+ : await loadBeignetConfig(targetDir);
634
+
635
+ await assertFeatureArtifactApp(targetDir, config, "schedule", {
636
+ requireServer: Boolean(options.route),
637
+ });
638
+
639
+ const files = scheduleFiles(names, config, { route: Boolean(options.route) });
640
+
641
+ return makeFeatureArtifact({
642
+ targetDir,
643
+ config,
644
+ force: Boolean(options.force),
645
+ dryRun: Boolean(options.dryRun),
646
+ name: `${names.feature.kebab}/${names.artifact.kebab}`,
647
+ files,
648
+ index: featureArtifactIndexFile("schedule", names, config),
649
+ dependencies: {
650
+ "@beignet/core": beignetDependencyVersion,
651
+ },
652
+ });
653
+ }
654
+
655
+ async function assertStandardApp(
656
+ targetDir: string,
657
+ config: ResolvedBeignetConfig,
658
+ ): Promise<void> {
659
+ const requiredFiles = [
660
+ config.paths.appContext,
661
+ config.paths.infrastructurePorts,
662
+ config.paths.ports,
663
+ config.paths.server,
664
+ config.paths.useCaseBuilder,
665
+ ];
666
+
667
+ for (const file of requiredFiles) {
668
+ try {
669
+ await stat(path.join(targetDir, file));
670
+ } catch {
671
+ throw new Error(
672
+ `beignet make resource expects a standard Beignet app. Missing ${file}.`,
673
+ );
674
+ }
675
+ }
676
+ }
677
+
678
+ async function assertUseCaseApp(
679
+ targetDir: string,
680
+ config: ResolvedBeignetConfig,
681
+ ): Promise<void> {
682
+ try {
683
+ await stat(path.join(targetDir, config.paths.useCaseBuilder));
684
+ } catch {
685
+ throw new Error(
686
+ `beignet make use-case expects a standard Beignet app. Missing ${config.paths.useCaseBuilder}.`,
687
+ );
688
+ }
689
+ }
690
+
691
+ async function assertTestApp(
692
+ targetDir: string,
693
+ names: UseCaseNames,
694
+ config: ResolvedBeignetConfig,
695
+ ): Promise<void> {
696
+ const requiredFiles = [
697
+ config.paths.appContext,
698
+ config.paths.infrastructurePorts,
699
+ useCaseFilePath(names, config),
700
+ ];
701
+
702
+ for (const file of requiredFiles) {
703
+ try {
704
+ await stat(path.join(targetDir, file));
705
+ } catch {
706
+ throw new Error(
707
+ `beignet make test expects a standard Beignet app with the target use case. Missing ${file}.`,
708
+ );
709
+ }
710
+ }
711
+ }
712
+
713
+ async function assertPortApp(
714
+ targetDir: string,
715
+ config: ResolvedBeignetConfig,
716
+ ): Promise<void> {
717
+ const requiredFiles = [config.paths.ports, config.paths.infrastructurePorts];
718
+
719
+ for (const file of requiredFiles) {
720
+ try {
721
+ await stat(path.join(targetDir, file));
722
+ } catch {
723
+ throw new Error(
724
+ `beignet make port expects a standard Beignet app. Missing ${file}.`,
725
+ );
726
+ }
727
+ }
728
+ }
729
+
730
+ async function assertAdapterApp(
731
+ targetDir: string,
732
+ names: PortNames,
733
+ config: ResolvedBeignetConfig,
734
+ ): Promise<void> {
735
+ const requiredFiles = [
736
+ config.paths.infrastructurePorts,
737
+ portFilePath(names, config),
738
+ ];
739
+
740
+ for (const file of requiredFiles) {
741
+ try {
742
+ await stat(path.join(targetDir, file));
743
+ } catch {
744
+ throw new Error(
745
+ `beignet make adapter expects an existing port in a standard Beignet app. Missing ${file}.`,
746
+ );
747
+ }
748
+ }
749
+
750
+ const infrastructurePorts = await readFile(
751
+ path.join(targetDir, config.paths.infrastructurePorts),
752
+ "utf8",
753
+ );
754
+ if (
755
+ !infrastructurePorts.includes(portStubEntry(names)) &&
756
+ !infrastructurePorts.includes(adapterEntry(names))
757
+ ) {
758
+ throw new Error(
759
+ `Could not find the generated ${names.interfaceName} infrastructure stub in ${config.paths.infrastructurePorts}. Wire ${names.adapterFactoryName}() manually or restore the generated stub before running make adapter.`,
760
+ );
761
+ }
762
+ }
763
+
764
+ async function assertPolicyApp(
765
+ targetDir: string,
766
+ config: ResolvedBeignetConfig,
767
+ ): Promise<void> {
768
+ try {
769
+ await stat(path.join(targetDir, config.paths.appContext));
770
+ } catch {
771
+ throw new Error(
772
+ `beignet make policy expects a standard Beignet app. Missing ${config.paths.appContext}.`,
773
+ );
774
+ }
775
+ }
776
+
777
+ async function assertFeatureArtifactApp(
778
+ targetDir: string,
779
+ config: ResolvedBeignetConfig,
780
+ artifact: "event" | "job" | "listener" | "schedule",
781
+ options: { requireServer?: boolean } = {},
782
+ ): Promise<void> {
783
+ const requiredFiles = [
784
+ config.paths.appContext,
785
+ ...(options.requireServer ? [config.paths.server] : []),
786
+ ];
787
+
788
+ for (const file of requiredFiles) {
789
+ try {
790
+ await stat(path.join(targetDir, file));
791
+ } catch {
792
+ throw new Error(
793
+ `beignet make ${artifact} expects a standard Beignet app. Missing ${file}.`,
794
+ );
795
+ }
796
+ }
797
+ }
798
+
799
+ type WriteGeneratedFileOptions = {
800
+ force: boolean;
801
+ dryRun: boolean;
802
+ };
803
+
804
+ type WriteGeneratedFileResult = "created" | "updated" | "skipped";
805
+
806
+ type PlannedGeneratedFile = GeneratedFile & {
807
+ result: WriteGeneratedFileResult;
808
+ };
809
+
810
+ async function writeGeneratedFile(
811
+ targetDir: string,
812
+ file: GeneratedFile,
813
+ options: WriteGeneratedFileOptions,
814
+ ): Promise<WriteGeneratedFileResult> {
815
+ const plannedFile = await planGeneratedFile(targetDir, file, {
816
+ force: options.force,
817
+ });
818
+
819
+ if (!options.dryRun) {
820
+ await writePlannedGeneratedFile(targetDir, plannedFile);
821
+ }
822
+
823
+ return plannedFile.result;
824
+ }
825
+
826
+ async function planGeneratedFiles(
827
+ targetDir: string,
828
+ files: GeneratedFile[],
829
+ options: { force: boolean },
830
+ ): Promise<PlannedGeneratedFile[]> {
831
+ const plannedFiles: PlannedGeneratedFile[] = [];
832
+
833
+ for (const file of files) {
834
+ plannedFiles.push(await planGeneratedFile(targetDir, file, options));
835
+ }
836
+
837
+ return plannedFiles;
838
+ }
839
+
840
+ async function planGeneratedFile(
841
+ targetDir: string,
842
+ file: GeneratedFile,
843
+ options: { force: boolean },
844
+ ): Promise<PlannedGeneratedFile> {
845
+ const destination = path.join(targetDir, file.path);
846
+ const existing = await readOptionalFile(destination);
847
+ if (existing === file.content) {
848
+ return { ...file, result: "skipped" };
849
+ }
850
+ if (existing !== undefined && !options.force) {
851
+ throw new Error(
852
+ `File ${file.path} already exists with different content. Pass --force to overwrite it.`,
853
+ );
854
+ }
855
+
856
+ return { ...file, result: existing === undefined ? "created" : "updated" };
857
+ }
858
+
859
+ async function writePlannedGeneratedFile(
860
+ targetDir: string,
861
+ file: PlannedGeneratedFile,
862
+ ): Promise<void> {
863
+ if (file.result === "skipped") return;
864
+
865
+ const destination = path.join(targetDir, file.path);
866
+ await mkdir(path.dirname(destination), { recursive: true });
867
+ await writeFile(destination, file.content);
868
+ }
869
+
870
+ async function fileExists(filePath: string): Promise<boolean> {
871
+ try {
872
+ await stat(filePath);
873
+ return true;
874
+ } catch {
875
+ return false;
876
+ }
877
+ }
878
+
879
+ async function readOptionalFile(filePath: string): Promise<string | undefined> {
880
+ try {
881
+ return await readFile(filePath, "utf8");
882
+ } catch {
883
+ return undefined;
884
+ }
885
+ }
886
+
887
+ async function makeFeatureArtifact(options: {
888
+ targetDir: string;
889
+ config: ResolvedBeignetConfig;
890
+ force: boolean;
891
+ dryRun: boolean;
892
+ name: string;
893
+ files: GeneratedFile[];
894
+ index: FeatureArtifactIndexFile;
895
+ dependencies: Record<string, (packageJson: PackageJsonLike) => string>;
896
+ }): Promise<MakeResult> {
897
+ const plannedFiles = await planGeneratedFiles(
898
+ options.targetDir,
899
+ options.files,
900
+ {
901
+ force: options.force,
902
+ },
903
+ );
904
+ const createdFiles = plannedFiles
905
+ .filter((file) => file.result === "created")
906
+ .map((file) => file.path);
907
+ const updatedFiles = plannedFiles
908
+ .filter((file) => file.result === "updated")
909
+ .map((file) => file.path);
910
+ const skippedFiles = plannedFiles
911
+ .filter((file) => file.result === "skipped")
912
+ .map((file) => file.path);
913
+
914
+ if (!options.dryRun) {
915
+ for (const file of plannedFiles) {
916
+ await writePlannedGeneratedFile(options.targetDir, file);
917
+ }
918
+ }
919
+
920
+ const indexResult = await updateFeatureArtifactIndex(
921
+ options.targetDir,
922
+ options.index,
923
+ { dryRun: options.dryRun },
924
+ );
925
+ if (indexResult === "created") createdFiles.push(options.index.path);
926
+ if (indexResult === "updated") updatedFiles.push(options.index.path);
927
+ if (indexResult === "skipped") skippedFiles.push(options.index.path);
928
+
929
+ if (
930
+ await updatePackageDependencies(options.targetDir, options.dependencies, {
931
+ dryRun: options.dryRun,
932
+ })
933
+ ) {
934
+ updatedFiles.push("package.json");
935
+ }
936
+
937
+ return {
938
+ name: options.name,
939
+ targetDir: options.targetDir,
940
+ dryRun: options.dryRun,
941
+ files: [...options.files.map((file) => file.path), options.index.path],
942
+ createdFiles,
943
+ updatedFiles,
944
+ skippedFiles,
945
+ };
946
+ }
947
+
948
+ type PackageJsonLike = {
949
+ dependencies?: Record<string, string>;
950
+ devDependencies?: Record<string, string>;
951
+ };
952
+
953
+ async function updatePackageDependencies(
954
+ targetDir: string,
955
+ dependencies: Record<string, (packageJson: PackageJsonLike) => string>,
956
+ options: { dryRun: boolean },
957
+ ): Promise<boolean> {
958
+ const filePath = path.join(targetDir, "package.json");
959
+ const original = await readFile(filePath, "utf8");
960
+ const packageJson = JSON.parse(original) as PackageJsonLike;
961
+ packageJson.dependencies = packageJson.dependencies ?? {};
962
+
963
+ for (const [name, version] of Object.entries(dependencies)) {
964
+ packageJson.dependencies[name] ??= version(packageJson);
965
+ }
966
+
967
+ const next = `${JSON.stringify(packageJson, null, "\t")}\n`;
968
+ if (next === original) return false;
969
+ if (!options.dryRun) await writeFile(filePath, next);
970
+ return true;
971
+ }
972
+
973
+ function beignetDependencyVersion(packageJson: PackageJsonLike): string {
974
+ return (
975
+ packageJson.dependencies?.["@beignet/core"] ??
976
+ packageJson.dependencies?.["@beignet/next"] ??
977
+ "*"
978
+ );
979
+ }
980
+
981
+ async function updateResourceWiring(
982
+ targetDir: string,
983
+ names: ResourceNames,
984
+ config: ResolvedBeignetConfig,
985
+ persistenceOrOptions:
986
+ | ResourcePersistence
987
+ | { dryRun: boolean; persistence?: ResourcePersistence },
988
+ maybeOptions?: { dryRun: boolean },
989
+ ): Promise<string[]> {
990
+ const persistence =
991
+ typeof persistenceOrOptions === "string"
992
+ ? persistenceOrOptions
993
+ : (persistenceOrOptions.persistence ?? "memory");
994
+ const options =
995
+ typeof persistenceOrOptions === "string"
996
+ ? (maybeOptions as { dryRun: boolean })
997
+ : persistenceOrOptions;
998
+ const updated = new Set<string>();
999
+ if (await updatePackageJson(targetDir, options)) updated.add("package.json");
1000
+ if (await updatePortsIndex(targetDir, names, config, options)) {
1001
+ updated.add(config.paths.ports);
1002
+ }
1003
+ if (await updateInfrastructurePorts(targetDir, names, config, options)) {
1004
+ updated.add(config.paths.infrastructurePorts);
1005
+ }
1006
+ if (
1007
+ persistence === "drizzle" &&
1008
+ (await updateDrizzleSchemaIndex(targetDir, names, config, options))
1009
+ ) {
1010
+ updated.add(drizzleSchemaIndexPath(config));
1011
+ }
1012
+ if (
1013
+ persistence === "drizzle" &&
1014
+ (await updateDrizzleRepositories(targetDir, names, config, options))
1015
+ ) {
1016
+ updated.add(drizzleRepositoriesPath(config));
1017
+ }
1018
+ if (
1019
+ persistence === "memory" &&
1020
+ (await updateServerTransactionPorts(targetDir, names, config, options))
1021
+ ) {
1022
+ updated.add(config.paths.server);
1023
+ }
1024
+ if (await updateOpenApiRoute(targetDir, names, config, options)) {
1025
+ updated.add(config.paths.openapiRoute);
1026
+ }
1027
+ const routesFile = await updateServerRoutes(
1028
+ targetDir,
1029
+ names,
1030
+ config,
1031
+ options,
1032
+ );
1033
+ if (routesFile) updated.add(routesFile);
1034
+
1035
+ return [...updated];
1036
+ }
1037
+
1038
+ async function updatePackageJson(
1039
+ targetDir: string,
1040
+ options: { dryRun: boolean },
1041
+ ): Promise<boolean> {
1042
+ const filePath = path.join(targetDir, "package.json");
1043
+ const original = await readFile(filePath, "utf8");
1044
+ const packageJson = JSON.parse(original) as {
1045
+ scripts?: Record<string, string>;
1046
+ };
1047
+ packageJson.scripts = packageJson.scripts ?? {};
1048
+ if (packageJson.scripts.test) return false;
1049
+
1050
+ packageJson.scripts.test = "bun test";
1051
+ const next = `${JSON.stringify(packageJson, null, "\t")}\n`;
1052
+ if (!options.dryRun) await writeFile(filePath, next);
1053
+ return next !== original;
1054
+ }
1055
+
1056
+ async function updatePortsIndex(
1057
+ targetDir: string,
1058
+ names: ResourceNames,
1059
+ config: ResolvedBeignetConfig,
1060
+ options: { dryRun: boolean },
1061
+ ): Promise<boolean> {
1062
+ const filePath = path.join(targetDir, config.paths.ports);
1063
+ const original = await readFile(filePath, "utf8");
1064
+ let next = original;
1065
+ const repositoryPath = resourcePortFilePath(names, config);
1066
+ const importLine = `import type { ${names.singularPascal}Repository } from "${relativeModule(config.paths.ports, repositoryPath)}";`;
1067
+
1068
+ if (!next.includes(importLine)) {
1069
+ next = insertAfterImports(next, importLine);
1070
+ }
1071
+
1072
+ const entry = `\t${names.pluralCamel}: ${names.singularPascal}Repository;`;
1073
+ if (!next.includes(entry.trim())) {
1074
+ next = insertTypeProperty(
1075
+ next,
1076
+ "AppPorts",
1077
+ entry.trim(),
1078
+ `Could not find AppPorts in ${config.paths.ports}. Add ${entry.trim()} manually, or restore the generated ports file before running make resource.`,
1079
+ );
1080
+ }
1081
+
1082
+ const transactionPorts = typeBodySource(next, "AppTransactionPorts");
1083
+ if (
1084
+ transactionPorts !== undefined &&
1085
+ !transactionPorts.includes(entry.trim())
1086
+ ) {
1087
+ next = replaceTransactionPortsRequired(
1088
+ next,
1089
+ entry,
1090
+ `Could not find AppTransactionPorts in ${config.paths.ports}. Add ${entry.trim()} manually, or restore the generated ports file before running make resource.`,
1091
+ );
1092
+ }
1093
+
1094
+ if (next === original) return false;
1095
+ if (!options.dryRun) await writeFile(filePath, next);
1096
+ return true;
1097
+ }
1098
+
1099
+ async function updatePortIndex(
1100
+ targetDir: string,
1101
+ names: PortNames,
1102
+ config: ResolvedBeignetConfig,
1103
+ options: { dryRun: boolean },
1104
+ ): Promise<boolean> {
1105
+ const filePath = path.join(targetDir, config.paths.ports);
1106
+ const original = await readFile(filePath, "utf8");
1107
+ let next = original;
1108
+ const portPath = path.join(
1109
+ path.dirname(config.paths.ports),
1110
+ `${names.kebab}.ts`,
1111
+ );
1112
+ const importLine = `import type { ${names.interfaceName} } from "${relativeModule(config.paths.ports, portPath)}";`;
1113
+
1114
+ if (!next.includes(importLine)) {
1115
+ next = insertAfterImports(next, importLine);
1116
+ }
1117
+
1118
+ const entry = `\t${names.camel}: ${names.interfaceName};`;
1119
+ if (!next.includes(entry.trim())) {
1120
+ next = insertTypeProperty(
1121
+ next,
1122
+ "AppPorts",
1123
+ entry.trim(),
1124
+ `Could not find AppPorts in ${config.paths.ports}. Add ${entry.trim()} manually, or restore the generated ports file before running make port.`,
1125
+ );
1126
+ }
1127
+
1128
+ if (next === original) return false;
1129
+ if (!options.dryRun) await writeFile(filePath, next);
1130
+ return true;
1131
+ }
1132
+
1133
+ async function updateInfrastructurePortStub(
1134
+ targetDir: string,
1135
+ names: PortNames,
1136
+ config: ResolvedBeignetConfig,
1137
+ options: { dryRun: boolean },
1138
+ ): Promise<boolean> {
1139
+ const filePath = path.join(targetDir, config.paths.infrastructurePorts);
1140
+ const original = await readFile(filePath, "utf8");
1141
+ let next = original;
1142
+ const portPath = portFilePath(names, config);
1143
+ const importLine = `import type { ${names.interfaceName} } from "${relativeModule(config.paths.infrastructurePorts, portPath)}";`;
1144
+
1145
+ if (!next.includes(importLine)) {
1146
+ next = insertAfterImports(next, importLine);
1147
+ }
1148
+
1149
+ const entry = portStubEntry(names);
1150
+ const definePortsBody = definePortsBodySource(next);
1151
+ if (
1152
+ definePortsBody === undefined ||
1153
+ !hasTopLevelObjectProperty(definePortsBody, names.camel)
1154
+ ) {
1155
+ next = insertDefinePortsProperty(
1156
+ next,
1157
+ entry.trim(),
1158
+ `Could not find definePorts({ in ${config.paths.infrastructurePorts}. Add the ${names.interfaceName} stub manually, or restore the generated infrastructure ports file before running make port.`,
1159
+ );
1160
+ }
1161
+
1162
+ if (next === original) return false;
1163
+ if (!options.dryRun) await writeFile(filePath, next);
1164
+ return true;
1165
+ }
1166
+
1167
+ async function updateInfrastructureAdapterWiring(
1168
+ targetDir: string,
1169
+ names: PortNames,
1170
+ config: ResolvedBeignetConfig,
1171
+ options: { dryRun: boolean },
1172
+ ): Promise<boolean> {
1173
+ const filePath = path.join(targetDir, config.paths.infrastructurePorts);
1174
+ const original = await readFile(filePath, "utf8");
1175
+ let next = original;
1176
+ const adapterPath = adapterFilePath(names, config);
1177
+ const adapterImportLine = `import { ${names.adapterFactoryName} } from "${relativeModule(config.paths.infrastructurePorts, adapterPath)}";`;
1178
+
1179
+ if (!next.includes(adapterImportLine)) {
1180
+ next = insertAfterImports(next, adapterImportLine);
1181
+ }
1182
+
1183
+ const portPath = portFilePath(names, config);
1184
+ const portImportLine = `import type { ${names.interfaceName} } from "${relativeModule(config.paths.infrastructurePorts, portPath)}";`;
1185
+ if (next.includes(portImportLine)) {
1186
+ next = next.replace(`${portImportLine}\n`, "");
1187
+ }
1188
+
1189
+ const adapter = adapterEntry(names);
1190
+ const stub = portStubEntry(names);
1191
+ if (next.includes(stub)) {
1192
+ next = next.replace(stub, adapter);
1193
+ } else if (!next.includes(adapter)) {
1194
+ throw new Error(
1195
+ `Could not find the generated ${names.interfaceName} infrastructure stub in ${config.paths.infrastructurePorts}. Wire ${names.adapterFactoryName}() manually or restore the generated stub before running make adapter.`,
1196
+ );
1197
+ }
1198
+
1199
+ if (next === original) return false;
1200
+ if (!options.dryRun) await writeFile(filePath, next);
1201
+ return true;
1202
+ }
1203
+
1204
+ async function updateInfrastructurePorts(
1205
+ targetDir: string,
1206
+ names: ResourceNames,
1207
+ config: ResolvedBeignetConfig,
1208
+ options: { dryRun: boolean },
1209
+ ): Promise<boolean> {
1210
+ const filePath = path.join(targetDir, config.paths.infrastructurePorts);
1211
+ const original = await readFile(filePath, "utf8");
1212
+ let next = original;
1213
+ const repositoryPath = path.join(
1214
+ path.dirname(config.paths.infrastructurePorts),
1215
+ names.pluralKebab,
1216
+ `in-memory-${names.singularKebab}-repository.ts`,
1217
+ );
1218
+ const importLine = `import { createInMemory${names.singularPascal}Repository } from "${relativeModule(config.paths.infrastructurePorts, repositoryPath)}";`;
1219
+
1220
+ if (!next.includes(importLine)) {
1221
+ next = insertAfterImports(next, importLine);
1222
+ }
1223
+
1224
+ const repositoryConst = `const ${names.pluralCamel} = createInMemory${names.singularPascal}Repository();`;
1225
+ if (!next.includes(repositoryConst)) {
1226
+ next = replaceRequired(
1227
+ next,
1228
+ /(\nexport const appPorts = definePorts(?:<[^>]+>)?\(\{)/,
1229
+ `\n${repositoryConst}\n$1`,
1230
+ `Could not find definePorts({ in ${config.paths.infrastructurePorts}. Add ${repositoryConst} manually, or restore the generated infrastructure ports file before running make resource.`,
1231
+ );
1232
+ }
1233
+
1234
+ const entry = `\t${names.pluralCamel},`;
1235
+ const definePortsBody = definePortsBodySource(next);
1236
+ if (
1237
+ definePortsBody === undefined ||
1238
+ !hasTopLevelObjectProperty(definePortsBody, names.pluralCamel)
1239
+ ) {
1240
+ next = insertDefinePortsProperty(
1241
+ next,
1242
+ entry.trim(),
1243
+ `Could not find definePorts({ in ${config.paths.infrastructurePorts}. Add ${entry.trim()} to appPorts manually, or restore the generated infrastructure ports file before running make resource.`,
1244
+ );
1245
+ }
1246
+
1247
+ const transactionEntry = `\t\t${names.pluralCamel},`;
1248
+ const uowBody = noopUnitOfWorkBodySource(next);
1249
+ if (
1250
+ uowBody !== undefined &&
1251
+ !new RegExp(`\\b${names.pluralCamel}\\b\\s*,`).test(uowBody)
1252
+ ) {
1253
+ next = insertNoopUnitOfWorkProperty(
1254
+ next,
1255
+ transactionEntry.trim(),
1256
+ `Could not find the generated Unit of Work port list in ${config.paths.infrastructurePorts}. Add ${transactionEntry.trim()} manually, or restore the generated infrastructure ports file before running make resource.`,
1257
+ );
1258
+ }
1259
+
1260
+ if (next === original) return false;
1261
+ if (!options.dryRun) await writeFile(filePath, next);
1262
+ return true;
1263
+ }
1264
+
1265
+ async function updateDrizzleSchemaIndex(
1266
+ targetDir: string,
1267
+ names: ResourceNames,
1268
+ config: ResolvedBeignetConfig,
1269
+ options: { dryRun: boolean },
1270
+ ): Promise<boolean> {
1271
+ const filePath = path.join(targetDir, drizzleSchemaIndexPath(config));
1272
+ if (!(await fileExists(filePath))) return false;
1273
+
1274
+ const original = await readFile(filePath, "utf8");
1275
+ const exportLine = `export { ${names.pluralCamel} } from "./${names.pluralKebab}";`;
1276
+ if (original.includes(exportLine)) return false;
1277
+
1278
+ const next = `${original.trimEnd()}\n${exportLine}\n`;
1279
+ if (!options.dryRun) await writeFile(filePath, next);
1280
+ return true;
1281
+ }
1282
+
1283
+ async function updateDrizzleRepositories(
1284
+ targetDir: string,
1285
+ names: ResourceNames,
1286
+ config: ResolvedBeignetConfig,
1287
+ options: { dryRun: boolean },
1288
+ ): Promise<boolean> {
1289
+ const filePath = path.join(targetDir, drizzleRepositoriesPath(config));
1290
+ if (!(await fileExists(filePath))) return false;
1291
+
1292
+ const original = await readFile(filePath, "utf8");
1293
+ let next = original;
1294
+ const repositoryPath = drizzleResourceRepositoryFilePath(names, config);
1295
+ const importLine = `import { createDrizzle${names.singularPascal}Repository } from "${relativeModule(drizzleRepositoriesPath(config), repositoryPath)}";`;
1296
+ const repositoriesBody = createRepositoriesReturnBodySource(next);
1297
+ const entry = `\t\t${names.pluralCamel}: createDrizzle${names.singularPascal}Repository(db),`;
1298
+
1299
+ if (repositoriesBody === undefined) {
1300
+ throw new Error(
1301
+ `Could not find return { in createRepositories in ${drizzleRepositoriesPath(config)}. Add ${entry.trim()} manually, or restore the generated repositories file before running make resource.`,
1302
+ );
1303
+ }
1304
+
1305
+ if (!next.includes(importLine)) {
1306
+ next = insertAfterImports(next, importLine);
1307
+ }
1308
+
1309
+ if (!hasTopLevelObjectProperty(repositoriesBody, names.pluralCamel)) {
1310
+ next = insertCreateRepositoriesProperty(
1311
+ next,
1312
+ entry.trim(),
1313
+ `Could not find return { in createRepositories in ${drizzleRepositoriesPath(config)}. Add ${entry.trim()} manually, or restore the generated repositories file before running make resource.`,
1314
+ );
1315
+ }
1316
+
1317
+ if (next === original) return false;
1318
+ if (!options.dryRun) await writeFile(filePath, next);
1319
+ return true;
1320
+ }
1321
+
1322
+ async function updateServerTransactionPorts(
1323
+ targetDir: string,
1324
+ names: ResourceNames,
1325
+ config: ResolvedBeignetConfig,
1326
+ options: { dryRun: boolean },
1327
+ ): Promise<boolean> {
1328
+ const filePath = path.join(targetDir, config.paths.server);
1329
+ const original = await readFile(filePath, "utf8");
1330
+ let next = original;
1331
+ const transactionPortsBody = drizzleTransactionPortsBodySource(next);
1332
+
1333
+ if (
1334
+ transactionPortsBody === undefined ||
1335
+ hasTopLevelObjectProperty(transactionPortsBody, names.pluralCamel)
1336
+ ) {
1337
+ return false;
1338
+ }
1339
+
1340
+ if (!/\bappPorts\b/.test(next)) {
1341
+ throw new Error(
1342
+ `Could not find appPorts in ${config.paths.server}. Add ${names.pluralCamel}: appPorts.${names.pluralCamel} to createTransactionPorts manually, or restore the generated server file before running make resource.`,
1343
+ );
1344
+ }
1345
+
1346
+ const entry = `\t\t\t\t\t\t${names.pluralCamel}: appPorts.${names.pluralCamel},`;
1347
+ next = insertDrizzleTransactionPortsProperty(
1348
+ next,
1349
+ entry.trim(),
1350
+ `Could not find createTransactionPorts({ in ${config.paths.server}. Add ${entry.trim()} manually, or restore the generated server file before running make resource.`,
1351
+ );
1352
+
1353
+ if (next === original) return false;
1354
+ if (!options.dryRun) await writeFile(filePath, next);
1355
+ return true;
1356
+ }
1357
+
1358
+ async function updateOpenApiRoute(
1359
+ targetDir: string,
1360
+ names: ResourceNames,
1361
+ config: ResolvedBeignetConfig,
1362
+ options: { dryRun: boolean },
1363
+ ): Promise<boolean> {
1364
+ const filePath = path.join(targetDir, config.paths.openapiRoute);
1365
+ if (!(await fileExists(filePath))) return false;
1366
+
1367
+ const original = await readFile(filePath, "utf8");
1368
+ const firstArg = firstCreateOpenAPIHandlerArgInfo(original);
1369
+ if (!firstArg?.text.startsWith("[")) return false;
1370
+
1371
+ const listName = `list${names.pluralPascal}`;
1372
+ const createName = `create${names.singularPascal}`;
1373
+ const listedContracts = identifiersFromArrayExpression(firstArg.text);
1374
+ const missingContracts = [listName, createName].filter(
1375
+ (contractName) => !listedContracts.has(contractName),
1376
+ );
1377
+ if (missingContracts.length === 0) return false;
1378
+
1379
+ const importLine = `import { create${names.singularPascal}, list${names.pluralPascal} } from "${aliasModule(resourceContractFilePath(names, config))}";`;
1380
+ const nextArray = appendToArrayExpression(firstArg.text, missingContracts);
1381
+ let next = `${original.slice(0, firstArg.start)}${nextArray}${original.slice(
1382
+ firstArg.end,
1383
+ )}`;
1384
+
1385
+ if (!next.includes(importLine)) {
1386
+ next = insertAfterImports(next, importLine);
1387
+ }
1388
+
1389
+ if (next === original) return false;
1390
+ if (!options.dryRun) await writeFile(filePath, next);
1391
+ return true;
1392
+ }
1393
+
1394
+ async function updateServerRoutes(
1395
+ targetDir: string,
1396
+ names: ResourceNames,
1397
+ config: ResolvedBeignetConfig,
1398
+ options: { dryRun: boolean },
1399
+ ): Promise<string | undefined> {
1400
+ const filePath = path.join(targetDir, config.paths.server);
1401
+ const original = await readFile(filePath, "utf8");
1402
+ const routeRegistryPath = routeRegistryFileFromServerSource(
1403
+ original,
1404
+ config.paths.server,
1405
+ );
1406
+
1407
+ if (routeRegistryPath) {
1408
+ await assertImportedRoutesFileExists(targetDir, routeRegistryPath);
1409
+ return updateDefineRoutesFile(
1410
+ targetDir,
1411
+ routeRegistryPath,
1412
+ names,
1413
+ config,
1414
+ options,
1415
+ );
1416
+ }
1417
+
1418
+ return updateDefineRoutesFile(
1419
+ targetDir,
1420
+ config.paths.server,
1421
+ names,
1422
+ config,
1423
+ options,
1424
+ );
1425
+ }
1426
+
1427
+ async function updateDefineRoutesFile(
1428
+ targetDir: string,
1429
+ file: string,
1430
+ names: ResourceNames,
1431
+ config: ResolvedBeignetConfig,
1432
+ options: { dryRun: boolean },
1433
+ ): Promise<string | undefined> {
1434
+ const filePath = path.join(targetDir, file);
1435
+ const original = await readFile(filePath, "utf8");
1436
+ let next = original;
1437
+ const routeGroupName = `${names.singularCamel}Routes`;
1438
+ const routeGroupPath = path.join(
1439
+ config.paths.features,
1440
+ names.pluralKebab,
1441
+ "routes.ts",
1442
+ );
1443
+ const importLine = `import { ${routeGroupName} } from "${relativeModule(file, routeGroupPath)}";`;
1444
+
1445
+ if (!next.includes(importLine)) {
1446
+ next = insertAfterImports(next, importLine);
1447
+ }
1448
+
1449
+ if (!routeGroupIsRegistered(next, routeGroupName)) {
1450
+ const firstArg = firstDefineRoutesArgInfo(next);
1451
+ if (!firstArg?.text.startsWith("[")) {
1452
+ throw new Error(
1453
+ `Could not find a central defineRoutes([...]) registration in ${file}. Import ${routeGroupName} and add it to defineRoutes manually.`,
1454
+ );
1455
+ }
1456
+
1457
+ const nextArray = appendToArrayExpression(firstArg.text, [routeGroupName]);
1458
+ next = `${next.slice(0, firstArg.start)}${nextArray}${next.slice(
1459
+ firstArg.end,
1460
+ )}`;
1461
+ }
1462
+
1463
+ if (!routeGroupIsRegistered(next, routeGroupName)) {
1464
+ throw new Error(
1465
+ `Could not find a central defineRoutes([...]) registration in ${file}. Import ${routeGroupName} and add it to defineRoutes manually.`,
1466
+ );
1467
+ }
1468
+
1469
+ if (next === original) return undefined;
1470
+ if (!options.dryRun) await writeFile(filePath, next);
1471
+ return file;
1472
+ }
1473
+
1474
+ function routeGroupIsRegistered(
1475
+ source: string,
1476
+ routeGroupName: string,
1477
+ ): boolean {
1478
+ const firstArg = firstDefineRoutesArgInfo(source);
1479
+ if (!firstArg?.text.startsWith("[")) return false;
1480
+
1481
+ return identifiersFromArrayExpression(firstArg.text).has(routeGroupName);
1482
+ }
1483
+
1484
+ function routeRegistryFileFromServerSource(
1485
+ source: string,
1486
+ serverFile: string,
1487
+ ): string | undefined {
1488
+ const serverOptions = firstCreateNextServerArgInfo(source);
1489
+ if (!serverOptions) return undefined;
1490
+
1491
+ const routeIdentifier = routeOptionIdentifier(serverOptions.text);
1492
+ if (!routeIdentifier) return undefined;
1493
+
1494
+ const imported = parseNamedImportSources(source).get(routeIdentifier);
1495
+ if (!imported) return undefined;
1496
+
1497
+ return resolveImportFile(imported.sourcePath, serverFile);
1498
+ }
1499
+
1500
+ function routeOptionIdentifier(source: string): string | undefined {
1501
+ const explicitMatch = /(?:^|[,{])\s*routes\s*:\s*([A-Za-z_$][\w$]*)/m.exec(
1502
+ source,
1503
+ );
1504
+ if (explicitMatch) return explicitMatch[1];
1505
+
1506
+ if (/(?:^|[,{])\s*routes\s*(?:[,}])/m.test(source)) {
1507
+ return "routes";
1508
+ }
1509
+
1510
+ return undefined;
1511
+ }
1512
+
1513
+ function parseNamedImportSources(
1514
+ source: string,
1515
+ ): Map<string, { importedName: string; sourcePath: string }> {
1516
+ const imports = new Map<
1517
+ string,
1518
+ { importedName: string; sourcePath: string }
1519
+ >();
1520
+ const importRegex = /import\s+\{([^}]+)\}\s+from\s+["']([^"']+)["']/g;
1521
+
1522
+ for (const match of source.matchAll(importRegex)) {
1523
+ for (const member of match[1].split(",")) {
1524
+ const parsed = parseImportMember(member);
1525
+ if (!parsed) continue;
1526
+ imports.set(parsed.localName, {
1527
+ importedName: parsed.importedName,
1528
+ sourcePath: match[2],
1529
+ });
1530
+ }
1531
+ }
1532
+
1533
+ return imports;
1534
+ }
1535
+
1536
+ function parseImportMember(
1537
+ member: string,
1538
+ ): { importedName: string; localName: string } | undefined {
1539
+ const trimmed = member.trim();
1540
+ if (!trimmed) return undefined;
1541
+
1542
+ const aliasMatch = /^([A-Za-z_$][\w$]*)\s+as\s+([A-Za-z_$][\w$]*)$/.exec(
1543
+ trimmed,
1544
+ );
1545
+ if (aliasMatch) {
1546
+ return {
1547
+ importedName: aliasMatch[1],
1548
+ localName: aliasMatch[2],
1549
+ };
1550
+ }
1551
+
1552
+ if (/^[A-Za-z_$][\w$]*$/.test(trimmed)) {
1553
+ return {
1554
+ importedName: trimmed,
1555
+ localName: trimmed,
1556
+ };
1557
+ }
1558
+
1559
+ return undefined;
1560
+ }
1561
+
1562
+ function resolveImportFile(
1563
+ sourcePath: string,
1564
+ importerFile: string,
1565
+ ): string | undefined {
1566
+ if (sourcePath.startsWith("@/")) {
1567
+ return `${sourcePath.slice(2)}.ts`;
1568
+ }
1569
+
1570
+ if (sourcePath.startsWith(".")) {
1571
+ return `${normalizePath(path.join(path.dirname(importerFile), sourcePath))}.ts`;
1572
+ }
1573
+
1574
+ return undefined;
1575
+ }
1576
+
1577
+ async function assertImportedRoutesFileExists(
1578
+ targetDir: string,
1579
+ file: string,
1580
+ ): Promise<void> {
1581
+ try {
1582
+ await stat(path.join(targetDir, file));
1583
+ } catch {
1584
+ throw new Error(
1585
+ `Could not find the route registry ${file}. Restore the imported routes file or add the new route group manually.`,
1586
+ );
1587
+ }
1588
+ }
1589
+
1590
+ function firstDefineRoutesArgInfo(
1591
+ source: string,
1592
+ ): { text: string; start: number; end: number } | undefined {
1593
+ const match = /defineRoutes(?:<[^>]+>)?\(/.exec(source);
1594
+ if (!match) return undefined;
1595
+
1596
+ return firstCallArgInfo(source, match.index + match[0].length);
1597
+ }
1598
+
1599
+ function firstCreateNextServerArgInfo(
1600
+ source: string,
1601
+ ): { text: string; start: number; end: number } | undefined {
1602
+ const match = /createNextServer(?:<[^>]+>)?\(/.exec(source);
1603
+ if (!match) return undefined;
1604
+
1605
+ return firstCallArgInfo(source, match.index + match[0].length);
1606
+ }
1607
+
1608
+ function firstCreateOpenAPIHandlerArgInfo(
1609
+ source: string,
1610
+ ): { text: string; start: number; end: number } | undefined {
1611
+ const start = source.indexOf("createOpenAPIHandler(");
1612
+ if (start === -1) return undefined;
1613
+
1614
+ return firstCallArgInfo(source, start + "createOpenAPIHandler(".length);
1615
+ }
1616
+
1617
+ function firstCallArgInfo(
1618
+ source: string,
1619
+ argStart: number,
1620
+ ): { text: string; start: number; end: number } | undefined {
1621
+ let depth = 0;
1622
+ let inString: string | undefined;
1623
+ let escaped = false;
1624
+
1625
+ for (let index = argStart; index < source.length; index++) {
1626
+ const char = source[index];
1627
+ if (inString) {
1628
+ if (escaped) {
1629
+ escaped = false;
1630
+ } else if (char === "\\") {
1631
+ escaped = true;
1632
+ } else if (char === inString) {
1633
+ inString = undefined;
1634
+ }
1635
+ continue;
1636
+ }
1637
+
1638
+ if (char === '"' || char === "'" || char === "`") {
1639
+ inString = char;
1640
+ continue;
1641
+ }
1642
+
1643
+ if (char === "[" || char === "(" || char === "{") {
1644
+ depth++;
1645
+ continue;
1646
+ }
1647
+
1648
+ if (char === "]" || char === ")" || char === "}") {
1649
+ if (depth === 0) {
1650
+ return trimmedSlice(source, argStart, index);
1651
+ }
1652
+ depth--;
1653
+ continue;
1654
+ }
1655
+
1656
+ if (char === "," && depth === 0) {
1657
+ return trimmedSlice(source, argStart, index);
1658
+ }
1659
+ }
1660
+
1661
+ return undefined;
1662
+ }
1663
+
1664
+ function trimmedSlice(
1665
+ source: string,
1666
+ start: number,
1667
+ end: number,
1668
+ ): { text: string; start: number; end: number } {
1669
+ let trimmedStart = start;
1670
+ let trimmedEnd = end;
1671
+
1672
+ while (trimmedStart < trimmedEnd && /\s/.test(source[trimmedStart])) {
1673
+ trimmedStart++;
1674
+ }
1675
+ while (trimmedEnd > trimmedStart && /\s/.test(source[trimmedEnd - 1])) {
1676
+ trimmedEnd--;
1677
+ }
1678
+
1679
+ return {
1680
+ text: source.slice(trimmedStart, trimmedEnd),
1681
+ start: trimmedStart,
1682
+ end: trimmedEnd,
1683
+ };
1684
+ }
1685
+
1686
+ function identifiersFromArrayExpression(expression: string): Set<string> {
1687
+ const withoutBrackets = expression.replace(/^\[/, "").replace(/\]$/, "");
1688
+
1689
+ return new Set(
1690
+ Array.from(
1691
+ withoutBrackets.matchAll(/\b[A-Za-z_$][\w$]*\b/g),
1692
+ ([name]) => name,
1693
+ ),
1694
+ );
1695
+ }
1696
+
1697
+ function appendToArrayExpression(expression: string, names: string[]): string {
1698
+ const closingBracket = /\]\s*$/.exec(expression);
1699
+ if (!closingBracket) return expression;
1700
+
1701
+ const beforeClosingBracket = expression.slice(0, closingBracket.index);
1702
+ const closingBracketText = expression.slice(closingBracket.index);
1703
+ const inner = beforeClosingBracket.replace(/^\[/, "");
1704
+ if (!inner.trim()) return `[${names.join(", ")}]`;
1705
+
1706
+ if (!expression.includes("\n")) {
1707
+ const separator = /,\s*$/.test(inner) ? " " : ", ";
1708
+ return `${beforeClosingBracket}${separator}${names.join(", ")}${closingBracketText}`;
1709
+ }
1710
+
1711
+ const itemIndent =
1712
+ inner
1713
+ .split("\n")
1714
+ .find((line) => line.trim())
1715
+ ?.match(/^[\t ]*/)?.[0] ?? "\t";
1716
+ const closingIndent = inner.match(/\n([\t ]*)$/)?.[1] ?? "";
1717
+ const trimmedBeforeClosingBracket = beforeClosingBracket.replace(/\s*$/, "");
1718
+ const appendedNames = names.join(`,\n${itemIndent}`);
1719
+
1720
+ if (/,\s*$/.test(inner)) {
1721
+ return `${trimmedBeforeClosingBracket}\n${itemIndent}${appendedNames},\n${closingIndent}${closingBracketText}`;
1722
+ }
1723
+
1724
+ return `${trimmedBeforeClosingBracket},\n${itemIndent}${appendedNames}${closingBracketText}`;
1725
+ }
1726
+
1727
+ function insertAfterImports(source: string, line: string): string {
1728
+ const lines = source.split("\n");
1729
+ let lastImportIndex = -1;
1730
+
1731
+ for (let index = 0; index < lines.length; index++) {
1732
+ if (lines[index].startsWith("import ")) {
1733
+ lastImportIndex = index;
1734
+ }
1735
+ }
1736
+
1737
+ if (lastImportIndex === -1) {
1738
+ return `${line}\n${source}`;
1739
+ }
1740
+
1741
+ lines.splice(lastImportIndex + 1, 0, line);
1742
+ return lines.join("\n");
1743
+ }
1744
+
1745
+ function replaceRequired(
1746
+ source: string,
1747
+ searchValue: RegExp,
1748
+ replaceValue: string,
1749
+ message: string,
1750
+ ): string {
1751
+ const next = source.replace(searchValue, replaceValue);
1752
+ if (next === source) throw new Error(message);
1753
+ return next;
1754
+ }
1755
+
1756
+ function insertTypeProperty(
1757
+ source: string,
1758
+ typeName: string,
1759
+ property: string,
1760
+ message: string,
1761
+ ): string {
1762
+ const match = new RegExp(`export\\s+type\\s+${typeName}\\b`).exec(source);
1763
+ if (!match) throw new Error(message);
1764
+
1765
+ const openBrace = source.indexOf("{", match.index);
1766
+ if (openBrace === -1) throw new Error(message);
1767
+
1768
+ const closeBrace = matchingBraceIndex(source, openBrace);
1769
+ if (closeBrace === -1) throw new Error(message);
1770
+
1771
+ return insertBeforeClosingBrace(source, openBrace, closeBrace, property);
1772
+ }
1773
+
1774
+ function typeBodySource(source: string, typeName: string): string | undefined {
1775
+ const match = new RegExp(`export\\s+type\\s+${typeName}\\b`).exec(source);
1776
+ if (!match) return undefined;
1777
+
1778
+ const openBrace = source.indexOf("{", match.index);
1779
+ if (openBrace === -1) return undefined;
1780
+
1781
+ const closeBrace = matchingBraceIndex(source, openBrace);
1782
+ if (closeBrace === -1) return undefined;
1783
+
1784
+ return source.slice(openBrace + 1, closeBrace);
1785
+ }
1786
+
1787
+ function insertDefinePortsProperty(
1788
+ source: string,
1789
+ property: string,
1790
+ message: string,
1791
+ ): string {
1792
+ const body = definePortsBodyRange(source);
1793
+ if (!body) throw new Error(message);
1794
+
1795
+ return insertBeforeClosingBrace(
1796
+ source,
1797
+ body.openBrace,
1798
+ body.closeBrace,
1799
+ property,
1800
+ );
1801
+ }
1802
+
1803
+ function insertNoopUnitOfWorkProperty(
1804
+ source: string,
1805
+ property: string,
1806
+ message: string,
1807
+ ): string {
1808
+ const body = noopUnitOfWorkBodyRange(source);
1809
+ if (!body) throw new Error(message);
1810
+
1811
+ return insertBeforeClosingBrace(
1812
+ source,
1813
+ body.openBrace,
1814
+ body.closeBrace,
1815
+ property,
1816
+ );
1817
+ }
1818
+
1819
+ function insertDrizzleTransactionPortsProperty(
1820
+ source: string,
1821
+ property: string,
1822
+ message: string,
1823
+ ): string {
1824
+ const body = drizzleTransactionPortsBodyRange(source);
1825
+ if (!body) throw new Error(message);
1826
+
1827
+ return insertBeforeClosingBrace(
1828
+ source,
1829
+ body.openBrace,
1830
+ body.closeBrace,
1831
+ property,
1832
+ );
1833
+ }
1834
+
1835
+ function insertCreateRepositoriesProperty(
1836
+ source: string,
1837
+ property: string,
1838
+ message: string,
1839
+ ): string {
1840
+ const body = createRepositoriesReturnBodyRange(source);
1841
+ if (!body) throw new Error(message);
1842
+
1843
+ return insertBeforeClosingBrace(
1844
+ source,
1845
+ body.openBrace,
1846
+ body.closeBrace,
1847
+ property,
1848
+ );
1849
+ }
1850
+
1851
+ function noopUnitOfWorkBodySource(source: string): string | undefined {
1852
+ const body = noopUnitOfWorkBodyRange(source);
1853
+ if (!body) return undefined;
1854
+
1855
+ return source.slice(body.openBrace + 1, body.closeBrace);
1856
+ }
1857
+
1858
+ function drizzleTransactionPortsBodySource(source: string): string | undefined {
1859
+ const body = drizzleTransactionPortsBodyRange(source);
1860
+ if (!body) return undefined;
1861
+
1862
+ return source.slice(body.openBrace + 1, body.closeBrace);
1863
+ }
1864
+
1865
+ function createRepositoriesReturnBodySource(
1866
+ source: string,
1867
+ ): string | undefined {
1868
+ const body = createRepositoriesReturnBodyRange(source);
1869
+ if (!body) return undefined;
1870
+
1871
+ return source.slice(body.openBrace + 1, body.closeBrace);
1872
+ }
1873
+
1874
+ function definePortsBodySource(source: string): string | undefined {
1875
+ const body = definePortsBodyRange(source);
1876
+ if (!body) return undefined;
1877
+
1878
+ return source.slice(body.openBrace + 1, body.closeBrace);
1879
+ }
1880
+
1881
+ function definePortsBodyRange(
1882
+ source: string,
1883
+ ): { openBrace: number; closeBrace: number } | undefined {
1884
+ const match = /definePorts(?:<[^>]+>)?\(\s*\{/.exec(source);
1885
+ if (!match) return undefined;
1886
+
1887
+ const openBrace = source.indexOf("{", match.index);
1888
+ const closeBrace = matchingBraceIndex(source, openBrace);
1889
+ if (closeBrace === -1) return undefined;
1890
+
1891
+ return { openBrace, closeBrace };
1892
+ }
1893
+
1894
+ function hasTopLevelObjectProperty(
1895
+ source: string,
1896
+ propertyName: string,
1897
+ ): boolean {
1898
+ let depth = 0;
1899
+ let inString: string | undefined;
1900
+ let escaped = false;
1901
+ let lineComment = false;
1902
+ let blockComment = false;
1903
+
1904
+ for (let index = 0; index < source.length; index++) {
1905
+ const char = source[index];
1906
+ const next = source[index + 1];
1907
+
1908
+ if (lineComment) {
1909
+ if (char === "\n") lineComment = false;
1910
+ continue;
1911
+ }
1912
+
1913
+ if (blockComment) {
1914
+ if (char === "*" && next === "/") {
1915
+ blockComment = false;
1916
+ index++;
1917
+ }
1918
+ continue;
1919
+ }
1920
+
1921
+ if (inString) {
1922
+ if (escaped) {
1923
+ escaped = false;
1924
+ } else if (char === "\\") {
1925
+ escaped = true;
1926
+ } else if (char === inString) {
1927
+ inString = undefined;
1928
+ }
1929
+ continue;
1930
+ }
1931
+
1932
+ if (char === "/" && next === "/") {
1933
+ lineComment = true;
1934
+ index++;
1935
+ continue;
1936
+ }
1937
+
1938
+ if (char === "/" && next === "*") {
1939
+ blockComment = true;
1940
+ index++;
1941
+ continue;
1942
+ }
1943
+
1944
+ if (char === '"' || char === "'" || char === "`") {
1945
+ inString = char;
1946
+ continue;
1947
+ }
1948
+
1949
+ if (char === "{" || char === "[" || char === "(") {
1950
+ depth++;
1951
+ continue;
1952
+ }
1953
+
1954
+ if (char === "}" || char === "]" || char === ")") {
1955
+ depth = Math.max(0, depth - 1);
1956
+ continue;
1957
+ }
1958
+
1959
+ if (depth !== 0 || !identifierStartPattern.test(char)) {
1960
+ continue;
1961
+ }
1962
+
1963
+ const property = identifierAt(source, index);
1964
+ if (property !== propertyName) {
1965
+ index += property.length - 1;
1966
+ continue;
1967
+ }
1968
+
1969
+ let cursor = index + property.length;
1970
+ while (/\s/.test(source[cursor] ?? "")) cursor++;
1971
+ if (source[cursor] === ":" || source[cursor] === ",") return true;
1972
+ index = cursor;
1973
+ }
1974
+
1975
+ return false;
1976
+ }
1977
+
1978
+ const identifierStartPattern = /[$A-Z_a-z]/;
1979
+ const identifierPartPattern = /[$\w]/;
1980
+
1981
+ function identifierAt(source: string, index: number): string {
1982
+ let cursor = index;
1983
+ while (identifierPartPattern.test(source[cursor] ?? "")) {
1984
+ cursor++;
1985
+ }
1986
+
1987
+ return source.slice(index, cursor);
1988
+ }
1989
+
1990
+ function noopUnitOfWorkBodyRange(
1991
+ source: string,
1992
+ ): { openBrace: number; closeBrace: number } | undefined {
1993
+ const match = /createNoopUnitOfWork\(\s*\(\)\s*=>\s*\(\s*\{/.exec(source);
1994
+ if (!match) return undefined;
1995
+
1996
+ const openBrace = source.indexOf("{", match.index);
1997
+ const closeBrace = matchingBraceIndex(source, openBrace);
1998
+ if (closeBrace === -1) return undefined;
1999
+
2000
+ return { openBrace, closeBrace };
2001
+ }
2002
+
2003
+ function drizzleTransactionPortsBodyRange(
2004
+ source: string,
2005
+ ): { openBrace: number; closeBrace: number } | undefined {
2006
+ const match = /createTransactionPorts\s*:\s*\([^)]*\)\s*=>\s*\(\s*\{/.exec(
2007
+ source,
2008
+ );
2009
+ if (!match) return undefined;
2010
+
2011
+ const openBrace = source.indexOf("{", match.index);
2012
+ const closeBrace = matchingBraceIndex(source, openBrace);
2013
+ if (closeBrace === -1) return undefined;
2014
+
2015
+ return { openBrace, closeBrace };
2016
+ }
2017
+
2018
+ function createRepositoriesReturnBodyRange(
2019
+ source: string,
2020
+ ): { openBrace: number; closeBrace: number } | undefined {
2021
+ const functionBody = createRepositoriesFunctionBodyRange(source);
2022
+ if (!functionBody) return undefined;
2023
+
2024
+ const functionSource = source.slice(
2025
+ functionBody.openBrace + 1,
2026
+ functionBody.closeBrace,
2027
+ );
2028
+ const match = /return\s*\{\s*/.exec(functionSource);
2029
+ if (!match) return undefined;
2030
+
2031
+ const openBrace = source.indexOf(
2032
+ "{",
2033
+ functionBody.openBrace + 1 + match.index,
2034
+ );
2035
+ if (openBrace === -1) return undefined;
2036
+ const closeBrace = matchingDelimiterIndex(source, openBrace, "{", "}");
2037
+ if (closeBrace === -1 || closeBrace > functionBody.closeBrace) {
2038
+ return undefined;
2039
+ }
2040
+
2041
+ return { openBrace, closeBrace };
2042
+ }
2043
+
2044
+ function createRepositoriesFunctionBodyRange(
2045
+ source: string,
2046
+ ): { openBrace: number; closeBrace: number } | undefined {
2047
+ const match = /(?:export\s+)?function\s+createRepositories\b/.exec(source);
2048
+ if (!match) return undefined;
2049
+
2050
+ const openParen = source.indexOf("(", match.index);
2051
+ if (openParen === -1) return undefined;
2052
+ const closeParen = matchingDelimiterIndex(source, openParen, "(", ")");
2053
+ if (closeParen === -1) return undefined;
2054
+
2055
+ const bodyMatch = /(?:\s*:[^{]+)?\s*\{/.exec(source.slice(closeParen + 1));
2056
+ if (!bodyMatch) return undefined;
2057
+
2058
+ const openBrace =
2059
+ closeParen + 1 + bodyMatch.index + bodyMatch[0].lastIndexOf("{");
2060
+ const closeBrace = matchingDelimiterIndex(source, openBrace, "{", "}");
2061
+ if (closeBrace === -1) return undefined;
2062
+
2063
+ return { openBrace, closeBrace };
2064
+ }
2065
+
2066
+ function insertBeforeClosingBrace(
2067
+ source: string,
2068
+ openBrace: number,
2069
+ closeBrace: number,
2070
+ property: string,
2071
+ ): string {
2072
+ const body = source.slice(openBrace + 1, closeBrace);
2073
+ const propertyIndent = detectPropertyIndent(body);
2074
+ const closingIndent =
2075
+ source.slice(0, closeBrace).match(/\n([\t ]*)$/)?.[1] ?? "";
2076
+ const trimmedBody = body.replace(/\s*$/, "");
2077
+ const nextBody = `${trimmedBody}\n${propertyIndent}${property}\n${closingIndent}`;
2078
+
2079
+ return `${source.slice(0, openBrace + 1)}${nextBody}${source.slice(
2080
+ closeBrace,
2081
+ )}`;
2082
+ }
2083
+
2084
+ function detectPropertyIndent(body: string): string {
2085
+ return (
2086
+ body
2087
+ .split("\n")
2088
+ .find((line) => line.trim())
2089
+ ?.match(/^[\t ]*/)?.[0] ?? "\t"
2090
+ );
2091
+ }
2092
+
2093
+ function matchingBraceIndex(source: string, openBrace: number): number {
2094
+ return matchingDelimiterIndex(source, openBrace, "{", "}");
2095
+ }
2096
+
2097
+ function matchingDelimiterIndex(
2098
+ source: string,
2099
+ openIndex: number,
2100
+ open: string,
2101
+ close: string,
2102
+ ): number {
2103
+ let depth = 0;
2104
+ let inString: string | undefined;
2105
+ let escaped = false;
2106
+
2107
+ for (let index = openIndex; index < source.length; index++) {
2108
+ const char = source[index];
2109
+ if (inString) {
2110
+ if (escaped) {
2111
+ escaped = false;
2112
+ } else if (char === "\\") {
2113
+ escaped = true;
2114
+ } else if (char === inString) {
2115
+ inString = undefined;
2116
+ }
2117
+ continue;
2118
+ }
2119
+
2120
+ if (char === '"' || char === "'" || char === "`") {
2121
+ inString = char;
2122
+ continue;
2123
+ }
2124
+
2125
+ if (char === open) {
2126
+ depth++;
2127
+ continue;
2128
+ }
2129
+
2130
+ if (char === close) {
2131
+ depth--;
2132
+ if (depth === 0) return index;
2133
+ }
2134
+ }
2135
+
2136
+ return -1;
2137
+ }
2138
+
2139
+ function replaceTransactionPortsRequired(
2140
+ source: string,
2141
+ entry: string,
2142
+ message: string,
2143
+ ): string {
2144
+ return insertTypeProperty(
2145
+ source,
2146
+ "AppTransactionPorts",
2147
+ entry.trim(),
2148
+ message,
2149
+ );
2150
+ }
2151
+
2152
+ function aliasModule(filePath: string): string {
2153
+ return `@/${modulePath(filePath)}`;
2154
+ }
2155
+
2156
+ function relativeModule(fromFile: string, toFile: string): string {
2157
+ const fromDir = path.posix.dirname(normalizePath(fromFile));
2158
+ const relative = path.posix.relative(fromDir, normalizePath(toFile));
2159
+ const specifier = modulePath(relative);
2160
+
2161
+ if (specifier.startsWith("../")) return specifier;
2162
+ if (specifier === "..") return specifier;
2163
+ return specifier.startsWith(".") ? specifier : `./${specifier}`;
2164
+ }
2165
+
2166
+ function modulePath(filePath: string): string {
2167
+ return directoryPath(filePath)
2168
+ .replace(/\.(?:[cm]?[jt]sx?)$/, "")
2169
+ .replace(/\/index$/, "");
2170
+ }
2171
+
2172
+ function resourceNames(input: string): ResourceNames {
2173
+ const pluralKebab = normalizeResourceName(input);
2174
+ const singularKebab = singularize(pluralKebab);
2175
+
2176
+ const names = {
2177
+ input,
2178
+ pluralKebab,
2179
+ singularKebab,
2180
+ pluralCamel: camelCase(pluralKebab),
2181
+ singularCamel: camelCase(singularKebab),
2182
+ singularPascal: pascalCase(singularKebab),
2183
+ pluralPascal: pascalCase(pluralKebab),
2184
+ };
2185
+
2186
+ assertGeneratedIdentifiers(input, "Resource name", [
2187
+ names.pluralCamel,
2188
+ names.singularCamel,
2189
+ names.singularPascal,
2190
+ names.pluralPascal,
2191
+ ]);
2192
+
2193
+ return names;
2194
+ }
2195
+
2196
+ function useCaseNames(input: string): UseCaseNames {
2197
+ const parts = input
2198
+ .trim()
2199
+ .split("/")
2200
+ .map((part) => part.trim())
2201
+ .filter(Boolean);
2202
+
2203
+ if (parts.length !== 2) {
2204
+ throw new Error(
2205
+ "Use case name must use feature/action format, for example projects/archive-project.",
2206
+ );
2207
+ }
2208
+
2209
+ const feature = identifierNames(parts[0], "Use case feature");
2210
+ const action = identifierNames(parts[1], "Use case action");
2211
+ const exportName = `${action.camel}UseCase`;
2212
+
2213
+ assertGeneratedIdentifiers(parts[0], "Use case feature", [
2214
+ feature.camel,
2215
+ feature.pascal,
2216
+ ]);
2217
+ assertGeneratedIdentifiers(parts[1], "Use case action", [
2218
+ action.pascal,
2219
+ exportName,
2220
+ ]);
2221
+
2222
+ return {
2223
+ input,
2224
+ feature,
2225
+ action,
2226
+ exportName,
2227
+ operationName: `${feature.camel}.${action.camel}`,
2228
+ kind: inferUseCaseKind(action.kebab),
2229
+ };
2230
+ }
2231
+
2232
+ function identifierNames(input: string, label: string): IdentifierNames {
2233
+ const kebab = normalizeName(input, label);
2234
+ return {
2235
+ input,
2236
+ kebab,
2237
+ camel: camelCase(kebab),
2238
+ pascal: pascalCase(kebab),
2239
+ };
2240
+ }
2241
+
2242
+ function portNames(input: string): PortNames {
2243
+ const names = identifierNames(input, "Port name");
2244
+ const resolvedNames = {
2245
+ ...names,
2246
+ interfaceName: `${names.pascal}Port`,
2247
+ inputName: `Execute${names.pascal}Input`,
2248
+ adapterFactoryName: `create${names.pascal}Adapter`,
2249
+ };
2250
+
2251
+ assertGeneratedIdentifiers(input, "Port name", [
2252
+ resolvedNames.camel,
2253
+ resolvedNames.pascal,
2254
+ resolvedNames.interfaceName,
2255
+ resolvedNames.inputName,
2256
+ resolvedNames.adapterFactoryName,
2257
+ ]);
2258
+
2259
+ return resolvedNames;
2260
+ }
2261
+
2262
+ function policyNames(input: string): PolicyNames {
2263
+ const names = identifierNames(input, "Policy name");
2264
+ const resolvedNames = {
2265
+ ...names,
2266
+ policyName: `${names.camel}Policy`,
2267
+ };
2268
+
2269
+ assertGeneratedIdentifiers(input, "Policy name", [resolvedNames.policyName]);
2270
+
2271
+ return resolvedNames;
2272
+ }
2273
+
2274
+ function featureArtifactNames(
2275
+ input: string,
2276
+ label: string,
2277
+ ): FeatureArtifactNames {
2278
+ const parts = artifactNameParts(input, label);
2279
+ const feature = identifierNames(parts[0], `${label} feature`);
2280
+ const artifact = identifierNames(parts[1], `${label} name`);
2281
+ const featureSingularKebab = singularize(feature.kebab);
2282
+ const names = {
2283
+ input,
2284
+ feature,
2285
+ featureSingularKebab,
2286
+ featureSingularCamel: camelCase(featureSingularKebab),
2287
+ featureSingularPascal: pascalCase(featureSingularKebab),
2288
+ artifact,
2289
+ stableName: `${feature.kebab}.${artifact.kebab}`,
2290
+ };
2291
+
2292
+ assertGeneratedIdentifiers(parts[0], `${label} feature`, [
2293
+ names.feature.camel,
2294
+ names.feature.pascal,
2295
+ names.featureSingularCamel,
2296
+ names.featureSingularPascal,
2297
+ ]);
2298
+ assertGeneratedIdentifiers(parts[1], `${label} name`, [
2299
+ names.artifact.camel,
2300
+ names.artifact.pascal,
2301
+ ]);
2302
+
2303
+ return names;
2304
+ }
2305
+
2306
+ function eventNames(input: string): EventNames {
2307
+ const names = featureArtifactNames(normalizeEventReference(input), "Event");
2308
+ const eventExportName = `${names.featureSingularPascal}${names.artifact.pascal}`;
2309
+
2310
+ assertGeneratedIdentifiers(input, "Event name", [eventExportName]);
2311
+
2312
+ return {
2313
+ ...names,
2314
+ eventName: names.stableName,
2315
+ eventExportName,
2316
+ payloadSchemaName: `${eventExportName}PayloadSchema`,
2317
+ payloadTypeName: `${eventExportName}Payload`,
2318
+ };
2319
+ }
2320
+
2321
+ function jobNames(input: string): JobNames {
2322
+ const names = featureArtifactNames(input, "Job");
2323
+ const jobExportName = `${names.artifact.pascal}Job`;
2324
+
2325
+ assertGeneratedIdentifiers(input, "Job name", [jobExportName]);
2326
+
2327
+ return {
2328
+ ...names,
2329
+ jobName: names.stableName,
2330
+ jobExportName,
2331
+ payloadSchemaName: `${jobExportName}PayloadSchema`,
2332
+ payloadTypeName: `${jobExportName}Payload`,
2333
+ };
2334
+ }
2335
+
2336
+ function listenerNames(input: string, eventInput: string): ListenerNames {
2337
+ const names = featureArtifactNames(input, "Listener");
2338
+ const listenerExportName = names.artifact.camel;
2339
+
2340
+ assertGeneratedIdentifiers(input, "Listener name", [listenerExportName]);
2341
+
2342
+ return {
2343
+ ...names,
2344
+ listenerName: names.stableName,
2345
+ listenerExportName,
2346
+ event: eventNames(eventInput),
2347
+ };
2348
+ }
2349
+
2350
+ function scheduleNames(
2351
+ input: string,
2352
+ options: { cron?: string; timezone?: string },
2353
+ ): ScheduleNames {
2354
+ const names = featureArtifactNames(input, "Schedule");
2355
+ const scheduleExportName = `${names.artifact.pascal}Schedule`;
2356
+
2357
+ assertGeneratedIdentifiers(input, "Schedule name", [scheduleExportName]);
2358
+
2359
+ return {
2360
+ ...names,
2361
+ scheduleName: names.stableName,
2362
+ scheduleExportName,
2363
+ payloadSchemaName: `${scheduleExportName}PayloadSchema`,
2364
+ payloadTypeName: `${scheduleExportName}Payload`,
2365
+ cron: options.cron?.trim() || "0 9 * * *",
2366
+ timezone: options.timezone?.trim() || undefined,
2367
+ };
2368
+ }
2369
+
2370
+ function normalizeEventReference(input: string): string {
2371
+ const trimmed = input.trim();
2372
+ if (trimmed.includes("/")) return trimmed;
2373
+
2374
+ const dotIndex = trimmed.indexOf(".");
2375
+ if (dotIndex === -1) return trimmed;
2376
+
2377
+ return `${trimmed.slice(0, dotIndex)}/${trimmed.slice(dotIndex + 1)}`;
2378
+ }
2379
+
2380
+ function artifactNameParts(input: string, label: string): [string, string] {
2381
+ const parts = input
2382
+ .trim()
2383
+ .split("/")
2384
+ .map((part) => part.trim())
2385
+ .filter(Boolean);
2386
+
2387
+ if (parts.length !== 2) {
2388
+ throw new Error(
2389
+ `${label} name must use feature/name format, for example posts/published.`,
2390
+ );
2391
+ }
2392
+
2393
+ return [parts[0], parts[1]];
2394
+ }
2395
+
2396
+ function portStubEntry(names: PortNames): string {
2397
+ return `\t${names.camel}: {\n\t\tasync execute() {\n\t\t\tthrow new Error("${names.interfaceName} is not implemented.");\n\t\t},\n\t} satisfies ${names.interfaceName},`;
2398
+ }
2399
+
2400
+ function adapterEntry(names: PortNames): string {
2401
+ return `\t${names.camel}: ${names.adapterFactoryName}(),`;
2402
+ }
2403
+
2404
+ function normalizeResourceName(input: string): string {
2405
+ return normalizeName(input, "Resource name");
2406
+ }
2407
+
2408
+ function normalizeName(input: string, label: string): string {
2409
+ const normalized = input
2410
+ .trim()
2411
+ .toLowerCase()
2412
+ .replaceAll(/[^a-z0-9]+/g, "-")
2413
+ .replaceAll(/^-+|-+$/g, "");
2414
+
2415
+ if (!normalized) {
2416
+ throw new Error(`${label} is required.`);
2417
+ }
2418
+
2419
+ return normalized;
2420
+ }
2421
+
2422
+ function assertGeneratedIdentifiers(
2423
+ input: string,
2424
+ label: string,
2425
+ identifiers: readonly string[],
2426
+ ): void {
2427
+ for (const identifier of new Set(identifiers)) {
2428
+ if (isValidGeneratedIdentifier(identifier)) {
2429
+ continue;
2430
+ }
2431
+
2432
+ throw new Error(
2433
+ `${label} "${input}" generates invalid JavaScript identifier "${identifier}". Use a name that starts with a letter and is not a reserved word.`,
2434
+ );
2435
+ }
2436
+ }
2437
+
2438
+ function isValidGeneratedIdentifier(identifier: string): boolean {
2439
+ return (
2440
+ /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(identifier) &&
2441
+ !reservedJavaScriptIdentifiers.has(identifier)
2442
+ );
2443
+ }
2444
+
2445
+ const reservedJavaScriptIdentifiers = new Set([
2446
+ "arguments",
2447
+ "await",
2448
+ "break",
2449
+ "case",
2450
+ "catch",
2451
+ "class",
2452
+ "const",
2453
+ "continue",
2454
+ "debugger",
2455
+ "default",
2456
+ "delete",
2457
+ "do",
2458
+ "else",
2459
+ "enum",
2460
+ "eval",
2461
+ "export",
2462
+ "extends",
2463
+ "false",
2464
+ "finally",
2465
+ "for",
2466
+ "function",
2467
+ "if",
2468
+ "implements",
2469
+ "import",
2470
+ "in",
2471
+ "instanceof",
2472
+ "interface",
2473
+ "let",
2474
+ "new",
2475
+ "null",
2476
+ "package",
2477
+ "private",
2478
+ "protected",
2479
+ "public",
2480
+ "return",
2481
+ "static",
2482
+ "super",
2483
+ "switch",
2484
+ "this",
2485
+ "throw",
2486
+ "true",
2487
+ "try",
2488
+ "typeof",
2489
+ "var",
2490
+ "void",
2491
+ "while",
2492
+ "with",
2493
+ "yield",
2494
+ ]);
2495
+
2496
+ function inferUseCaseKind(action: string): "command" | "query" {
2497
+ if (/^(count|find|get|list|search)-/.test(`${action}-`)) {
2498
+ return "query";
2499
+ }
2500
+ return "command";
2501
+ }
2502
+
2503
+ function singularize(value: string): string {
2504
+ if (value.endsWith("ies") && value.length > 3) {
2505
+ return `${value.slice(0, -3)}y`;
2506
+ }
2507
+ if (value.endsWith("ses") && value.length > 3) {
2508
+ return value.slice(0, -2);
2509
+ }
2510
+ if (value.endsWith("s") && value.length > 1) {
2511
+ return value.slice(0, -1);
2512
+ }
2513
+ return value;
2514
+ }
2515
+
2516
+ function camelCase(value: string): string {
2517
+ return value.replaceAll(/-([a-z0-9])/g, (_, char: string) =>
2518
+ char.toUpperCase(),
2519
+ );
2520
+ }
2521
+
2522
+ function pascalCase(value: string): string {
2523
+ const camel = camelCase(value);
2524
+ return `${camel.charAt(0).toUpperCase()}${camel.slice(1)}`;
2525
+ }
2526
+
2527
+ function resourceFiles(
2528
+ names: ResourceNames,
2529
+ config: ResolvedBeignetConfig,
2530
+ persistence: ResourcePersistence = "memory",
2531
+ ): GeneratedFile[] {
2532
+ const useCaseDir = resourceUseCaseDir(names, config);
2533
+ const infraDir = infrastructureDir(config);
2534
+ const featureDir = resourceFeatureDir(names, config);
2535
+ const files: GeneratedFile[] = [
2536
+ {
2537
+ path: resourceContractFilePath(names, config),
2538
+ content: contractFile(names, config),
2539
+ },
2540
+ {
2541
+ path: path.join(useCaseDir, "schemas.ts"),
2542
+ content: schemasFile(names),
2543
+ },
2544
+ {
2545
+ path: path.join(useCaseDir, `list-${names.pluralKebab}.ts`),
2546
+ content: listUseCaseFile(names, config),
2547
+ },
2548
+ {
2549
+ path: path.join(useCaseDir, `create-${names.singularKebab}.ts`),
2550
+ content: createUseCaseFile(names, config),
2551
+ },
2552
+ {
2553
+ path: path.join(useCaseDir, "index.ts"),
2554
+ content: useCasesIndexFile(names),
2555
+ },
2556
+ {
2557
+ path: resourcePortFilePath(names, config),
2558
+ content: repositoryPortFile(names, config),
2559
+ },
2560
+ {
2561
+ path: path.join(
2562
+ infraDir,
2563
+ names.pluralKebab,
2564
+ `in-memory-${names.singularKebab}-repository.ts`,
2565
+ ),
2566
+ content: inMemoryRepositoryFile(names, config),
2567
+ },
2568
+ {
2569
+ path: path.join(featureDir, "routes.ts"),
2570
+ content: routeGroupFile(names, config),
2571
+ },
2572
+ {
2573
+ path: resourceTestFilePath(names, config),
2574
+ content: testFile(names, config),
2575
+ },
2576
+ ];
2577
+
2578
+ if (persistence === "drizzle") {
2579
+ files.push(
2580
+ {
2581
+ path: drizzleResourceSchemaFilePath(names, config),
2582
+ content: drizzleSchemaFile(names),
2583
+ },
2584
+ {
2585
+ path: drizzleResourceRepositoryFilePath(names, config),
2586
+ content: drizzleRepositoryFile(names, config),
2587
+ },
2588
+ );
2589
+ }
2590
+
2591
+ return files;
2592
+ }
2593
+
2594
+ function contractFiles(
2595
+ names: ResourceNames,
2596
+ config: ResolvedBeignetConfig,
2597
+ ): GeneratedFile[] {
2598
+ return [
2599
+ {
2600
+ path: resourceContractFilePath(names, config),
2601
+ content: standaloneContractFile(names),
2602
+ },
2603
+ ];
2604
+ }
2605
+
2606
+ function useCaseFiles(
2607
+ names: UseCaseNames,
2608
+ config: ResolvedBeignetConfig,
2609
+ ): GeneratedFile[] {
2610
+ const filePath = useCaseFilePath(names, config);
2611
+
2612
+ return [
2613
+ {
2614
+ path: filePath,
2615
+ content: standaloneUseCaseFile(names, config, filePath),
2616
+ },
2617
+ ];
2618
+ }
2619
+
2620
+ function testFiles(
2621
+ names: UseCaseNames,
2622
+ config: ResolvedBeignetConfig,
2623
+ ): GeneratedFile[] {
2624
+ return [
2625
+ {
2626
+ path: useCaseTestFilePath(names, config),
2627
+ content: useCaseTestFile(names, config),
2628
+ },
2629
+ ];
2630
+ }
2631
+
2632
+ function portFiles(
2633
+ names: PortNames,
2634
+ config: ResolvedBeignetConfig,
2635
+ ): GeneratedFile[] {
2636
+ return [
2637
+ {
2638
+ path: portFilePath(names, config),
2639
+ content: portFile(names),
2640
+ },
2641
+ ];
2642
+ }
2643
+
2644
+ function adapterFiles(
2645
+ names: PortNames,
2646
+ config: ResolvedBeignetConfig,
2647
+ ): GeneratedFile[] {
2648
+ return [
2649
+ {
2650
+ path: adapterFilePath(names, config),
2651
+ content: adapterFile(names, config),
2652
+ },
2653
+ ];
2654
+ }
2655
+
2656
+ function policyFiles(
2657
+ names: PolicyNames,
2658
+ config: ResolvedBeignetConfig,
2659
+ ): GeneratedFile[] {
2660
+ return [
2661
+ {
2662
+ path: policyFilePath(names, config),
2663
+ content: policyFile(names, config),
2664
+ },
2665
+ ];
2666
+ }
2667
+
2668
+ function eventFiles(
2669
+ names: EventNames,
2670
+ config: ResolvedBeignetConfig,
2671
+ ): GeneratedFile[] {
2672
+ return [
2673
+ {
2674
+ path: eventFilePath(names, config),
2675
+ content: eventFile(names),
2676
+ },
2677
+ ];
2678
+ }
2679
+
2680
+ function jobFiles(
2681
+ names: JobNames,
2682
+ config: ResolvedBeignetConfig,
2683
+ ): GeneratedFile[] {
2684
+ return [
2685
+ {
2686
+ path: jobFilePath(names, config),
2687
+ content: jobFile(names, config),
2688
+ },
2689
+ ];
2690
+ }
2691
+
2692
+ function listenerFiles(
2693
+ names: ListenerNames,
2694
+ config: ResolvedBeignetConfig,
2695
+ ): GeneratedFile[] {
2696
+ return [
2697
+ {
2698
+ path: listenerFilePath(names, config),
2699
+ content: listenerFile(names, config),
2700
+ },
2701
+ ];
2702
+ }
2703
+
2704
+ function scheduleFiles(
2705
+ names: ScheduleNames,
2706
+ config: ResolvedBeignetConfig,
2707
+ options: { route: boolean },
2708
+ ): GeneratedFile[] {
2709
+ const files: GeneratedFile[] = [
2710
+ {
2711
+ path: scheduleFilePath(names, config),
2712
+ content: scheduleFile(names, config),
2713
+ },
2714
+ ];
2715
+
2716
+ if (options.route) {
2717
+ files.push({
2718
+ path: scheduleRouteFilePath(names, config),
2719
+ content: scheduleRouteFile(names, config),
2720
+ });
2721
+ }
2722
+
2723
+ return files;
2724
+ }
2725
+
2726
+ function portFilePath(names: PortNames, config: ResolvedBeignetConfig): string {
2727
+ return path.join(path.dirname(config.paths.ports), `${names.kebab}.ts`);
2728
+ }
2729
+
2730
+ function adapterFilePath(
2731
+ names: PortNames,
2732
+ config: ResolvedBeignetConfig,
2733
+ ): string {
2734
+ return path.join(
2735
+ path.dirname(config.paths.infrastructurePorts),
2736
+ names.kebab,
2737
+ `${names.kebab}-adapter.ts`,
2738
+ );
2739
+ }
2740
+
2741
+ function infrastructureDir(config: ResolvedBeignetConfig): string {
2742
+ return path.dirname(config.paths.infrastructurePorts);
2743
+ }
2744
+
2745
+ function drizzleSchemaIndexPath(config: ResolvedBeignetConfig): string {
2746
+ return path.join(infrastructureDir(config), "db", "schema", "index.ts");
2747
+ }
2748
+
2749
+ function drizzleRepositoriesPath(config: ResolvedBeignetConfig): string {
2750
+ return path.join(infrastructureDir(config), "db", "repositories.ts");
2751
+ }
2752
+
2753
+ function drizzleResourceSchemaFilePath(
2754
+ names: ResourceNames,
2755
+ config: ResolvedBeignetConfig,
2756
+ ): string {
2757
+ return path.join(
2758
+ infrastructureDir(config),
2759
+ "db",
2760
+ "schema",
2761
+ `${names.pluralKebab}.ts`,
2762
+ );
2763
+ }
2764
+
2765
+ function drizzleResourceRepositoryFilePath(
2766
+ names: ResourceNames,
2767
+ config: ResolvedBeignetConfig,
2768
+ ): string {
2769
+ return path.join(
2770
+ infrastructureDir(config),
2771
+ names.pluralKebab,
2772
+ `drizzle-${names.singularKebab}-repository.ts`,
2773
+ );
2774
+ }
2775
+
2776
+ async function detectResourcePersistence(
2777
+ targetDir: string,
2778
+ config: ResolvedBeignetConfig,
2779
+ ): Promise<ResourcePersistence> {
2780
+ return (await fileExists(
2781
+ path.join(targetDir, drizzleRepositoriesPath(config)),
2782
+ ))
2783
+ ? "drizzle"
2784
+ : "memory";
2785
+ }
2786
+
2787
+ function resourceFeatureDir(
2788
+ names: ResourceNames,
2789
+ config: ResolvedBeignetConfig,
2790
+ ): string {
2791
+ return path.join(config.paths.features, names.pluralKebab);
2792
+ }
2793
+
2794
+ function usesFeatureOwnedContracts(config: ResolvedBeignetConfig): boolean {
2795
+ return (
2796
+ directoryPath(config.paths.contracts) ===
2797
+ directoryPath(config.paths.features)
2798
+ );
2799
+ }
2800
+
2801
+ function usesFeatureOwnedUseCases(config: ResolvedBeignetConfig): boolean {
2802
+ return (
2803
+ directoryPath(config.paths.useCases) ===
2804
+ directoryPath(config.paths.features)
2805
+ );
2806
+ }
2807
+
2808
+ function usesFeatureOwnedPolicies(config: ResolvedBeignetConfig): boolean {
2809
+ return (
2810
+ directoryPath(config.paths.policies) ===
2811
+ directoryPath(config.paths.features)
2812
+ );
2813
+ }
2814
+
2815
+ function usesFeatureOwnedTests(config: ResolvedBeignetConfig): boolean {
2816
+ return (
2817
+ usesFeatureOwnedUseCases(config) &&
2818
+ directoryPath(config.paths.tests) === "tests"
2819
+ );
2820
+ }
2821
+
2822
+ function resourceContractFilePath(
2823
+ names: ResourceNames,
2824
+ config: ResolvedBeignetConfig,
2825
+ ): string {
2826
+ if (usesFeatureOwnedContracts(config)) {
2827
+ return path.join(resourceFeatureDir(names, config), "contracts.ts");
2828
+ }
2829
+
2830
+ return path.join(config.paths.contracts, `${names.pluralKebab}.ts`);
2831
+ }
2832
+
2833
+ function useCaseFeatureDir(
2834
+ feature: string,
2835
+ config: ResolvedBeignetConfig,
2836
+ ): string {
2837
+ if (usesFeatureOwnedUseCases(config)) {
2838
+ return path.join(config.paths.features, feature, "use-cases");
2839
+ }
2840
+
2841
+ return path.join(config.paths.useCases, feature);
2842
+ }
2843
+
2844
+ function resourceUseCaseDir(
2845
+ names: ResourceNames,
2846
+ config: ResolvedBeignetConfig,
2847
+ ): string {
2848
+ return useCaseFeatureDir(names.pluralKebab, config);
2849
+ }
2850
+
2851
+ function resourceUseCaseIndexPath(
2852
+ names: ResourceNames,
2853
+ config: ResolvedBeignetConfig,
2854
+ ): string {
2855
+ return path.join(resourceUseCaseDir(names, config), "index.ts");
2856
+ }
2857
+
2858
+ function resourcePortFilePath(
2859
+ names: ResourceNames,
2860
+ config: ResolvedBeignetConfig,
2861
+ ): string {
2862
+ if (usesFeatureOwnedContracts(config)) {
2863
+ return path.join(resourceFeatureDir(names, config), "ports.ts");
2864
+ }
2865
+
2866
+ return path.join(
2867
+ path.dirname(config.paths.ports),
2868
+ `${names.singularKebab}-repository.ts`,
2869
+ );
2870
+ }
2871
+
2872
+ function resourceTestFilePath(
2873
+ names: ResourceNames,
2874
+ config: ResolvedBeignetConfig,
2875
+ ): string {
2876
+ if (usesFeatureOwnedTests(config)) {
2877
+ return path.join(
2878
+ resourceFeatureDir(names, config),
2879
+ "tests",
2880
+ `${names.pluralKebab}.test.ts`,
2881
+ );
2882
+ }
2883
+
2884
+ return path.join(config.paths.tests, `${names.pluralKebab}.test.ts`);
2885
+ }
2886
+
2887
+ function useCaseTestFilePath(
2888
+ names: UseCaseNames,
2889
+ config: ResolvedBeignetConfig,
2890
+ ): string {
2891
+ if (usesFeatureOwnedTests(config)) {
2892
+ return path.join(
2893
+ config.paths.features,
2894
+ names.feature.kebab,
2895
+ "tests",
2896
+ `${names.action.kebab}.test.ts`,
2897
+ );
2898
+ }
2899
+
2900
+ return path.join(
2901
+ config.paths.tests,
2902
+ names.feature.kebab,
2903
+ `${names.action.kebab}.test.ts`,
2904
+ );
2905
+ }
2906
+
2907
+ function policyFilePath(
2908
+ names: PolicyNames,
2909
+ config: ResolvedBeignetConfig,
2910
+ ): string {
2911
+ if (usesFeatureOwnedPolicies(config)) {
2912
+ return path.join(config.paths.features, names.kebab, "policy.ts");
2913
+ }
2914
+
2915
+ return path.join(config.paths.policies, `${names.kebab}.ts`);
2916
+ }
2917
+
2918
+ function featureArtifactDir(
2919
+ names: FeatureArtifactNames,
2920
+ kind: "events" | "jobs" | "listeners" | "schedules",
2921
+ config: ResolvedBeignetConfig,
2922
+ ): string {
2923
+ const featureDir = path.join(config.paths.features, names.feature.kebab);
2924
+
2925
+ if (kind === "events") {
2926
+ return path.join(featureDir, "domain", "events");
2927
+ }
2928
+
2929
+ return path.join(featureDir, kind);
2930
+ }
2931
+
2932
+ function eventFilePath(
2933
+ names: EventNames,
2934
+ config: ResolvedBeignetConfig,
2935
+ ): string {
2936
+ return path.join(
2937
+ featureArtifactDir(names, "events", config),
2938
+ `${names.artifact.kebab}.ts`,
2939
+ );
2940
+ }
2941
+
2942
+ function jobFilePath(names: JobNames, config: ResolvedBeignetConfig): string {
2943
+ return path.join(
2944
+ featureArtifactDir(names, "jobs", config),
2945
+ `${names.artifact.kebab}.ts`,
2946
+ );
2947
+ }
2948
+
2949
+ function listenerFilePath(
2950
+ names: ListenerNames,
2951
+ config: ResolvedBeignetConfig,
2952
+ ): string {
2953
+ return path.join(
2954
+ featureArtifactDir(names, "listeners", config),
2955
+ `${names.artifact.kebab}.ts`,
2956
+ );
2957
+ }
2958
+
2959
+ function scheduleFilePath(
2960
+ names: ScheduleNames,
2961
+ config: ResolvedBeignetConfig,
2962
+ ): string {
2963
+ return path.join(
2964
+ featureArtifactDir(names, "schedules", config),
2965
+ `${names.artifact.kebab}.ts`,
2966
+ );
2967
+ }
2968
+
2969
+ function scheduleRouteFilePath(
2970
+ names: ScheduleNames,
2971
+ config: ResolvedBeignetConfig,
2972
+ ): string {
2973
+ return path.join(
2974
+ config.paths.routes,
2975
+ "cron",
2976
+ names.feature.kebab,
2977
+ names.artifact.kebab,
2978
+ "route.ts",
2979
+ );
2980
+ }
2981
+
2982
+ function useCaseFilePath(
2983
+ names: UseCaseNames,
2984
+ config: ResolvedBeignetConfig,
2985
+ ): string {
2986
+ return path.join(
2987
+ useCaseFeatureDir(names.feature.kebab, config),
2988
+ `${names.action.kebab}.ts`,
2989
+ );
2990
+ }
2991
+
2992
+ function useCaseIndexFile(
2993
+ names: UseCaseNames,
2994
+ config: ResolvedBeignetConfig,
2995
+ ): GeneratedFile {
2996
+ const filePath = path.join(
2997
+ useCaseFeatureDir(names.feature.kebab, config),
2998
+ "index.ts",
2999
+ );
3000
+ return {
3001
+ path: filePath,
3002
+ content: useCaseIndexExport(names),
3003
+ };
3004
+ }
3005
+
3006
+ async function updateUseCaseIndex(
3007
+ targetDir: string,
3008
+ file: GeneratedFile,
3009
+ options: { dryRun: boolean },
3010
+ ): Promise<WriteGeneratedFileResult> {
3011
+ const destination = path.join(targetDir, file.path);
3012
+ const existing = await readOptionalFile(destination);
3013
+ if (existing === undefined) {
3014
+ if (!options.dryRun) {
3015
+ await mkdir(path.dirname(destination), { recursive: true });
3016
+ await writeFile(destination, file.content);
3017
+ }
3018
+ return "created";
3019
+ }
3020
+
3021
+ if (existing.includes(file.content.trim())) {
3022
+ return "skipped";
3023
+ }
3024
+
3025
+ const next = `${existing.trimEnd()}\n${file.content}`;
3026
+ if (!options.dryRun) await writeFile(destination, next);
3027
+ return "updated";
3028
+ }
3029
+
3030
+ type FeatureArtifactIndexFile = {
3031
+ path: string;
3032
+ importName: string;
3033
+ artifactPath: string;
3034
+ registryName: string;
3035
+ };
3036
+
3037
+ function featureArtifactIndexFile(
3038
+ kind: "event" | "job" | "listener" | "schedule",
3039
+ names: EventNames | JobNames | ListenerNames | ScheduleNames,
3040
+ config: ResolvedBeignetConfig,
3041
+ ): FeatureArtifactIndexFile {
3042
+ const artifactPath =
3043
+ kind === "event"
3044
+ ? eventFilePath(names as EventNames, config)
3045
+ : kind === "job"
3046
+ ? jobFilePath(names as JobNames, config)
3047
+ : kind === "listener"
3048
+ ? listenerFilePath(names as ListenerNames, config)
3049
+ : scheduleFilePath(names as ScheduleNames, config);
3050
+ const registrySuffix =
3051
+ kind === "event"
3052
+ ? "Events"
3053
+ : kind === "job"
3054
+ ? "Jobs"
3055
+ : kind === "listener"
3056
+ ? "Listeners"
3057
+ : "Schedules";
3058
+ const importName =
3059
+ kind === "event"
3060
+ ? (names as EventNames).eventExportName
3061
+ : kind === "job"
3062
+ ? (names as JobNames).jobExportName
3063
+ : kind === "listener"
3064
+ ? (names as ListenerNames).listenerExportName
3065
+ : (names as ScheduleNames).scheduleExportName;
3066
+
3067
+ return {
3068
+ path: path.join(path.dirname(artifactPath), "index.ts"),
3069
+ importName,
3070
+ artifactPath,
3071
+ registryName: `${names.featureSingularCamel}${registrySuffix}`,
3072
+ };
3073
+ }
3074
+
3075
+ async function updateFeatureArtifactIndex(
3076
+ targetDir: string,
3077
+ file: FeatureArtifactIndexFile,
3078
+ options: { dryRun: boolean },
3079
+ ): Promise<WriteGeneratedFileResult> {
3080
+ const destination = path.join(targetDir, file.path);
3081
+ const existing = await readOptionalFile(destination);
3082
+ const specifier = relativeModule(file.path, file.artifactPath);
3083
+ const importLine = `import { ${file.importName} } from "${specifier}";`;
3084
+ const exportLine = `export { ${file.importName} } from "${specifier}";`;
3085
+
3086
+ if (existing === undefined) {
3087
+ const content = `${importLine}
3088
+
3089
+ ${exportLine}
3090
+
3091
+ export const ${file.registryName} = [${file.importName}] as const;
3092
+ `;
3093
+ if (!options.dryRun) {
3094
+ await mkdir(path.dirname(destination), { recursive: true });
3095
+ await writeFile(destination, content);
3096
+ }
3097
+ return "created";
3098
+ }
3099
+
3100
+ let next = existing;
3101
+
3102
+ if (!next.includes(importLine)) {
3103
+ next = insertAfterImports(next, importLine);
3104
+ }
3105
+
3106
+ if (!next.includes(exportLine)) {
3107
+ const registryExport = new RegExp(
3108
+ `export\\s+const\\s+${file.registryName}\\b`,
3109
+ ).exec(next);
3110
+ next = registryExport
3111
+ ? `${next.slice(0, registryExport.index).trimEnd()}\n${exportLine}\n\n${next.slice(
3112
+ registryExport.index,
3113
+ )}`
3114
+ : `${next.trimEnd()}\n${exportLine}\n`;
3115
+ }
3116
+
3117
+ const registry = arrayInitializerInfo(next, file.registryName);
3118
+ if (!registry) {
3119
+ next = `${next.trimEnd()}\n\nexport const ${file.registryName} = [${file.importName}] as const;\n`;
3120
+ } else if (
3121
+ !identifiersFromArrayExpression(registry.text).has(file.importName)
3122
+ ) {
3123
+ const nextArray = appendToArrayExpression(registry.text, [file.importName]);
3124
+ next = `${next.slice(0, registry.start)}${nextArray}${next.slice(
3125
+ registry.end,
3126
+ )}`;
3127
+ }
3128
+
3129
+ if (next === existing) return "skipped";
3130
+ if (!options.dryRun) await writeFile(destination, next);
3131
+ return "updated";
3132
+ }
3133
+
3134
+ function arrayInitializerInfo(
3135
+ source: string,
3136
+ constName: string,
3137
+ ): { text: string; start: number; end: number } | undefined {
3138
+ const match = new RegExp(`\\bconst\\s+${constName}\\s*=`).exec(source);
3139
+ if (!match) return undefined;
3140
+
3141
+ const openBracket = source.indexOf("[", match.index);
3142
+ if (openBracket === -1) return undefined;
3143
+
3144
+ const closeBracket = matchingDelimiterIndex(source, openBracket, "[", "]");
3145
+ if (closeBracket === -1) return undefined;
3146
+
3147
+ return {
3148
+ text: source.slice(openBracket, closeBracket + 1),
3149
+ start: openBracket,
3150
+ end: closeBracket + 1,
3151
+ };
3152
+ }
3153
+
3154
+ function schemasFile(names: ResourceNames): string {
3155
+ return `import { z } from "zod";
3156
+
3157
+ export const ${names.singularPascal}Schema = z.object({
3158
+ id: z.string().uuid(),
3159
+ name: z.string().min(1),
3160
+ createdAt: z.string().datetime(),
3161
+ });
3162
+
3163
+ export const List${names.pluralPascal}InputSchema = z.object({
3164
+ limit: z.coerce.number().int().min(1).max(100).default(20),
3165
+ offset: z.coerce.number().int().min(0).default(0),
3166
+ });
3167
+
3168
+ export const List${names.pluralPascal}OutputSchema = z.object({
3169
+ ${names.pluralCamel}: z.array(${names.singularPascal}Schema),
3170
+ total: z.number().int().min(0),
3171
+ limit: z.number().int().min(1),
3172
+ offset: z.number().int().min(0),
3173
+ });
3174
+
3175
+ export const Create${names.singularPascal}InputSchema = z.object({
3176
+ name: z.string().min(1).max(120),
3177
+ });
3178
+
3179
+ export type ${names.singularPascal} = z.infer<typeof ${names.singularPascal}Schema>;
3180
+ export type Create${names.singularPascal}Input = z.infer<
3181
+ typeof Create${names.singularPascal}InputSchema
3182
+ >;
3183
+ export type List${names.pluralPascal}Input = z.infer<
3184
+ typeof List${names.pluralPascal}InputSchema
3185
+ >;
3186
+ `;
3187
+ }
3188
+
3189
+ function listUseCaseFile(
3190
+ names: ResourceNames,
3191
+ config: ResolvedBeignetConfig,
3192
+ ): string {
3193
+ const filePath = path.join(
3194
+ resourceUseCaseDir(names, config),
3195
+ `list-${names.pluralKebab}.ts`,
3196
+ );
3197
+
3198
+ return `import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
3199
+ import {
3200
+ List${names.pluralPascal}InputSchema,
3201
+ List${names.pluralPascal}OutputSchema,
3202
+ } from "./schemas";
3203
+
3204
+ export const list${names.pluralPascal}UseCase = useCase
3205
+ .query("${names.pluralCamel}.list")
3206
+ .input(List${names.pluralPascal}InputSchema)
3207
+ .output(List${names.pluralPascal}OutputSchema)
3208
+ .run(async ({ ctx, input }) => {
3209
+ const result = await ctx.ports.${names.pluralCamel}.list(input);
3210
+ return {
3211
+ ${names.pluralCamel}: result.${names.pluralCamel},
3212
+ total: result.total,
3213
+ limit: input.limit,
3214
+ offset: input.offset,
3215
+ };
3216
+ });
3217
+ `;
3218
+ }
3219
+
3220
+ function createUseCaseFile(
3221
+ names: ResourceNames,
3222
+ config: ResolvedBeignetConfig,
3223
+ ): string {
3224
+ const filePath = path.join(
3225
+ resourceUseCaseDir(names, config),
3226
+ `create-${names.singularKebab}.ts`,
3227
+ );
3228
+
3229
+ return `import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
3230
+ import {
3231
+ Create${names.singularPascal}InputSchema,
3232
+ ${names.singularPascal}Schema,
3233
+ } from "./schemas";
3234
+
3235
+ export const create${names.singularPascal}UseCase = useCase
3236
+ .command("${names.pluralCamel}.create")
3237
+ .input(Create${names.singularPascal}InputSchema)
3238
+ .output(${names.singularPascal}Schema)
3239
+ .run(async ({ ctx, input }) => ctx.ports.${names.pluralCamel}.create(input));
3240
+ `;
3241
+ }
3242
+
3243
+ function useCasesIndexFile(names: ResourceNames): string {
3244
+ return `export { create${names.singularPascal}UseCase } from "./create-${names.singularKebab}";
3245
+ export { list${names.pluralPascal}UseCase } from "./list-${names.pluralKebab}";
3246
+ export {
3247
+ Create${names.singularPascal}InputSchema,
3248
+ List${names.pluralPascal}InputSchema,
3249
+ List${names.pluralPascal}OutputSchema,
3250
+ ${names.singularPascal}Schema,
3251
+ type Create${names.singularPascal}Input,
3252
+ type List${names.pluralPascal}Input,
3253
+ type ${names.singularPascal},
3254
+ } from "./schemas";
3255
+ `;
3256
+ }
3257
+
3258
+ function repositoryPortFile(
3259
+ names: ResourceNames,
3260
+ config: ResolvedBeignetConfig,
3261
+ ): string {
3262
+ return `import type {
3263
+ Create${names.singularPascal}Input,
3264
+ List${names.pluralPascal}Input,
3265
+ ${names.singularPascal},
3266
+ } from "${aliasModule(path.join(resourceUseCaseDir(names, config), "schemas.ts"))}";
3267
+
3268
+ export type List${names.pluralPascal}Result = {
3269
+ ${names.pluralCamel}: ${names.singularPascal}[];
3270
+ total: number;
3271
+ };
3272
+
3273
+ export interface ${names.singularPascal}Repository {
3274
+ list(input: List${names.pluralPascal}Input): Promise<List${names.pluralPascal}Result>;
3275
+ create(input: Create${names.singularPascal}Input): Promise<${names.singularPascal}>;
3276
+ }
3277
+ `;
3278
+ }
3279
+
3280
+ function inMemoryRepositoryFile(
3281
+ names: ResourceNames,
3282
+ config: ResolvedBeignetConfig,
3283
+ ): string {
3284
+ const repositoryPortPath = resourcePortFilePath(names, config);
3285
+
3286
+ return `import type { ${names.singularPascal}Repository } from "${aliasModule(repositoryPortPath)}";
3287
+ import type {
3288
+ Create${names.singularPascal}Input,
3289
+ List${names.pluralPascal}Input,
3290
+ ${names.singularPascal},
3291
+ } from "${aliasModule(path.join(resourceUseCaseDir(names, config), "schemas.ts"))}";
3292
+
3293
+ export function createInMemory${names.singularPascal}Repository(
3294
+ seed: ${names.singularPascal}[] = [],
3295
+ ): ${names.singularPascal}Repository {
3296
+ const ${names.pluralCamel} = new Map(seed.map((${names.singularCamel}) => [${names.singularCamel}.id, ${names.singularCamel}]));
3297
+
3298
+ return {
3299
+ async list(input: List${names.pluralPascal}Input) {
3300
+ const all${names.pluralPascal} = Array.from(${names.pluralCamel}.values()).sort(
3301
+ (left, right) => right.createdAt.localeCompare(left.createdAt),
3302
+ );
3303
+
3304
+ return {
3305
+ ${names.pluralCamel}: all${names.pluralPascal}.slice(
3306
+ input.offset,
3307
+ input.offset + input.limit,
3308
+ ),
3309
+ total: all${names.pluralPascal}.length,
3310
+ };
3311
+ },
3312
+ async create(input: Create${names.singularPascal}Input) {
3313
+ const ${names.singularCamel}: ${names.singularPascal} = {
3314
+ id: crypto.randomUUID(),
3315
+ name: input.name,
3316
+ createdAt: new Date().toISOString(),
3317
+ };
3318
+ ${names.pluralCamel}.set(${names.singularCamel}.id, ${names.singularCamel});
3319
+ return ${names.singularCamel};
3320
+ },
3321
+ };
3322
+ }
3323
+ `;
3324
+ }
3325
+
3326
+ function drizzleSchemaFile(names: ResourceNames): string {
3327
+ return `import { sqliteTable, text } from "drizzle-orm/sqlite-core";
3328
+
3329
+ export const ${names.pluralCamel} = sqliteTable("${names.pluralKebab.replaceAll("-", "_")}", {
3330
+ id: text("id").primaryKey(),
3331
+ name: text("name").notNull(),
3332
+ createdAt: text("created_at").notNull(),
3333
+ });
3334
+ `;
3335
+ }
3336
+
3337
+ function drizzleRepositoryFile(
3338
+ names: ResourceNames,
3339
+ config: ResolvedBeignetConfig,
3340
+ ): string {
3341
+ const repositoryPortPath = resourcePortFilePath(names, config);
3342
+ const schemaPath = drizzleSchemaIndexPath(config);
3343
+
3344
+ return `import type { DrizzleTursoDatabase } from "@beignet/provider-drizzle-turso";
3345
+ import { count, desc } from "drizzle-orm";
3346
+ import type { ${names.singularPascal}Repository } from "${aliasModule(repositoryPortPath)}";
3347
+ import type {
3348
+ Create${names.singularPascal}Input,
3349
+ List${names.pluralPascal}Input,
3350
+ ${names.singularPascal},
3351
+ } from "${aliasModule(path.join(resourceUseCaseDir(names, config), "schemas.ts"))}";
3352
+ import * as schema from "${aliasModule(schemaPath)}";
3353
+
3354
+ type ${names.singularPascal}Row = typeof schema.${names.pluralCamel}.$inferSelect;
3355
+
3356
+ function to${names.singularPascal}(row: ${names.singularPascal}Row): ${names.singularPascal} {
3357
+ return {
3358
+ id: row.id,
3359
+ name: row.name,
3360
+ createdAt: row.createdAt,
3361
+ };
3362
+ }
3363
+
3364
+ export function createDrizzle${names.singularPascal}Repository(
3365
+ db: DrizzleTursoDatabase<typeof schema>,
3366
+ ): ${names.singularPascal}Repository {
3367
+ return {
3368
+ async list(input: List${names.pluralPascal}Input) {
3369
+ const rows = await db
3370
+ .select()
3371
+ .from(schema.${names.pluralCamel})
3372
+ .orderBy(desc(schema.${names.pluralCamel}.createdAt))
3373
+ .limit(input.limit)
3374
+ .offset(input.offset);
3375
+ const [{ total }] = await db
3376
+ .select({ total: count() })
3377
+ .from(schema.${names.pluralCamel});
3378
+
3379
+ return {
3380
+ ${names.pluralCamel}: rows.map(to${names.singularPascal}),
3381
+ total,
3382
+ };
3383
+ },
3384
+ async create(input: Create${names.singularPascal}Input) {
3385
+ const [row] = await db
3386
+ .insert(schema.${names.pluralCamel})
3387
+ .values({
3388
+ id: crypto.randomUUID(),
3389
+ name: input.name,
3390
+ createdAt: new Date().toISOString(),
3391
+ })
3392
+ .returning();
3393
+
3394
+ if (!row) {
3395
+ throw new Error("Failed to create ${names.singularKebab}.");
3396
+ }
3397
+
3398
+ return to${names.singularPascal}(row);
3399
+ },
3400
+ };
3401
+ }
3402
+ `;
3403
+ }
3404
+
3405
+ function contractFile(
3406
+ names: ResourceNames,
3407
+ config: ResolvedBeignetConfig,
3408
+ ): string {
3409
+ return `import { createContractGroup } from "@beignet/core/contracts";
3410
+ import { z } from "zod";
3411
+ import {
3412
+ create${names.singularPascal}UseCase,
3413
+ list${names.pluralPascal}UseCase,
3414
+ } from "${aliasModule(resourceUseCaseIndexPath(names, config))}";
3415
+
3416
+ const ErrorResponseSchema = z.object({
3417
+ code: z.string(),
3418
+ message: z.string(),
3419
+ requestId: z.string().optional(),
3420
+ });
3421
+
3422
+ const ${names.pluralCamel} = createContractGroup()
3423
+ .namespace("${names.pluralCamel}")
3424
+ .responses({
3425
+ 500: ErrorResponseSchema,
3426
+ });
3427
+
3428
+ export const list${names.pluralPascal} = ${names.pluralCamel}
3429
+ .get("/api/${names.pluralKebab}")
3430
+ .query(list${names.pluralPascal}UseCase.inputSchema)
3431
+ .responses({
3432
+ 200: list${names.pluralPascal}UseCase.outputSchema,
3433
+ });
3434
+
3435
+ export const create${names.singularPascal} = ${names.pluralCamel}
3436
+ .post("/api/${names.pluralKebab}")
3437
+ .body(create${names.singularPascal}UseCase.inputSchema)
3438
+ .responses({
3439
+ 201: create${names.singularPascal}UseCase.outputSchema,
3440
+ });
3441
+ `;
3442
+ }
3443
+
3444
+ function standaloneContractFile(names: ResourceNames): string {
3445
+ return `import { createContractGroup } from "@beignet/core/contracts";
3446
+ import { z } from "zod";
3447
+
3448
+ const ErrorResponseSchema = z.object({
3449
+ code: z.string(),
3450
+ message: z.string(),
3451
+ requestId: z.string().optional(),
3452
+ });
3453
+
3454
+ const ${names.pluralCamel} = createContractGroup()
3455
+ .namespace("${names.pluralCamel}")
3456
+ .responses({
3457
+ 500: ErrorResponseSchema,
3458
+ });
3459
+
3460
+ export const ${names.singularCamel}Schema = z.object({
3461
+ id: z.string().uuid(),
3462
+ name: z.string().min(1),
3463
+ });
3464
+
3465
+ export const list${names.pluralPascal} = ${names.pluralCamel}
3466
+ .get("/api/${names.pluralKebab}")
3467
+ .query(
3468
+ z.object({
3469
+ limit: z.coerce.number().int().min(1).max(100).default(20),
3470
+ offset: z.coerce.number().int().min(0).default(0),
3471
+ }),
3472
+ )
3473
+ .responses({
3474
+ 200: z.object({
3475
+ ${names.pluralCamel}: z.array(${names.singularCamel}Schema),
3476
+ total: z.number().int().min(0),
3477
+ limit: z.number().int().min(1),
3478
+ offset: z.number().int().min(0),
3479
+ }),
3480
+ });
3481
+
3482
+ export const ${names.pluralCamel}Contracts = [list${names.pluralPascal}];
3483
+ `;
3484
+ }
3485
+
3486
+ function standaloneUseCaseFile(
3487
+ names: UseCaseNames,
3488
+ config: ResolvedBeignetConfig,
3489
+ filePath: string,
3490
+ ): string {
3491
+ return `import { z } from "zod";
3492
+ import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
3493
+
3494
+ export const ${names.action.pascal}InputSchema = z.object({});
3495
+
3496
+ export const ${names.action.pascal}OutputSchema = z.object({
3497
+ ok: z.literal(true),
3498
+ });
3499
+
3500
+ export type ${names.action.pascal}Input = z.infer<
3501
+ typeof ${names.action.pascal}InputSchema
3502
+ >;
3503
+ export type ${names.action.pascal}Output = z.infer<
3504
+ typeof ${names.action.pascal}OutputSchema
3505
+ >;
3506
+
3507
+ export const ${names.exportName} = useCase
3508
+ .${names.kind}("${names.operationName}")
3509
+ .input(${names.action.pascal}InputSchema)
3510
+ .output(${names.action.pascal}OutputSchema)
3511
+ .run(async () => ({ ok: true }));
3512
+ `;
3513
+ }
3514
+
3515
+ function useCaseTestFile(
3516
+ names: UseCaseNames,
3517
+ config: ResolvedBeignetConfig,
3518
+ ): string {
3519
+ const filePath = useCaseTestFilePath(names, config);
3520
+
3521
+ return `import { describe, expect, it } from "bun:test";
3522
+ import { createUseCaseTester } from "@beignet/core/application";
3523
+ import { createInMemoryDevtools } from "@beignet/devtools";
3524
+ import { createAnonymousActor, createMemoryStorage, createNoopUnitOfWork } from "@beignet/core/ports";
3525
+ import type { AppContext } from "${aliasModule(config.paths.appContext)}";
3526
+ import { appPorts } from "${aliasModule(config.paths.infrastructurePorts)}";
3527
+ import { ${names.exportName} } from "${relativeModule(filePath, useCaseFilePath(names, config))}";
3528
+
3529
+ describe("${names.exportName}", () => {
3530
+ it("runs ${names.action.camel}", async () => {
3531
+ const testPorts = {
3532
+ ...appPorts,
3533
+ uow: createNoopUnitOfWork(
3534
+ () => appPorts as unknown as AppContext["ports"],
3535
+ ) as AppContext["ports"]["uow"],
3536
+ devtools: createInMemoryDevtools(),
3537
+ storage: createMemoryStorage(),
3538
+ } as AppContext["ports"];
3539
+ const actor = createAnonymousActor();
3540
+ const tester = createUseCaseTester<AppContext>(() => ({
3541
+ requestId: "test-request",
3542
+ actor,
3543
+ auth: null,
3544
+ gate: testPorts.gate.bind({ actor, auth: null }),
3545
+ ports: testPorts,
3546
+ }));
3547
+
3548
+ const result = await tester.run(${names.exportName}, {});
3549
+
3550
+ expect(result).toEqual({ ok: true });
3551
+ });
3552
+ });
3553
+ `;
3554
+ }
3555
+
3556
+ function portFile(names: PortNames): string {
3557
+ return `export type ${names.inputName} = Record<string, unknown>;
3558
+
3559
+ export interface ${names.interfaceName} {
3560
+ execute(input: ${names.inputName}): Promise<void>;
3561
+ }
3562
+
3563
+ export type Fake${names.pascal}Call = {
3564
+ input: ${names.inputName};
3565
+ };
3566
+
3567
+ export interface Fake${names.interfaceName} extends ${names.interfaceName} {
3568
+ calls: Fake${names.pascal}Call[];
3569
+ }
3570
+
3571
+ export function createFake${names.pascal}Port(): Fake${names.interfaceName} {
3572
+ const calls: Fake${names.pascal}Call[] = [];
3573
+
3574
+ return {
3575
+ calls,
3576
+ async execute(input) {
3577
+ calls.push({ input });
3578
+ },
3579
+ };
3580
+ }
3581
+ `;
3582
+ }
3583
+
3584
+ function adapterFile(names: PortNames, config: ResolvedBeignetConfig): string {
3585
+ const filePath = adapterFilePath(names, config);
3586
+ return `import type { ${names.interfaceName} } from "${relativeModule(filePath, portFilePath(names, config))}";
3587
+
3588
+ export function ${names.adapterFactoryName}(): ${names.interfaceName} {
3589
+ return {
3590
+ async execute() {
3591
+ throw new Error("${names.interfaceName} is not implemented.");
3592
+ },
3593
+ };
3594
+ }
3595
+ `;
3596
+ }
3597
+
3598
+ function policyFile(names: PolicyNames, config: ResolvedBeignetConfig): string {
3599
+ const filePath = policyFilePath(names, config);
3600
+
3601
+ return `import { definePolicy, deny } from "@beignet/core/ports";
3602
+ import type { AppContext } from "${relativeModule(filePath, config.paths.appContext)}";
3603
+
3604
+ export const ${names.policyName} = definePolicy({
3605
+ "${names.camel}.view": (_ctx: AppContext) => true,
3606
+ "${names.camel}.manage": (_ctx: AppContext) =>
3607
+ deny("You are not allowed to manage this ${names.kebab}."),
3608
+ });
3609
+ `;
3610
+ }
3611
+
3612
+ function eventFile(names: EventNames): string {
3613
+ return `import { defineEvent } from "@beignet/core/events";
3614
+ import { z } from "zod";
3615
+
3616
+ export const ${names.payloadSchemaName} = z.object({
3617
+ \tid: z.string().uuid(),
3618
+ });
3619
+
3620
+ export type ${names.payloadTypeName} = z.infer<typeof ${names.payloadSchemaName}>;
3621
+
3622
+ export const ${names.eventExportName} = defineEvent("${names.eventName}", {
3623
+ \tpayload: ${names.payloadSchemaName},
3624
+ });
3625
+ `;
3626
+ }
3627
+
3628
+ function jobFile(names: JobNames, config: ResolvedBeignetConfig): string {
3629
+ return `import { createJobHandlers } from "@beignet/core/jobs";
3630
+ import { z } from "zod";
3631
+ import type { AppContext } from "${aliasModule(config.paths.appContext)}";
3632
+
3633
+ const jobs = createJobHandlers<AppContext>();
3634
+
3635
+ export const ${names.payloadSchemaName} = z.object({
3636
+ \tid: z.string().uuid(),
3637
+ });
3638
+
3639
+ export type ${names.payloadTypeName} = z.infer<typeof ${names.payloadSchemaName}>;
3640
+
3641
+ export const ${names.jobExportName} = jobs.defineJob("${names.jobName}", {
3642
+ \tpayload: ${names.payloadSchemaName},
3643
+ \tretry: {
3644
+ \t\tattempts: 3,
3645
+ \t},
3646
+ \tasync handle({ payload, ctx }) {
3647
+ \t\tctx.ports.logger.info("Job handled", {
3648
+ \t\t\tjobName: "${names.jobName}",
3649
+ \t\t\tpayload,
3650
+ \t\t});
3651
+ \t},
3652
+ });
3653
+ `;
3654
+ }
3655
+
3656
+ function listenerFile(
3657
+ names: ListenerNames,
3658
+ config: ResolvedBeignetConfig,
3659
+ ): string {
3660
+ const filePath = listenerFilePath(names, config);
3661
+
3662
+ return `import { createEventHandlers } from "@beignet/core/events";
3663
+ import type { AppContext } from "${aliasModule(config.paths.appContext)}";
3664
+ import { ${names.event.eventExportName} } from "${relativeModule(filePath, eventFilePath(names.event, config))}";
3665
+
3666
+ const events = createEventHandlers<AppContext>();
3667
+
3668
+ export const ${names.listenerExportName} = events.defineListener(${names.event.eventExportName}, {
3669
+ \tname: "${names.listenerName}",
3670
+ \tasync handle({ payload, ctx }) {
3671
+ \t\tctx.ports.logger.info("Listener handled", {
3672
+ \t\t\tlistenerName: "${names.listenerName}",
3673
+ \t\t\tpayload,
3674
+ \t\t});
3675
+ \t},
3676
+ });
3677
+ `;
3678
+ }
3679
+
3680
+ function scheduleFile(
3681
+ names: ScheduleNames,
3682
+ config: ResolvedBeignetConfig,
3683
+ ): string {
3684
+ return `import { createScheduleHandlers } from "@beignet/core/schedules";
3685
+ import { z } from "zod";
3686
+ import type { AppContext } from "${aliasModule(config.paths.appContext)}";
3687
+
3688
+ const schedules = createScheduleHandlers<AppContext>();
3689
+
3690
+ export const ${names.payloadSchemaName} = z.object({
3691
+ \tdate: z.string(),
3692
+ });
3693
+
3694
+ export type ${names.payloadTypeName} = z.infer<typeof ${names.payloadSchemaName}>;
3695
+
3696
+ export const ${names.scheduleExportName} = schedules.defineSchedule(
3697
+ \t"${names.scheduleName}",
3698
+ \t{
3699
+ \t\tcron: "${names.cron}",
3700
+ ${names.timezone ? `\t\ttimezone: "${names.timezone}",\n` : ""}\t\tpayload: ${names.payloadSchemaName},
3701
+ \t\tcreatePayload({ run }) {
3702
+ \t\t\tconst date = run.scheduledAt ?? run.triggeredAt;
3703
+
3704
+ \t\t\treturn {
3705
+ \t\t\t\tdate: date.toISOString().slice(0, 10),
3706
+ \t\t\t};
3707
+ \t\t},
3708
+ \t\tasync handle({ payload, ctx }) {
3709
+ \t\t\tctx.ports.logger.info("Schedule handled", {
3710
+ \t\t\t\tscheduleName: "${names.scheduleName}",
3711
+ \t\t\t\tdate: payload.date,
3712
+ \t\t\t});
3713
+ \t\t},
3714
+ \t},
3715
+ );
3716
+ `;
3717
+ }
3718
+
3719
+ function scheduleRouteFile(
3720
+ names: ScheduleNames,
3721
+ config: ResolvedBeignetConfig,
3722
+ ): string {
3723
+ return `import { createInlineScheduleRunner } from "@beignet/core/schedules";
3724
+ import type { AppContext } from "${aliasModule(config.paths.appContext)}";
3725
+ import { ${names.scheduleExportName} } from "${aliasModule(scheduleFilePath(names, config))}";
3726
+ import { env } from "@/lib/env";
3727
+ import { server } from "${aliasModule(config.paths.server)}";
3728
+
3729
+ async function run${names.artifact.pascal}(request: Request) {
3730
+ \tconst cronSecret = env.CRON_SECRET;
3731
+
3732
+ \tif (!cronSecret) {
3733
+ \t\treturn Response.json(
3734
+ \t\t\t{
3735
+ \t\t\t\tok: false,
3736
+ \t\t\t\terror: "CRON_SECRET is not configured.",
3737
+ \t\t\t\tscheduleName: ${names.scheduleExportName}.name,
3738
+ \t\t\t},
3739
+ \t\t\t{ status: 500 },
3740
+ \t\t);
3741
+ \t}
3742
+
3743
+ \tif (
3744
+ \t\trequest.headers.get("authorization") !== \`Bearer \${cronSecret}\`
3745
+ \t) {
3746
+ \t\treturn Response.json({ error: "Unauthorized" }, { status: 401 });
3747
+ \t}
3748
+
3749
+ \tconst ctx = await server.createContextFromNext();
3750
+ \tconst runner = createInlineScheduleRunner<AppContext>({
3751
+ \t\tctx,
3752
+ \t\tonStart({ run, schedule }) {
3753
+ \t\t\tctx.ports.devtools.record({
3754
+ \t\t\t\ttype: "schedule",
3755
+ \t\t\t\twatcher: "schedules",
3756
+ \t\t\t\trequestId: ctx.requestId,
3757
+ \t\t\t\tscheduleName: schedule.name,
3758
+ \t\t\t\tstatus: "started",
3759
+ \t\t\t\tcron: schedule.cron,
3760
+ \t\t\t\ttimezone: schedule.timezone,
3761
+ \t\t\t\tdetails: {
3762
+ \t\t\t\t\tsource: run.source,
3763
+ \t\t\t\t\tscheduledAt: run.scheduledAt?.toISOString(),
3764
+ \t\t\t\t},
3765
+ \t\t\t});
3766
+ \t\t},
3767
+ \t\tonSuccess({ run, schedule }) {
3768
+ \t\t\tctx.ports.devtools.record({
3769
+ \t\t\t\ttype: "schedule",
3770
+ \t\t\t\twatcher: "schedules",
3771
+ \t\t\t\trequestId: ctx.requestId,
3772
+ \t\t\t\tscheduleName: schedule.name,
3773
+ \t\t\t\tstatus: "completed",
3774
+ \t\t\t\tcron: schedule.cron,
3775
+ \t\t\t\ttimezone: schedule.timezone,
3776
+ \t\t\t\tdetails: {
3777
+ \t\t\t\t\tsource: run.source,
3778
+ \t\t\t\t\tscheduledAt: run.scheduledAt?.toISOString(),
3779
+ \t\t\t\t},
3780
+ \t\t\t});
3781
+ \t\t},
3782
+ \t\tonError({ error, run, schedule }) {
3783
+ \t\t\tctx.ports.devtools.record({
3784
+ \t\t\t\ttype: "schedule",
3785
+ \t\t\t\twatcher: "schedules",
3786
+ \t\t\t\trequestId: ctx.requestId,
3787
+ \t\t\t\tscheduleName: schedule.name,
3788
+ \t\t\t\tstatus: "failed",
3789
+ \t\t\t\tcron: schedule.cron,
3790
+ \t\t\t\ttimezone: schedule.timezone,
3791
+ \t\t\t\tdetails: {
3792
+ \t\t\t\t\terror,
3793
+ \t\t\t\t\tsource: run.source,
3794
+ \t\t\t\t\tscheduledAt: run.scheduledAt?.toISOString(),
3795
+ \t\t\t\t},
3796
+ \t\t\t});
3797
+ \t\t\tctx.ports.logger.error("Schedule failed", {
3798
+ \t\t\t\terror,
3799
+ \t\t\t\tscheduleName: schedule.name,
3800
+ \t\t\t});
3801
+ \t\t},
3802
+ \t\tonHookError({ error, hook, schedule }) {
3803
+ \t\t\tctx.ports.logger.warn("Schedule lifecycle hook failed", {
3804
+ \t\t\t\terror,
3805
+ \t\t\t\thook,
3806
+ \t\t\t\tscheduleName: schedule.name,
3807
+ \t\t\t});
3808
+ \t\t},
3809
+ \t});
3810
+
3811
+ \ttry {
3812
+ \t\tawait runner.run(${names.scheduleExportName}, {
3813
+ \t\t\tsource: "cron-route",
3814
+ \t\t});
3815
+ \t} catch {
3816
+ \t\treturn Response.json({ error: "Schedule failed" }, { status: 500 });
3817
+ \t}
3818
+
3819
+ \treturn Response.json({ ok: true });
3820
+ }
3821
+
3822
+ export const GET = run${names.artifact.pascal};
3823
+ export const POST = run${names.artifact.pascal};
3824
+ `;
3825
+ }
3826
+
3827
+ function useCaseIndexExport(names: UseCaseNames): string {
3828
+ return `export {
3829
+ ${names.action.pascal}InputSchema,
3830
+ ${names.action.pascal}OutputSchema,
3831
+ ${names.exportName},
3832
+ type ${names.action.pascal}Input,
3833
+ type ${names.action.pascal}Output,
3834
+ } from "./${names.action.kebab}";
3835
+ `;
3836
+ }
3837
+
3838
+ function routeGroupFile(
3839
+ names: ResourceNames,
3840
+ config: ResolvedBeignetConfig,
3841
+ ): string {
3842
+ return `import { defineRouteGroup } from "@beignet/next";
3843
+ import type { AppContext } from "${relativeModule(path.join(config.paths.features, names.pluralKebab, "routes.ts"), config.paths.appContext)}";
3844
+ import { create${names.singularPascal}, list${names.pluralPascal} } from "${relativeModule(path.join(config.paths.features, names.pluralKebab, "routes.ts"), resourceContractFilePath(names, config))}";
3845
+ import {
3846
+ create${names.singularPascal}UseCase,
3847
+ list${names.pluralPascal}UseCase,
3848
+ } from "${relativeModule(path.join(config.paths.features, names.pluralKebab, "routes.ts"), resourceUseCaseIndexPath(names, config))}";
3849
+
3850
+ export const ${names.singularCamel}Routes = defineRouteGroup<AppContext>({
3851
+ name: "${names.pluralKebab}",
3852
+ routes: [
3853
+ {
3854
+ contract: list${names.pluralPascal},
3855
+ handle: async ({ ctx, query }) => ({
3856
+ status: 200,
3857
+ body: await list${names.pluralPascal}UseCase.run({ ctx, input: query }),
3858
+ }),
3859
+ },
3860
+ {
3861
+ contract: create${names.singularPascal},
3862
+ handle: async ({ ctx, body }) => ({
3863
+ status: 201,
3864
+ body: await create${names.singularPascal}UseCase.run({ ctx, input: body }),
3865
+ }),
3866
+ },
3867
+ ],
3868
+ });
3869
+ `;
3870
+ }
3871
+
3872
+ function testFile(names: ResourceNames, config: ResolvedBeignetConfig): string {
3873
+ const repositoryPath = path.join(
3874
+ path.dirname(config.paths.infrastructurePorts),
3875
+ names.pluralKebab,
3876
+ `in-memory-${names.singularKebab}-repository.ts`,
3877
+ );
3878
+
3879
+ return `import { describe, expect, it } from "bun:test";
3880
+ import { createUseCaseTester } from "@beignet/core/application";
3881
+ import { createInMemoryDevtools } from "@beignet/devtools";
3882
+ import { createAnonymousActor, createMemoryStorage, createNoopUnitOfWork } from "@beignet/core/ports";
3883
+ import type { AppContext } from "${aliasModule(config.paths.appContext)}";
3884
+ import { appPorts } from "${aliasModule(config.paths.infrastructurePorts)}";
3885
+ import { createInMemory${names.singularPascal}Repository } from "${aliasModule(repositoryPath)}";
3886
+ import {
3887
+ create${names.singularPascal}UseCase,
3888
+ list${names.pluralPascal}UseCase,
3889
+ } from "${aliasModule(resourceUseCaseIndexPath(names, config))}";
3890
+
3891
+ describe("${names.pluralCamel} resource", () => {
3892
+ it("creates and lists ${names.pluralCamel}", async () => {
3893
+ const ${names.pluralCamel} = createInMemory${names.singularPascal}Repository();
3894
+ const testPorts = {
3895
+ ...appPorts,
3896
+ ${names.pluralCamel},
3897
+ uow: createNoopUnitOfWork(() => ({
3898
+ ...(appPorts as unknown as AppContext["ports"]),
3899
+ ${names.pluralCamel},
3900
+ })) as AppContext["ports"]["uow"],
3901
+ devtools: createInMemoryDevtools(),
3902
+ storage: createMemoryStorage(),
3903
+ } as AppContext["ports"];
3904
+ const actor = createAnonymousActor();
3905
+ const tester = createUseCaseTester<AppContext>(() => ({
3906
+ requestId: "test-request",
3907
+ actor,
3908
+ auth: null,
3909
+ gate: testPorts.gate.bind({ actor, auth: null }),
3910
+ ports: testPorts,
3911
+ }));
3912
+
3913
+ const ctx = await tester.ctx();
3914
+ const created = await tester.run(
3915
+ create${names.singularPascal}UseCase,
3916
+ { name: "First ${names.singularPascal}" },
3917
+ { ctx },
3918
+ );
3919
+ const result = await tester.run(
3920
+ list${names.pluralPascal}UseCase,
3921
+ { limit: 20, offset: 0 },
3922
+ { ctx },
3923
+ );
3924
+
3925
+ expect(created.name).toBe("First ${names.singularPascal}");
3926
+ expect(result.total).toBe(1);
3927
+ expect(result.${names.pluralCamel}).toEqual([created]);
3928
+ });
3929
+ });
3930
+ `;
3931
+ }