@beignet/cli 0.0.1 → 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 +231 -0
  2. package/README.md +497 -90
  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 +39 -0
  16. package/dist/config.d.ts.map +1 -1
  17. package/dist/config.js +26 -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 +12 -0
  24. package/dist/create.d.ts.map +1 -1
  25. package/dist/create.js +19 -24
  26. package/dist/create.js.map +1 -1
  27. package/dist/db.d.ts +37 -0
  28. package/dist/db.d.ts.map +1 -0
  29. package/dist/db.js +146 -0
  30. package/dist/db.js.map +1 -0
  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 -7
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +710 -400
  38. package/dist/index.js.map +1 -1
  39. package/dist/inspect.d.ts +45 -2
  40. package/dist/inspect.d.ts.map +1 -1
  41. package/dist/inspect.js +2191 -100
  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 +22 -1
  48. package/dist/lint.d.ts.map +1 -1
  49. package/dist/lint.js +370 -38
  50. package/dist/lint.js.map +1 -1
  51. package/dist/make.d.ts +109 -1
  52. package/dist/make.d.ts.map +1 -1
  53. package/dist/make.js +2225 -333
  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 +12 -8
  68. package/dist/templates.d.ts.map +1 -1
  69. package/dist/templates.js +2144 -385
  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 +47 -0
  80. package/src/create-prompts.ts +182 -0
  81. package/src/create.ts +32 -28
  82. package/src/db.ts +222 -0
  83. package/src/github-annotations.ts +37 -0
  84. package/src/index.ts +1119 -535
  85. package/src/inspect.ts +3372 -134
  86. package/src/lib.ts +45 -0
  87. package/src/lint.ts +533 -45
  88. package/src/make.ts +3010 -397
  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 +2282 -462
  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,80 +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 {
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
16
  import {
17
+ type CompletionShell,
18
+ completionShellChoices,
38
19
  type FeatureName,
39
20
  featureChoices,
40
21
  type IntegrationName,
41
22
  integrationChoices,
23
+ type MakeFeatureAddon,
24
+ makeFeatureAddonChoices,
42
25
  type PackageManager,
43
26
  type PresetName,
27
+ packageManagerChoices,
44
28
  presetChoices,
45
29
  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
- };
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";
70
35
 
71
36
  type CliContext = CommandContext & {
72
37
  readonly process: StricliProcess;
73
38
  };
74
39
 
40
+ type OutputFormat = "human" | "json" | "github";
41
+
42
+ const outputFormatChoices = [
43
+ "human",
44
+ "json",
45
+ "github",
46
+ ] as const satisfies readonly OutputFormat[];
47
+
75
48
  type CreateFlags = {
76
49
  template: TemplateName;
77
- preset: PresetName;
50
+ preset?: PresetName;
78
51
  packageManager?: PackageManager;
79
- feature?: readonly FeatureName[];
80
52
  features?: readonly FeatureName[];
81
- integration?: readonly IntegrationName[];
82
53
  integrations?: readonly IntegrationName[];
54
+ yes?: boolean;
83
55
  force?: boolean;
56
+ dryRun?: boolean;
57
+ json?: boolean;
58
+ };
59
+
60
+ type CompletionFlags = {
61
+ shell?: CompletionShell;
62
+ json?: boolean;
84
63
  };
85
64
 
86
65
  type MakeFlags = {
@@ -89,6 +68,17 @@ type MakeFlags = {
89
68
  json?: boolean;
90
69
  };
91
70
 
71
+ type MakeResourceFlags = MakeFlags & {
72
+ auth?: boolean;
73
+ tenant?: boolean;
74
+ events?: boolean;
75
+ softDelete?: boolean;
76
+ };
77
+
78
+ type MakeFeatureFlags = MakeFlags & {
79
+ with?: readonly MakeFeatureAddon[];
80
+ };
81
+
92
82
  type MakeListenerFlags = MakeFlags & {
93
83
  event?: string;
94
84
  };
@@ -99,25 +89,61 @@ type MakeScheduleFlags = MakeFlags & {
99
89
  route?: boolean;
100
90
  };
101
91
 
102
- type JsonFlags = {
92
+ type RoutesFlags = {
93
+ json?: boolean;
94
+ cwd?: string;
95
+ };
96
+
97
+ type LintFlags = {
98
+ json?: boolean;
99
+ cwd?: string;
100
+ format?: OutputFormat;
101
+ };
102
+
103
+ type DbFlags = {
103
104
  json?: boolean;
105
+ dryRun?: boolean;
106
+ };
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;
104
129
  };
105
130
 
106
131
  type DoctorFlags = {
107
132
  json?: boolean;
108
133
  strict?: boolean;
109
134
  fix?: boolean;
135
+ cwd?: string;
136
+ format?: OutputFormat;
110
137
  };
111
138
 
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
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
+ };
121
147
 
122
148
  const forceFlag = {
123
149
  kind: "boolean",
@@ -148,15 +174,78 @@ const parsedStringFlag = (brief: string) =>
148
174
  brief,
149
175
  }) as const;
150
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
+
151
189
  const makeFlagParameters = {
152
190
  force: forceFlag,
153
191
  dryRun: dryRunFlag,
154
192
  json: jsonFlag,
155
193
  } satisfies FlagParametersForType<MakeFlags, CliContext>;
156
194
 
157
- const jsonFlagParameters = {
195
+ const routesFlagParameters = {
196
+ json: jsonFlag,
197
+ cwd: cwdFlag,
198
+ } satisfies FlagParametersForType<RoutesFlags, CliContext>;
199
+
200
+ const lintFlagParameters = {
201
+ json: jsonFlag,
202
+ cwd: cwdFlag,
203
+ format: formatFlag,
204
+ } satisfies FlagParametersForType<LintFlags, CliContext>;
205
+
206
+ const dbFlagParameters = {
158
207
  json: jsonFlag,
159
- } satisfies FlagParametersForType<JsonFlags, CliContext>;
208
+ dryRun: dryRunFlag,
209
+ } satisfies FlagParametersForType<DbFlags, CliContext>;
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>;
160
249
 
161
250
  const namePositional = {
162
251
  kind: "tuple",
@@ -169,12 +258,40 @@ const namePositional = {
169
258
  ],
170
259
  } as const;
171
260
 
172
- 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
+ >({
173
287
  docs: {
174
288
  brief: "Create a new Beignet app.",
175
289
  fullDescription: `Available features: ${featureChoices.join(", ")}
176
290
 
177
- 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.`,
178
295
  },
179
296
  parameters: {
180
297
  flags: {
@@ -187,8 +304,8 @@ Available integrations: ${integrationChoices.join(", ")}`,
187
304
  preset: {
188
305
  kind: "enum",
189
306
  values: presetChoices,
190
- default: "standard",
191
- brief: "Starter preset to generate.",
307
+ optional: true,
308
+ brief: "Starter preset to generate. Defaults to standard.",
192
309
  },
193
310
  packageManager: {
194
311
  kind: "enum",
@@ -196,35 +313,29 @@ Available integrations: ${integrationChoices.join(", ")}`,
196
313
  optional: true,
197
314
  brief: "Package manager to use in next-step commands.",
198
315
  },
199
- feature: {
200
- kind: "enum",
201
- values: featureChoices,
202
- optional: true,
203
- variadic: true,
204
- brief: "Add a starter feature. Repeatable.",
205
- },
206
316
  features: {
207
317
  kind: "enum",
208
318
  values: featureChoices,
209
319
  optional: true,
210
320
  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.",
321
+ brief: "Add starter features as a comma-separated list.",
219
322
  },
220
323
  integrations: {
221
324
  kind: "enum",
222
325
  values: integrationChoices,
223
326
  optional: true,
224
327
  variadic: ",",
225
- 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.",
226
335
  },
227
336
  force: forceFlag,
337
+ dryRun: dryRunFlag,
338
+ json: jsonFlag,
228
339
  },
229
340
  positional: {
230
341
  kind: "tuple",
@@ -233,45 +344,92 @@ Available integrations: ${integrationChoices.join(", ")}`,
233
344
  parse: parseString,
234
345
  placeholder: "directory",
235
346
  brief: "Project directory to create.",
347
+ optional: true,
236
348
  },
237
349
  ],
238
350
  },
239
351
  },
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 }));
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
+ };
259
413
  },
260
414
  });
261
415
 
262
- const routesCommand = buildCommand<JsonFlags, [], CliContext>({
416
+ const routesCommand = buildCommand<RoutesFlags, [], CliContext>({
263
417
  docs: {
264
418
  brief: "Inspect registered Beignet routes.",
265
419
  },
266
420
  parameters: {
267
- flags: jsonFlagParameters,
421
+ flags: routesFlagParameters,
268
422
  },
269
- async func(flags) {
270
- const result = await inspectApp();
271
- writeOutput(
272
- this,
273
- flags.json ? JSON.stringify(result, null, 2) : formatRoutes(result),
274
- );
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
+ };
275
433
  },
276
434
  });
277
435
 
@@ -294,252 +452,484 @@ const doctorCommand = buildCommand<DoctorFlags, [], CliContext>({
294
452
  withNegated: false,
295
453
  brief: "Apply low-risk fixes before reporting.",
296
454
  },
455
+ cwd: cwdFlag,
456
+ format: formatFlag,
297
457
  },
298
458
  },
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
- }
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
+ };
318
494
  },
319
495
  });
320
496
 
321
- const lintCommand = buildCommand<JsonFlags, [], CliContext>({
497
+ const lintCommand = buildCommand<LintFlags, [], CliContext>({
322
498
  docs: {
323
499
  brief: "Check Beignet dependency direction conventions.",
324
500
  },
325
501
  parameters: {
326
- flags: jsonFlagParameters,
502
+ flags: lintFlagParameters,
327
503
  },
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
- }
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
+ };
337
522
  },
338
523
  });
339
524
 
340
- const makeResourceCommand = buildCommand<MakeFlags, [string], CliContext>({
525
+ type DatabaseCommandName = "generate" | "migrate" | "reset" | "seed";
526
+
527
+ function databaseCommand(command: DatabaseCommandName) {
528
+ return buildCommand<DbFlags, [], CliContext>({
529
+ docs: {
530
+ brief: `Run the app's db:${command} script.`,
531
+ },
532
+ parameters: {
533
+ flags: dbFlagParameters,
534
+ },
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
+ };
555
+ },
556
+ });
557
+ }
558
+
559
+ const dbRoutes = buildRouteMap({
341
560
  docs: {
342
- brief: "Generate a feature resource.",
561
+ brief: "Run Beignet database lifecycle commands.",
343
562
  },
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
- );
563
+ routes: {
564
+ generate: databaseCommand("generate"),
565
+ migrate: databaseCommand("migrate"),
566
+ reset: databaseCommand("reset"),
567
+ seed: databaseCommand("seed"),
360
568
  },
361
569
  });
362
570
 
363
- const makeContractCommand = buildCommand<MakeFlags, [string], CliContext>({
571
+ const taskRunCommand = buildCommand<TaskRunFlags, [string], CliContext>({
364
572
  docs: {
365
- brief: "Generate a contract group.",
573
+ brief: "Run an app-owned operational task.",
366
574
  },
367
575
  parameters: {
368
- flags: makeFlagParameters,
576
+ flags: taskRunFlagParameters,
369
577
  positional: namePositional,
370
578
  },
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
- );
579
+ loader: async () => {
580
+ const { runAppTask } = await import("./task.js");
581
+
582
+ return async function runTask(
583
+ this: CliContext,
584
+ flags: TaskRunFlags,
585
+ name: string,
586
+ ) {
587
+ const result = await runAppTask({
588
+ name,
589
+ input: flags.input,
590
+ modulePath: flags.module,
591
+ });
592
+
593
+ writeOutput(
594
+ this,
595
+ flags.json
596
+ ? JSON.stringify(result, null, 2)
597
+ : appTaskRunNextSteps(result),
598
+ );
599
+ };
383
600
  },
384
601
  });
385
602
 
386
- const makeUseCaseCommand = buildCommand<MakeFlags, [string], CliContext>({
603
+ const taskRoutes = buildRouteMap({
387
604
  docs: {
388
- brief: "Generate a use case.",
605
+ brief: "Run Beignet app operational tasks.",
389
606
  },
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
- );
607
+ routes: {
608
+ run: taskRunCommand,
406
609
  },
407
610
  });
408
611
 
409
- const makeTestCommand = buildCommand<MakeFlags, [string], CliContext>({
612
+ const outboxDrainCommand = buildCommand<OutboxDrainFlags, [], CliContext>({
410
613
  docs: {
411
- brief: "Generate a use case test.",
614
+ brief: "Run one app-owned outbox drain pass.",
412
615
  },
413
616
  parameters: {
414
- flags: makeFlagParameters,
415
- positional: namePositional,
617
+ flags: outboxDrainFlagParameters,
416
618
  },
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
- );
619
+ loader: async () => {
620
+ const { runOutboxDrain } = await import("./outbox.js");
621
+
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
+ };
427
635
  },
428
636
  });
429
637
 
430
- const makePortCommand = buildCommand<MakeFlags, [string], CliContext>({
638
+ const outboxRoutes = buildRouteMap({
431
639
  docs: {
432
- brief: "Generate an application port.",
640
+ brief: "Run Beignet outbox operations.",
433
641
  },
434
- parameters: {
435
- flags: makeFlagParameters,
436
- positional: namePositional,
642
+ routes: {
643
+ drain: outboxDrainCommand,
437
644
  },
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
- );
645
+ });
646
+
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
+ },
683
+ },
684
+ );
685
+
686
+ const scheduleRoutes = buildRouteMap({
687
+ docs: {
688
+ brief: "Run Beignet schedule operations.",
689
+ },
690
+ routes: {
691
+ run: scheduleRunCommand,
448
692
  },
449
693
  });
450
694
 
451
- const makeAdapterCommand = buildCommand<MakeFlags, [string], CliContext>({
695
+ const completionShellFlagParameters = {
696
+ shell: {
697
+ kind: "enum",
698
+ values: completionShellChoices,
699
+ optional: true,
700
+ brief: "Shell to manage completions for. Defaults to $SHELL.",
701
+ },
702
+ json: jsonFlag,
703
+ } satisfies FlagParametersForType<CompletionFlags, CliContext>;
704
+
705
+ const completionInstallCommand = buildCommand<CompletionFlags, [], CliContext>({
452
706
  docs: {
453
- brief: "Generate a port adapter.",
707
+ brief: "Install beignet shell completions for bash or zsh.",
454
708
  },
455
709
  parameters: {
456
- flags: makeFlagParameters,
457
- positional: namePositional,
710
+ flags: completionShellFlagParameters,
458
711
  },
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
- );
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
+ };
471
726
  },
472
727
  });
473
728
 
474
- const makePolicyCommand = buildCommand<MakeFlags, [string], CliContext>({
729
+ const completionUninstallCommand = buildCommand<
730
+ CompletionFlags,
731
+ [],
732
+ CliContext
733
+ >({
475
734
  docs: {
476
- brief: "Generate an authorization policy.",
735
+ brief: "Remove beignet shell completions for bash or zsh.",
477
736
  },
478
737
  parameters: {
479
- flags: makeFlagParameters,
480
- positional: namePositional,
738
+ flags: completionShellFlagParameters,
481
739
  },
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
- );
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
+ };
494
757
  },
495
758
  });
496
759
 
497
- const makeEventCommand = buildCommand<MakeFlags, [string], CliContext>({
760
+ const completionRoutes = buildRouteMap({
498
761
  docs: {
499
- brief: "Generate a feature event.",
500
- },
501
- parameters: {
502
- flags: makeFlagParameters,
503
- positional: namePositional,
762
+ brief: "Manage beignet shell completions.",
504
763
  },
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
- );
764
+ routes: {
765
+ install: completionInstallCommand,
766
+ uninstall: completionUninstallCommand,
515
767
  },
516
768
  });
517
769
 
518
- const makeJobCommand = buildCommand<MakeFlags, [string], CliContext>({
770
+ const makeResourceCommand = buildCommand<
771
+ MakeResourceFlags,
772
+ [string],
773
+ CliContext
774
+ >({
519
775
  docs: {
520
- brief: "Generate a feature job.",
776
+ brief: "Generate a CRUD-shaped resource slice.",
521
777
  },
522
778
  parameters: {
523
- flags: makeFlagParameters,
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>,
524
809
  positional: namePositional,
525
810
  },
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
- );
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
+ };
536
835
  },
537
836
  });
538
837
 
539
- const makeListenerFlagParameters = {
540
- ...makeFlagParameters,
541
- event: parsedStringFlag("Event to listen to, for example posts/published."),
542
- } satisfies FlagParametersForType<MakeListenerFlags, CliContext>;
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
+ },
879
+ },
880
+ );
881
+
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
+ }
543
933
 
544
934
  const makeListenerCommand = buildCommand<
545
935
  MakeListenerFlags,
@@ -550,43 +940,44 @@ const makeListenerCommand = buildCommand<
550
940
  brief: "Generate a feature event listener.",
551
941
  },
552
942
  parameters: {
553
- flags: makeListenerFlagParameters,
943
+ flags: {
944
+ ...makeFlagParameters,
945
+ event: parsedStringFlag(
946
+ "Event to listen to, for example posts/published.",
947
+ ),
948
+ } satisfies FlagParametersForType<MakeListenerFlags, CliContext>,
554
949
  positional: namePositional,
555
950
  },
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
- }
951
+ loader: async () => {
952
+ const { makeListener } = await import("./make.js");
562
953
 
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
- );
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
+ };
575
978
  },
576
979
  });
577
980
 
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
981
  const makeScheduleCommand = buildCommand<
591
982
  MakeScheduleFlags,
592
983
  [string],
@@ -596,24 +987,42 @@ const makeScheduleCommand = buildCommand<
596
987
  brief: "Generate a feature schedule.",
597
988
  },
598
989
  parameters: {
599
- flags: makeScheduleFlagParameters,
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>,
600
1001
  positional: namePositional,
601
1002
  },
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
- );
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
+ };
617
1026
  },
618
1027
  });
619
1028
 
@@ -622,17 +1031,75 @@ const makeRoutes = buildRouteMap({
622
1031
  brief: "Generate Beignet app files.",
623
1032
  },
624
1033
  routes: {
625
- adapter: makeAdapterCommand,
626
- contract: makeContractCommand,
627
- event: makeEventCommand,
628
- job: makeJobCommand,
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
+ ),
1054
+ feature: makeFeatureCommand,
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
+ ),
629
1065
  listener: makeListenerCommand,
630
- policy: makePolicyCommand,
631
- 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
+ ),
632
1076
  resource: makeResourceCommand,
633
1077
  schedule: makeScheduleCommand,
634
- test: makeTestCommand,
635
- 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
+ ),
636
1103
  },
637
1104
  });
638
1105
 
@@ -641,19 +1108,27 @@ const rootRoutes = buildRouteMap({
641
1108
  brief: "Beignet CLI",
642
1109
  fullDescription: `Create apps, generate framework files, inspect routes, and check Beignet conventions.
643
1110
 
644
- create-beignet <directory> is equivalent to beignet create <directory>.`,
1111
+ Run npm create beignet@latest (or bun create beignet) to scaffold a new app.`,
645
1112
  },
646
1113
  routes: {
1114
+ completion: completionRoutes,
647
1115
  create: createCommand,
1116
+ db: dbRoutes,
648
1117
  doctor: doctorCommand,
649
1118
  lint: lintCommand,
650
1119
  make: makeRoutes,
1120
+ outbox: outboxRoutes,
651
1121
  routes: routesCommand,
1122
+ schedule: scheduleRoutes,
1123
+ task: taskRoutes,
652
1124
  },
653
1125
  });
654
1126
 
655
1127
  const cli = buildApplication(rootRoutes, {
656
1128
  name: "beignet",
1129
+ versionInfo: {
1130
+ currentVersion: getCliVersion(),
1131
+ },
657
1132
  scanner: {
658
1133
  caseStyle: "allow-kebab-for-camel",
659
1134
  },
@@ -667,7 +1142,7 @@ const cli = buildApplication(rootRoutes, {
667
1142
  commandErrorResult: (error) => error.message,
668
1143
  },
669
1144
  },
670
- determineExitCode: () => 1,
1145
+ determineExitCode: () => 2,
671
1146
  });
672
1147
 
673
1148
  function uniqueValues<T>(values: readonly T[]): T[] {
@@ -682,12 +1157,108 @@ function formatCliException(error: unknown): string {
682
1157
  return error instanceof Error ? error.message : String(error);
683
1158
  }
684
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
+
685
1246
  function nextSteps(
686
- result: Awaited<ReturnType<typeof createProject>>,
1247
+ result: CreateNextStepsResult,
687
1248
  options: { integrations?: readonly IntegrationName[] } = {},
688
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
+
689
1259
  const pm = result.packageManager;
690
1260
  const run = pm === "npm" ? "npm run" : `${pm} run`;
1261
+ const cli = packageRunnerFromPackageManager(pm);
691
1262
  const envFileStep = result.files.includes(".env.example")
692
1263
  ? " cp .env.example .env.local\n"
693
1264
  : "";
@@ -697,14 +1268,53 @@ function nextSteps(
697
1268
  envRequiredIntegrations.length > 0
698
1269
  ? " Fill .env.local for selected provider integrations before starting the app.\n"
699
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
+ : "";
700
1284
 
701
1285
  return `Created ${result.name} at ${result.targetDir}
702
1286
 
703
1287
  Next steps:
704
1288
  cd ${result.targetDir}
705
1289
  ${pm} install
706
- ${envFileStep}${envStep}\
707
- ${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
+ }
708
1318
  }
709
1319
 
710
1320
  function isEnvRequiredProviderIntegration(
@@ -717,233 +1327,168 @@ function isEnvRequiredProviderIntegration(
717
1327
  );
718
1328
  }
719
1329
 
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.`;
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
+ };
739
1340
  }
740
1341
 
741
- function makeContractNextSteps(
742
- result: Awaited<ReturnType<typeof makeContract>>,
1342
+ function makeNextSteps(
1343
+ result: MakeNextStepsResult,
1344
+ noun: string,
1345
+ steps: readonly string[],
743
1346
  ): 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");
1347
+ const { changedFiles, skippedFiles } = changedFileLines(result);
750
1348
 
751
- return `${result.dryRun ? "Would create" : "Created"} ${result.name} contract in ${result.targetDir}
1349
+ return `${result.dryRun ? "Would create" : "Created"} ${result.name} ${noun} in ${result.targetDir}
752
1350
 
753
1351
  Changed files:
754
1352
  ${changedFiles || " none"}
755
1353
  ${skippedFiles ? `\nSkipped identical files:\n${skippedFiles}` : ""}
756
1354
 
757
1355
  Next steps:
758
- Add route handlers for the generated contracts.
759
- Register the exported contract list with OpenAPI if this app publishes docs.`;
1356
+ ${steps.map((step) => ` ${step}`).join("\n")}`;
760
1357
  }
761
1358
 
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.`;
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
+ ]);
781
1366
  }
782
1367
 
783
- function makeTestNextSteps(
784
- result: Awaited<ReturnType<typeof makeTest>>,
1368
+ function databaseCommandNextSteps(
1369
+ result: DatabaseCommandNextStepsResult,
785
1370
  ): 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");
1371
+ const command = [result.runner, ...result.args].join(" ");
1372
+ const prefix = result.dryRun ? "Would run" : "Ran";
792
1373
 
793
- return `${result.dryRun ? "Would create" : "Created"} ${result.name} test in ${result.targetDir}
1374
+ return `${prefix} ${result.script} in ${result.cwd}
794
1375
 
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.`;
1376
+ Command:
1377
+ ${command}`;
802
1378
  }
803
1379
 
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.`;
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
+ ]);
823
1387
  }
824
1388
 
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.`;
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
+ ]);
844
1394
  }
845
1395
 
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.`;
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
+ ]);
865
1401
  }
866
1402
 
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(...).`;
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
+ ]);
886
1408
  }
887
1409
 
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");
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
+ ]);
1415
+ }
895
1416
 
896
- return `${result.dryRun ? "Would create" : "Created"} ${result.name} job in ${result.targetDir}
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
+ ]);
1422
+ }
897
1423
 
898
- Changed files:
899
- ${changedFiles || " none"}
900
- ${skippedFiles ? `\nSkipped identical files:\n${skippedFiles}` : ""}
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
+ ]);
1429
+ }
901
1430
 
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.`;
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
+ ]);
905
1436
  }
906
1437
 
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");
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
+ ]);
1443
+ }
916
1444
 
917
- return `${result.dryRun ? "Would create" : "Created"} ${result.name} listener in ${result.targetDir}
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
+ ]);
1450
+ }
918
1451
 
919
- Changed files:
920
- ${changedFiles || " none"}
921
- ${skippedFiles ? `\nSkipped identical files:\n${skippedFiles}` : ""}
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
+ ]);
1457
+ }
922
1458
 
923
- Next steps:
924
- Replace the starter handler with the event reaction.
925
- Register the listener collection from infrastructure startup.`;
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
+ ]);
926
1464
  }
927
1465
 
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");
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
+ ]);
1471
+ }
937
1472
 
938
- return `${result.dryRun ? "Would create" : "Created"} ${result.name} schedule in ${result.targetDir}
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
+ ]);
1478
+ }
939
1479
 
940
- Changed files:
941
- ${changedFiles || " none"}
942
- ${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
+ }
943
1486
 
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.`;
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
+ ]);
947
1492
  }
948
1493
 
949
1494
  export async function main(
@@ -952,7 +1497,15 @@ export async function main(
952
1497
  ): Promise<void> {
953
1498
  if (inputs.length === 0) {
954
1499
  await run(cli, ["--help"], context);
955
- 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);
956
1509
  return;
957
1510
  }
958
1511
 
@@ -960,11 +1513,42 @@ export async function main(
960
1513
  normalizeExitCode(context.process);
961
1514
  }
962
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
+
963
1541
  function normalizeExitCode(proc: StricliProcess): void {
964
- if (
965
- typeof proc.exitCode === "number" &&
966
- (proc.exitCode < 0 || proc.exitCode > 127)
967
- ) {
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) {
968
1552
  proc.exitCode = 1;
969
1553
  }
970
1554
  }
@@ -987,6 +1571,6 @@ function isCliEntrypoint(): boolean {
987
1571
  if (isCliEntrypoint()) {
988
1572
  main().catch((error: unknown) => {
989
1573
  console.error(error instanceof Error ? error.message : String(error));
990
- process.exitCode = 1;
1574
+ process.exitCode = 2;
991
1575
  });
992
1576
  }