@beignet/cli 0.0.3 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/CHANGELOG.md +205 -0
  2. package/README.md +379 -61
  3. package/dist/ansi.d.ts +10 -0
  4. package/dist/ansi.d.ts.map +1 -0
  5. package/dist/ansi.js +20 -0
  6. package/dist/ansi.js.map +1 -0
  7. package/dist/choices.d.ts +72 -0
  8. package/dist/choices.d.ts.map +1 -0
  9. package/dist/choices.js +88 -0
  10. package/dist/choices.js.map +1 -0
  11. package/dist/completion.d.ts +47 -0
  12. package/dist/completion.d.ts.map +1 -0
  13. package/dist/completion.js +123 -0
  14. package/dist/completion.js.map +1 -0
  15. package/dist/config.d.ts +8 -0
  16. package/dist/config.d.ts.map +1 -1
  17. package/dist/config.js +8 -0
  18. package/dist/config.js.map +1 -1
  19. package/dist/create-prompts.d.ts +42 -0
  20. package/dist/create-prompts.d.ts.map +1 -0
  21. package/dist/create-prompts.js +136 -0
  22. package/dist/create-prompts.js.map +1 -0
  23. package/dist/create.d.ts +4 -1
  24. package/dist/create.d.ts.map +1 -1
  25. package/dist/create.js +16 -26
  26. package/dist/create.js.map +1 -1
  27. package/dist/db.d.ts +1 -0
  28. package/dist/db.d.ts.map +1 -1
  29. package/dist/db.js +37 -2
  30. package/dist/db.js.map +1 -1
  31. package/dist/github-annotations.d.ts +18 -0
  32. package/dist/github-annotations.d.ts.map +1 -0
  33. package/dist/github-annotations.js +22 -0
  34. package/dist/github-annotations.js.map +1 -0
  35. package/dist/index.d.ts +1 -9
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +657 -588
  38. package/dist/index.js.map +1 -1
  39. package/dist/inspect.d.ts +21 -2
  40. package/dist/inspect.d.ts.map +1 -1
  41. package/dist/inspect.js +1938 -131
  42. package/dist/inspect.js.map +1 -1
  43. package/dist/lib.d.ts +20 -0
  44. package/dist/lib.d.ts.map +1 -0
  45. package/dist/lib.js +17 -0
  46. package/dist/lib.js.map +1 -0
  47. package/dist/lint.d.ts +10 -1
  48. package/dist/lint.d.ts.map +1 -1
  49. package/dist/lint.js +340 -33
  50. package/dist/lint.js.map +1 -1
  51. package/dist/make.d.ts +20 -3
  52. package/dist/make.d.ts.map +1 -1
  53. package/dist/make.js +1756 -394
  54. package/dist/make.js.map +1 -1
  55. package/dist/outbox.d.ts +24 -0
  56. package/dist/outbox.d.ts.map +1 -0
  57. package/dist/outbox.js +138 -0
  58. package/dist/outbox.js.map +1 -0
  59. package/dist/schedule.d.ts +36 -0
  60. package/dist/schedule.d.ts.map +1 -0
  61. package/dist/schedule.js +155 -0
  62. package/dist/schedule.js.map +1 -0
  63. package/dist/task.d.ts +26 -0
  64. package/dist/task.d.ts.map +1 -0
  65. package/dist/task.js +106 -0
  66. package/dist/task.js.map +1 -0
  67. package/dist/templates.d.ts +3 -32
  68. package/dist/templates.d.ts.map +1 -1
  69. package/dist/templates.js +1002 -527
  70. package/dist/templates.js.map +1 -1
  71. package/dist/version.d.ts +8 -0
  72. package/dist/version.d.ts.map +1 -0
  73. package/dist/version.js +18 -0
  74. package/dist/version.js.map +1 -0
  75. package/package.json +9 -8
  76. package/src/ansi.ts +30 -0
  77. package/src/choices.ts +137 -0
  78. package/src/completion.ts +169 -0
  79. package/src/config.ts +16 -0
  80. package/src/create-prompts.ts +182 -0
  81. package/src/create.ts +24 -31
  82. package/src/db.ts +60 -4
  83. package/src/github-annotations.ts +37 -0
  84. package/src/index.ts +1067 -803
  85. package/src/inspect.ts +2859 -115
  86. package/src/lib.ts +45 -0
  87. package/src/lint.ts +493 -39
  88. package/src/make.ts +2181 -405
  89. package/src/outbox.ts +249 -0
  90. package/src/schedule.ts +272 -0
  91. package/src/task.ts +169 -0
  92. package/src/templates.ts +1073 -567
  93. package/src/version.ts +20 -0
  94. package/dist/create-bin.d.ts +0 -3
  95. package/dist/create-bin.d.ts.map +0 -1
  96. package/dist/create-bin.js +0 -9
  97. package/dist/create-bin.js.map +0 -1
  98. package/src/create-bin.ts +0 -11
package/src/index.ts CHANGED
@@ -7,97 +7,59 @@ import {
7
7
  buildRouteMap,
8
8
  type CommandContext,
9
9
  type FlagParametersForType,
10
+ proposeCompletions,
10
11
  run,
11
12
  type StricliDynamicCommandContext,
12
13
  type StricliProcess,
13
14
  text_en,
14
15
  } from "@stricli/core";
15
- import type { CreateOptions } from "./create.js";
16
- import { createProject } from "./create.js";
17
- import { type DatabaseCommand, runDatabaseCommand } from "./db.js";
18
- import {
19
- applyDoctorFixes,
20
- formatDoctor,
21
- formatRoutes,
22
- inspectApp,
23
- } from "./inspect.js";
24
- import { formatLint, lintApp } from "./lint.js";
25
- import {
26
- makeAdapter,
27
- makeContract,
28
- makeEvent,
29
- makeFactory,
30
- makeFeature,
31
- makeFeatureAddonChoices,
32
- makeJob,
33
- makeListener,
34
- makeNotification,
35
- makePolicy,
36
- makePort,
37
- makeResource,
38
- makeSchedule,
39
- makeSeed,
40
- makeTest,
41
- makeUpload,
42
- makeUseCase,
43
- } from "./make.js";
44
-
45
- export type { MakeFeatureAddon } from "./make.js";
46
-
47
16
  import {
17
+ type CompletionShell,
18
+ completionShellChoices,
48
19
  type FeatureName,
49
20
  featureChoices,
50
21
  type IntegrationName,
51
22
  integrationChoices,
23
+ type MakeFeatureAddon,
24
+ makeFeatureAddonChoices,
52
25
  type PackageManager,
53
26
  type PresetName,
27
+ packageManagerChoices,
54
28
  presetChoices,
55
29
  type TemplateName,
56
- } from "./templates.js";
57
-
58
- export {
59
- applyDoctorFixes,
60
- type CreateOptions,
61
- createProject,
62
- formatDoctor,
63
- formatLint,
64
- formatRoutes,
65
- type IntegrationName,
66
- inspectApp,
67
- lintApp,
68
- makeAdapter,
69
- makeContract,
70
- makeEvent,
71
- makeFactory,
72
- makeFeature,
73
- makeFeatureAddonChoices,
74
- makeJob,
75
- makeListener,
76
- makeNotification,
77
- makePolicy,
78
- makePort,
79
- makeResource,
80
- makeSchedule,
81
- makeSeed,
82
- makeTest,
83
- makeUpload,
84
- makeUseCase,
85
- runDatabaseCommand,
86
- };
30
+ templateChoices,
31
+ } from "./choices.js";
32
+ // Type-only: the runtime module stays behind the create command's lazy loader.
33
+ import type { CreateSelections } from "./create-prompts.js";
34
+ import { getCliVersion } from "./version.js";
87
35
 
88
36
  type CliContext = CommandContext & {
89
37
  readonly process: StricliProcess;
90
38
  };
91
39
 
40
+ type OutputFormat = "human" | "json" | "github";
41
+
42
+ const outputFormatChoices = [
43
+ "human",
44
+ "json",
45
+ "github",
46
+ ] as const satisfies readonly OutputFormat[];
47
+
92
48
  type CreateFlags = {
93
49
  template: TemplateName;
94
- preset: PresetName;
50
+ preset?: PresetName;
95
51
  packageManager?: PackageManager;
96
- feature?: readonly FeatureName[];
97
52
  features?: readonly FeatureName[];
98
- integration?: readonly IntegrationName[];
99
53
  integrations?: readonly IntegrationName[];
54
+ yes?: boolean;
100
55
  force?: boolean;
56
+ dryRun?: boolean;
57
+ json?: boolean;
58
+ };
59
+
60
+ type CompletionFlags = {
61
+ shell?: CompletionShell;
62
+ json?: boolean;
101
63
  };
102
64
 
103
65
  type MakeFlags = {
@@ -106,8 +68,15 @@ type MakeFlags = {
106
68
  json?: boolean;
107
69
  };
108
70
 
71
+ type MakeResourceFlags = MakeFlags & {
72
+ auth?: boolean;
73
+ tenant?: boolean;
74
+ events?: boolean;
75
+ softDelete?: boolean;
76
+ };
77
+
109
78
  type MakeFeatureFlags = MakeFlags & {
110
- with?: readonly (typeof makeFeatureAddonChoices)[number][];
79
+ with?: readonly MakeFeatureAddon[];
111
80
  };
112
81
 
113
82
  type MakeListenerFlags = MakeFlags & {
@@ -120,8 +89,15 @@ type MakeScheduleFlags = MakeFlags & {
120
89
  route?: boolean;
121
90
  };
122
91
 
123
- type JsonFlags = {
92
+ type RoutesFlags = {
93
+ json?: boolean;
94
+ cwd?: string;
95
+ };
96
+
97
+ type LintFlags = {
124
98
  json?: boolean;
99
+ cwd?: string;
100
+ format?: OutputFormat;
125
101
  };
126
102
 
127
103
  type DbFlags = {
@@ -129,21 +105,45 @@ type DbFlags = {
129
105
  dryRun?: boolean;
130
106
  };
131
107
 
108
+ type TaskRunFlags = {
109
+ json?: boolean;
110
+ input?: string;
111
+ module?: string;
112
+ };
113
+
114
+ type OutboxDrainFlags = {
115
+ json?: boolean;
116
+ module?: string;
117
+ batchSize?: number;
118
+ };
119
+
120
+ type ScheduleRunFlags = {
121
+ json?: boolean;
122
+ module?: string;
123
+ payload?: string;
124
+ id?: string;
125
+ attempt?: number;
126
+ scheduledAt?: string;
127
+ triggeredAt?: string;
128
+ source?: string;
129
+ };
130
+
132
131
  type DoctorFlags = {
133
132
  json?: boolean;
134
133
  strict?: boolean;
135
134
  fix?: boolean;
135
+ cwd?: string;
136
+ format?: OutputFormat;
136
137
  };
137
138
 
138
- const templateChoices = ["next"] as const satisfies readonly TemplateName[];
139
- const packageManagerChoices = [
140
- "bun",
141
- "npm",
142
- "pnpm",
143
- "yarn",
144
- ] as const satisfies readonly PackageManager[];
145
-
146
139
  const parseString = (input: string): string => input;
140
+ const parsePositiveInteger = (input: string): number => {
141
+ const value = Number(input);
142
+ if (!Number.isInteger(value) || value <= 0) {
143
+ throw new Error("Expected a positive integer.");
144
+ }
145
+ return value;
146
+ };
147
147
 
148
148
  const forceFlag = {
149
149
  kind: "boolean",
@@ -174,21 +174,79 @@ const parsedStringFlag = (brief: string) =>
174
174
  brief,
175
175
  }) as const;
176
176
 
177
+ const cwdFlag = parsedStringFlag(
178
+ "App directory to inspect. Defaults to the current working directory.",
179
+ );
180
+
181
+ const formatFlag = {
182
+ kind: "enum",
183
+ values: outputFormatChoices,
184
+ optional: true,
185
+ brief:
186
+ "Output format. Defaults to human, or github when GITHUB_ACTIONS=true.",
187
+ } as const;
188
+
177
189
  const makeFlagParameters = {
178
190
  force: forceFlag,
179
191
  dryRun: dryRunFlag,
180
192
  json: jsonFlag,
181
193
  } satisfies FlagParametersForType<MakeFlags, CliContext>;
182
194
 
183
- const jsonFlagParameters = {
195
+ const routesFlagParameters = {
196
+ json: jsonFlag,
197
+ cwd: cwdFlag,
198
+ } satisfies FlagParametersForType<RoutesFlags, CliContext>;
199
+
200
+ const lintFlagParameters = {
184
201
  json: jsonFlag,
185
- } satisfies FlagParametersForType<JsonFlags, CliContext>;
202
+ cwd: cwdFlag,
203
+ format: formatFlag,
204
+ } satisfies FlagParametersForType<LintFlags, CliContext>;
186
205
 
187
206
  const dbFlagParameters = {
188
207
  json: jsonFlag,
189
208
  dryRun: dryRunFlag,
190
209
  } satisfies FlagParametersForType<DbFlags, CliContext>;
191
210
 
211
+ const taskRunFlagParameters = {
212
+ json: jsonFlag,
213
+ input: parsedStringFlag("JSON input passed to the task schema."),
214
+ module: parsedStringFlag(
215
+ "Task registry module. Defaults to server/tasks.ts.",
216
+ ),
217
+ } satisfies FlagParametersForType<TaskRunFlags, CliContext>;
218
+
219
+ const outboxDrainFlagParameters = {
220
+ json: jsonFlag,
221
+ module: parsedStringFlag(
222
+ "Outbox registry module. Defaults to server/outbox.ts.",
223
+ ),
224
+ batchSize: {
225
+ kind: "parsed",
226
+ parse: parsePositiveInteger,
227
+ optional: true,
228
+ brief: "Maximum messages to claim in one drain pass.",
229
+ },
230
+ } satisfies FlagParametersForType<OutboxDrainFlags, CliContext>;
231
+
232
+ const scheduleRunFlagParameters = {
233
+ json: jsonFlag,
234
+ module: parsedStringFlag(
235
+ "Schedule registry module. Defaults to server/schedules.ts.",
236
+ ),
237
+ payload: parsedStringFlag("JSON payload passed to the schedule schema."),
238
+ id: parsedStringFlag("Provider or app schedule run ID."),
239
+ attempt: {
240
+ kind: "parsed",
241
+ parse: parsePositiveInteger,
242
+ optional: true,
243
+ brief: "One-based provider attempt number.",
244
+ },
245
+ scheduledAt: parsedStringFlag("Provider scheduled timestamp."),
246
+ triggeredAt: parsedStringFlag("Schedule trigger timestamp."),
247
+ source: parsedStringFlag("Provider or app source label."),
248
+ } satisfies FlagParametersForType<ScheduleRunFlags, CliContext>;
249
+
192
250
  const namePositional = {
193
251
  kind: "tuple",
194
252
  parameters: [
@@ -200,12 +258,40 @@ const namePositional = {
200
258
  ],
201
259
  } as const;
202
260
 
203
- const createCommand = buildCommand<CreateFlags, [string], CliContext>({
261
+ function resolveOutputFormat(flags: {
262
+ json?: boolean;
263
+ format?: OutputFormat;
264
+ }): OutputFormat {
265
+ if (flags.json) {
266
+ if (flags.format && flags.format !== "json") {
267
+ throw new Error(
268
+ `--json conflicts with --format ${flags.format}. Pass one of them.`,
269
+ );
270
+ }
271
+ return "json";
272
+ }
273
+
274
+ if (flags.format) return flags.format;
275
+ return process.env.GITHUB_ACTIONS === "true" ? "github" : "human";
276
+ }
277
+
278
+ function useColor(): boolean {
279
+ return process.stdout.isTTY === true && !process.env.NO_COLOR;
280
+ }
281
+
282
+ const createCommand = buildCommand<
283
+ CreateFlags,
284
+ [string | undefined],
285
+ CliContext
286
+ >({
204
287
  docs: {
205
288
  brief: "Create a new Beignet app.",
206
289
  fullDescription: `Available features: ${featureChoices.join(", ")}
207
290
 
208
- Available integrations: ${integrationChoices.join(", ")}`,
291
+ Available integrations: ${integrationChoices.join(", ")}
292
+
293
+ Running create on an interactive terminal without selection flags opens a
294
+ prompt-based setup. Pass --yes or any selection flag to skip the prompts.`,
209
295
  },
210
296
  parameters: {
211
297
  flags: {
@@ -218,8 +304,8 @@ Available integrations: ${integrationChoices.join(", ")}`,
218
304
  preset: {
219
305
  kind: "enum",
220
306
  values: presetChoices,
221
- default: "standard",
222
- brief: "Starter preset to generate.",
307
+ optional: true,
308
+ brief: "Starter preset to generate. Defaults to standard.",
223
309
  },
224
310
  packageManager: {
225
311
  kind: "enum",
@@ -227,35 +313,29 @@ Available integrations: ${integrationChoices.join(", ")}`,
227
313
  optional: true,
228
314
  brief: "Package manager to use in next-step commands.",
229
315
  },
230
- feature: {
231
- kind: "enum",
232
- values: featureChoices,
233
- optional: true,
234
- variadic: true,
235
- brief: "Add a starter feature. Repeatable.",
236
- },
237
316
  features: {
238
317
  kind: "enum",
239
318
  values: featureChoices,
240
319
  optional: true,
241
320
  variadic: ",",
242
- brief: "Add comma-separated starter features.",
243
- },
244
- integration: {
245
- kind: "enum",
246
- values: integrationChoices,
247
- optional: true,
248
- variadic: true,
249
- brief: "Add a first-party integration. Repeatable.",
321
+ brief: "Add starter features as a comma-separated list.",
250
322
  },
251
323
  integrations: {
252
324
  kind: "enum",
253
325
  values: integrationChoices,
254
326
  optional: true,
255
327
  variadic: ",",
256
- brief: "Add comma-separated first-party integrations.",
328
+ brief: "Add first-party integrations as a comma-separated list.",
329
+ },
330
+ yes: {
331
+ kind: "boolean",
332
+ optional: true,
333
+ withNegated: false,
334
+ brief: "Skip interactive prompts and use the defaults.",
257
335
  },
258
336
  force: forceFlag,
337
+ dryRun: dryRunFlag,
338
+ json: jsonFlag,
259
339
  },
260
340
  positional: {
261
341
  kind: "tuple",
@@ -264,45 +344,92 @@ Available integrations: ${integrationChoices.join(", ")}`,
264
344
  parse: parseString,
265
345
  placeholder: "directory",
266
346
  brief: "Project directory to create.",
347
+ optional: true,
267
348
  },
268
349
  ],
269
350
  },
270
351
  },
271
- async func(flags, directory) {
272
- const integrations = uniqueValues([
273
- ...(flags.integration ?? []),
274
- ...(flags.integrations ?? []),
275
- ]);
276
- const result = await createProject({
277
- name: directory,
278
- template: flags.template,
279
- preset: flags.preset,
280
- features: uniqueValues([
281
- ...(flags.feature ?? []),
282
- ...(flags.features ?? []),
283
- ]),
284
- packageManager: flags.packageManager,
285
- integrations,
286
- force: Boolean(flags.force),
287
- });
288
-
289
- writeOutput(this, nextSteps(result, { integrations }));
352
+ loader: async () => {
353
+ const { createProject } = await import("./create.js");
354
+ const { promptCreateSelections, shouldPromptInteractively } = await import(
355
+ "./create-prompts.js"
356
+ );
357
+
358
+ return async function runCreate(
359
+ this: CliContext,
360
+ flags: CreateFlags,
361
+ directory: string | undefined,
362
+ ) {
363
+ let selections: CreateSelections = {
364
+ directory,
365
+ preset: flags.preset,
366
+ features: flags.features,
367
+ integrations: flags.integrations,
368
+ packageManager: flags.packageManager,
369
+ };
370
+
371
+ const interactive = shouldPromptInteractively(
372
+ { ...selections, yes: flags.yes },
373
+ {
374
+ stdin: process.stdin.isTTY === true,
375
+ stdout: process.stdout.isTTY === true,
376
+ },
377
+ );
378
+ if (interactive) {
379
+ const answers = await promptCreateSelections(selections);
380
+ if (answers === undefined) {
381
+ this.process.stderr.write("Cancelled.\n");
382
+ this.process.exitCode = 2;
383
+ return;
384
+ }
385
+ selections = answers;
386
+ }
387
+
388
+ if (selections.directory === undefined) {
389
+ throw new Error(
390
+ "Project directory is required. Pass beignet create <directory>, or run beignet create in an interactive terminal.",
391
+ );
392
+ }
393
+
394
+ const integrations = uniqueValues([...(selections.integrations ?? [])]);
395
+ const result = await createProject({
396
+ name: selections.directory,
397
+ template: flags.template,
398
+ preset: selections.preset ?? "standard",
399
+ features: uniqueValues([...(selections.features ?? [])]),
400
+ packageManager: selections.packageManager,
401
+ integrations,
402
+ force: Boolean(flags.force),
403
+ dryRun: Boolean(flags.dryRun),
404
+ });
405
+
406
+ writeOutput(
407
+ this,
408
+ flags.json
409
+ ? JSON.stringify(result, null, 2)
410
+ : nextSteps(result, { integrations }),
411
+ );
412
+ };
290
413
  },
291
414
  });
292
415
 
293
- const routesCommand = buildCommand<JsonFlags, [], CliContext>({
416
+ const routesCommand = buildCommand<RoutesFlags, [], CliContext>({
294
417
  docs: {
295
418
  brief: "Inspect registered Beignet routes.",
296
419
  },
297
420
  parameters: {
298
- flags: jsonFlagParameters,
421
+ flags: routesFlagParameters,
299
422
  },
300
- async func(flags) {
301
- const result = await inspectApp();
302
- writeOutput(
303
- this,
304
- flags.json ? JSON.stringify(result, null, 2) : formatRoutes(result),
305
- );
423
+ loader: async () => {
424
+ const { formatRoutes, inspectApp } = await import("./inspect.js");
425
+
426
+ return async function runRoutes(this: CliContext, flags: RoutesFlags) {
427
+ const result = await inspectApp({ cwd: flags.cwd });
428
+ writeOutput(
429
+ this,
430
+ flags.json ? JSON.stringify(result, null, 2) : formatRoutes(result),
431
+ );
432
+ };
306
433
  },
307
434
  });
308
435
 
@@ -325,50 +452,79 @@ const doctorCommand = buildCommand<DoctorFlags, [], CliContext>({
325
452
  withNegated: false,
326
453
  brief: "Apply low-risk fixes before reporting.",
327
454
  },
455
+ cwd: cwdFlag,
456
+ format: formatFlag,
328
457
  },
329
458
  },
330
- async func(flags) {
331
- const fixes = flags.fix
332
- ? await applyDoctorFixes({ strict: Boolean(flags.strict) })
333
- : [];
334
- const result = await inspectApp({ strict: Boolean(flags.strict) });
335
- result.fixes = fixes;
336
- writeOutput(
337
- this,
338
- flags.json ? JSON.stringify(result, null, 2) : formatDoctor(result),
339
- );
340
- if (
341
- result.diagnostics.some(
342
- (diagnostic) =>
343
- diagnostic.severity === "error" ||
344
- (flags.strict && diagnostic.severity === "warning"),
345
- )
346
- ) {
347
- this.process.exitCode = 1;
348
- }
459
+ loader: async () => {
460
+ const { applyDoctorFixes, formatDoctor, formatDoctorGithub, inspectApp } =
461
+ await import("./inspect.js");
462
+
463
+ return async function runDoctor(this: CliContext, flags: DoctorFlags) {
464
+ const format = resolveOutputFormat(flags);
465
+ const fixes = flags.fix
466
+ ? await applyDoctorFixes({
467
+ cwd: flags.cwd,
468
+ strict: Boolean(flags.strict),
469
+ })
470
+ : [];
471
+ const result = await inspectApp({
472
+ cwd: flags.cwd,
473
+ strict: Boolean(flags.strict),
474
+ });
475
+ result.fixes = fixes;
476
+ writeOutput(
477
+ this,
478
+ format === "json"
479
+ ? JSON.stringify(result, null, 2)
480
+ : format === "github"
481
+ ? formatDoctorGithub(result)
482
+ : formatDoctor(result, { color: useColor() }),
483
+ );
484
+ if (
485
+ result.diagnostics.some(
486
+ (diagnostic) =>
487
+ diagnostic.severity === "error" ||
488
+ (flags.strict && diagnostic.severity === "warning"),
489
+ )
490
+ ) {
491
+ this.process.exitCode = 1;
492
+ }
493
+ };
349
494
  },
350
495
  });
351
496
 
352
- const lintCommand = buildCommand<JsonFlags, [], CliContext>({
497
+ const lintCommand = buildCommand<LintFlags, [], CliContext>({
353
498
  docs: {
354
499
  brief: "Check Beignet dependency direction conventions.",
355
500
  },
356
501
  parameters: {
357
- flags: jsonFlagParameters,
502
+ flags: lintFlagParameters,
358
503
  },
359
- async func(flags) {
360
- const result = await lintApp();
361
- writeOutput(
362
- this,
363
- flags.json ? JSON.stringify(result, null, 2) : formatLint(result),
364
- );
365
- if (result.diagnostics.length > 0) {
366
- this.process.exitCode = 1;
367
- }
504
+ loader: async () => {
505
+ const { formatLint, formatLintGithub, lintApp } = await import("./lint.js");
506
+
507
+ return async function runLint(this: CliContext, flags: LintFlags) {
508
+ const format = resolveOutputFormat(flags);
509
+ const result = await lintApp({ cwd: flags.cwd });
510
+ writeOutput(
511
+ this,
512
+ format === "json"
513
+ ? JSON.stringify(result, null, 2)
514
+ : format === "github"
515
+ ? formatLintGithub(result)
516
+ : formatLint(result, { color: useColor() }),
517
+ );
518
+ if (result.diagnostics.length > 0) {
519
+ this.process.exitCode = 1;
520
+ }
521
+ };
368
522
  },
369
523
  });
370
524
 
371
- function databaseCommand(command: DatabaseCommand) {
525
+ type DatabaseCommandName = "generate" | "migrate" | "reset" | "seed";
526
+
527
+ function databaseCommand(command: DatabaseCommandName) {
372
528
  return buildCommand<DbFlags, [], CliContext>({
373
529
  docs: {
374
530
  brief: `Run the app's db:${command} script.`,
@@ -376,22 +532,26 @@ function databaseCommand(command: DatabaseCommand) {
376
532
  parameters: {
377
533
  flags: dbFlagParameters,
378
534
  },
379
- async func(flags) {
380
- const result = await runDatabaseCommand({
381
- command,
382
- captureOutput: Boolean(flags.json),
383
- dryRun: Boolean(flags.dryRun),
384
- });
385
-
386
- writeOutput(
387
- this,
388
- flags.json
389
- ? JSON.stringify(result, null, 2)
390
- : databaseCommandNextSteps(result),
391
- );
392
- if (result.exitCode !== 0) {
393
- this.process.exitCode = result.exitCode;
394
- }
535
+ loader: async () => {
536
+ const { runDatabaseCommand } = await import("./db.js");
537
+
538
+ return async function runDb(this: CliContext, flags: DbFlags) {
539
+ const result = await runDatabaseCommand({
540
+ command,
541
+ captureOutput: Boolean(flags.json),
542
+ dryRun: Boolean(flags.dryRun),
543
+ });
544
+
545
+ writeOutput(
546
+ this,
547
+ flags.json
548
+ ? JSON.stringify(result, null, 2)
549
+ : databaseCommandNextSteps(result),
550
+ );
551
+ if (result.exitCode !== 0) {
552
+ this.process.exitCode = result.exitCode;
553
+ }
554
+ };
395
555
  },
396
556
  });
397
557
  }
@@ -408,409 +568,461 @@ const dbRoutes = buildRouteMap({
408
568
  },
409
569
  });
410
570
 
411
- const makeResourceCommand = buildCommand<MakeFlags, [string], CliContext>({
571
+ const taskRunCommand = buildCommand<TaskRunFlags, [string], CliContext>({
412
572
  docs: {
413
- brief: "Generate a feature resource.",
573
+ brief: "Run an app-owned operational task.",
414
574
  },
415
575
  parameters: {
416
- flags: makeFlagParameters,
576
+ flags: taskRunFlagParameters,
417
577
  positional: namePositional,
418
578
  },
419
- async func(flags, name) {
420
- const result = await makeResource({
421
- name,
422
- force: Boolean(flags.force),
423
- dryRun: Boolean(flags.dryRun),
424
- });
425
- writeOutput(
426
- this,
427
- flags.json
428
- ? JSON.stringify(result, null, 2)
429
- : makeResourceNextSteps(result),
430
- );
431
- },
432
- });
579
+ loader: async () => {
580
+ const { runAppTask } = await import("./task.js");
433
581
 
434
- const makeFeatureCommand = buildCommand<MakeFeatureFlags, [string], CliContext>(
435
- {
436
- docs: {
437
- brief: "Generate a contract-first feature slice.",
438
- },
439
- parameters: {
440
- flags: {
441
- ...makeFlagParameters,
442
- with: {
443
- kind: "enum",
444
- values: makeFeatureAddonChoices,
445
- optional: true,
446
- variadic: ",",
447
- brief:
448
- "Add feature-owned artifacts. Supports policy, event/events, job/jobs, and upload/uploads.",
449
- },
450
- } satisfies FlagParametersForType<MakeFeatureFlags, CliContext>,
451
- positional: namePositional,
452
- },
453
- async func(flags, name) {
454
- const result = await makeFeature({
582
+ return async function runTask(
583
+ this: CliContext,
584
+ flags: TaskRunFlags,
585
+ name: string,
586
+ ) {
587
+ const result = await runAppTask({
455
588
  name,
456
- with: flags.with,
457
- force: Boolean(flags.force),
458
- dryRun: Boolean(flags.dryRun),
589
+ input: flags.input,
590
+ modulePath: flags.module,
459
591
  });
592
+
460
593
  writeOutput(
461
594
  this,
462
595
  flags.json
463
596
  ? JSON.stringify(result, null, 2)
464
- : makeFeatureNextSteps(result),
597
+ : appTaskRunNextSteps(result),
465
598
  );
466
- },
467
- },
468
- );
469
-
470
- const makeContractCommand = buildCommand<MakeFlags, [string], CliContext>({
471
- docs: {
472
- brief: "Generate a contract group.",
473
- },
474
- parameters: {
475
- flags: makeFlagParameters,
476
- positional: namePositional,
477
- },
478
- async func(flags, name) {
479
- const result = await makeContract({
480
- name,
481
- force: Boolean(flags.force),
482
- dryRun: Boolean(flags.dryRun),
483
- });
484
- writeOutput(
485
- this,
486
- flags.json
487
- ? JSON.stringify(result, null, 2)
488
- : makeContractNextSteps(result),
489
- );
599
+ };
490
600
  },
491
601
  });
492
602
 
493
- const makeUseCaseCommand = buildCommand<MakeFlags, [string], CliContext>({
603
+ const taskRoutes = buildRouteMap({
494
604
  docs: {
495
- brief: "Generate a use case.",
496
- },
497
- parameters: {
498
- flags: makeFlagParameters,
499
- positional: namePositional,
605
+ brief: "Run Beignet app operational tasks.",
500
606
  },
501
- async func(flags, name) {
502
- const result = await makeUseCase({
503
- name,
504
- force: Boolean(flags.force),
505
- dryRun: Boolean(flags.dryRun),
506
- });
507
- writeOutput(
508
- this,
509
- flags.json
510
- ? JSON.stringify(result, null, 2)
511
- : makeUseCaseNextSteps(result),
512
- );
607
+ routes: {
608
+ run: taskRunCommand,
513
609
  },
514
610
  });
515
611
 
516
- const makeTestCommand = buildCommand<MakeFlags, [string], CliContext>({
612
+ const outboxDrainCommand = buildCommand<OutboxDrainFlags, [], CliContext>({
517
613
  docs: {
518
- brief: "Generate a use case test.",
614
+ brief: "Run one app-owned outbox drain pass.",
519
615
  },
520
616
  parameters: {
521
- flags: makeFlagParameters,
522
- positional: namePositional,
523
- },
524
- async func(flags, name) {
525
- const result = await makeTest({
526
- name,
527
- force: Boolean(flags.force),
528
- dryRun: Boolean(flags.dryRun),
529
- });
530
- writeOutput(
531
- this,
532
- flags.json ? JSON.stringify(result, null, 2) : makeTestNextSteps(result),
533
- );
617
+ flags: outboxDrainFlagParameters,
534
618
  },
535
- });
619
+ loader: async () => {
620
+ const { runOutboxDrain } = await import("./outbox.js");
536
621
 
537
- const makePortCommand = buildCommand<MakeFlags, [string], CliContext>({
538
- docs: {
539
- brief: "Generate an application port.",
540
- },
541
- parameters: {
542
- flags: makeFlagParameters,
543
- positional: namePositional,
544
- },
545
- async func(flags, name) {
546
- const result = await makePort({
547
- name,
548
- force: Boolean(flags.force),
549
- dryRun: Boolean(flags.dryRun),
550
- });
551
- writeOutput(
552
- this,
553
- flags.json ? JSON.stringify(result, null, 2) : makePortNextSteps(result),
554
- );
622
+ return async function runDrain(this: CliContext, flags: OutboxDrainFlags) {
623
+ const result = await runOutboxDrain({
624
+ modulePath: flags.module,
625
+ batchSize: flags.batchSize,
626
+ });
627
+
628
+ writeOutput(
629
+ this,
630
+ flags.json
631
+ ? JSON.stringify(result, null, 2)
632
+ : outboxDrainNextSteps(result),
633
+ );
634
+ };
555
635
  },
556
636
  });
557
637
 
558
- const makeAdapterCommand = buildCommand<MakeFlags, [string], CliContext>({
638
+ const outboxRoutes = buildRouteMap({
559
639
  docs: {
560
- brief: "Generate a port adapter.",
561
- },
562
- parameters: {
563
- flags: makeFlagParameters,
564
- positional: namePositional,
640
+ brief: "Run Beignet outbox operations.",
565
641
  },
566
- async func(flags, name) {
567
- const result = await makeAdapter({
568
- name,
569
- force: Boolean(flags.force),
570
- dryRun: Boolean(flags.dryRun),
571
- });
572
- writeOutput(
573
- this,
574
- flags.json
575
- ? JSON.stringify(result, null, 2)
576
- : makeAdapterNextSteps(result),
577
- );
642
+ routes: {
643
+ drain: outboxDrainCommand,
578
644
  },
579
645
  });
580
646
 
581
- const makePolicyCommand = buildCommand<MakeFlags, [string], CliContext>({
582
- docs: {
583
- brief: "Generate an authorization policy.",
584
- },
585
- parameters: {
586
- flags: makeFlagParameters,
587
- positional: namePositional,
588
- },
589
- async func(flags, name) {
590
- const result = await makePolicy({
591
- name,
592
- force: Boolean(flags.force),
593
- dryRun: Boolean(flags.dryRun),
594
- });
595
- writeOutput(
596
- this,
597
- flags.json
598
- ? JSON.stringify(result, null, 2)
599
- : makePolicyNextSteps(result),
600
- );
647
+ const scheduleRunCommand = buildCommand<ScheduleRunFlags, [string], CliContext>(
648
+ {
649
+ docs: {
650
+ brief: "Run an app-owned schedule.",
651
+ },
652
+ parameters: {
653
+ flags: scheduleRunFlagParameters,
654
+ positional: namePositional,
655
+ },
656
+ loader: async () => {
657
+ const { runAppSchedule } = await import("./schedule.js");
658
+
659
+ return async function runSchedule(
660
+ this: CliContext,
661
+ flags: ScheduleRunFlags,
662
+ name: string,
663
+ ) {
664
+ const result = await runAppSchedule({
665
+ name,
666
+ modulePath: flags.module,
667
+ payload: flags.payload,
668
+ id: flags.id,
669
+ attempt: flags.attempt,
670
+ scheduledAt: flags.scheduledAt,
671
+ triggeredAt: flags.triggeredAt,
672
+ source: flags.source,
673
+ });
674
+
675
+ writeOutput(
676
+ this,
677
+ flags.json
678
+ ? JSON.stringify(result, null, 2)
679
+ : scheduleRunNextSteps(result),
680
+ );
681
+ };
682
+ },
601
683
  },
602
- });
684
+ );
603
685
 
604
- const makeEventCommand = buildCommand<MakeFlags, [string], CliContext>({
686
+ const scheduleRoutes = buildRouteMap({
605
687
  docs: {
606
- brief: "Generate a feature event.",
607
- },
608
- parameters: {
609
- flags: makeFlagParameters,
610
- positional: namePositional,
688
+ brief: "Run Beignet schedule operations.",
611
689
  },
612
- async func(flags, name) {
613
- const result = await makeEvent({
614
- name,
615
- force: Boolean(flags.force),
616
- dryRun: Boolean(flags.dryRun),
617
- });
618
- writeOutput(
619
- this,
620
- flags.json ? JSON.stringify(result, null, 2) : makeEventNextSteps(result),
621
- );
690
+ routes: {
691
+ run: scheduleRunCommand,
622
692
  },
623
693
  });
624
694
 
625
- const makeJobCommand = buildCommand<MakeFlags, [string], CliContext>({
626
- docs: {
627
- brief: "Generate a feature job.",
628
- },
629
- parameters: {
630
- flags: makeFlagParameters,
631
- positional: namePositional,
632
- },
633
- async func(flags, name) {
634
- const result = await makeJob({
635
- name,
636
- force: Boolean(flags.force),
637
- dryRun: Boolean(flags.dryRun),
638
- });
639
- writeOutput(
640
- this,
641
- flags.json ? JSON.stringify(result, null, 2) : makeJobNextSteps(result),
642
- );
695
+ const completionShellFlagParameters = {
696
+ shell: {
697
+ kind: "enum",
698
+ values: completionShellChoices,
699
+ optional: true,
700
+ brief: "Shell to manage completions for. Defaults to $SHELL.",
643
701
  },
644
- });
702
+ json: jsonFlag,
703
+ } satisfies FlagParametersForType<CompletionFlags, CliContext>;
645
704
 
646
- const makeFactoryCommand = buildCommand<MakeFlags, [string], CliContext>({
705
+ const completionInstallCommand = buildCommand<CompletionFlags, [], CliContext>({
647
706
  docs: {
648
- brief: "Generate a feature test data factory.",
707
+ brief: "Install beignet shell completions for bash or zsh.",
649
708
  },
650
709
  parameters: {
651
- flags: makeFlagParameters,
652
- positional: namePositional,
710
+ flags: completionShellFlagParameters,
653
711
  },
654
- async func(flags, name) {
655
- const result = await makeFactory({
656
- name,
657
- force: Boolean(flags.force),
658
- dryRun: Boolean(flags.dryRun),
659
- });
660
- writeOutput(
661
- this,
662
- flags.json
663
- ? JSON.stringify(result, null, 2)
664
- : makeFactoryNextSteps(result),
665
- );
712
+ loader: async () => {
713
+ const { installCompletions } = await import("./completion.js");
714
+
715
+ return async function runInstall(this: CliContext, flags: CompletionFlags) {
716
+ const result = await installCompletions({ shell: flags.shell });
717
+ writeOutput(
718
+ this,
719
+ flags.json
720
+ ? JSON.stringify(result, null, 2)
721
+ : `${result.status === "updated" ? "Updated" : "Installed"} ${result.shell} completions in ${result.file}
722
+
723
+ Restart your shell or source the file to activate them.`,
724
+ );
725
+ };
666
726
  },
667
727
  });
668
728
 
669
- const makeSeedCommand = buildCommand<MakeFlags, [string], CliContext>({
729
+ const completionUninstallCommand = buildCommand<
730
+ CompletionFlags,
731
+ [],
732
+ CliContext
733
+ >({
670
734
  docs: {
671
- brief: "Generate a feature seed.",
735
+ brief: "Remove beignet shell completions for bash or zsh.",
672
736
  },
673
737
  parameters: {
674
- flags: makeFlagParameters,
675
- positional: namePositional,
738
+ flags: completionShellFlagParameters,
676
739
  },
677
- async func(flags, name) {
678
- const result = await makeSeed({
679
- name,
680
- force: Boolean(flags.force),
681
- dryRun: Boolean(flags.dryRun),
682
- });
683
- writeOutput(
684
- this,
685
- flags.json ? JSON.stringify(result, null, 2) : makeSeedNextSteps(result),
686
- );
740
+ loader: async () => {
741
+ const { uninstallCompletions } = await import("./completion.js");
742
+
743
+ return async function runUninstall(
744
+ this: CliContext,
745
+ flags: CompletionFlags,
746
+ ) {
747
+ const result = await uninstallCompletions({ shell: flags.shell });
748
+ writeOutput(
749
+ this,
750
+ flags.json
751
+ ? JSON.stringify(result, null, 2)
752
+ : result.status === "removed"
753
+ ? `Removed ${result.shell} completions from ${result.file}`
754
+ : `No beignet completions found in ${result.file}`,
755
+ );
756
+ };
687
757
  },
688
758
  });
689
759
 
690
- const makeNotificationCommand = buildCommand<MakeFlags, [string], CliContext>({
760
+ const completionRoutes = buildRouteMap({
691
761
  docs: {
692
- brief: "Generate a feature notification.",
693
- },
694
- parameters: {
695
- flags: makeFlagParameters,
696
- positional: namePositional,
762
+ brief: "Manage beignet shell completions.",
697
763
  },
698
- async func(flags, name) {
699
- const result = await makeNotification({
700
- name,
701
- force: Boolean(flags.force),
702
- dryRun: Boolean(flags.dryRun),
703
- });
704
- writeOutput(
705
- this,
706
- flags.json
707
- ? JSON.stringify(result, null, 2)
708
- : makeNotificationNextSteps(result),
709
- );
764
+ routes: {
765
+ install: completionInstallCommand,
766
+ uninstall: completionUninstallCommand,
710
767
  },
711
768
  });
712
769
 
713
- const makeListenerFlagParameters = {
714
- ...makeFlagParameters,
715
- event: parsedStringFlag("Event to listen to, for example posts/published."),
716
- } satisfies FlagParametersForType<MakeListenerFlags, CliContext>;
717
-
718
- const makeListenerCommand = buildCommand<
719
- MakeListenerFlags,
770
+ const makeResourceCommand = buildCommand<
771
+ MakeResourceFlags,
720
772
  [string],
721
773
  CliContext
722
774
  >({
723
775
  docs: {
724
- brief: "Generate a feature event listener.",
776
+ brief: "Generate a CRUD-shaped resource slice.",
725
777
  },
726
778
  parameters: {
727
- flags: makeListenerFlagParameters,
728
- positional: namePositional,
729
- },
730
- async func(flags, name) {
731
- if (!flags.event) {
732
- throw new Error(
733
- "beignet make listener requires --event feature/name, for example --event posts/published.",
734
- );
735
- }
736
-
737
- const result = await makeListener({
738
- name,
739
- event: flags.event,
740
- force: Boolean(flags.force),
741
- dryRun: Boolean(flags.dryRun),
742
- });
743
- writeOutput(
744
- this,
745
- flags.json
746
- ? JSON.stringify(result, null, 2)
747
- : makeListenerNextSteps(result),
748
- );
779
+ flags: {
780
+ ...makeFlagParameters,
781
+ auth: {
782
+ kind: "boolean",
783
+ optional: true,
784
+ withNegated: false,
785
+ brief:
786
+ "Generate policy metadata and use-case authorization checks for mutating routes.",
787
+ },
788
+ tenant: {
789
+ kind: "boolean",
790
+ optional: true,
791
+ withNegated: false,
792
+ brief:
793
+ "Generate tenant-scoped schemas, repository methods, and use-case checks.",
794
+ },
795
+ events: {
796
+ kind: "boolean",
797
+ optional: true,
798
+ withNegated: false,
799
+ brief: "Generate domain event definitions and publish resource events.",
800
+ },
801
+ softDelete: {
802
+ kind: "boolean",
803
+ optional: true,
804
+ withNegated: false,
805
+ brief:
806
+ "Generate soft-delete persistence that archives rows with deletedAt.",
807
+ },
808
+ } satisfies FlagParametersForType<MakeResourceFlags, CliContext>,
809
+ positional: namePositional,
810
+ },
811
+ loader: async () => {
812
+ const { makeResource } = await import("./make.js");
813
+
814
+ return async function runMakeResource(
815
+ this: CliContext,
816
+ flags: MakeResourceFlags,
817
+ name: string,
818
+ ) {
819
+ const result = await makeResource({
820
+ name,
821
+ force: Boolean(flags.force),
822
+ dryRun: Boolean(flags.dryRun),
823
+ auth: Boolean(flags.auth),
824
+ tenant: Boolean(flags.tenant),
825
+ events: Boolean(flags.events),
826
+ softDelete: Boolean(flags.softDelete),
827
+ });
828
+ writeOutput(
829
+ this,
830
+ flags.json
831
+ ? JSON.stringify(result, null, 2)
832
+ : makeResourceNextSteps(result),
833
+ );
834
+ };
749
835
  },
750
836
  });
751
837
 
752
- const makeScheduleFlagParameters = {
753
- ...makeFlagParameters,
754
- cron: parsedStringFlag("Cron expression. Defaults to 0 9 * * *."),
755
- timezone: parsedStringFlag("IANA timezone, for example America/Chicago."),
756
- route: {
757
- kind: "boolean",
758
- optional: true,
759
- withNegated: false,
760
- brief: "Generate a Next.js cron route for this schedule.",
838
+ const makeFeatureCommand = buildCommand<MakeFeatureFlags, [string], CliContext>(
839
+ {
840
+ docs: {
841
+ brief: "Generate a contract-first feature slice.",
842
+ },
843
+ parameters: {
844
+ flags: {
845
+ ...makeFlagParameters,
846
+ with: {
847
+ kind: "enum",
848
+ values: makeFeatureAddonChoices,
849
+ optional: true,
850
+ variadic: ",",
851
+ brief:
852
+ "Add feature-owned artifacts. Supports policy, task/tasks, event/events, job/jobs, notification/notifications, ui, and upload/uploads.",
853
+ },
854
+ } satisfies FlagParametersForType<MakeFeatureFlags, CliContext>,
855
+ positional: namePositional,
856
+ },
857
+ loader: async () => {
858
+ const { makeFeature } = await import("./make.js");
859
+
860
+ return async function runMakeFeature(
861
+ this: CliContext,
862
+ flags: MakeFeatureFlags,
863
+ name: string,
864
+ ) {
865
+ const result = await makeFeature({
866
+ name,
867
+ with: flags.with,
868
+ force: Boolean(flags.force),
869
+ dryRun: Boolean(flags.dryRun),
870
+ });
871
+ writeOutput(
872
+ this,
873
+ flags.json
874
+ ? JSON.stringify(result, null, 2)
875
+ : makeFeatureNextSteps(result),
876
+ );
877
+ };
878
+ },
761
879
  },
762
- } satisfies FlagParametersForType<MakeScheduleFlags, CliContext>;
880
+ );
763
881
 
764
- const makeScheduleCommand = buildCommand<
765
- MakeScheduleFlags,
882
+ type MakeGeneratorName =
883
+ | "makeAdapter"
884
+ | "makeContract"
885
+ | "makeEvent"
886
+ | "makeFactory"
887
+ | "makeJob"
888
+ | "makeNotification"
889
+ | "makePolicy"
890
+ | "makePort"
891
+ | "makeSeed"
892
+ | "makeTask"
893
+ | "makeTest"
894
+ | "makeUpload"
895
+ | "makeUseCase";
896
+
897
+ function simpleMakeCommand(
898
+ generator: MakeGeneratorName,
899
+ brief: string,
900
+ formatNextSteps: (result: MakeNextStepsResult) => string,
901
+ ) {
902
+ return buildCommand<MakeFlags, [string], CliContext>({
903
+ docs: {
904
+ brief,
905
+ },
906
+ parameters: {
907
+ flags: makeFlagParameters,
908
+ positional: namePositional,
909
+ },
910
+ loader: async () => {
911
+ const generators = await import("./make.js");
912
+
913
+ return async function runMake(
914
+ this: CliContext,
915
+ flags: MakeFlags,
916
+ name: string,
917
+ ) {
918
+ const result = await generators[generator]({
919
+ name,
920
+ force: Boolean(flags.force),
921
+ dryRun: Boolean(flags.dryRun),
922
+ });
923
+ writeOutput(
924
+ this,
925
+ flags.json
926
+ ? JSON.stringify(result, null, 2)
927
+ : formatNextSteps(result),
928
+ );
929
+ };
930
+ },
931
+ });
932
+ }
933
+
934
+ const makeListenerCommand = buildCommand<
935
+ MakeListenerFlags,
766
936
  [string],
767
937
  CliContext
768
938
  >({
769
939
  docs: {
770
- brief: "Generate a feature schedule.",
940
+ brief: "Generate a feature event listener.",
771
941
  },
772
942
  parameters: {
773
- flags: makeScheduleFlagParameters,
943
+ flags: {
944
+ ...makeFlagParameters,
945
+ event: parsedStringFlag(
946
+ "Event to listen to, for example posts/published.",
947
+ ),
948
+ } satisfies FlagParametersForType<MakeListenerFlags, CliContext>,
774
949
  positional: namePositional,
775
950
  },
776
- async func(flags, name) {
777
- const result = await makeSchedule({
778
- name,
779
- cron: flags.cron,
780
- timezone: flags.timezone,
781
- route: Boolean(flags.route),
782
- force: Boolean(flags.force),
783
- dryRun: Boolean(flags.dryRun),
784
- });
785
- writeOutput(
786
- this,
787
- flags.json
788
- ? JSON.stringify(result, null, 2)
789
- : makeScheduleNextSteps(result),
790
- );
951
+ loader: async () => {
952
+ const { makeListener } = await import("./make.js");
953
+
954
+ return async function runMakeListener(
955
+ this: CliContext,
956
+ flags: MakeListenerFlags,
957
+ name: string,
958
+ ) {
959
+ if (!flags.event) {
960
+ throw new Error(
961
+ "beignet make listener requires --event feature/name, for example --event posts/published.",
962
+ );
963
+ }
964
+
965
+ const result = await makeListener({
966
+ name,
967
+ event: flags.event,
968
+ force: Boolean(flags.force),
969
+ dryRun: Boolean(flags.dryRun),
970
+ });
971
+ writeOutput(
972
+ this,
973
+ flags.json
974
+ ? JSON.stringify(result, null, 2)
975
+ : makeListenerNextSteps(result),
976
+ );
977
+ };
791
978
  },
792
979
  });
793
980
 
794
- const makeUploadCommand = buildCommand<MakeFlags, [string], CliContext>({
981
+ const makeScheduleCommand = buildCommand<
982
+ MakeScheduleFlags,
983
+ [string],
984
+ CliContext
985
+ >({
795
986
  docs: {
796
- brief: "Generate a feature upload.",
987
+ brief: "Generate a feature schedule.",
797
988
  },
798
989
  parameters: {
799
- flags: makeFlagParameters,
990
+ flags: {
991
+ ...makeFlagParameters,
992
+ cron: parsedStringFlag("Cron expression. Defaults to 0 9 * * *."),
993
+ timezone: parsedStringFlag("IANA timezone, for example America/Chicago."),
994
+ route: {
995
+ kind: "boolean",
996
+ optional: true,
997
+ withNegated: false,
998
+ brief: "Generate a Next.js cron route for this schedule.",
999
+ },
1000
+ } satisfies FlagParametersForType<MakeScheduleFlags, CliContext>,
800
1001
  positional: namePositional,
801
1002
  },
802
- async func(flags, name) {
803
- const result = await makeUpload({
804
- name,
805
- force: Boolean(flags.force),
806
- dryRun: Boolean(flags.dryRun),
807
- });
808
- writeOutput(
809
- this,
810
- flags.json
811
- ? JSON.stringify(result, null, 2)
812
- : makeUploadNextSteps(result),
813
- );
1003
+ loader: async () => {
1004
+ const { makeSchedule } = await import("./make.js");
1005
+
1006
+ return async function runMakeSchedule(
1007
+ this: CliContext,
1008
+ flags: MakeScheduleFlags,
1009
+ name: string,
1010
+ ) {
1011
+ const result = await makeSchedule({
1012
+ name,
1013
+ cron: flags.cron,
1014
+ timezone: flags.timezone,
1015
+ route: Boolean(flags.route),
1016
+ force: Boolean(flags.force),
1017
+ dryRun: Boolean(flags.dryRun),
1018
+ });
1019
+ writeOutput(
1020
+ this,
1021
+ flags.json
1022
+ ? JSON.stringify(result, null, 2)
1023
+ : makeScheduleNextSteps(result),
1024
+ );
1025
+ };
814
1026
  },
815
1027
  });
816
1028
 
@@ -819,22 +1031,75 @@ const makeRoutes = buildRouteMap({
819
1031
  brief: "Generate Beignet app files.",
820
1032
  },
821
1033
  routes: {
822
- adapter: makeAdapterCommand,
823
- contract: makeContractCommand,
824
- event: makeEventCommand,
825
- factory: makeFactoryCommand,
1034
+ adapter: simpleMakeCommand(
1035
+ "makeAdapter",
1036
+ "Generate a port adapter.",
1037
+ makeAdapterNextSteps,
1038
+ ),
1039
+ contract: simpleMakeCommand(
1040
+ "makeContract",
1041
+ "Generate a contract group.",
1042
+ makeContractNextSteps,
1043
+ ),
1044
+ event: simpleMakeCommand(
1045
+ "makeEvent",
1046
+ "Generate a feature event.",
1047
+ makeEventNextSteps,
1048
+ ),
1049
+ factory: simpleMakeCommand(
1050
+ "makeFactory",
1051
+ "Generate a feature test data factory.",
1052
+ makeFactoryNextSteps,
1053
+ ),
826
1054
  feature: makeFeatureCommand,
827
- job: makeJobCommand,
828
- notification: makeNotificationCommand,
1055
+ job: simpleMakeCommand(
1056
+ "makeJob",
1057
+ "Generate a feature job.",
1058
+ makeJobNextSteps,
1059
+ ),
1060
+ notification: simpleMakeCommand(
1061
+ "makeNotification",
1062
+ "Generate a feature notification.",
1063
+ makeNotificationNextSteps,
1064
+ ),
829
1065
  listener: makeListenerCommand,
830
- policy: makePolicyCommand,
831
- port: makePortCommand,
1066
+ policy: simpleMakeCommand(
1067
+ "makePolicy",
1068
+ "Generate an authorization policy.",
1069
+ makePolicyNextSteps,
1070
+ ),
1071
+ port: simpleMakeCommand(
1072
+ "makePort",
1073
+ "Generate an application port.",
1074
+ makePortNextSteps,
1075
+ ),
832
1076
  resource: makeResourceCommand,
833
1077
  schedule: makeScheduleCommand,
834
- seed: makeSeedCommand,
835
- test: makeTestCommand,
836
- upload: makeUploadCommand,
837
- useCase: makeUseCaseCommand,
1078
+ seed: simpleMakeCommand(
1079
+ "makeSeed",
1080
+ "Generate a feature seed.",
1081
+ makeSeedNextSteps,
1082
+ ),
1083
+ task: simpleMakeCommand(
1084
+ "makeTask",
1085
+ "Generate a feature operational task.",
1086
+ makeTaskNextSteps,
1087
+ ),
1088
+ test: simpleMakeCommand(
1089
+ "makeTest",
1090
+ "Generate a use case test.",
1091
+ makeTestNextSteps,
1092
+ ),
1093
+ upload: simpleMakeCommand(
1094
+ "makeUpload",
1095
+ "Generate a feature upload.",
1096
+ makeUploadNextSteps,
1097
+ ),
1098
+ useCase: simpleMakeCommand(
1099
+ "makeUseCase",
1100
+ "Generate a use case.",
1101
+ makeUseCaseNextSteps,
1102
+ ),
838
1103
  },
839
1104
  });
840
1105
 
@@ -843,20 +1108,27 @@ const rootRoutes = buildRouteMap({
843
1108
  brief: "Beignet CLI",
844
1109
  fullDescription: `Create apps, generate framework files, inspect routes, and check Beignet conventions.
845
1110
 
846
- create-beignet <directory> is equivalent to beignet create <directory>.`,
1111
+ Run npm create beignet@latest (or bun create beignet) to scaffold a new app.`,
847
1112
  },
848
1113
  routes: {
1114
+ completion: completionRoutes,
849
1115
  create: createCommand,
850
1116
  db: dbRoutes,
851
1117
  doctor: doctorCommand,
852
1118
  lint: lintCommand,
853
1119
  make: makeRoutes,
1120
+ outbox: outboxRoutes,
854
1121
  routes: routesCommand,
1122
+ schedule: scheduleRoutes,
1123
+ task: taskRoutes,
855
1124
  },
856
1125
  });
857
1126
 
858
1127
  const cli = buildApplication(rootRoutes, {
859
1128
  name: "beignet",
1129
+ versionInfo: {
1130
+ currentVersion: getCliVersion(),
1131
+ },
860
1132
  scanner: {
861
1133
  caseStyle: "allow-kebab-for-camel",
862
1134
  },
@@ -870,7 +1142,7 @@ const cli = buildApplication(rootRoutes, {
870
1142
  commandErrorResult: (error) => error.message,
871
1143
  },
872
1144
  },
873
- determineExitCode: () => 1,
1145
+ determineExitCode: () => 2,
874
1146
  });
875
1147
 
876
1148
  function uniqueValues<T>(values: readonly T[]): T[] {
@@ -885,12 +1157,108 @@ function formatCliException(error: unknown): string {
885
1157
  return error instanceof Error ? error.message : String(error);
886
1158
  }
887
1159
 
1160
+ type CreateNextStepsResult = {
1161
+ name: string;
1162
+ targetDir: string;
1163
+ dryRun: boolean;
1164
+ files: string[];
1165
+ packageManager: PackageManager;
1166
+ };
1167
+
1168
+ type MakeNextStepsResult = {
1169
+ name: string;
1170
+ targetDir: string;
1171
+ dryRun: boolean;
1172
+ createdFiles: string[];
1173
+ updatedFiles: string[];
1174
+ skippedFiles: string[];
1175
+ };
1176
+
1177
+ type DatabaseCommandNextStepsResult = {
1178
+ script: string;
1179
+ cwd: string;
1180
+ runner: string;
1181
+ args: string[];
1182
+ dryRun: boolean;
1183
+ };
1184
+
1185
+ type AppTaskRunNextStepsResult = {
1186
+ name: string;
1187
+ durationMs: number;
1188
+ output: unknown;
1189
+ };
1190
+
1191
+ type OutboxDrainNextStepsResult = {
1192
+ durationMs: number;
1193
+ result: {
1194
+ claimed: number;
1195
+ delivered: number;
1196
+ retried: number;
1197
+ deadLettered: number;
1198
+ };
1199
+ };
1200
+
1201
+ type ScheduleRunNextStepsResult = {
1202
+ name: string;
1203
+ durationMs: number;
1204
+ run: {
1205
+ source: string;
1206
+ id?: string;
1207
+ attempt?: number;
1208
+ scheduledAt?: string;
1209
+ triggeredAt?: string;
1210
+ };
1211
+ };
1212
+
1213
+ function appTaskRunNextSteps(result: AppTaskRunNextStepsResult): string {
1214
+ const output =
1215
+ result.output === undefined
1216
+ ? "undefined"
1217
+ : JSON.stringify(result.output, null, 2);
1218
+
1219
+ return `Ran task ${result.name} in ${result.durationMs}ms
1220
+
1221
+ Output:
1222
+ ${output}`;
1223
+ }
1224
+
1225
+ function outboxDrainNextSteps(result: OutboxDrainNextStepsResult): string {
1226
+ return `Drained outbox in ${result.durationMs}ms
1227
+
1228
+ Result:
1229
+ claimed: ${result.result.claimed}
1230
+ delivered: ${result.result.delivered}
1231
+ retried: ${result.result.retried}
1232
+ deadLettered: ${result.result.deadLettered}`;
1233
+ }
1234
+
1235
+ function scheduleRunNextSteps(result: ScheduleRunNextStepsResult): string {
1236
+ return `Ran schedule ${result.name} in ${result.durationMs}ms
1237
+
1238
+ Run:
1239
+ source: ${result.run.source}
1240
+ id: ${result.run.id ?? "none"}
1241
+ attempt: ${result.run.attempt ?? "none"}
1242
+ scheduledAt: ${result.run.scheduledAt ?? "none"}
1243
+ triggeredAt: ${result.run.triggeredAt ?? "none"}`;
1244
+ }
1245
+
888
1246
  function nextSteps(
889
- result: Awaited<ReturnType<typeof createProject>>,
1247
+ result: CreateNextStepsResult,
890
1248
  options: { integrations?: readonly IntegrationName[] } = {},
891
1249
  ): string {
1250
+ if (result.dryRun) {
1251
+ const plannedFiles = result.files.map((file) => ` ${file}`).join("\n");
1252
+
1253
+ return `Would create ${result.name} at ${result.targetDir}
1254
+
1255
+ Planned files:
1256
+ ${plannedFiles}`;
1257
+ }
1258
+
892
1259
  const pm = result.packageManager;
893
1260
  const run = pm === "npm" ? "npm run" : `${pm} run`;
1261
+ const cli = packageRunnerFromPackageManager(pm);
894
1262
  const envFileStep = result.files.includes(".env.example")
895
1263
  ? " cp .env.example .env.local\n"
896
1264
  : "";
@@ -900,14 +1268,53 @@ function nextSteps(
900
1268
  envRequiredIntegrations.length > 0
901
1269
  ? " Fill .env.local for selected provider integrations before starting the app.\n"
902
1270
  : "";
1271
+ const hasDatabase = result.files.some((file) => file.startsWith("infra/db/"));
1272
+ const databaseStep = hasDatabase
1273
+ ? `
1274
+ Prepare the database:
1275
+ ${cli} db generate
1276
+ ${cli} db migrate
1277
+ `
1278
+ : "";
1279
+ const featureDatabaseSteps = hasDatabase
1280
+ ? `
1281
+ ${cli} db generate
1282
+ ${cli} db migrate`
1283
+ : "";
903
1284
 
904
1285
  return `Created ${result.name} at ${result.targetDir}
905
1286
 
906
1287
  Next steps:
907
1288
  cd ${result.targetDir}
908
1289
  ${pm} install
909
- ${envFileStep}${envStep}\
910
- ${run} dev`;
1290
+ ${envFileStep}${envStep}${databaseStep}
1291
+ Start the app:
1292
+ ${run} dev
1293
+
1294
+ Inspect the app:
1295
+ ${cli} routes
1296
+ ${cli} lint
1297
+ ${cli} doctor
1298
+
1299
+ Generate a feature:
1300
+ ${cli} make feature projects${featureDatabaseSteps}
1301
+ ${run} test
1302
+ ${run} typecheck
1303
+ ${cli} lint
1304
+ ${cli} doctor`;
1305
+ }
1306
+
1307
+ function packageRunnerFromPackageManager(pm: PackageManager): string {
1308
+ switch (pm) {
1309
+ case "npm":
1310
+ return "npm run beignet --";
1311
+ case "pnpm":
1312
+ return "pnpm beignet";
1313
+ case "yarn":
1314
+ return "yarn beignet";
1315
+ default:
1316
+ return "bun beignet";
1317
+ }
911
1318
  }
912
1319
 
913
1320
  function isEnvRequiredProviderIntegration(
@@ -920,29 +1327,46 @@ function isEnvRequiredProviderIntegration(
920
1327
  );
921
1328
  }
922
1329
 
923
- function makeResourceNextSteps(
924
- result: Awaited<ReturnType<typeof makeResource>>,
1330
+ function changedFileLines(result: MakeNextStepsResult): {
1331
+ changedFiles: string;
1332
+ skippedFiles: string;
1333
+ } {
1334
+ return {
1335
+ changedFiles: [...result.createdFiles, ...result.updatedFiles]
1336
+ .map((file) => ` ${file}`)
1337
+ .join("\n"),
1338
+ skippedFiles: result.skippedFiles.map((file) => ` ${file}`).join("\n"),
1339
+ };
1340
+ }
1341
+
1342
+ function makeNextSteps(
1343
+ result: MakeNextStepsResult,
1344
+ noun: string,
1345
+ steps: readonly string[],
925
1346
  ): string {
926
- const changedFiles = [...result.createdFiles, ...result.updatedFiles]
927
- .map((file) => ` ${file}`)
928
- .join("\n");
929
- const skippedFiles = result.skippedFiles
930
- .map((file) => ` ${file}`)
931
- .join("\n");
1347
+ const { changedFiles, skippedFiles } = changedFileLines(result);
932
1348
 
933
- return `${result.dryRun ? "Would create" : "Created"} ${result.name} resource in ${result.targetDir}
1349
+ return `${result.dryRun ? "Would create" : "Created"} ${result.name} ${noun} in ${result.targetDir}
934
1350
 
935
1351
  Changed files:
936
1352
  ${changedFiles || " none"}
937
1353
  ${skippedFiles ? `\nSkipped identical files:\n${skippedFiles}` : ""}
938
1354
 
939
1355
  Next steps:
940
- Review the generated schemas and repository fields.
941
- Run your app's typecheck and test commands.`;
1356
+ ${steps.map((step) => ` ${step}`).join("\n")}`;
1357
+ }
1358
+
1359
+ function makeResourceNextSteps(result: MakeNextStepsResult): string {
1360
+ return makeNextSteps(result, "resource", [
1361
+ "Use make resource when the feature maps cleanly to a REST resource with repository-backed persistence.",
1362
+ "Review the generated schemas and repository fields.",
1363
+ "If this app uses Drizzle, run beignet db generate and beignet db migrate so the new schema is ready.",
1364
+ "Run your app's test and typecheck commands, then beignet lint and beignet doctor.",
1365
+ ]);
942
1366
  }
943
1367
 
944
1368
  function databaseCommandNextSteps(
945
- result: Awaited<ReturnType<typeof runDatabaseCommand>>,
1369
+ result: DatabaseCommandNextStepsResult,
946
1370
  ): string {
947
1371
  const command = [result.runner, ...result.args].join(" ");
948
1372
  const prefix = result.dryRun ? "Would run" : "Ran";
@@ -953,317 +1377,118 @@ Command:
953
1377
  ${command}`;
954
1378
  }
955
1379
 
956
- function makeFeatureNextSteps(
957
- result: Awaited<ReturnType<typeof makeFeature>>,
958
- ): string {
959
- const changedFiles = [...result.createdFiles, ...result.updatedFiles]
960
- .map((file) => ` ${file}`)
961
- .join("\n");
962
- const skippedFiles = result.skippedFiles
963
- .map((file) => ` ${file}`)
964
- .join("\n");
965
-
966
- return `${result.dryRun ? "Would create" : "Created"} ${result.name} feature slice in ${result.targetDir}
967
-
968
- Changed files:
969
- ${changedFiles || " none"}
970
- ${skippedFiles ? `\nSkipped identical files:\n${skippedFiles}` : ""}
971
-
972
- Next steps:
973
- Treat the generated name field as a placeholder and shape the contracts, use cases, and repository around the feature's real workflow.
974
- Run your app's typecheck, test, beignet lint, and beignet doctor commands.`;
1380
+ function makeFeatureNextSteps(result: MakeNextStepsResult): string {
1381
+ return makeNextSteps(result, "feature slice", [
1382
+ "Use make feature for product capabilities and workflows. Use make resource when the concept is mostly CRUD-shaped.",
1383
+ "Treat the generated name field as a placeholder and shape the contracts, use cases, and repository around the feature's real workflow.",
1384
+ "If this app uses Drizzle, run beignet db generate and beignet db migrate so the new schema is ready.",
1385
+ "Run your app's test and typecheck commands, then beignet lint and beignet doctor.",
1386
+ ]);
975
1387
  }
976
1388
 
977
- function makeContractNextSteps(
978
- result: Awaited<ReturnType<typeof makeContract>>,
979
- ): string {
980
- const changedFiles = [...result.createdFiles, ...result.updatedFiles]
981
- .map((file) => ` ${file}`)
982
- .join("\n");
983
- const skippedFiles = result.skippedFiles
984
- .map((file) => ` ${file}`)
985
- .join("\n");
986
-
987
- return `${result.dryRun ? "Would create" : "Created"} ${result.name} contract in ${result.targetDir}
988
-
989
- Changed files:
990
- ${changedFiles || " none"}
991
- ${skippedFiles ? `\nSkipped identical files:\n${skippedFiles}` : ""}
992
-
993
- Next steps:
994
- Add route handlers for the generated contracts.
995
- Register the exported contract list with OpenAPI if this app publishes docs.`;
1389
+ function makeContractNextSteps(result: MakeNextStepsResult): string {
1390
+ return makeNextSteps(result, "contract", [
1391
+ "Add route handlers for the generated contracts.",
1392
+ "Register the exported contract list with OpenAPI if this app publishes docs.",
1393
+ ]);
996
1394
  }
997
1395
 
998
- function makeUseCaseNextSteps(
999
- result: Awaited<ReturnType<typeof makeUseCase>>,
1000
- ): string {
1001
- const changedFiles = [...result.createdFiles, ...result.updatedFiles]
1002
- .map((file) => ` ${file}`)
1003
- .join("\n");
1004
- const skippedFiles = result.skippedFiles
1005
- .map((file) => ` ${file}`)
1006
- .join("\n");
1007
-
1008
- return `${result.dryRun ? "Would create" : "Created"} ${result.name} use case in ${result.targetDir}
1009
-
1010
- Changed files:
1011
- ${changedFiles || " none"}
1012
- ${skippedFiles ? `\nSkipped identical files:\n${skippedFiles}` : ""}
1013
-
1014
- Next steps:
1015
- Replace the starter input and output schemas with domain-specific shapes.
1016
- Add a focused use case test before wiring it to an HTTP route.`;
1396
+ function makeTaskNextSteps(result: MakeNextStepsResult): string {
1397
+ return makeNextSteps(result, "task", [
1398
+ "Replace the starter input schema and handler with the operational workflow.",
1399
+ "Keep business rules in use cases and call them from the task handler.",
1400
+ ]);
1017
1401
  }
1018
1402
 
1019
- function makeTestNextSteps(
1020
- result: Awaited<ReturnType<typeof makeTest>>,
1021
- ): string {
1022
- const changedFiles = [...result.createdFiles, ...result.updatedFiles]
1023
- .map((file) => ` ${file}`)
1024
- .join("\n");
1025
- const skippedFiles = result.skippedFiles
1026
- .map((file) => ` ${file}`)
1027
- .join("\n");
1028
-
1029
- return `${result.dryRun ? "Would create" : "Created"} ${result.name} test in ${result.targetDir}
1030
-
1031
- Changed files:
1032
- ${changedFiles || " none"}
1033
- ${skippedFiles ? `\nSkipped identical files:\n${skippedFiles}` : ""}
1034
-
1035
- Next steps:
1036
- Replace the starter input, context, and assertion with behavior-specific coverage.
1037
- Run your app's test command.`;
1403
+ function makeUseCaseNextSteps(result: MakeNextStepsResult): string {
1404
+ return makeNextSteps(result, "use case", [
1405
+ "Replace the starter input and output schemas with domain-specific shapes.",
1406
+ "Add a focused use case test before wiring it to an HTTP route.",
1407
+ ]);
1038
1408
  }
1039
1409
 
1040
- function makePortNextSteps(
1041
- result: Awaited<ReturnType<typeof makePort>>,
1042
- ): string {
1043
- const changedFiles = [...result.createdFiles, ...result.updatedFiles]
1044
- .map((file) => ` ${file}`)
1045
- .join("\n");
1046
- const skippedFiles = result.skippedFiles
1047
- .map((file) => ` ${file}`)
1048
- .join("\n");
1049
-
1050
- return `${result.dryRun ? "Would create" : "Created"} ${result.name} port in ${result.targetDir}
1051
-
1052
- Changed files:
1053
- ${changedFiles || " none"}
1054
- ${skippedFiles ? `\nSkipped identical files:\n${skippedFiles}` : ""}
1055
-
1056
- Next steps:
1057
- Replace the starter execute method with domain-specific operations.
1058
- Replace the generated infrastructure stub with a real port adapter.`;
1410
+ function makeTestNextSteps(result: MakeNextStepsResult): string {
1411
+ return makeNextSteps(result, "test", [
1412
+ "Replace the starter input, context, and assertion with behavior-specific coverage.",
1413
+ "Run your app's test command.",
1414
+ ]);
1059
1415
  }
1060
1416
 
1061
- function makePolicyNextSteps(
1062
- result: Awaited<ReturnType<typeof makePolicy>>,
1063
- ): string {
1064
- const changedFiles = [...result.createdFiles, ...result.updatedFiles]
1065
- .map((file) => ` ${file}`)
1066
- .join("\n");
1067
- const skippedFiles = result.skippedFiles
1068
- .map((file) => ` ${file}`)
1069
- .join("\n");
1070
-
1071
- return `${result.dryRun ? "Would create" : "Created"} ${result.name} policy in ${result.targetDir}
1072
-
1073
- Changed files:
1074
- ${changedFiles || " none"}
1075
- ${skippedFiles ? `\nSkipped identical files:\n${skippedFiles}` : ""}
1076
-
1077
- Next steps:
1078
- Replace the starter abilities with domain-specific authorization rules.
1079
- Register the policy with createGate(...) and bind it in request context.`;
1417
+ function makePortNextSteps(result: MakeNextStepsResult): string {
1418
+ return makeNextSteps(result, "port", [
1419
+ "Replace the starter execute method with domain-specific operations.",
1420
+ "Replace the generated infrastructure stub with a real port adapter.",
1421
+ ]);
1080
1422
  }
1081
1423
 
1082
- function makeAdapterNextSteps(
1083
- result: Awaited<ReturnType<typeof makeAdapter>>,
1084
- ): string {
1085
- const changedFiles = [...result.createdFiles, ...result.updatedFiles]
1086
- .map((file) => ` ${file}`)
1087
- .join("\n");
1088
- const skippedFiles = result.skippedFiles
1089
- .map((file) => ` ${file}`)
1090
- .join("\n");
1091
-
1092
- return `${result.dryRun ? "Would create" : "Created"} ${result.name} port adapter in ${result.targetDir}
1093
-
1094
- Changed files:
1095
- ${changedFiles || " none"}
1096
- ${skippedFiles ? `\nSkipped identical files:\n${skippedFiles}` : ""}
1097
-
1098
- Next steps:
1099
- Replace the generated throwing implementation with real infrastructure code.
1100
- Keep the adapter behind the port interface so use cases stay infrastructure-agnostic.`;
1424
+ function makePolicyNextSteps(result: MakeNextStepsResult): string {
1425
+ return makeNextSteps(result, "policy", [
1426
+ "Replace the starter abilities with domain-specific authorization rules.",
1427
+ "Register the policy with createGate(...) and bind it in request context.",
1428
+ ]);
1101
1429
  }
1102
1430
 
1103
- function makeEventNextSteps(
1104
- result: Awaited<ReturnType<typeof makeEvent>>,
1105
- ): string {
1106
- const changedFiles = [...result.createdFiles, ...result.updatedFiles]
1107
- .map((file) => ` ${file}`)
1108
- .join("\n");
1109
- const skippedFiles = result.skippedFiles
1110
- .map((file) => ` ${file}`)
1111
- .join("\n");
1112
-
1113
- return `${result.dryRun ? "Would create" : "Created"} ${result.name} event in ${result.targetDir}
1114
-
1115
- Changed files:
1116
- ${changedFiles || " none"}
1117
- ${skippedFiles ? `\nSkipped identical files:\n${skippedFiles}` : ""}
1118
-
1119
- Next steps:
1120
- Replace the starter payload schema with the domain fact shape.
1121
- Emit the event from a use case with .emits(...) and events.record(...).`;
1431
+ function makeAdapterNextSteps(result: MakeNextStepsResult): string {
1432
+ return makeNextSteps(result, "port adapter", [
1433
+ "Replace the generated throwing implementation with real infrastructure code.",
1434
+ "Keep the adapter behind the port interface so use cases stay infrastructure-agnostic.",
1435
+ ]);
1122
1436
  }
1123
1437
 
1124
- function makeJobNextSteps(result: Awaited<ReturnType<typeof makeJob>>): string {
1125
- const changedFiles = [...result.createdFiles, ...result.updatedFiles]
1126
- .map((file) => ` ${file}`)
1127
- .join("\n");
1128
- const skippedFiles = result.skippedFiles
1129
- .map((file) => ` ${file}`)
1130
- .join("\n");
1131
-
1132
- return `${result.dryRun ? "Would create" : "Created"} ${result.name} job in ${result.targetDir}
1133
-
1134
- Changed files:
1135
- ${changedFiles || " none"}
1136
- ${skippedFiles ? `\nSkipped identical files:\n${skippedFiles}` : ""}
1137
-
1138
- Next steps:
1139
- Replace the starter payload and handler with the real background work.
1140
- Dispatch the job through ctx.ports.jobs from a use case, listener, or schedule.`;
1438
+ function makeEventNextSteps(result: MakeNextStepsResult): string {
1439
+ return makeNextSteps(result, "event", [
1440
+ "Replace the starter payload schema with the domain fact shape.",
1441
+ "Emit the event from a use case with .emits(...) and events.record(...).",
1442
+ ]);
1141
1443
  }
1142
1444
 
1143
- function makeFactoryNextSteps(
1144
- result: Awaited<ReturnType<typeof makeFactory>>,
1145
- ): string {
1146
- const changedFiles = [...result.createdFiles, ...result.updatedFiles]
1147
- .map((file) => ` ${file}`)
1148
- .join("\n");
1149
- const skippedFiles = result.skippedFiles
1150
- .map((file) => ` ${file}`)
1151
- .join("\n");
1152
-
1153
- return `${result.dryRun ? "Would create" : "Created"} ${result.name} factory in ${result.targetDir}
1154
-
1155
- Changed files:
1156
- ${changedFiles || " none"}
1157
- ${skippedFiles ? `\nSkipped identical files:\n${skippedFiles}` : ""}
1158
-
1159
- Next steps:
1160
- Replace the starter defaults with realistic test data for this feature.
1161
- Persist through app-owned repository ports so tests avoid raw database clients.`;
1445
+ function makeJobNextSteps(result: MakeNextStepsResult): string {
1446
+ return makeNextSteps(result, "job", [
1447
+ "Replace the starter payload and handler with the real background work.",
1448
+ "Dispatch the job through ctx.ports.jobs from a use case, listener, or schedule.",
1449
+ ]);
1162
1450
  }
1163
1451
 
1164
- function makeSeedNextSteps(
1165
- result: Awaited<ReturnType<typeof makeSeed>>,
1166
- ): string {
1167
- const changedFiles = [...result.createdFiles, ...result.updatedFiles]
1168
- .map((file) => ` ${file}`)
1169
- .join("\n");
1170
- const skippedFiles = result.skippedFiles
1171
- .map((file) => ` ${file}`)
1172
- .join("\n");
1173
-
1174
- return `${result.dryRun ? "Would create" : "Created"} ${result.name} seed in ${result.targetDir}
1175
-
1176
- Changed files:
1177
- ${changedFiles || " none"}
1178
- ${skippedFiles ? `\nSkipped identical files:\n${skippedFiles}` : ""}
1179
-
1180
- Next steps:
1181
- Replace the starter record with idempotent local or demo data.
1182
- Import the feature seed from your app-owned database seed entrypoint and run it with runSeeds(...).`;
1452
+ function makeFactoryNextSteps(result: MakeNextStepsResult): string {
1453
+ return makeNextSteps(result, "factory", [
1454
+ "Replace the starter defaults with realistic test data for this feature.",
1455
+ "Persist through app-owned repository ports so tests avoid raw database clients.",
1456
+ ]);
1183
1457
  }
1184
1458
 
1185
- function makeNotificationNextSteps(
1186
- result: Awaited<ReturnType<typeof makeNotification>>,
1187
- ): string {
1188
- const changedFiles = [...result.createdFiles, ...result.updatedFiles]
1189
- .map((file) => ` ${file}`)
1190
- .join("\n");
1191
- const skippedFiles = result.skippedFiles
1192
- .map((file) => ` ${file}`)
1193
- .join("\n");
1194
-
1195
- return `${result.dryRun ? "Would create" : "Created"} ${result.name} notification in ${result.targetDir}
1196
-
1197
- Changed files:
1198
- ${changedFiles || " none"}
1199
- ${skippedFiles ? `\nSkipped identical files:\n${skippedFiles}` : ""}
1200
-
1201
- Next steps:
1202
- Replace the starter payload and channels with the real user-facing message.
1203
- Send the notification through ctx.ports.notifications so delivery can run inline in tests and through jobs in production.`;
1459
+ function makeSeedNextSteps(result: MakeNextStepsResult): string {
1460
+ return makeNextSteps(result, "seed", [
1461
+ "Replace the starter record with idempotent local or demo data.",
1462
+ "Import the feature seed from your app-owned database seed entrypoint and run it with runSeeds(...).",
1463
+ ]);
1204
1464
  }
1205
1465
 
1206
- function makeListenerNextSteps(
1207
- result: Awaited<ReturnType<typeof makeListener>>,
1208
- ): string {
1209
- const changedFiles = [...result.createdFiles, ...result.updatedFiles]
1210
- .map((file) => ` ${file}`)
1211
- .join("\n");
1212
- const skippedFiles = result.skippedFiles
1213
- .map((file) => ` ${file}`)
1214
- .join("\n");
1215
-
1216
- return `${result.dryRun ? "Would create" : "Created"} ${result.name} listener in ${result.targetDir}
1217
-
1218
- Changed files:
1219
- ${changedFiles || " none"}
1220
- ${skippedFiles ? `\nSkipped identical files:\n${skippedFiles}` : ""}
1221
-
1222
- Next steps:
1223
- Replace the starter handler with the event reaction.
1224
- Register the listener collection from infrastructure startup.`;
1466
+ function makeNotificationNextSteps(result: MakeNextStepsResult): string {
1467
+ return makeNextSteps(result, "notification", [
1468
+ "Replace the starter payload and channels with the real user-facing message.",
1469
+ "Send the notification through ctx.ports.notifications so delivery can run inline in tests and through jobs in production.",
1470
+ ]);
1225
1471
  }
1226
1472
 
1227
- function makeScheduleNextSteps(
1228
- result: Awaited<ReturnType<typeof makeSchedule>>,
1229
- ): string {
1230
- const changedFiles = [...result.createdFiles, ...result.updatedFiles]
1231
- .map((file) => ` ${file}`)
1232
- .join("\n");
1233
- const skippedFiles = result.skippedFiles
1234
- .map((file) => ` ${file}`)
1235
- .join("\n");
1236
-
1237
- return `${result.dryRun ? "Would create" : "Created"} ${result.name} schedule in ${result.targetDir}
1238
-
1239
- Changed files:
1240
- ${changedFiles || " none"}
1241
- ${skippedFiles ? `\nSkipped identical files:\n${skippedFiles}` : ""}
1242
-
1243
- Next steps:
1244
- Replace the starter payload and handler with the scheduled workflow.
1245
- Trigger it from a cron route, worker, or provider adapter.`;
1473
+ function makeListenerNextSteps(result: MakeNextStepsResult): string {
1474
+ return makeNextSteps(result, "listener", [
1475
+ "Replace the starter handler with the event reaction.",
1476
+ "Register the listener collection from infrastructure startup.",
1477
+ ]);
1246
1478
  }
1247
1479
 
1248
- function makeUploadNextSteps(
1249
- result: Awaited<ReturnType<typeof makeUpload>>,
1250
- ): string {
1251
- const changedFiles = [...result.createdFiles, ...result.updatedFiles]
1252
- .map((file) => ` ${file}`)
1253
- .join("\n");
1254
- const skippedFiles = result.skippedFiles
1255
- .map((file) => ` ${file}`)
1256
- .join("\n");
1257
-
1258
- return `${result.dryRun ? "Would create" : "Created"} ${result.name} upload in ${result.targetDir}
1259
-
1260
- Changed files:
1261
- ${changedFiles || " none"}
1262
- ${skippedFiles ? `\nSkipped identical files:\n${skippedFiles}` : ""}
1480
+ function makeScheduleNextSteps(result: MakeNextStepsResult): string {
1481
+ return makeNextSteps(result, "schedule", [
1482
+ "Replace the starter payload and handler with the scheduled workflow.",
1483
+ "Trigger it from a cron route, worker, or provider adapter.",
1484
+ ]);
1485
+ }
1263
1486
 
1264
- Next steps:
1265
- Replace the starter metadata, constraints, key, authorization, and onComplete behavior.
1266
- Register the upload in createUploadRouter(...) and expose it with createUploadRoute(...).`;
1487
+ function makeUploadNextSteps(result: MakeNextStepsResult): string {
1488
+ return makeNextSteps(result, "upload", [
1489
+ "Replace the starter metadata, constraints, key, authorization, and onComplete behavior.",
1490
+ "Register the upload in createUploadRouter(...) and expose it with createUploadRoute(...).",
1491
+ ]);
1267
1492
  }
1268
1493
 
1269
1494
  export async function main(
@@ -1272,7 +1497,15 @@ export async function main(
1272
1497
  ): Promise<void> {
1273
1498
  if (inputs.length === 0) {
1274
1499
  await run(cli, ["--help"], context);
1275
- context.process.exitCode = 1;
1500
+ context.process.exitCode = 2;
1501
+ return;
1502
+ }
1503
+
1504
+ // Shell completion scripts call `beignet completion propose <words...>`.
1505
+ // Handle it before stricli parses the inputs: the trailing words are a
1506
+ // partial command line, not flags for this CLI.
1507
+ if (inputs[0] === "completion" && inputs[1] === "propose") {
1508
+ await writeCompletionProposals(inputs.slice(2), context);
1276
1509
  return;
1277
1510
  }
1278
1511
 
@@ -1280,11 +1513,42 @@ export async function main(
1280
1513
  normalizeExitCode(context.process);
1281
1514
  }
1282
1515
 
1516
+ async function writeCompletionProposals(
1517
+ inputs: readonly string[],
1518
+ context: StricliDynamicCommandContext<CliContext>,
1519
+ ): Promise<void> {
1520
+ try {
1521
+ const proposals = await proposeCompletions(
1522
+ cli,
1523
+ inputs.length === 0 ? [""] : inputs,
1524
+ context,
1525
+ );
1526
+ for (const proposal of proposals) {
1527
+ context.process.stdout.write(`${proposal.completion}\n`);
1528
+ }
1529
+ } catch {
1530
+ // Completion failures must never break the shell; propose nothing.
1531
+ }
1532
+ }
1533
+
1534
+ // Stricli reports usage and internal failures as negative ExitCode values
1535
+ // (-5..-1). Node keeps the negative value on process.exitCode while Bun wraps
1536
+ // it to 251..255 on assignment, so both forms map to the usage exit code.
1537
+ const stricliErrorExitCodes = new Set([
1538
+ -5, -4, -3, -2, -1, 251, 252, 253, 254, 255,
1539
+ ]);
1540
+
1283
1541
  function normalizeExitCode(proc: StricliProcess): void {
1284
- if (
1285
- typeof proc.exitCode === "number" &&
1286
- (proc.exitCode < 0 || proc.exitCode > 127)
1287
- ) {
1542
+ if (typeof proc.exitCode !== "number") {
1543
+ return;
1544
+ }
1545
+
1546
+ if (stricliErrorExitCodes.has(proc.exitCode)) {
1547
+ proc.exitCode = 2;
1548
+ return;
1549
+ }
1550
+
1551
+ if (proc.exitCode > 127) {
1288
1552
  proc.exitCode = 1;
1289
1553
  }
1290
1554
  }
@@ -1307,6 +1571,6 @@ function isCliEntrypoint(): boolean {
1307
1571
  if (isCliEntrypoint()) {
1308
1572
  main().catch((error: unknown) => {
1309
1573
  console.error(error instanceof Error ? error.message : String(error));
1310
- process.exitCode = 1;
1574
+ process.exitCode = 2;
1311
1575
  });
1312
1576
  }