@beignet/cli 0.0.3 → 0.0.5

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