@beignet/cli 0.0.3 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/CHANGELOG.md +205 -0
  2. package/README.md +379 -61
  3. package/dist/ansi.d.ts +10 -0
  4. package/dist/ansi.d.ts.map +1 -0
  5. package/dist/ansi.js +20 -0
  6. package/dist/ansi.js.map +1 -0
  7. package/dist/choices.d.ts +72 -0
  8. package/dist/choices.d.ts.map +1 -0
  9. package/dist/choices.js +88 -0
  10. package/dist/choices.js.map +1 -0
  11. package/dist/completion.d.ts +47 -0
  12. package/dist/completion.d.ts.map +1 -0
  13. package/dist/completion.js +123 -0
  14. package/dist/completion.js.map +1 -0
  15. package/dist/config.d.ts +8 -0
  16. package/dist/config.d.ts.map +1 -1
  17. package/dist/config.js +8 -0
  18. package/dist/config.js.map +1 -1
  19. package/dist/create-prompts.d.ts +42 -0
  20. package/dist/create-prompts.d.ts.map +1 -0
  21. package/dist/create-prompts.js +136 -0
  22. package/dist/create-prompts.js.map +1 -0
  23. package/dist/create.d.ts +4 -1
  24. package/dist/create.d.ts.map +1 -1
  25. package/dist/create.js +16 -26
  26. package/dist/create.js.map +1 -1
  27. package/dist/db.d.ts +1 -0
  28. package/dist/db.d.ts.map +1 -1
  29. package/dist/db.js +37 -2
  30. package/dist/db.js.map +1 -1
  31. package/dist/github-annotations.d.ts +18 -0
  32. package/dist/github-annotations.d.ts.map +1 -0
  33. package/dist/github-annotations.js +22 -0
  34. package/dist/github-annotations.js.map +1 -0
  35. package/dist/index.d.ts +1 -9
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +657 -588
  38. package/dist/index.js.map +1 -1
  39. package/dist/inspect.d.ts +21 -2
  40. package/dist/inspect.d.ts.map +1 -1
  41. package/dist/inspect.js +1938 -131
  42. package/dist/inspect.js.map +1 -1
  43. package/dist/lib.d.ts +20 -0
  44. package/dist/lib.d.ts.map +1 -0
  45. package/dist/lib.js +17 -0
  46. package/dist/lib.js.map +1 -0
  47. package/dist/lint.d.ts +10 -1
  48. package/dist/lint.d.ts.map +1 -1
  49. package/dist/lint.js +340 -33
  50. package/dist/lint.js.map +1 -1
  51. package/dist/make.d.ts +20 -3
  52. package/dist/make.d.ts.map +1 -1
  53. package/dist/make.js +1756 -394
  54. package/dist/make.js.map +1 -1
  55. package/dist/outbox.d.ts +24 -0
  56. package/dist/outbox.d.ts.map +1 -0
  57. package/dist/outbox.js +138 -0
  58. package/dist/outbox.js.map +1 -0
  59. package/dist/schedule.d.ts +36 -0
  60. package/dist/schedule.d.ts.map +1 -0
  61. package/dist/schedule.js +155 -0
  62. package/dist/schedule.js.map +1 -0
  63. package/dist/task.d.ts +26 -0
  64. package/dist/task.d.ts.map +1 -0
  65. package/dist/task.js +106 -0
  66. package/dist/task.js.map +1 -0
  67. package/dist/templates.d.ts +3 -32
  68. package/dist/templates.d.ts.map +1 -1
  69. package/dist/templates.js +1002 -527
  70. package/dist/templates.js.map +1 -1
  71. package/dist/version.d.ts +8 -0
  72. package/dist/version.d.ts.map +1 -0
  73. package/dist/version.js +18 -0
  74. package/dist/version.js.map +1 -0
  75. package/package.json +9 -8
  76. package/src/ansi.ts +30 -0
  77. package/src/choices.ts +137 -0
  78. package/src/completion.ts +169 -0
  79. package/src/config.ts +16 -0
  80. package/src/create-prompts.ts +182 -0
  81. package/src/create.ts +24 -31
  82. package/src/db.ts +60 -4
  83. package/src/github-annotations.ts +37 -0
  84. package/src/index.ts +1067 -803
  85. package/src/inspect.ts +2859 -115
  86. package/src/lib.ts +45 -0
  87. package/src/lint.ts +493 -39
  88. package/src/make.ts +2181 -405
  89. package/src/outbox.ts +249 -0
  90. package/src/schedule.ts +272 -0
  91. package/src/task.ts +169 -0
  92. package/src/templates.ts +1073 -567
  93. package/src/version.ts +20 -0
  94. package/dist/create-bin.d.ts +0 -3
  95. package/dist/create-bin.d.ts.map +0 -1
  96. package/dist/create-bin.js +0 -9
  97. package/dist/create-bin.js.map +0 -1
  98. package/src/create-bin.ts +0 -11
package/dist/index.js CHANGED
@@ -1,22 +1,22 @@
1
1
  #!/usr/bin/env node
2
2
  import { realpathSync } from "node:fs";
3
3
  import { fileURLToPath } from "node:url";
4
- import { buildApplication, buildCommand, buildRouteMap, run, text_en, } from "@stricli/core";
5
- import { createProject } from "./create.js";
6
- import { runDatabaseCommand } from "./db.js";
7
- import { applyDoctorFixes, formatDoctor, formatRoutes, inspectApp, } from "./inspect.js";
8
- import { formatLint, lintApp } from "./lint.js";
9
- import { makeAdapter, makeContract, makeEvent, makeFactory, makeFeature, makeFeatureAddonChoices, makeJob, makeListener, makeNotification, makePolicy, makePort, makeResource, makeSchedule, makeSeed, makeTest, makeUpload, makeUseCase, } from "./make.js";
10
- import { featureChoices, integrationChoices, presetChoices, } from "./templates.js";
11
- export { applyDoctorFixes, createProject, formatDoctor, formatLint, formatRoutes, inspectApp, lintApp, makeAdapter, makeContract, makeEvent, makeFactory, makeFeature, makeFeatureAddonChoices, makeJob, makeListener, makeNotification, makePolicy, makePort, makeResource, makeSchedule, makeSeed, makeTest, makeUpload, makeUseCase, runDatabaseCommand, };
12
- const templateChoices = ["next"];
13
- const packageManagerChoices = [
14
- "bun",
15
- "npm",
16
- "pnpm",
17
- "yarn",
4
+ import { buildApplication, buildCommand, buildRouteMap, proposeCompletions, run, text_en, } from "@stricli/core";
5
+ import { completionShellChoices, featureChoices, integrationChoices, makeFeatureAddonChoices, packageManagerChoices, presetChoices, templateChoices, } from "./choices.js";
6
+ import { getCliVersion } from "./version.js";
7
+ const outputFormatChoices = [
8
+ "human",
9
+ "json",
10
+ "github",
18
11
  ];
19
12
  const parseString = (input) => input;
13
+ const parsePositiveInteger = (input) => {
14
+ const value = Number(input);
15
+ if (!Number.isInteger(value) || value <= 0) {
16
+ throw new Error("Expected a positive integer.");
17
+ }
18
+ return value;
19
+ };
20
20
  const forceFlag = {
21
21
  kind: "boolean",
22
22
  optional: true,
@@ -41,18 +41,61 @@ const parsedStringFlag = (brief) => ({
41
41
  optional: true,
42
42
  brief,
43
43
  });
44
+ const cwdFlag = parsedStringFlag("App directory to inspect. Defaults to the current working directory.");
45
+ const formatFlag = {
46
+ kind: "enum",
47
+ values: outputFormatChoices,
48
+ optional: true,
49
+ brief: "Output format. Defaults to human, or github when GITHUB_ACTIONS=true.",
50
+ };
44
51
  const makeFlagParameters = {
45
52
  force: forceFlag,
46
53
  dryRun: dryRunFlag,
47
54
  json: jsonFlag,
48
55
  };
49
- const jsonFlagParameters = {
56
+ const routesFlagParameters = {
57
+ json: jsonFlag,
58
+ cwd: cwdFlag,
59
+ };
60
+ const lintFlagParameters = {
50
61
  json: jsonFlag,
62
+ cwd: cwdFlag,
63
+ format: formatFlag,
51
64
  };
52
65
  const dbFlagParameters = {
53
66
  json: jsonFlag,
54
67
  dryRun: dryRunFlag,
55
68
  };
69
+ const taskRunFlagParameters = {
70
+ json: jsonFlag,
71
+ input: parsedStringFlag("JSON input passed to the task schema."),
72
+ module: parsedStringFlag("Task registry module. Defaults to server/tasks.ts."),
73
+ };
74
+ const outboxDrainFlagParameters = {
75
+ json: jsonFlag,
76
+ module: parsedStringFlag("Outbox registry module. Defaults to server/outbox.ts."),
77
+ batchSize: {
78
+ kind: "parsed",
79
+ parse: parsePositiveInteger,
80
+ optional: true,
81
+ brief: "Maximum messages to claim in one drain pass.",
82
+ },
83
+ };
84
+ const scheduleRunFlagParameters = {
85
+ json: jsonFlag,
86
+ module: parsedStringFlag("Schedule registry module. Defaults to server/schedules.ts."),
87
+ payload: parsedStringFlag("JSON payload passed to the schedule schema."),
88
+ id: parsedStringFlag("Provider or app schedule run ID."),
89
+ attempt: {
90
+ kind: "parsed",
91
+ parse: parsePositiveInteger,
92
+ optional: true,
93
+ brief: "One-based provider attempt number.",
94
+ },
95
+ scheduledAt: parsedStringFlag("Provider scheduled timestamp."),
96
+ triggeredAt: parsedStringFlag("Schedule trigger timestamp."),
97
+ source: parsedStringFlag("Provider or app source label."),
98
+ };
56
99
  const namePositional = {
57
100
  kind: "tuple",
58
101
  parameters: [
@@ -63,12 +106,29 @@ const namePositional = {
63
106
  },
64
107
  ],
65
108
  };
109
+ function resolveOutputFormat(flags) {
110
+ if (flags.json) {
111
+ if (flags.format && flags.format !== "json") {
112
+ throw new Error(`--json conflicts with --format ${flags.format}. Pass one of them.`);
113
+ }
114
+ return "json";
115
+ }
116
+ if (flags.format)
117
+ return flags.format;
118
+ return process.env.GITHUB_ACTIONS === "true" ? "github" : "human";
119
+ }
120
+ function useColor() {
121
+ return process.stdout.isTTY === true && !process.env.NO_COLOR;
122
+ }
66
123
  const createCommand = buildCommand({
67
124
  docs: {
68
125
  brief: "Create a new Beignet app.",
69
126
  fullDescription: `Available features: ${featureChoices.join(", ")}
70
127
 
71
- Available integrations: ${integrationChoices.join(", ")}`,
128
+ Available integrations: ${integrationChoices.join(", ")}
129
+
130
+ Running create on an interactive terminal without selection flags opens a
131
+ prompt-based setup. Pass --yes or any selection flag to skip the prompts.`,
72
132
  },
73
133
  parameters: {
74
134
  flags: {
@@ -81,8 +141,8 @@ Available integrations: ${integrationChoices.join(", ")}`,
81
141
  preset: {
82
142
  kind: "enum",
83
143
  values: presetChoices,
84
- default: "standard",
85
- brief: "Starter preset to generate.",
144
+ optional: true,
145
+ brief: "Starter preset to generate. Defaults to standard.",
86
146
  },
87
147
  packageManager: {
88
148
  kind: "enum",
@@ -90,35 +150,29 @@ Available integrations: ${integrationChoices.join(", ")}`,
90
150
  optional: true,
91
151
  brief: "Package manager to use in next-step commands.",
92
152
  },
93
- feature: {
94
- kind: "enum",
95
- values: featureChoices,
96
- optional: true,
97
- variadic: true,
98
- brief: "Add a starter feature. Repeatable.",
99
- },
100
153
  features: {
101
154
  kind: "enum",
102
155
  values: featureChoices,
103
156
  optional: true,
104
157
  variadic: ",",
105
- brief: "Add comma-separated starter features.",
106
- },
107
- integration: {
108
- kind: "enum",
109
- values: integrationChoices,
110
- optional: true,
111
- variadic: true,
112
- brief: "Add a first-party integration. Repeatable.",
158
+ brief: "Add starter features as a comma-separated list.",
113
159
  },
114
160
  integrations: {
115
161
  kind: "enum",
116
162
  values: integrationChoices,
117
163
  optional: true,
118
164
  variadic: ",",
119
- brief: "Add comma-separated first-party integrations.",
165
+ brief: "Add first-party integrations as a comma-separated list.",
166
+ },
167
+ yes: {
168
+ kind: "boolean",
169
+ optional: true,
170
+ withNegated: false,
171
+ brief: "Skip interactive prompts and use the defaults.",
120
172
  },
121
173
  force: forceFlag,
174
+ dryRun: dryRunFlag,
175
+ json: jsonFlag,
122
176
  },
123
177
  positional: {
124
178
  kind: "tuple",
@@ -127,28 +181,53 @@ Available integrations: ${integrationChoices.join(", ")}`,
127
181
  parse: parseString,
128
182
  placeholder: "directory",
129
183
  brief: "Project directory to create.",
184
+ optional: true,
130
185
  },
131
186
  ],
132
187
  },
133
188
  },
134
- async func(flags, directory) {
135
- const integrations = uniqueValues([
136
- ...(flags.integration ?? []),
137
- ...(flags.integrations ?? []),
138
- ]);
139
- const result = await createProject({
140
- name: directory,
141
- template: flags.template,
142
- preset: flags.preset,
143
- features: uniqueValues([
144
- ...(flags.feature ?? []),
145
- ...(flags.features ?? []),
146
- ]),
147
- packageManager: flags.packageManager,
148
- integrations,
149
- force: Boolean(flags.force),
150
- });
151
- writeOutput(this, nextSteps(result, { integrations }));
189
+ loader: async () => {
190
+ const { createProject } = await import("./create.js");
191
+ const { promptCreateSelections, shouldPromptInteractively } = await import("./create-prompts.js");
192
+ return async function runCreate(flags, directory) {
193
+ let selections = {
194
+ directory,
195
+ preset: flags.preset,
196
+ features: flags.features,
197
+ integrations: flags.integrations,
198
+ packageManager: flags.packageManager,
199
+ };
200
+ const interactive = shouldPromptInteractively({ ...selections, yes: flags.yes }, {
201
+ stdin: process.stdin.isTTY === true,
202
+ stdout: process.stdout.isTTY === true,
203
+ });
204
+ if (interactive) {
205
+ const answers = await promptCreateSelections(selections);
206
+ if (answers === undefined) {
207
+ this.process.stderr.write("Cancelled.\n");
208
+ this.process.exitCode = 2;
209
+ return;
210
+ }
211
+ selections = answers;
212
+ }
213
+ if (selections.directory === undefined) {
214
+ throw new Error("Project directory is required. Pass beignet create <directory>, or run beignet create in an interactive terminal.");
215
+ }
216
+ const integrations = uniqueValues([...(selections.integrations ?? [])]);
217
+ const result = await createProject({
218
+ name: selections.directory,
219
+ template: flags.template,
220
+ preset: selections.preset ?? "standard",
221
+ features: uniqueValues([...(selections.features ?? [])]),
222
+ packageManager: selections.packageManager,
223
+ integrations,
224
+ force: Boolean(flags.force),
225
+ dryRun: Boolean(flags.dryRun),
226
+ });
227
+ writeOutput(this, flags.json
228
+ ? JSON.stringify(result, null, 2)
229
+ : nextSteps(result, { integrations }));
230
+ };
152
231
  },
153
232
  });
154
233
  const routesCommand = buildCommand({
@@ -156,11 +235,14 @@ const routesCommand = buildCommand({
156
235
  brief: "Inspect registered Beignet routes.",
157
236
  },
158
237
  parameters: {
159
- flags: jsonFlagParameters,
238
+ flags: routesFlagParameters,
160
239
  },
161
- async func(flags) {
162
- const result = await inspectApp();
163
- writeOutput(this, flags.json ? JSON.stringify(result, null, 2) : formatRoutes(result));
240
+ loader: async () => {
241
+ const { formatRoutes, inspectApp } = await import("./inspect.js");
242
+ return async function runRoutes(flags) {
243
+ const result = await inspectApp({ cwd: flags.cwd });
244
+ writeOutput(this, flags.json ? JSON.stringify(result, null, 2) : formatRoutes(result));
245
+ };
164
246
  },
165
247
  });
166
248
  const doctorCommand = buildCommand({
@@ -182,19 +264,35 @@ const doctorCommand = buildCommand({
182
264
  withNegated: false,
183
265
  brief: "Apply low-risk fixes before reporting.",
184
266
  },
267
+ cwd: cwdFlag,
268
+ format: formatFlag,
185
269
  },
186
270
  },
187
- async func(flags) {
188
- const fixes = flags.fix
189
- ? await applyDoctorFixes({ strict: Boolean(flags.strict) })
190
- : [];
191
- const result = await inspectApp({ strict: Boolean(flags.strict) });
192
- result.fixes = fixes;
193
- writeOutput(this, flags.json ? JSON.stringify(result, null, 2) : formatDoctor(result));
194
- if (result.diagnostics.some((diagnostic) => diagnostic.severity === "error" ||
195
- (flags.strict && diagnostic.severity === "warning"))) {
196
- this.process.exitCode = 1;
197
- }
271
+ loader: async () => {
272
+ const { applyDoctorFixes, formatDoctor, formatDoctorGithub, inspectApp } = await import("./inspect.js");
273
+ return async function runDoctor(flags) {
274
+ const format = resolveOutputFormat(flags);
275
+ const fixes = flags.fix
276
+ ? await applyDoctorFixes({
277
+ cwd: flags.cwd,
278
+ strict: Boolean(flags.strict),
279
+ })
280
+ : [];
281
+ const result = await inspectApp({
282
+ cwd: flags.cwd,
283
+ strict: Boolean(flags.strict),
284
+ });
285
+ result.fixes = fixes;
286
+ writeOutput(this, format === "json"
287
+ ? JSON.stringify(result, null, 2)
288
+ : format === "github"
289
+ ? formatDoctorGithub(result)
290
+ : formatDoctor(result, { color: useColor() }));
291
+ if (result.diagnostics.some((diagnostic) => diagnostic.severity === "error" ||
292
+ (flags.strict && diagnostic.severity === "warning"))) {
293
+ this.process.exitCode = 1;
294
+ }
295
+ };
198
296
  },
199
297
  });
200
298
  const lintCommand = buildCommand({
@@ -202,14 +300,22 @@ const lintCommand = buildCommand({
202
300
  brief: "Check Beignet dependency direction conventions.",
203
301
  },
204
302
  parameters: {
205
- flags: jsonFlagParameters,
206
- },
207
- async func(flags) {
208
- const result = await lintApp();
209
- writeOutput(this, flags.json ? JSON.stringify(result, null, 2) : formatLint(result));
210
- if (result.diagnostics.length > 0) {
211
- this.process.exitCode = 1;
212
- }
303
+ flags: lintFlagParameters,
304
+ },
305
+ loader: async () => {
306
+ const { formatLint, formatLintGithub, lintApp } = await import("./lint.js");
307
+ return async function runLint(flags) {
308
+ const format = resolveOutputFormat(flags);
309
+ const result = await lintApp({ cwd: flags.cwd });
310
+ writeOutput(this, format === "json"
311
+ ? JSON.stringify(result, null, 2)
312
+ : format === "github"
313
+ ? formatLintGithub(result)
314
+ : formatLint(result, { color: useColor() }));
315
+ if (result.diagnostics.length > 0) {
316
+ this.process.exitCode = 1;
317
+ }
318
+ };
213
319
  },
214
320
  });
215
321
  function databaseCommand(command) {
@@ -220,18 +326,21 @@ function databaseCommand(command) {
220
326
  parameters: {
221
327
  flags: dbFlagParameters,
222
328
  },
223
- async func(flags) {
224
- const result = await runDatabaseCommand({
225
- command,
226
- captureOutput: Boolean(flags.json),
227
- dryRun: Boolean(flags.dryRun),
228
- });
229
- writeOutput(this, flags.json
230
- ? JSON.stringify(result, null, 2)
231
- : databaseCommandNextSteps(result));
232
- if (result.exitCode !== 0) {
233
- this.process.exitCode = result.exitCode;
234
- }
329
+ loader: async () => {
330
+ const { runDatabaseCommand } = await import("./db.js");
331
+ return async function runDb(flags) {
332
+ const result = await runDatabaseCommand({
333
+ command,
334
+ captureOutput: Boolean(flags.json),
335
+ dryRun: Boolean(flags.dryRun),
336
+ });
337
+ writeOutput(this, flags.json
338
+ ? JSON.stringify(result, null, 2)
339
+ : databaseCommandNextSteps(result));
340
+ if (result.exitCode !== 0) {
341
+ this.process.exitCode = result.exitCode;
342
+ }
343
+ };
235
344
  },
236
345
  });
237
346
  }
@@ -246,330 +355,325 @@ const dbRoutes = buildRouteMap({
246
355
  seed: databaseCommand("seed"),
247
356
  },
248
357
  });
249
- const makeResourceCommand = buildCommand({
250
- docs: {
251
- brief: "Generate a feature resource.",
252
- },
253
- parameters: {
254
- flags: makeFlagParameters,
255
- positional: namePositional,
256
- },
257
- async func(flags, name) {
258
- const result = await makeResource({
259
- name,
260
- force: Boolean(flags.force),
261
- dryRun: Boolean(flags.dryRun),
262
- });
263
- writeOutput(this, flags.json
264
- ? JSON.stringify(result, null, 2)
265
- : makeResourceNextSteps(result));
266
- },
267
- });
268
- const makeFeatureCommand = buildCommand({
358
+ const taskRunCommand = buildCommand({
269
359
  docs: {
270
- brief: "Generate a contract-first feature slice.",
360
+ brief: "Run an app-owned operational task.",
271
361
  },
272
362
  parameters: {
273
- flags: {
274
- ...makeFlagParameters,
275
- with: {
276
- kind: "enum",
277
- values: makeFeatureAddonChoices,
278
- optional: true,
279
- variadic: ",",
280
- brief: "Add feature-owned artifacts. Supports policy, event/events, job/jobs, and upload/uploads.",
281
- },
282
- },
363
+ flags: taskRunFlagParameters,
283
364
  positional: namePositional,
284
365
  },
285
- async func(flags, name) {
286
- const result = await makeFeature({
287
- name,
288
- with: flags.with,
289
- force: Boolean(flags.force),
290
- dryRun: Boolean(flags.dryRun),
291
- });
292
- writeOutput(this, flags.json
293
- ? JSON.stringify(result, null, 2)
294
- : makeFeatureNextSteps(result));
366
+ loader: async () => {
367
+ const { runAppTask } = await import("./task.js");
368
+ return async function runTask(flags, name) {
369
+ const result = await runAppTask({
370
+ name,
371
+ input: flags.input,
372
+ modulePath: flags.module,
373
+ });
374
+ writeOutput(this, flags.json
375
+ ? JSON.stringify(result, null, 2)
376
+ : appTaskRunNextSteps(result));
377
+ };
295
378
  },
296
379
  });
297
- const makeContractCommand = buildCommand({
380
+ const taskRoutes = buildRouteMap({
298
381
  docs: {
299
- brief: "Generate a contract group.",
300
- },
301
- parameters: {
302
- flags: makeFlagParameters,
303
- positional: namePositional,
382
+ brief: "Run Beignet app operational tasks.",
304
383
  },
305
- async func(flags, name) {
306
- const result = await makeContract({
307
- name,
308
- force: Boolean(flags.force),
309
- dryRun: Boolean(flags.dryRun),
310
- });
311
- writeOutput(this, flags.json
312
- ? JSON.stringify(result, null, 2)
313
- : makeContractNextSteps(result));
384
+ routes: {
385
+ run: taskRunCommand,
314
386
  },
315
387
  });
316
- const makeUseCaseCommand = buildCommand({
388
+ const outboxDrainCommand = buildCommand({
317
389
  docs: {
318
- brief: "Generate a use case.",
390
+ brief: "Run one app-owned outbox drain pass.",
319
391
  },
320
392
  parameters: {
321
- flags: makeFlagParameters,
322
- positional: namePositional,
323
- },
324
- async func(flags, name) {
325
- const result = await makeUseCase({
326
- name,
327
- force: Boolean(flags.force),
328
- dryRun: Boolean(flags.dryRun),
329
- });
330
- writeOutput(this, flags.json
331
- ? JSON.stringify(result, null, 2)
332
- : makeUseCaseNextSteps(result));
393
+ flags: outboxDrainFlagParameters,
394
+ },
395
+ loader: async () => {
396
+ const { runOutboxDrain } = await import("./outbox.js");
397
+ return async function runDrain(flags) {
398
+ const result = await runOutboxDrain({
399
+ modulePath: flags.module,
400
+ batchSize: flags.batchSize,
401
+ });
402
+ writeOutput(this, flags.json
403
+ ? JSON.stringify(result, null, 2)
404
+ : outboxDrainNextSteps(result));
405
+ };
333
406
  },
334
407
  });
335
- const makeTestCommand = buildCommand({
408
+ const outboxRoutes = buildRouteMap({
336
409
  docs: {
337
- brief: "Generate a use case test.",
410
+ brief: "Run Beignet outbox operations.",
338
411
  },
339
- parameters: {
340
- flags: makeFlagParameters,
341
- positional: namePositional,
342
- },
343
- async func(flags, name) {
344
- const result = await makeTest({
345
- name,
346
- force: Boolean(flags.force),
347
- dryRun: Boolean(flags.dryRun),
348
- });
349
- writeOutput(this, flags.json ? JSON.stringify(result, null, 2) : makeTestNextSteps(result));
412
+ routes: {
413
+ drain: outboxDrainCommand,
350
414
  },
351
415
  });
352
- const makePortCommand = buildCommand({
416
+ const scheduleRunCommand = buildCommand({
353
417
  docs: {
354
- brief: "Generate an application port.",
418
+ brief: "Run an app-owned schedule.",
355
419
  },
356
420
  parameters: {
357
- flags: makeFlagParameters,
421
+ flags: scheduleRunFlagParameters,
358
422
  positional: namePositional,
359
423
  },
360
- async func(flags, name) {
361
- const result = await makePort({
362
- name,
363
- force: Boolean(flags.force),
364
- dryRun: Boolean(flags.dryRun),
365
- });
366
- writeOutput(this, flags.json ? JSON.stringify(result, null, 2) : makePortNextSteps(result));
424
+ loader: async () => {
425
+ const { runAppSchedule } = await import("./schedule.js");
426
+ return async function runSchedule(flags, name) {
427
+ const result = await runAppSchedule({
428
+ name,
429
+ modulePath: flags.module,
430
+ payload: flags.payload,
431
+ id: flags.id,
432
+ attempt: flags.attempt,
433
+ scheduledAt: flags.scheduledAt,
434
+ triggeredAt: flags.triggeredAt,
435
+ source: flags.source,
436
+ });
437
+ writeOutput(this, flags.json
438
+ ? JSON.stringify(result, null, 2)
439
+ : scheduleRunNextSteps(result));
440
+ };
367
441
  },
368
442
  });
369
- const makeAdapterCommand = buildCommand({
443
+ const scheduleRoutes = buildRouteMap({
370
444
  docs: {
371
- brief: "Generate a port adapter.",
372
- },
373
- parameters: {
374
- flags: makeFlagParameters,
375
- positional: namePositional,
445
+ brief: "Run Beignet schedule operations.",
376
446
  },
377
- async func(flags, name) {
378
- const result = await makeAdapter({
379
- name,
380
- force: Boolean(flags.force),
381
- dryRun: Boolean(flags.dryRun),
382
- });
383
- writeOutput(this, flags.json
384
- ? JSON.stringify(result, null, 2)
385
- : makeAdapterNextSteps(result));
447
+ routes: {
448
+ run: scheduleRunCommand,
386
449
  },
387
450
  });
388
- const makePolicyCommand = buildCommand({
389
- docs: {
390
- brief: "Generate an authorization policy.",
391
- },
392
- parameters: {
393
- flags: makeFlagParameters,
394
- positional: namePositional,
395
- },
396
- async func(flags, name) {
397
- const result = await makePolicy({
398
- name,
399
- force: Boolean(flags.force),
400
- dryRun: Boolean(flags.dryRun),
401
- });
402
- writeOutput(this, flags.json
403
- ? JSON.stringify(result, null, 2)
404
- : makePolicyNextSteps(result));
451
+ const completionShellFlagParameters = {
452
+ shell: {
453
+ kind: "enum",
454
+ values: completionShellChoices,
455
+ optional: true,
456
+ brief: "Shell to manage completions for. Defaults to $SHELL.",
405
457
  },
406
- });
407
- const makeEventCommand = buildCommand({
458
+ json: jsonFlag,
459
+ };
460
+ const completionInstallCommand = buildCommand({
408
461
  docs: {
409
- brief: "Generate a feature event.",
462
+ brief: "Install beignet shell completions for bash or zsh.",
410
463
  },
411
464
  parameters: {
412
- flags: makeFlagParameters,
413
- positional: namePositional,
465
+ flags: completionShellFlagParameters,
414
466
  },
415
- async func(flags, name) {
416
- const result = await makeEvent({
417
- name,
418
- force: Boolean(flags.force),
419
- dryRun: Boolean(flags.dryRun),
420
- });
421
- writeOutput(this, flags.json ? JSON.stringify(result, null, 2) : makeEventNextSteps(result));
467
+ loader: async () => {
468
+ const { installCompletions } = await import("./completion.js");
469
+ return async function runInstall(flags) {
470
+ const result = await installCompletions({ shell: flags.shell });
471
+ writeOutput(this, flags.json
472
+ ? JSON.stringify(result, null, 2)
473
+ : `${result.status === "updated" ? "Updated" : "Installed"} ${result.shell} completions in ${result.file}
474
+
475
+ Restart your shell or source the file to activate them.`);
476
+ };
422
477
  },
423
478
  });
424
- const makeJobCommand = buildCommand({
479
+ const completionUninstallCommand = buildCommand({
425
480
  docs: {
426
- brief: "Generate a feature job.",
481
+ brief: "Remove beignet shell completions for bash or zsh.",
427
482
  },
428
483
  parameters: {
429
- flags: makeFlagParameters,
430
- positional: namePositional,
484
+ flags: completionShellFlagParameters,
431
485
  },
432
- async func(flags, name) {
433
- const result = await makeJob({
434
- name,
435
- force: Boolean(flags.force),
436
- dryRun: Boolean(flags.dryRun),
437
- });
438
- writeOutput(this, flags.json ? JSON.stringify(result, null, 2) : makeJobNextSteps(result));
486
+ loader: async () => {
487
+ const { uninstallCompletions } = await import("./completion.js");
488
+ return async function runUninstall(flags) {
489
+ const result = await uninstallCompletions({ shell: flags.shell });
490
+ writeOutput(this, flags.json
491
+ ? JSON.stringify(result, null, 2)
492
+ : result.status === "removed"
493
+ ? `Removed ${result.shell} completions from ${result.file}`
494
+ : `No beignet completions found in ${result.file}`);
495
+ };
439
496
  },
440
497
  });
441
- const makeFactoryCommand = buildCommand({
498
+ const completionRoutes = buildRouteMap({
442
499
  docs: {
443
- brief: "Generate a feature test data factory.",
444
- },
445
- parameters: {
446
- flags: makeFlagParameters,
447
- positional: namePositional,
500
+ brief: "Manage beignet shell completions.",
448
501
  },
449
- async func(flags, name) {
450
- const result = await makeFactory({
451
- name,
452
- force: Boolean(flags.force),
453
- dryRun: Boolean(flags.dryRun),
454
- });
455
- writeOutput(this, flags.json
456
- ? JSON.stringify(result, null, 2)
457
- : makeFactoryNextSteps(result));
502
+ routes: {
503
+ install: completionInstallCommand,
504
+ uninstall: completionUninstallCommand,
458
505
  },
459
506
  });
460
- const makeSeedCommand = buildCommand({
507
+ const makeResourceCommand = buildCommand({
461
508
  docs: {
462
- brief: "Generate a feature seed.",
509
+ brief: "Generate a CRUD-shaped resource slice.",
463
510
  },
464
511
  parameters: {
465
- flags: makeFlagParameters,
512
+ flags: {
513
+ ...makeFlagParameters,
514
+ auth: {
515
+ kind: "boolean",
516
+ optional: true,
517
+ withNegated: false,
518
+ brief: "Generate policy metadata and use-case authorization checks for mutating routes.",
519
+ },
520
+ tenant: {
521
+ kind: "boolean",
522
+ optional: true,
523
+ withNegated: false,
524
+ brief: "Generate tenant-scoped schemas, repository methods, and use-case checks.",
525
+ },
526
+ events: {
527
+ kind: "boolean",
528
+ optional: true,
529
+ withNegated: false,
530
+ brief: "Generate domain event definitions and publish resource events.",
531
+ },
532
+ softDelete: {
533
+ kind: "boolean",
534
+ optional: true,
535
+ withNegated: false,
536
+ brief: "Generate soft-delete persistence that archives rows with deletedAt.",
537
+ },
538
+ },
466
539
  positional: namePositional,
467
540
  },
468
- async func(flags, name) {
469
- const result = await makeSeed({
470
- name,
471
- force: Boolean(flags.force),
472
- dryRun: Boolean(flags.dryRun),
473
- });
474
- writeOutput(this, flags.json ? JSON.stringify(result, null, 2) : makeSeedNextSteps(result));
541
+ loader: async () => {
542
+ const { makeResource } = await import("./make.js");
543
+ return async function runMakeResource(flags, name) {
544
+ const result = await makeResource({
545
+ name,
546
+ force: Boolean(flags.force),
547
+ dryRun: Boolean(flags.dryRun),
548
+ auth: Boolean(flags.auth),
549
+ tenant: Boolean(flags.tenant),
550
+ events: Boolean(flags.events),
551
+ softDelete: Boolean(flags.softDelete),
552
+ });
553
+ writeOutput(this, flags.json
554
+ ? JSON.stringify(result, null, 2)
555
+ : makeResourceNextSteps(result));
556
+ };
475
557
  },
476
558
  });
477
- const makeNotificationCommand = buildCommand({
559
+ const makeFeatureCommand = buildCommand({
478
560
  docs: {
479
- brief: "Generate a feature notification.",
561
+ brief: "Generate a contract-first feature slice.",
480
562
  },
481
563
  parameters: {
482
- flags: makeFlagParameters,
564
+ flags: {
565
+ ...makeFlagParameters,
566
+ with: {
567
+ kind: "enum",
568
+ values: makeFeatureAddonChoices,
569
+ optional: true,
570
+ variadic: ",",
571
+ brief: "Add feature-owned artifacts. Supports policy, task/tasks, event/events, job/jobs, notification/notifications, ui, and upload/uploads.",
572
+ },
573
+ },
483
574
  positional: namePositional,
484
575
  },
485
- async func(flags, name) {
486
- const result = await makeNotification({
487
- name,
488
- force: Boolean(flags.force),
489
- dryRun: Boolean(flags.dryRun),
490
- });
491
- writeOutput(this, flags.json
492
- ? JSON.stringify(result, null, 2)
493
- : makeNotificationNextSteps(result));
576
+ loader: async () => {
577
+ const { makeFeature } = await import("./make.js");
578
+ return async function runMakeFeature(flags, name) {
579
+ const result = await makeFeature({
580
+ name,
581
+ with: flags.with,
582
+ force: Boolean(flags.force),
583
+ dryRun: Boolean(flags.dryRun),
584
+ });
585
+ writeOutput(this, flags.json
586
+ ? JSON.stringify(result, null, 2)
587
+ : makeFeatureNextSteps(result));
588
+ };
494
589
  },
495
590
  });
496
- const makeListenerFlagParameters = {
497
- ...makeFlagParameters,
498
- event: parsedStringFlag("Event to listen to, for example posts/published."),
499
- };
591
+ function simpleMakeCommand(generator, brief, formatNextSteps) {
592
+ return buildCommand({
593
+ docs: {
594
+ brief,
595
+ },
596
+ parameters: {
597
+ flags: makeFlagParameters,
598
+ positional: namePositional,
599
+ },
600
+ loader: async () => {
601
+ const generators = await import("./make.js");
602
+ return async function runMake(flags, name) {
603
+ const result = await generators[generator]({
604
+ name,
605
+ force: Boolean(flags.force),
606
+ dryRun: Boolean(flags.dryRun),
607
+ });
608
+ writeOutput(this, flags.json
609
+ ? JSON.stringify(result, null, 2)
610
+ : formatNextSteps(result));
611
+ };
612
+ },
613
+ });
614
+ }
500
615
  const makeListenerCommand = buildCommand({
501
616
  docs: {
502
617
  brief: "Generate a feature event listener.",
503
618
  },
504
619
  parameters: {
505
- flags: makeListenerFlagParameters,
620
+ flags: {
621
+ ...makeFlagParameters,
622
+ event: parsedStringFlag("Event to listen to, for example posts/published."),
623
+ },
506
624
  positional: namePositional,
507
625
  },
508
- async func(flags, name) {
509
- if (!flags.event) {
510
- throw new Error("beignet make listener requires --event feature/name, for example --event posts/published.");
511
- }
512
- const result = await makeListener({
513
- name,
514
- event: flags.event,
515
- force: Boolean(flags.force),
516
- dryRun: Boolean(flags.dryRun),
517
- });
518
- writeOutput(this, flags.json
519
- ? JSON.stringify(result, null, 2)
520
- : makeListenerNextSteps(result));
626
+ loader: async () => {
627
+ const { makeListener } = await import("./make.js");
628
+ return async function runMakeListener(flags, name) {
629
+ if (!flags.event) {
630
+ throw new Error("beignet make listener requires --event feature/name, for example --event posts/published.");
631
+ }
632
+ const result = await makeListener({
633
+ name,
634
+ event: flags.event,
635
+ force: Boolean(flags.force),
636
+ dryRun: Boolean(flags.dryRun),
637
+ });
638
+ writeOutput(this, flags.json
639
+ ? JSON.stringify(result, null, 2)
640
+ : makeListenerNextSteps(result));
641
+ };
521
642
  },
522
643
  });
523
- const makeScheduleFlagParameters = {
524
- ...makeFlagParameters,
525
- cron: parsedStringFlag("Cron expression. Defaults to 0 9 * * *."),
526
- timezone: parsedStringFlag("IANA timezone, for example America/Chicago."),
527
- route: {
528
- kind: "boolean",
529
- optional: true,
530
- withNegated: false,
531
- brief: "Generate a Next.js cron route for this schedule.",
532
- },
533
- };
534
644
  const makeScheduleCommand = buildCommand({
535
645
  docs: {
536
646
  brief: "Generate a feature schedule.",
537
647
  },
538
648
  parameters: {
539
- flags: makeScheduleFlagParameters,
540
- positional: namePositional,
541
- },
542
- async func(flags, name) {
543
- const result = await makeSchedule({
544
- name,
545
- cron: flags.cron,
546
- timezone: flags.timezone,
547
- route: Boolean(flags.route),
548
- force: Boolean(flags.force),
549
- dryRun: Boolean(flags.dryRun),
550
- });
551
- writeOutput(this, flags.json
552
- ? JSON.stringify(result, null, 2)
553
- : makeScheduleNextSteps(result));
554
- },
555
- });
556
- const makeUploadCommand = buildCommand({
557
- docs: {
558
- brief: "Generate a feature upload.",
559
- },
560
- parameters: {
561
- flags: makeFlagParameters,
649
+ flags: {
650
+ ...makeFlagParameters,
651
+ cron: parsedStringFlag("Cron expression. Defaults to 0 9 * * *."),
652
+ timezone: parsedStringFlag("IANA timezone, for example America/Chicago."),
653
+ route: {
654
+ kind: "boolean",
655
+ optional: true,
656
+ withNegated: false,
657
+ brief: "Generate a Next.js cron route for this schedule.",
658
+ },
659
+ },
562
660
  positional: namePositional,
563
661
  },
564
- async func(flags, name) {
565
- const result = await makeUpload({
566
- name,
567
- force: Boolean(flags.force),
568
- dryRun: Boolean(flags.dryRun),
569
- });
570
- writeOutput(this, flags.json
571
- ? JSON.stringify(result, null, 2)
572
- : makeUploadNextSteps(result));
662
+ loader: async () => {
663
+ const { makeSchedule } = await import("./make.js");
664
+ return async function runMakeSchedule(flags, name) {
665
+ const result = await makeSchedule({
666
+ name,
667
+ cron: flags.cron,
668
+ timezone: flags.timezone,
669
+ route: Boolean(flags.route),
670
+ force: Boolean(flags.force),
671
+ dryRun: Boolean(flags.dryRun),
672
+ });
673
+ writeOutput(this, flags.json
674
+ ? JSON.stringify(result, null, 2)
675
+ : makeScheduleNextSteps(result));
676
+ };
573
677
  },
574
678
  });
575
679
  const makeRoutes = buildRouteMap({
@@ -577,22 +681,23 @@ const makeRoutes = buildRouteMap({
577
681
  brief: "Generate Beignet app files.",
578
682
  },
579
683
  routes: {
580
- adapter: makeAdapterCommand,
581
- contract: makeContractCommand,
582
- event: makeEventCommand,
583
- factory: makeFactoryCommand,
684
+ adapter: simpleMakeCommand("makeAdapter", "Generate a port adapter.", makeAdapterNextSteps),
685
+ contract: simpleMakeCommand("makeContract", "Generate a contract group.", makeContractNextSteps),
686
+ event: simpleMakeCommand("makeEvent", "Generate a feature event.", makeEventNextSteps),
687
+ factory: simpleMakeCommand("makeFactory", "Generate a feature test data factory.", makeFactoryNextSteps),
584
688
  feature: makeFeatureCommand,
585
- job: makeJobCommand,
586
- notification: makeNotificationCommand,
689
+ job: simpleMakeCommand("makeJob", "Generate a feature job.", makeJobNextSteps),
690
+ notification: simpleMakeCommand("makeNotification", "Generate a feature notification.", makeNotificationNextSteps),
587
691
  listener: makeListenerCommand,
588
- policy: makePolicyCommand,
589
- port: makePortCommand,
692
+ policy: simpleMakeCommand("makePolicy", "Generate an authorization policy.", makePolicyNextSteps),
693
+ port: simpleMakeCommand("makePort", "Generate an application port.", makePortNextSteps),
590
694
  resource: makeResourceCommand,
591
695
  schedule: makeScheduleCommand,
592
- seed: makeSeedCommand,
593
- test: makeTestCommand,
594
- upload: makeUploadCommand,
595
- useCase: makeUseCaseCommand,
696
+ seed: simpleMakeCommand("makeSeed", "Generate a feature seed.", makeSeedNextSteps),
697
+ task: simpleMakeCommand("makeTask", "Generate a feature operational task.", makeTaskNextSteps),
698
+ test: simpleMakeCommand("makeTest", "Generate a use case test.", makeTestNextSteps),
699
+ upload: simpleMakeCommand("makeUpload", "Generate a feature upload.", makeUploadNextSteps),
700
+ useCase: simpleMakeCommand("makeUseCase", "Generate a use case.", makeUseCaseNextSteps),
596
701
  },
597
702
  });
598
703
  const rootRoutes = buildRouteMap({
@@ -600,19 +705,26 @@ const rootRoutes = buildRouteMap({
600
705
  brief: "Beignet CLI",
601
706
  fullDescription: `Create apps, generate framework files, inspect routes, and check Beignet conventions.
602
707
 
603
- create-beignet <directory> is equivalent to beignet create <directory>.`,
708
+ Run npm create beignet@latest (or bun create beignet) to scaffold a new app.`,
604
709
  },
605
710
  routes: {
711
+ completion: completionRoutes,
606
712
  create: createCommand,
607
713
  db: dbRoutes,
608
714
  doctor: doctorCommand,
609
715
  lint: lintCommand,
610
716
  make: makeRoutes,
717
+ outbox: outboxRoutes,
611
718
  routes: routesCommand,
719
+ schedule: scheduleRoutes,
720
+ task: taskRoutes,
612
721
  },
613
722
  });
614
723
  const cli = buildApplication(rootRoutes, {
615
724
  name: "beignet",
725
+ versionInfo: {
726
+ currentVersion: getCliVersion(),
727
+ },
616
728
  scanner: {
617
729
  caseStyle: "allow-kebab-for-camel",
618
730
  },
@@ -626,7 +738,7 @@ const cli = buildApplication(rootRoutes, {
626
738
  commandErrorResult: (error) => error.message,
627
739
  },
628
740
  },
629
- determineExitCode: () => 1,
741
+ determineExitCode: () => 2,
630
742
  });
631
743
  function uniqueValues(values) {
632
744
  return [...new Set(values)];
@@ -637,9 +749,45 @@ function writeOutput(context, output) {
637
749
  function formatCliException(error) {
638
750
  return error instanceof Error ? error.message : String(error);
639
751
  }
752
+ function appTaskRunNextSteps(result) {
753
+ const output = result.output === undefined
754
+ ? "undefined"
755
+ : JSON.stringify(result.output, null, 2);
756
+ return `Ran task ${result.name} in ${result.durationMs}ms
757
+
758
+ Output:
759
+ ${output}`;
760
+ }
761
+ function outboxDrainNextSteps(result) {
762
+ return `Drained outbox in ${result.durationMs}ms
763
+
764
+ Result:
765
+ claimed: ${result.result.claimed}
766
+ delivered: ${result.result.delivered}
767
+ retried: ${result.result.retried}
768
+ deadLettered: ${result.result.deadLettered}`;
769
+ }
770
+ function scheduleRunNextSteps(result) {
771
+ return `Ran schedule ${result.name} in ${result.durationMs}ms
772
+
773
+ Run:
774
+ source: ${result.run.source}
775
+ id: ${result.run.id ?? "none"}
776
+ attempt: ${result.run.attempt ?? "none"}
777
+ scheduledAt: ${result.run.scheduledAt ?? "none"}
778
+ triggeredAt: ${result.run.triggeredAt ?? "none"}`;
779
+ }
640
780
  function nextSteps(result, options = {}) {
781
+ if (result.dryRun) {
782
+ const plannedFiles = result.files.map((file) => ` ${file}`).join("\n");
783
+ return `Would create ${result.name} at ${result.targetDir}
784
+
785
+ Planned files:
786
+ ${plannedFiles}`;
787
+ }
641
788
  const pm = result.packageManager;
642
789
  const run = pm === "npm" ? "npm run" : `${pm} run`;
790
+ const cli = packageRunnerFromPackageManager(pm);
643
791
  const envFileStep = result.files.includes(".env.example")
644
792
  ? " cp .env.example .env.local\n"
645
793
  : "";
@@ -647,35 +795,83 @@ function nextSteps(result, options = {}) {
647
795
  const envStep = envRequiredIntegrations.length > 0
648
796
  ? " Fill .env.local for selected provider integrations before starting the app.\n"
649
797
  : "";
798
+ const hasDatabase = result.files.some((file) => file.startsWith("infra/db/"));
799
+ const databaseStep = hasDatabase
800
+ ? `
801
+ Prepare the database:
802
+ ${cli} db generate
803
+ ${cli} db migrate
804
+ `
805
+ : "";
806
+ const featureDatabaseSteps = hasDatabase
807
+ ? `
808
+ ${cli} db generate
809
+ ${cli} db migrate`
810
+ : "";
650
811
  return `Created ${result.name} at ${result.targetDir}
651
812
 
652
813
  Next steps:
653
814
  cd ${result.targetDir}
654
815
  ${pm} install
655
- ${envFileStep}${envStep}\
656
- ${run} dev`;
816
+ ${envFileStep}${envStep}${databaseStep}
817
+ Start the app:
818
+ ${run} dev
819
+
820
+ Inspect the app:
821
+ ${cli} routes
822
+ ${cli} lint
823
+ ${cli} doctor
824
+
825
+ Generate a feature:
826
+ ${cli} make feature projects${featureDatabaseSteps}
827
+ ${run} test
828
+ ${run} typecheck
829
+ ${cli} lint
830
+ ${cli} doctor`;
831
+ }
832
+ function packageRunnerFromPackageManager(pm) {
833
+ switch (pm) {
834
+ case "npm":
835
+ return "npm run beignet --";
836
+ case "pnpm":
837
+ return "pnpm beignet";
838
+ case "yarn":
839
+ return "yarn beignet";
840
+ default:
841
+ return "bun beignet";
842
+ }
657
843
  }
658
844
  function isEnvRequiredProviderIntegration(integration) {
659
845
  return (integration === "inngest" ||
660
846
  integration === "resend" ||
661
847
  integration === "upstash-rate-limit");
662
848
  }
663
- function makeResourceNextSteps(result) {
664
- const changedFiles = [...result.createdFiles, ...result.updatedFiles]
665
- .map((file) => ` ${file}`)
666
- .join("\n");
667
- const skippedFiles = result.skippedFiles
668
- .map((file) => ` ${file}`)
669
- .join("\n");
670
- return `${result.dryRun ? "Would create" : "Created"} ${result.name} resource in ${result.targetDir}
849
+ function changedFileLines(result) {
850
+ return {
851
+ changedFiles: [...result.createdFiles, ...result.updatedFiles]
852
+ .map((file) => ` ${file}`)
853
+ .join("\n"),
854
+ skippedFiles: result.skippedFiles.map((file) => ` ${file}`).join("\n"),
855
+ };
856
+ }
857
+ function makeNextSteps(result, noun, steps) {
858
+ const { changedFiles, skippedFiles } = changedFileLines(result);
859
+ return `${result.dryRun ? "Would create" : "Created"} ${result.name} ${noun} in ${result.targetDir}
671
860
 
672
861
  Changed files:
673
862
  ${changedFiles || " none"}
674
863
  ${skippedFiles ? `\nSkipped identical files:\n${skippedFiles}` : ""}
675
864
 
676
865
  Next steps:
677
- Review the generated schemas and repository fields.
678
- Run your app's typecheck and test commands.`;
866
+ ${steps.map((step) => ` ${step}`).join("\n")}`;
867
+ }
868
+ function makeResourceNextSteps(result) {
869
+ return makeNextSteps(result, "resource", [
870
+ "Use make resource when the feature maps cleanly to a REST resource with repository-backed persistence.",
871
+ "Review the generated schemas and repository fields.",
872
+ "If this app uses Drizzle, run beignet db generate and beignet db migrate so the new schema is ready.",
873
+ "Run your app's test and typecheck commands, then beignet lint and beignet doctor.",
874
+ ]);
679
875
  }
680
876
  function databaseCommandNextSteps(result) {
681
877
  const command = [result.runner, ...result.args].join(" ");
@@ -686,272 +882,145 @@ Command:
686
882
  ${command}`;
687
883
  }
688
884
  function makeFeatureNextSteps(result) {
689
- const changedFiles = [...result.createdFiles, ...result.updatedFiles]
690
- .map((file) => ` ${file}`)
691
- .join("\n");
692
- const skippedFiles = result.skippedFiles
693
- .map((file) => ` ${file}`)
694
- .join("\n");
695
- return `${result.dryRun ? "Would create" : "Created"} ${result.name} feature slice in ${result.targetDir}
696
-
697
- Changed files:
698
- ${changedFiles || " none"}
699
- ${skippedFiles ? `\nSkipped identical files:\n${skippedFiles}` : ""}
700
-
701
- Next steps:
702
- Treat the generated name field as a placeholder and shape the contracts, use cases, and repository around the feature's real workflow.
703
- Run your app's typecheck, test, beignet lint, and beignet doctor commands.`;
885
+ return makeNextSteps(result, "feature slice", [
886
+ "Use make feature for product capabilities and workflows. Use make resource when the concept is mostly CRUD-shaped.",
887
+ "Treat the generated name field as a placeholder and shape the contracts, use cases, and repository around the feature's real workflow.",
888
+ "If this app uses Drizzle, run beignet db generate and beignet db migrate so the new schema is ready.",
889
+ "Run your app's test and typecheck commands, then beignet lint and beignet doctor.",
890
+ ]);
704
891
  }
705
892
  function makeContractNextSteps(result) {
706
- const changedFiles = [...result.createdFiles, ...result.updatedFiles]
707
- .map((file) => ` ${file}`)
708
- .join("\n");
709
- const skippedFiles = result.skippedFiles
710
- .map((file) => ` ${file}`)
711
- .join("\n");
712
- return `${result.dryRun ? "Would create" : "Created"} ${result.name} contract in ${result.targetDir}
713
-
714
- Changed files:
715
- ${changedFiles || " none"}
716
- ${skippedFiles ? `\nSkipped identical files:\n${skippedFiles}` : ""}
717
-
718
- Next steps:
719
- Add route handlers for the generated contracts.
720
- Register the exported contract list with OpenAPI if this app publishes docs.`;
893
+ return makeNextSteps(result, "contract", [
894
+ "Add route handlers for the generated contracts.",
895
+ "Register the exported contract list with OpenAPI if this app publishes docs.",
896
+ ]);
897
+ }
898
+ function makeTaskNextSteps(result) {
899
+ return makeNextSteps(result, "task", [
900
+ "Replace the starter input schema and handler with the operational workflow.",
901
+ "Keep business rules in use cases and call them from the task handler.",
902
+ ]);
721
903
  }
722
904
  function makeUseCaseNextSteps(result) {
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
- return `${result.dryRun ? "Would create" : "Created"} ${result.name} use case in ${result.targetDir}
730
-
731
- Changed files:
732
- ${changedFiles || " none"}
733
- ${skippedFiles ? `\nSkipped identical files:\n${skippedFiles}` : ""}
734
-
735
- Next steps:
736
- Replace the starter input and output schemas with domain-specific shapes.
737
- Add a focused use case test before wiring it to an HTTP route.`;
905
+ return makeNextSteps(result, "use case", [
906
+ "Replace the starter input and output schemas with domain-specific shapes.",
907
+ "Add a focused use case test before wiring it to an HTTP route.",
908
+ ]);
738
909
  }
739
910
  function makeTestNextSteps(result) {
740
- const changedFiles = [...result.createdFiles, ...result.updatedFiles]
741
- .map((file) => ` ${file}`)
742
- .join("\n");
743
- const skippedFiles = result.skippedFiles
744
- .map((file) => ` ${file}`)
745
- .join("\n");
746
- return `${result.dryRun ? "Would create" : "Created"} ${result.name} test in ${result.targetDir}
747
-
748
- Changed files:
749
- ${changedFiles || " none"}
750
- ${skippedFiles ? `\nSkipped identical files:\n${skippedFiles}` : ""}
751
-
752
- Next steps:
753
- Replace the starter input, context, and assertion with behavior-specific coverage.
754
- Run your app's test command.`;
911
+ return makeNextSteps(result, "test", [
912
+ "Replace the starter input, context, and assertion with behavior-specific coverage.",
913
+ "Run your app's test command.",
914
+ ]);
755
915
  }
756
916
  function makePortNextSteps(result) {
757
- const changedFiles = [...result.createdFiles, ...result.updatedFiles]
758
- .map((file) => ` ${file}`)
759
- .join("\n");
760
- const skippedFiles = result.skippedFiles
761
- .map((file) => ` ${file}`)
762
- .join("\n");
763
- return `${result.dryRun ? "Would create" : "Created"} ${result.name} port in ${result.targetDir}
764
-
765
- Changed files:
766
- ${changedFiles || " none"}
767
- ${skippedFiles ? `\nSkipped identical files:\n${skippedFiles}` : ""}
768
-
769
- Next steps:
770
- Replace the starter execute method with domain-specific operations.
771
- Replace the generated infrastructure stub with a real port adapter.`;
917
+ return makeNextSteps(result, "port", [
918
+ "Replace the starter execute method with domain-specific operations.",
919
+ "Replace the generated infrastructure stub with a real port adapter.",
920
+ ]);
772
921
  }
773
922
  function makePolicyNextSteps(result) {
774
- const changedFiles = [...result.createdFiles, ...result.updatedFiles]
775
- .map((file) => ` ${file}`)
776
- .join("\n");
777
- const skippedFiles = result.skippedFiles
778
- .map((file) => ` ${file}`)
779
- .join("\n");
780
- return `${result.dryRun ? "Would create" : "Created"} ${result.name} policy in ${result.targetDir}
781
-
782
- Changed files:
783
- ${changedFiles || " none"}
784
- ${skippedFiles ? `\nSkipped identical files:\n${skippedFiles}` : ""}
785
-
786
- Next steps:
787
- Replace the starter abilities with domain-specific authorization rules.
788
- Register the policy with createGate(...) and bind it in request context.`;
923
+ return makeNextSteps(result, "policy", [
924
+ "Replace the starter abilities with domain-specific authorization rules.",
925
+ "Register the policy with createGate(...) and bind it in request context.",
926
+ ]);
789
927
  }
790
928
  function makeAdapterNextSteps(result) {
791
- const changedFiles = [...result.createdFiles, ...result.updatedFiles]
792
- .map((file) => ` ${file}`)
793
- .join("\n");
794
- const skippedFiles = result.skippedFiles
795
- .map((file) => ` ${file}`)
796
- .join("\n");
797
- return `${result.dryRun ? "Would create" : "Created"} ${result.name} port adapter in ${result.targetDir}
798
-
799
- Changed files:
800
- ${changedFiles || " none"}
801
- ${skippedFiles ? `\nSkipped identical files:\n${skippedFiles}` : ""}
802
-
803
- Next steps:
804
- Replace the generated throwing implementation with real infrastructure code.
805
- Keep the adapter behind the port interface so use cases stay infrastructure-agnostic.`;
929
+ return makeNextSteps(result, "port adapter", [
930
+ "Replace the generated throwing implementation with real infrastructure code.",
931
+ "Keep the adapter behind the port interface so use cases stay infrastructure-agnostic.",
932
+ ]);
806
933
  }
807
934
  function makeEventNextSteps(result) {
808
- const changedFiles = [...result.createdFiles, ...result.updatedFiles]
809
- .map((file) => ` ${file}`)
810
- .join("\n");
811
- const skippedFiles = result.skippedFiles
812
- .map((file) => ` ${file}`)
813
- .join("\n");
814
- return `${result.dryRun ? "Would create" : "Created"} ${result.name} event 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 payload schema with the domain fact shape.
822
- Emit the event from a use case with .emits(...) and events.record(...).`;
935
+ return makeNextSteps(result, "event", [
936
+ "Replace the starter payload schema with the domain fact shape.",
937
+ "Emit the event from a use case with .emits(...) and events.record(...).",
938
+ ]);
823
939
  }
824
940
  function makeJobNextSteps(result) {
825
- const changedFiles = [...result.createdFiles, ...result.updatedFiles]
826
- .map((file) => ` ${file}`)
827
- .join("\n");
828
- const skippedFiles = result.skippedFiles
829
- .map((file) => ` ${file}`)
830
- .join("\n");
831
- return `${result.dryRun ? "Would create" : "Created"} ${result.name} job in ${result.targetDir}
832
-
833
- Changed files:
834
- ${changedFiles || " none"}
835
- ${skippedFiles ? `\nSkipped identical files:\n${skippedFiles}` : ""}
836
-
837
- Next steps:
838
- Replace the starter payload and handler with the real background work.
839
- Dispatch the job through ctx.ports.jobs from a use case, listener, or schedule.`;
941
+ return makeNextSteps(result, "job", [
942
+ "Replace the starter payload and handler with the real background work.",
943
+ "Dispatch the job through ctx.ports.jobs from a use case, listener, or schedule.",
944
+ ]);
840
945
  }
841
946
  function makeFactoryNextSteps(result) {
842
- const changedFiles = [...result.createdFiles, ...result.updatedFiles]
843
- .map((file) => ` ${file}`)
844
- .join("\n");
845
- const skippedFiles = result.skippedFiles
846
- .map((file) => ` ${file}`)
847
- .join("\n");
848
- return `${result.dryRun ? "Would create" : "Created"} ${result.name} factory in ${result.targetDir}
849
-
850
- Changed files:
851
- ${changedFiles || " none"}
852
- ${skippedFiles ? `\nSkipped identical files:\n${skippedFiles}` : ""}
853
-
854
- Next steps:
855
- Replace the starter defaults with realistic test data for this feature.
856
- Persist through app-owned repository ports so tests avoid raw database clients.`;
947
+ return makeNextSteps(result, "factory", [
948
+ "Replace the starter defaults with realistic test data for this feature.",
949
+ "Persist through app-owned repository ports so tests avoid raw database clients.",
950
+ ]);
857
951
  }
858
952
  function makeSeedNextSteps(result) {
859
- const changedFiles = [...result.createdFiles, ...result.updatedFiles]
860
- .map((file) => ` ${file}`)
861
- .join("\n");
862
- const skippedFiles = result.skippedFiles
863
- .map((file) => ` ${file}`)
864
- .join("\n");
865
- return `${result.dryRun ? "Would create" : "Created"} ${result.name} seed in ${result.targetDir}
866
-
867
- Changed files:
868
- ${changedFiles || " none"}
869
- ${skippedFiles ? `\nSkipped identical files:\n${skippedFiles}` : ""}
870
-
871
- Next steps:
872
- Replace the starter record with idempotent local or demo data.
873
- Import the feature seed from your app-owned database seed entrypoint and run it with runSeeds(...).`;
953
+ return makeNextSteps(result, "seed", [
954
+ "Replace the starter record with idempotent local or demo data.",
955
+ "Import the feature seed from your app-owned database seed entrypoint and run it with runSeeds(...).",
956
+ ]);
874
957
  }
875
958
  function makeNotificationNextSteps(result) {
876
- const changedFiles = [...result.createdFiles, ...result.updatedFiles]
877
- .map((file) => ` ${file}`)
878
- .join("\n");
879
- const skippedFiles = result.skippedFiles
880
- .map((file) => ` ${file}`)
881
- .join("\n");
882
- return `${result.dryRun ? "Would create" : "Created"} ${result.name} notification in ${result.targetDir}
883
-
884
- Changed files:
885
- ${changedFiles || " none"}
886
- ${skippedFiles ? `\nSkipped identical files:\n${skippedFiles}` : ""}
887
-
888
- Next steps:
889
- Replace the starter payload and channels with the real user-facing message.
890
- Send the notification through ctx.ports.notifications so delivery can run inline in tests and through jobs in production.`;
959
+ return makeNextSteps(result, "notification", [
960
+ "Replace the starter payload and channels with the real user-facing message.",
961
+ "Send the notification through ctx.ports.notifications so delivery can run inline in tests and through jobs in production.",
962
+ ]);
891
963
  }
892
964
  function makeListenerNextSteps(result) {
893
- const changedFiles = [...result.createdFiles, ...result.updatedFiles]
894
- .map((file) => ` ${file}`)
895
- .join("\n");
896
- const skippedFiles = result.skippedFiles
897
- .map((file) => ` ${file}`)
898
- .join("\n");
899
- return `${result.dryRun ? "Would create" : "Created"} ${result.name} listener in ${result.targetDir}
900
-
901
- Changed files:
902
- ${changedFiles || " none"}
903
- ${skippedFiles ? `\nSkipped identical files:\n${skippedFiles}` : ""}
904
-
905
- Next steps:
906
- Replace the starter handler with the event reaction.
907
- Register the listener collection from infrastructure startup.`;
965
+ return makeNextSteps(result, "listener", [
966
+ "Replace the starter handler with the event reaction.",
967
+ "Register the listener collection from infrastructure startup.",
968
+ ]);
908
969
  }
909
970
  function makeScheduleNextSteps(result) {
910
- const changedFiles = [...result.createdFiles, ...result.updatedFiles]
911
- .map((file) => ` ${file}`)
912
- .join("\n");
913
- const skippedFiles = result.skippedFiles
914
- .map((file) => ` ${file}`)
915
- .join("\n");
916
- return `${result.dryRun ? "Would create" : "Created"} ${result.name} schedule in ${result.targetDir}
917
-
918
- Changed files:
919
- ${changedFiles || " none"}
920
- ${skippedFiles ? `\nSkipped identical files:\n${skippedFiles}` : ""}
921
-
922
- Next steps:
923
- Replace the starter payload and handler with the scheduled workflow.
924
- Trigger it from a cron route, worker, or provider adapter.`;
971
+ return makeNextSteps(result, "schedule", [
972
+ "Replace the starter payload and handler with the scheduled workflow.",
973
+ "Trigger it from a cron route, worker, or provider adapter.",
974
+ ]);
925
975
  }
926
976
  function makeUploadNextSteps(result) {
927
- const changedFiles = [...result.createdFiles, ...result.updatedFiles]
928
- .map((file) => ` ${file}`)
929
- .join("\n");
930
- const skippedFiles = result.skippedFiles
931
- .map((file) => ` ${file}`)
932
- .join("\n");
933
- return `${result.dryRun ? "Would create" : "Created"} ${result.name} upload in ${result.targetDir}
934
-
935
- Changed files:
936
- ${changedFiles || " none"}
937
- ${skippedFiles ? `\nSkipped identical files:\n${skippedFiles}` : ""}
938
-
939
- Next steps:
940
- Replace the starter metadata, constraints, key, authorization, and onComplete behavior.
941
- Register the upload in createUploadRouter(...) and expose it with createUploadRoute(...).`;
977
+ return makeNextSteps(result, "upload", [
978
+ "Replace the starter metadata, constraints, key, authorization, and onComplete behavior.",
979
+ "Register the upload in createUploadRouter(...) and expose it with createUploadRoute(...).",
980
+ ]);
942
981
  }
943
982
  export async function main(inputs = process.argv.slice(2), context = { process }) {
944
983
  if (inputs.length === 0) {
945
984
  await run(cli, ["--help"], context);
946
- context.process.exitCode = 1;
985
+ context.process.exitCode = 2;
986
+ return;
987
+ }
988
+ // Shell completion scripts call `beignet completion propose <words...>`.
989
+ // Handle it before stricli parses the inputs: the trailing words are a
990
+ // partial command line, not flags for this CLI.
991
+ if (inputs[0] === "completion" && inputs[1] === "propose") {
992
+ await writeCompletionProposals(inputs.slice(2), context);
947
993
  return;
948
994
  }
949
995
  await run(cli, inputs, context);
950
996
  normalizeExitCode(context.process);
951
997
  }
998
+ async function writeCompletionProposals(inputs, context) {
999
+ try {
1000
+ const proposals = await proposeCompletions(cli, inputs.length === 0 ? [""] : inputs, context);
1001
+ for (const proposal of proposals) {
1002
+ context.process.stdout.write(`${proposal.completion}\n`);
1003
+ }
1004
+ }
1005
+ catch {
1006
+ // Completion failures must never break the shell; propose nothing.
1007
+ }
1008
+ }
1009
+ // Stricli reports usage and internal failures as negative ExitCode values
1010
+ // (-5..-1). Node keeps the negative value on process.exitCode while Bun wraps
1011
+ // it to 251..255 on assignment, so both forms map to the usage exit code.
1012
+ const stricliErrorExitCodes = new Set([
1013
+ -5, -4, -3, -2, -1, 251, 252, 253, 254, 255,
1014
+ ]);
952
1015
  function normalizeExitCode(proc) {
953
- if (typeof proc.exitCode === "number" &&
954
- (proc.exitCode < 0 || proc.exitCode > 127)) {
1016
+ if (typeof proc.exitCode !== "number") {
1017
+ return;
1018
+ }
1019
+ if (stricliErrorExitCodes.has(proc.exitCode)) {
1020
+ proc.exitCode = 2;
1021
+ return;
1022
+ }
1023
+ if (proc.exitCode > 127) {
955
1024
  proc.exitCode = 1;
956
1025
  }
957
1026
  }
@@ -970,7 +1039,7 @@ function isCliEntrypoint() {
970
1039
  if (isCliEntrypoint()) {
971
1040
  main().catch((error) => {
972
1041
  console.error(error instanceof Error ? error.message : String(error));
973
- process.exitCode = 1;
1042
+ process.exitCode = 2;
974
1043
  });
975
1044
  }
976
1045
  //# sourceMappingURL=index.js.map