@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/index.ts ADDED
@@ -0,0 +1,992 @@
1
+ #!/usr/bin/env node
2
+ import { realpathSync } from "node:fs";
3
+ import { fileURLToPath } from "node:url";
4
+ import {
5
+ buildApplication,
6
+ buildCommand,
7
+ buildRouteMap,
8
+ type CommandContext,
9
+ type FlagParametersForType,
10
+ run,
11
+ type StricliDynamicCommandContext,
12
+ type StricliProcess,
13
+ text_en,
14
+ } from "@stricli/core";
15
+ import type { CreateOptions } from "./create.js";
16
+ import { createProject } from "./create.js";
17
+ import {
18
+ applyDoctorFixes,
19
+ formatDoctor,
20
+ formatRoutes,
21
+ inspectApp,
22
+ } from "./inspect.js";
23
+ import { formatLint, lintApp } from "./lint.js";
24
+ import {
25
+ makeAdapter,
26
+ makeContract,
27
+ makeEvent,
28
+ makeJob,
29
+ makeListener,
30
+ makePolicy,
31
+ makePort,
32
+ makeResource,
33
+ makeSchedule,
34
+ makeTest,
35
+ makeUseCase,
36
+ } from "./make.js";
37
+ import {
38
+ type FeatureName,
39
+ featureChoices,
40
+ type IntegrationName,
41
+ integrationChoices,
42
+ type PackageManager,
43
+ type PresetName,
44
+ presetChoices,
45
+ type TemplateName,
46
+ } from "./templates.js";
47
+
48
+ export {
49
+ applyDoctorFixes,
50
+ type CreateOptions,
51
+ createProject,
52
+ formatDoctor,
53
+ formatLint,
54
+ formatRoutes,
55
+ type IntegrationName,
56
+ inspectApp,
57
+ lintApp,
58
+ makeAdapter,
59
+ makeContract,
60
+ makeEvent,
61
+ makeJob,
62
+ makeListener,
63
+ makePolicy,
64
+ makePort,
65
+ makeResource,
66
+ makeSchedule,
67
+ makeTest,
68
+ makeUseCase,
69
+ };
70
+
71
+ type CliContext = CommandContext & {
72
+ readonly process: StricliProcess;
73
+ };
74
+
75
+ type CreateFlags = {
76
+ template: TemplateName;
77
+ preset: PresetName;
78
+ packageManager?: PackageManager;
79
+ feature?: readonly FeatureName[];
80
+ features?: readonly FeatureName[];
81
+ integration?: readonly IntegrationName[];
82
+ integrations?: readonly IntegrationName[];
83
+ force?: boolean;
84
+ };
85
+
86
+ type MakeFlags = {
87
+ force?: boolean;
88
+ dryRun?: boolean;
89
+ json?: boolean;
90
+ };
91
+
92
+ type MakeListenerFlags = MakeFlags & {
93
+ event?: string;
94
+ };
95
+
96
+ type MakeScheduleFlags = MakeFlags & {
97
+ cron?: string;
98
+ timezone?: string;
99
+ route?: boolean;
100
+ };
101
+
102
+ type JsonFlags = {
103
+ json?: boolean;
104
+ };
105
+
106
+ type DoctorFlags = {
107
+ json?: boolean;
108
+ strict?: boolean;
109
+ fix?: boolean;
110
+ };
111
+
112
+ const templateChoices = ["next"] as const satisfies readonly TemplateName[];
113
+ const packageManagerChoices = [
114
+ "bun",
115
+ "npm",
116
+ "pnpm",
117
+ "yarn",
118
+ ] as const satisfies readonly PackageManager[];
119
+
120
+ const parseString = (input: string): string => input;
121
+
122
+ const forceFlag = {
123
+ kind: "boolean",
124
+ optional: true,
125
+ withNegated: false,
126
+ brief: "Overwrite conflicting files or write into a non-empty directory.",
127
+ } as const;
128
+
129
+ const dryRunFlag = {
130
+ kind: "boolean",
131
+ optional: true,
132
+ withNegated: false,
133
+ brief: "Preview generated changes without writing files.",
134
+ } as const;
135
+
136
+ const jsonFlag = {
137
+ kind: "boolean",
138
+ optional: true,
139
+ withNegated: false,
140
+ brief: "Print machine-readable JSON.",
141
+ } as const;
142
+
143
+ const parsedStringFlag = (brief: string) =>
144
+ ({
145
+ kind: "parsed",
146
+ parse: parseString,
147
+ optional: true,
148
+ brief,
149
+ }) as const;
150
+
151
+ const makeFlagParameters = {
152
+ force: forceFlag,
153
+ dryRun: dryRunFlag,
154
+ json: jsonFlag,
155
+ } satisfies FlagParametersForType<MakeFlags, CliContext>;
156
+
157
+ const jsonFlagParameters = {
158
+ json: jsonFlag,
159
+ } satisfies FlagParametersForType<JsonFlags, CliContext>;
160
+
161
+ const namePositional = {
162
+ kind: "tuple",
163
+ parameters: [
164
+ {
165
+ parse: parseString,
166
+ placeholder: "name",
167
+ brief: "Name for the generated artifact.",
168
+ },
169
+ ],
170
+ } as const;
171
+
172
+ const createCommand = buildCommand<CreateFlags, [string], CliContext>({
173
+ docs: {
174
+ brief: "Create a new Beignet app.",
175
+ fullDescription: `Available features: ${featureChoices.join(", ")}
176
+
177
+ Available integrations: ${integrationChoices.join(", ")}`,
178
+ },
179
+ parameters: {
180
+ flags: {
181
+ template: {
182
+ kind: "enum",
183
+ values: templateChoices,
184
+ default: "next",
185
+ brief: "Template to use.",
186
+ },
187
+ preset: {
188
+ kind: "enum",
189
+ values: presetChoices,
190
+ default: "standard",
191
+ brief: "Starter preset to generate.",
192
+ },
193
+ packageManager: {
194
+ kind: "enum",
195
+ values: packageManagerChoices,
196
+ optional: true,
197
+ brief: "Package manager to use in next-step commands.",
198
+ },
199
+ feature: {
200
+ kind: "enum",
201
+ values: featureChoices,
202
+ optional: true,
203
+ variadic: true,
204
+ brief: "Add a starter feature. Repeatable.",
205
+ },
206
+ features: {
207
+ kind: "enum",
208
+ values: featureChoices,
209
+ optional: true,
210
+ variadic: ",",
211
+ brief: "Add comma-separated starter features.",
212
+ },
213
+ integration: {
214
+ kind: "enum",
215
+ values: integrationChoices,
216
+ optional: true,
217
+ variadic: true,
218
+ brief: "Add a first-party integration. Repeatable.",
219
+ },
220
+ integrations: {
221
+ kind: "enum",
222
+ values: integrationChoices,
223
+ optional: true,
224
+ variadic: ",",
225
+ brief: "Add comma-separated first-party integrations.",
226
+ },
227
+ force: forceFlag,
228
+ },
229
+ positional: {
230
+ kind: "tuple",
231
+ parameters: [
232
+ {
233
+ parse: parseString,
234
+ placeholder: "directory",
235
+ brief: "Project directory to create.",
236
+ },
237
+ ],
238
+ },
239
+ },
240
+ async func(flags, directory) {
241
+ const integrations = uniqueValues([
242
+ ...(flags.integration ?? []),
243
+ ...(flags.integrations ?? []),
244
+ ]);
245
+ const result = await createProject({
246
+ name: directory,
247
+ template: flags.template,
248
+ preset: flags.preset,
249
+ features: uniqueValues([
250
+ ...(flags.feature ?? []),
251
+ ...(flags.features ?? []),
252
+ ]),
253
+ packageManager: flags.packageManager,
254
+ integrations,
255
+ force: Boolean(flags.force),
256
+ });
257
+
258
+ writeOutput(this, nextSteps(result, { integrations }));
259
+ },
260
+ });
261
+
262
+ const routesCommand = buildCommand<JsonFlags, [], CliContext>({
263
+ docs: {
264
+ brief: "Inspect registered Beignet routes.",
265
+ },
266
+ parameters: {
267
+ flags: jsonFlagParameters,
268
+ },
269
+ async func(flags) {
270
+ const result = await inspectApp();
271
+ writeOutput(
272
+ this,
273
+ flags.json ? JSON.stringify(result, null, 2) : formatRoutes(result),
274
+ );
275
+ },
276
+ });
277
+
278
+ const doctorCommand = buildCommand<DoctorFlags, [], CliContext>({
279
+ docs: {
280
+ brief: "Inspect app wiring and framework conventions.",
281
+ },
282
+ parameters: {
283
+ flags: {
284
+ json: jsonFlag,
285
+ strict: {
286
+ kind: "boolean",
287
+ optional: true,
288
+ withNegated: false,
289
+ brief: "Include CI-oriented warnings and fail on warnings.",
290
+ },
291
+ fix: {
292
+ kind: "boolean",
293
+ optional: true,
294
+ withNegated: false,
295
+ brief: "Apply low-risk fixes before reporting.",
296
+ },
297
+ },
298
+ },
299
+ async func(flags) {
300
+ const fixes = flags.fix
301
+ ? await applyDoctorFixes({ strict: Boolean(flags.strict) })
302
+ : [];
303
+ const result = await inspectApp({ strict: Boolean(flags.strict) });
304
+ result.fixes = fixes;
305
+ writeOutput(
306
+ this,
307
+ flags.json ? JSON.stringify(result, null, 2) : formatDoctor(result),
308
+ );
309
+ if (
310
+ result.diagnostics.some(
311
+ (diagnostic) =>
312
+ diagnostic.severity === "error" ||
313
+ (flags.strict && diagnostic.severity === "warning"),
314
+ )
315
+ ) {
316
+ this.process.exitCode = 1;
317
+ }
318
+ },
319
+ });
320
+
321
+ const lintCommand = buildCommand<JsonFlags, [], CliContext>({
322
+ docs: {
323
+ brief: "Check Beignet dependency direction conventions.",
324
+ },
325
+ parameters: {
326
+ flags: jsonFlagParameters,
327
+ },
328
+ async func(flags) {
329
+ const result = await lintApp();
330
+ writeOutput(
331
+ this,
332
+ flags.json ? JSON.stringify(result, null, 2) : formatLint(result),
333
+ );
334
+ if (result.diagnostics.length > 0) {
335
+ this.process.exitCode = 1;
336
+ }
337
+ },
338
+ });
339
+
340
+ const makeResourceCommand = buildCommand<MakeFlags, [string], CliContext>({
341
+ docs: {
342
+ brief: "Generate a feature resource.",
343
+ },
344
+ parameters: {
345
+ flags: makeFlagParameters,
346
+ positional: namePositional,
347
+ },
348
+ async func(flags, name) {
349
+ const result = await makeResource({
350
+ name,
351
+ force: Boolean(flags.force),
352
+ dryRun: Boolean(flags.dryRun),
353
+ });
354
+ writeOutput(
355
+ this,
356
+ flags.json
357
+ ? JSON.stringify(result, null, 2)
358
+ : makeResourceNextSteps(result),
359
+ );
360
+ },
361
+ });
362
+
363
+ const makeContractCommand = buildCommand<MakeFlags, [string], CliContext>({
364
+ docs: {
365
+ brief: "Generate a contract group.",
366
+ },
367
+ parameters: {
368
+ flags: makeFlagParameters,
369
+ positional: namePositional,
370
+ },
371
+ async func(flags, name) {
372
+ const result = await makeContract({
373
+ name,
374
+ force: Boolean(flags.force),
375
+ dryRun: Boolean(flags.dryRun),
376
+ });
377
+ writeOutput(
378
+ this,
379
+ flags.json
380
+ ? JSON.stringify(result, null, 2)
381
+ : makeContractNextSteps(result),
382
+ );
383
+ },
384
+ });
385
+
386
+ const makeUseCaseCommand = buildCommand<MakeFlags, [string], CliContext>({
387
+ docs: {
388
+ brief: "Generate a use case.",
389
+ },
390
+ parameters: {
391
+ flags: makeFlagParameters,
392
+ positional: namePositional,
393
+ },
394
+ async func(flags, name) {
395
+ const result = await makeUseCase({
396
+ name,
397
+ force: Boolean(flags.force),
398
+ dryRun: Boolean(flags.dryRun),
399
+ });
400
+ writeOutput(
401
+ this,
402
+ flags.json
403
+ ? JSON.stringify(result, null, 2)
404
+ : makeUseCaseNextSteps(result),
405
+ );
406
+ },
407
+ });
408
+
409
+ const makeTestCommand = buildCommand<MakeFlags, [string], CliContext>({
410
+ docs: {
411
+ brief: "Generate a use case test.",
412
+ },
413
+ parameters: {
414
+ flags: makeFlagParameters,
415
+ positional: namePositional,
416
+ },
417
+ async func(flags, name) {
418
+ const result = await makeTest({
419
+ name,
420
+ force: Boolean(flags.force),
421
+ dryRun: Boolean(flags.dryRun),
422
+ });
423
+ writeOutput(
424
+ this,
425
+ flags.json ? JSON.stringify(result, null, 2) : makeTestNextSteps(result),
426
+ );
427
+ },
428
+ });
429
+
430
+ const makePortCommand = buildCommand<MakeFlags, [string], CliContext>({
431
+ docs: {
432
+ brief: "Generate an application port.",
433
+ },
434
+ parameters: {
435
+ flags: makeFlagParameters,
436
+ positional: namePositional,
437
+ },
438
+ async func(flags, name) {
439
+ const result = await makePort({
440
+ name,
441
+ force: Boolean(flags.force),
442
+ dryRun: Boolean(flags.dryRun),
443
+ });
444
+ writeOutput(
445
+ this,
446
+ flags.json ? JSON.stringify(result, null, 2) : makePortNextSteps(result),
447
+ );
448
+ },
449
+ });
450
+
451
+ const makeAdapterCommand = buildCommand<MakeFlags, [string], CliContext>({
452
+ docs: {
453
+ brief: "Generate a port adapter.",
454
+ },
455
+ parameters: {
456
+ flags: makeFlagParameters,
457
+ positional: namePositional,
458
+ },
459
+ async func(flags, name) {
460
+ const result = await makeAdapter({
461
+ name,
462
+ force: Boolean(flags.force),
463
+ dryRun: Boolean(flags.dryRun),
464
+ });
465
+ writeOutput(
466
+ this,
467
+ flags.json
468
+ ? JSON.stringify(result, null, 2)
469
+ : makeAdapterNextSteps(result),
470
+ );
471
+ },
472
+ });
473
+
474
+ const makePolicyCommand = buildCommand<MakeFlags, [string], CliContext>({
475
+ docs: {
476
+ brief: "Generate an authorization policy.",
477
+ },
478
+ parameters: {
479
+ flags: makeFlagParameters,
480
+ positional: namePositional,
481
+ },
482
+ async func(flags, name) {
483
+ const result = await makePolicy({
484
+ name,
485
+ force: Boolean(flags.force),
486
+ dryRun: Boolean(flags.dryRun),
487
+ });
488
+ writeOutput(
489
+ this,
490
+ flags.json
491
+ ? JSON.stringify(result, null, 2)
492
+ : makePolicyNextSteps(result),
493
+ );
494
+ },
495
+ });
496
+
497
+ const makeEventCommand = buildCommand<MakeFlags, [string], CliContext>({
498
+ docs: {
499
+ brief: "Generate a feature event.",
500
+ },
501
+ parameters: {
502
+ flags: makeFlagParameters,
503
+ positional: namePositional,
504
+ },
505
+ async func(flags, name) {
506
+ const result = await makeEvent({
507
+ name,
508
+ force: Boolean(flags.force),
509
+ dryRun: Boolean(flags.dryRun),
510
+ });
511
+ writeOutput(
512
+ this,
513
+ flags.json ? JSON.stringify(result, null, 2) : makeEventNextSteps(result),
514
+ );
515
+ },
516
+ });
517
+
518
+ const makeJobCommand = buildCommand<MakeFlags, [string], CliContext>({
519
+ docs: {
520
+ brief: "Generate a feature job.",
521
+ },
522
+ parameters: {
523
+ flags: makeFlagParameters,
524
+ positional: namePositional,
525
+ },
526
+ async func(flags, name) {
527
+ const result = await makeJob({
528
+ name,
529
+ force: Boolean(flags.force),
530
+ dryRun: Boolean(flags.dryRun),
531
+ });
532
+ writeOutput(
533
+ this,
534
+ flags.json ? JSON.stringify(result, null, 2) : makeJobNextSteps(result),
535
+ );
536
+ },
537
+ });
538
+
539
+ const makeListenerFlagParameters = {
540
+ ...makeFlagParameters,
541
+ event: parsedStringFlag("Event to listen to, for example posts/published."),
542
+ } satisfies FlagParametersForType<MakeListenerFlags, CliContext>;
543
+
544
+ const makeListenerCommand = buildCommand<
545
+ MakeListenerFlags,
546
+ [string],
547
+ CliContext
548
+ >({
549
+ docs: {
550
+ brief: "Generate a feature event listener.",
551
+ },
552
+ parameters: {
553
+ flags: makeListenerFlagParameters,
554
+ positional: namePositional,
555
+ },
556
+ async func(flags, name) {
557
+ if (!flags.event) {
558
+ throw new Error(
559
+ "beignet make listener requires --event feature/name, for example --event posts/published.",
560
+ );
561
+ }
562
+
563
+ const result = await makeListener({
564
+ name,
565
+ event: flags.event,
566
+ force: Boolean(flags.force),
567
+ dryRun: Boolean(flags.dryRun),
568
+ });
569
+ writeOutput(
570
+ this,
571
+ flags.json
572
+ ? JSON.stringify(result, null, 2)
573
+ : makeListenerNextSteps(result),
574
+ );
575
+ },
576
+ });
577
+
578
+ const makeScheduleFlagParameters = {
579
+ ...makeFlagParameters,
580
+ cron: parsedStringFlag("Cron expression. Defaults to 0 9 * * *."),
581
+ timezone: parsedStringFlag("IANA timezone, for example America/Chicago."),
582
+ route: {
583
+ kind: "boolean",
584
+ optional: true,
585
+ withNegated: false,
586
+ brief: "Generate a Next.js cron route for this schedule.",
587
+ },
588
+ } satisfies FlagParametersForType<MakeScheduleFlags, CliContext>;
589
+
590
+ const makeScheduleCommand = buildCommand<
591
+ MakeScheduleFlags,
592
+ [string],
593
+ CliContext
594
+ >({
595
+ docs: {
596
+ brief: "Generate a feature schedule.",
597
+ },
598
+ parameters: {
599
+ flags: makeScheduleFlagParameters,
600
+ positional: namePositional,
601
+ },
602
+ async func(flags, name) {
603
+ const result = await makeSchedule({
604
+ name,
605
+ cron: flags.cron,
606
+ timezone: flags.timezone,
607
+ route: Boolean(flags.route),
608
+ force: Boolean(flags.force),
609
+ dryRun: Boolean(flags.dryRun),
610
+ });
611
+ writeOutput(
612
+ this,
613
+ flags.json
614
+ ? JSON.stringify(result, null, 2)
615
+ : makeScheduleNextSteps(result),
616
+ );
617
+ },
618
+ });
619
+
620
+ const makeRoutes = buildRouteMap({
621
+ docs: {
622
+ brief: "Generate Beignet app files.",
623
+ },
624
+ routes: {
625
+ adapter: makeAdapterCommand,
626
+ contract: makeContractCommand,
627
+ event: makeEventCommand,
628
+ job: makeJobCommand,
629
+ listener: makeListenerCommand,
630
+ policy: makePolicyCommand,
631
+ port: makePortCommand,
632
+ resource: makeResourceCommand,
633
+ schedule: makeScheduleCommand,
634
+ test: makeTestCommand,
635
+ useCase: makeUseCaseCommand,
636
+ },
637
+ });
638
+
639
+ const rootRoutes = buildRouteMap({
640
+ docs: {
641
+ brief: "Beignet CLI",
642
+ fullDescription: `Create apps, generate framework files, inspect routes, and check Beignet conventions.
643
+
644
+ create-beignet <directory> is equivalent to beignet create <directory>.`,
645
+ },
646
+ routes: {
647
+ create: createCommand,
648
+ doctor: doctorCommand,
649
+ lint: lintCommand,
650
+ make: makeRoutes,
651
+ routes: routesCommand,
652
+ },
653
+ });
654
+
655
+ const cli = buildApplication(rootRoutes, {
656
+ name: "beignet",
657
+ scanner: {
658
+ caseStyle: "allow-kebab-for-camel",
659
+ },
660
+ documentation: {
661
+ disableAnsiColor: true,
662
+ },
663
+ localization: {
664
+ text: {
665
+ ...text_en,
666
+ exceptionWhileRunningCommand: formatCliException,
667
+ commandErrorResult: (error) => error.message,
668
+ },
669
+ },
670
+ determineExitCode: () => 1,
671
+ });
672
+
673
+ function uniqueValues<T>(values: readonly T[]): T[] {
674
+ return [...new Set(values)];
675
+ }
676
+
677
+ function writeOutput(context: CliContext, output: string): void {
678
+ context.process.stdout.write(`${output}\n`);
679
+ }
680
+
681
+ function formatCliException(error: unknown): string {
682
+ return error instanceof Error ? error.message : String(error);
683
+ }
684
+
685
+ function nextSteps(
686
+ result: Awaited<ReturnType<typeof createProject>>,
687
+ options: { integrations?: readonly IntegrationName[] } = {},
688
+ ): string {
689
+ const pm = result.packageManager;
690
+ const run = pm === "npm" ? "npm run" : `${pm} run`;
691
+ const envFileStep = result.files.includes(".env.example")
692
+ ? " cp .env.example .env.local\n"
693
+ : "";
694
+ const envRequiredIntegrations =
695
+ options.integrations?.filter(isEnvRequiredProviderIntegration) ?? [];
696
+ const envStep =
697
+ envRequiredIntegrations.length > 0
698
+ ? " Fill .env.local for selected provider integrations before starting the app.\n"
699
+ : "";
700
+
701
+ return `Created ${result.name} at ${result.targetDir}
702
+
703
+ Next steps:
704
+ cd ${result.targetDir}
705
+ ${pm} install
706
+ ${envFileStep}${envStep}\
707
+ ${run} dev`;
708
+ }
709
+
710
+ function isEnvRequiredProviderIntegration(
711
+ integration: IntegrationName,
712
+ ): boolean {
713
+ return (
714
+ integration === "inngest" ||
715
+ integration === "resend" ||
716
+ integration === "upstash-rate-limit"
717
+ );
718
+ }
719
+
720
+ function makeResourceNextSteps(
721
+ result: Awaited<ReturnType<typeof makeResource>>,
722
+ ): string {
723
+ const changedFiles = [...result.createdFiles, ...result.updatedFiles]
724
+ .map((file) => ` ${file}`)
725
+ .join("\n");
726
+ const skippedFiles = result.skippedFiles
727
+ .map((file) => ` ${file}`)
728
+ .join("\n");
729
+
730
+ return `${result.dryRun ? "Would create" : "Created"} ${result.name} resource in ${result.targetDir}
731
+
732
+ Changed files:
733
+ ${changedFiles || " none"}
734
+ ${skippedFiles ? `\nSkipped identical files:\n${skippedFiles}` : ""}
735
+
736
+ Next steps:
737
+ Review the generated schemas and repository fields.
738
+ Run your app's typecheck and test commands.`;
739
+ }
740
+
741
+ function makeContractNextSteps(
742
+ result: Awaited<ReturnType<typeof makeContract>>,
743
+ ): string {
744
+ const changedFiles = [...result.createdFiles, ...result.updatedFiles]
745
+ .map((file) => ` ${file}`)
746
+ .join("\n");
747
+ const skippedFiles = result.skippedFiles
748
+ .map((file) => ` ${file}`)
749
+ .join("\n");
750
+
751
+ return `${result.dryRun ? "Would create" : "Created"} ${result.name} contract in ${result.targetDir}
752
+
753
+ Changed files:
754
+ ${changedFiles || " none"}
755
+ ${skippedFiles ? `\nSkipped identical files:\n${skippedFiles}` : ""}
756
+
757
+ Next steps:
758
+ Add route handlers for the generated contracts.
759
+ Register the exported contract list with OpenAPI if this app publishes docs.`;
760
+ }
761
+
762
+ function makeUseCaseNextSteps(
763
+ result: Awaited<ReturnType<typeof makeUseCase>>,
764
+ ): string {
765
+ const changedFiles = [...result.createdFiles, ...result.updatedFiles]
766
+ .map((file) => ` ${file}`)
767
+ .join("\n");
768
+ const skippedFiles = result.skippedFiles
769
+ .map((file) => ` ${file}`)
770
+ .join("\n");
771
+
772
+ return `${result.dryRun ? "Would create" : "Created"} ${result.name} use case in ${result.targetDir}
773
+
774
+ Changed files:
775
+ ${changedFiles || " none"}
776
+ ${skippedFiles ? `\nSkipped identical files:\n${skippedFiles}` : ""}
777
+
778
+ Next steps:
779
+ Replace the starter input and output schemas with domain-specific shapes.
780
+ Add a focused use case test before wiring it to an HTTP route.`;
781
+ }
782
+
783
+ function makeTestNextSteps(
784
+ result: Awaited<ReturnType<typeof makeTest>>,
785
+ ): string {
786
+ const changedFiles = [...result.createdFiles, ...result.updatedFiles]
787
+ .map((file) => ` ${file}`)
788
+ .join("\n");
789
+ const skippedFiles = result.skippedFiles
790
+ .map((file) => ` ${file}`)
791
+ .join("\n");
792
+
793
+ return `${result.dryRun ? "Would create" : "Created"} ${result.name} test in ${result.targetDir}
794
+
795
+ Changed files:
796
+ ${changedFiles || " none"}
797
+ ${skippedFiles ? `\nSkipped identical files:\n${skippedFiles}` : ""}
798
+
799
+ Next steps:
800
+ Replace the starter input, context, and assertion with behavior-specific coverage.
801
+ Run your app's test command.`;
802
+ }
803
+
804
+ function makePortNextSteps(
805
+ result: Awaited<ReturnType<typeof makePort>>,
806
+ ): string {
807
+ const changedFiles = [...result.createdFiles, ...result.updatedFiles]
808
+ .map((file) => ` ${file}`)
809
+ .join("\n");
810
+ const skippedFiles = result.skippedFiles
811
+ .map((file) => ` ${file}`)
812
+ .join("\n");
813
+
814
+ return `${result.dryRun ? "Would create" : "Created"} ${result.name} port in ${result.targetDir}
815
+
816
+ Changed files:
817
+ ${changedFiles || " none"}
818
+ ${skippedFiles ? `\nSkipped identical files:\n${skippedFiles}` : ""}
819
+
820
+ Next steps:
821
+ Replace the starter execute method with domain-specific operations.
822
+ Replace the generated infrastructure stub with a real port adapter.`;
823
+ }
824
+
825
+ function makePolicyNextSteps(
826
+ result: Awaited<ReturnType<typeof makePolicy>>,
827
+ ): string {
828
+ const changedFiles = [...result.createdFiles, ...result.updatedFiles]
829
+ .map((file) => ` ${file}`)
830
+ .join("\n");
831
+ const skippedFiles = result.skippedFiles
832
+ .map((file) => ` ${file}`)
833
+ .join("\n");
834
+
835
+ return `${result.dryRun ? "Would create" : "Created"} ${result.name} policy in ${result.targetDir}
836
+
837
+ Changed files:
838
+ ${changedFiles || " none"}
839
+ ${skippedFiles ? `\nSkipped identical files:\n${skippedFiles}` : ""}
840
+
841
+ Next steps:
842
+ Replace the starter abilities with domain-specific authorization rules.
843
+ Register the policy with createGate(...) and bind it in request context.`;
844
+ }
845
+
846
+ function makeAdapterNextSteps(
847
+ result: Awaited<ReturnType<typeof makeAdapter>>,
848
+ ): string {
849
+ const changedFiles = [...result.createdFiles, ...result.updatedFiles]
850
+ .map((file) => ` ${file}`)
851
+ .join("\n");
852
+ const skippedFiles = result.skippedFiles
853
+ .map((file) => ` ${file}`)
854
+ .join("\n");
855
+
856
+ return `${result.dryRun ? "Would create" : "Created"} ${result.name} port adapter in ${result.targetDir}
857
+
858
+ Changed files:
859
+ ${changedFiles || " none"}
860
+ ${skippedFiles ? `\nSkipped identical files:\n${skippedFiles}` : ""}
861
+
862
+ Next steps:
863
+ Replace the generated throwing implementation with real infrastructure code.
864
+ Keep the adapter behind the port interface so use cases stay infrastructure-agnostic.`;
865
+ }
866
+
867
+ function makeEventNextSteps(
868
+ result: Awaited<ReturnType<typeof makeEvent>>,
869
+ ): string {
870
+ const changedFiles = [...result.createdFiles, ...result.updatedFiles]
871
+ .map((file) => ` ${file}`)
872
+ .join("\n");
873
+ const skippedFiles = result.skippedFiles
874
+ .map((file) => ` ${file}`)
875
+ .join("\n");
876
+
877
+ return `${result.dryRun ? "Would create" : "Created"} ${result.name} event in ${result.targetDir}
878
+
879
+ Changed files:
880
+ ${changedFiles || " none"}
881
+ ${skippedFiles ? `\nSkipped identical files:\n${skippedFiles}` : ""}
882
+
883
+ Next steps:
884
+ Replace the starter payload schema with the domain fact shape.
885
+ Emit the event from a use case with .emits(...) and events.record(...).`;
886
+ }
887
+
888
+ function makeJobNextSteps(result: Awaited<ReturnType<typeof makeJob>>): string {
889
+ const changedFiles = [...result.createdFiles, ...result.updatedFiles]
890
+ .map((file) => ` ${file}`)
891
+ .join("\n");
892
+ const skippedFiles = result.skippedFiles
893
+ .map((file) => ` ${file}`)
894
+ .join("\n");
895
+
896
+ return `${result.dryRun ? "Would create" : "Created"} ${result.name} job in ${result.targetDir}
897
+
898
+ Changed files:
899
+ ${changedFiles || " none"}
900
+ ${skippedFiles ? `\nSkipped identical files:\n${skippedFiles}` : ""}
901
+
902
+ Next steps:
903
+ Replace the starter payload and handler with the real background work.
904
+ Dispatch the job through ctx.ports.jobs from a use case, listener, or schedule.`;
905
+ }
906
+
907
+ function makeListenerNextSteps(
908
+ result: Awaited<ReturnType<typeof makeListener>>,
909
+ ): string {
910
+ const changedFiles = [...result.createdFiles, ...result.updatedFiles]
911
+ .map((file) => ` ${file}`)
912
+ .join("\n");
913
+ const skippedFiles = result.skippedFiles
914
+ .map((file) => ` ${file}`)
915
+ .join("\n");
916
+
917
+ return `${result.dryRun ? "Would create" : "Created"} ${result.name} listener in ${result.targetDir}
918
+
919
+ Changed files:
920
+ ${changedFiles || " none"}
921
+ ${skippedFiles ? `\nSkipped identical files:\n${skippedFiles}` : ""}
922
+
923
+ Next steps:
924
+ Replace the starter handler with the event reaction.
925
+ Register the listener collection from infrastructure startup.`;
926
+ }
927
+
928
+ function makeScheduleNextSteps(
929
+ result: Awaited<ReturnType<typeof makeSchedule>>,
930
+ ): string {
931
+ const changedFiles = [...result.createdFiles, ...result.updatedFiles]
932
+ .map((file) => ` ${file}`)
933
+ .join("\n");
934
+ const skippedFiles = result.skippedFiles
935
+ .map((file) => ` ${file}`)
936
+ .join("\n");
937
+
938
+ return `${result.dryRun ? "Would create" : "Created"} ${result.name} schedule in ${result.targetDir}
939
+
940
+ Changed files:
941
+ ${changedFiles || " none"}
942
+ ${skippedFiles ? `\nSkipped identical files:\n${skippedFiles}` : ""}
943
+
944
+ Next steps:
945
+ Replace the starter payload and handler with the scheduled workflow.
946
+ Trigger it from a cron route, worker, or provider adapter.`;
947
+ }
948
+
949
+ export async function main(
950
+ inputs = process.argv.slice(2),
951
+ context: StricliDynamicCommandContext<CliContext> = { process },
952
+ ): Promise<void> {
953
+ if (inputs.length === 0) {
954
+ await run(cli, ["--help"], context);
955
+ context.process.exitCode = 1;
956
+ return;
957
+ }
958
+
959
+ await run(cli, inputs, context);
960
+ normalizeExitCode(context.process);
961
+ }
962
+
963
+ function normalizeExitCode(proc: StricliProcess): void {
964
+ if (
965
+ typeof proc.exitCode === "number" &&
966
+ (proc.exitCode < 0 || proc.exitCode > 127)
967
+ ) {
968
+ proc.exitCode = 1;
969
+ }
970
+ }
971
+
972
+ function isCliEntrypoint(): boolean {
973
+ const argvPath = process.argv[1];
974
+ if (!argvPath) {
975
+ return false;
976
+ }
977
+
978
+ try {
979
+ return (
980
+ realpathSync(fileURLToPath(import.meta.url)) === realpathSync(argvPath)
981
+ );
982
+ } catch {
983
+ return false;
984
+ }
985
+ }
986
+
987
+ if (isCliEntrypoint()) {
988
+ main().catch((error: unknown) => {
989
+ console.error(error instanceof Error ? error.message : String(error));
990
+ process.exitCode = 1;
991
+ });
992
+ }