@beignet/cli 0.0.1 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/CHANGELOG.md +231 -0
  2. package/README.md +497 -90
  3. package/dist/ansi.d.ts +10 -0
  4. package/dist/ansi.d.ts.map +1 -0
  5. package/dist/ansi.js +20 -0
  6. package/dist/ansi.js.map +1 -0
  7. package/dist/choices.d.ts +72 -0
  8. package/dist/choices.d.ts.map +1 -0
  9. package/dist/choices.js +88 -0
  10. package/dist/choices.js.map +1 -0
  11. package/dist/completion.d.ts +47 -0
  12. package/dist/completion.d.ts.map +1 -0
  13. package/dist/completion.js +123 -0
  14. package/dist/completion.js.map +1 -0
  15. package/dist/config.d.ts +39 -0
  16. package/dist/config.d.ts.map +1 -1
  17. package/dist/config.js +26 -0
  18. package/dist/config.js.map +1 -1
  19. package/dist/create-prompts.d.ts +42 -0
  20. package/dist/create-prompts.d.ts.map +1 -0
  21. package/dist/create-prompts.js +136 -0
  22. package/dist/create-prompts.js.map +1 -0
  23. package/dist/create.d.ts +12 -0
  24. package/dist/create.d.ts.map +1 -1
  25. package/dist/create.js +19 -24
  26. package/dist/create.js.map +1 -1
  27. package/dist/db.d.ts +37 -0
  28. package/dist/db.d.ts.map +1 -0
  29. package/dist/db.js +146 -0
  30. package/dist/db.js.map +1 -0
  31. package/dist/github-annotations.d.ts +18 -0
  32. package/dist/github-annotations.d.ts.map +1 -0
  33. package/dist/github-annotations.js +22 -0
  34. package/dist/github-annotations.js.map +1 -0
  35. package/dist/index.d.ts +1 -7
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +710 -400
  38. package/dist/index.js.map +1 -1
  39. package/dist/inspect.d.ts +45 -2
  40. package/dist/inspect.d.ts.map +1 -1
  41. package/dist/inspect.js +2191 -100
  42. package/dist/inspect.js.map +1 -1
  43. package/dist/lib.d.ts +20 -0
  44. package/dist/lib.d.ts.map +1 -0
  45. package/dist/lib.js +17 -0
  46. package/dist/lib.js.map +1 -0
  47. package/dist/lint.d.ts +22 -1
  48. package/dist/lint.d.ts.map +1 -1
  49. package/dist/lint.js +370 -38
  50. package/dist/lint.js.map +1 -1
  51. package/dist/make.d.ts +109 -1
  52. package/dist/make.d.ts.map +1 -1
  53. package/dist/make.js +2225 -333
  54. package/dist/make.js.map +1 -1
  55. package/dist/outbox.d.ts +24 -0
  56. package/dist/outbox.d.ts.map +1 -0
  57. package/dist/outbox.js +138 -0
  58. package/dist/outbox.js.map +1 -0
  59. package/dist/schedule.d.ts +36 -0
  60. package/dist/schedule.d.ts.map +1 -0
  61. package/dist/schedule.js +155 -0
  62. package/dist/schedule.js.map +1 -0
  63. package/dist/task.d.ts +26 -0
  64. package/dist/task.d.ts.map +1 -0
  65. package/dist/task.js +106 -0
  66. package/dist/task.js.map +1 -0
  67. package/dist/templates.d.ts +12 -8
  68. package/dist/templates.d.ts.map +1 -1
  69. package/dist/templates.js +2144 -385
  70. package/dist/templates.js.map +1 -1
  71. package/dist/version.d.ts +8 -0
  72. package/dist/version.d.ts.map +1 -0
  73. package/dist/version.js +18 -0
  74. package/dist/version.js.map +1 -0
  75. package/package.json +9 -8
  76. package/src/ansi.ts +30 -0
  77. package/src/choices.ts +137 -0
  78. package/src/completion.ts +169 -0
  79. package/src/config.ts +47 -0
  80. package/src/create-prompts.ts +182 -0
  81. package/src/create.ts +32 -28
  82. package/src/db.ts +222 -0
  83. package/src/github-annotations.ts +37 -0
  84. package/src/index.ts +1119 -535
  85. package/src/inspect.ts +3372 -134
  86. package/src/lib.ts +45 -0
  87. package/src/lint.ts +533 -45
  88. package/src/make.ts +3010 -397
  89. package/src/outbox.ts +249 -0
  90. package/src/schedule.ts +272 -0
  91. package/src/task.ts +169 -0
  92. package/src/templates.ts +2282 -462
  93. package/src/version.ts +20 -0
  94. package/dist/create-bin.d.ts +0 -3
  95. package/dist/create-bin.d.ts.map +0 -1
  96. package/dist/create-bin.js +0 -9
  97. package/dist/create-bin.js.map +0 -1
  98. package/src/create-bin.ts +0 -11
package/dist/index.js CHANGED
@@ -1,21 +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 { applyDoctorFixes, formatDoctor, formatRoutes, inspectApp, } from "./inspect.js";
7
- import { formatLint, lintApp } from "./lint.js";
8
- import { makeAdapter, makeContract, makeEvent, makeJob, makeListener, makePolicy, makePort, makeResource, makeSchedule, makeTest, makeUseCase, } from "./make.js";
9
- import { featureChoices, integrationChoices, presetChoices, } from "./templates.js";
10
- export { applyDoctorFixes, createProject, formatDoctor, formatLint, formatRoutes, inspectApp, lintApp, makeAdapter, makeContract, makeEvent, makeJob, makeListener, makePolicy, makePort, makeResource, makeSchedule, makeTest, makeUseCase, };
11
- const templateChoices = ["next"];
12
- const packageManagerChoices = [
13
- "bun",
14
- "npm",
15
- "pnpm",
16
- "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",
17
11
  ];
18
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
+ };
19
20
  const forceFlag = {
20
21
  kind: "boolean",
21
22
  optional: true,
@@ -40,13 +41,60 @@ const parsedStringFlag = (brief) => ({
40
41
  optional: true,
41
42
  brief,
42
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
+ };
43
51
  const makeFlagParameters = {
44
52
  force: forceFlag,
45
53
  dryRun: dryRunFlag,
46
54
  json: jsonFlag,
47
55
  };
48
- const jsonFlagParameters = {
56
+ const routesFlagParameters = {
57
+ json: jsonFlag,
58
+ cwd: cwdFlag,
59
+ };
60
+ const lintFlagParameters = {
61
+ json: jsonFlag,
62
+ cwd: cwdFlag,
63
+ format: formatFlag,
64
+ };
65
+ const dbFlagParameters = {
66
+ json: jsonFlag,
67
+ dryRun: dryRunFlag,
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 = {
49
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."),
50
98
  };
51
99
  const namePositional = {
52
100
  kind: "tuple",
@@ -58,12 +106,29 @@ const namePositional = {
58
106
  },
59
107
  ],
60
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
+ }
61
123
  const createCommand = buildCommand({
62
124
  docs: {
63
125
  brief: "Create a new Beignet app.",
64
126
  fullDescription: `Available features: ${featureChoices.join(", ")}
65
127
 
66
- 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.`,
67
132
  },
68
133
  parameters: {
69
134
  flags: {
@@ -76,8 +141,8 @@ Available integrations: ${integrationChoices.join(", ")}`,
76
141
  preset: {
77
142
  kind: "enum",
78
143
  values: presetChoices,
79
- default: "standard",
80
- brief: "Starter preset to generate.",
144
+ optional: true,
145
+ brief: "Starter preset to generate. Defaults to standard.",
81
146
  },
82
147
  packageManager: {
83
148
  kind: "enum",
@@ -85,35 +150,29 @@ Available integrations: ${integrationChoices.join(", ")}`,
85
150
  optional: true,
86
151
  brief: "Package manager to use in next-step commands.",
87
152
  },
88
- feature: {
89
- kind: "enum",
90
- values: featureChoices,
91
- optional: true,
92
- variadic: true,
93
- brief: "Add a starter feature. Repeatable.",
94
- },
95
153
  features: {
96
154
  kind: "enum",
97
155
  values: featureChoices,
98
156
  optional: true,
99
157
  variadic: ",",
100
- brief: "Add comma-separated starter features.",
101
- },
102
- integration: {
103
- kind: "enum",
104
- values: integrationChoices,
105
- optional: true,
106
- variadic: true,
107
- brief: "Add a first-party integration. Repeatable.",
158
+ brief: "Add starter features as a comma-separated list.",
108
159
  },
109
160
  integrations: {
110
161
  kind: "enum",
111
162
  values: integrationChoices,
112
163
  optional: true,
113
164
  variadic: ",",
114
- 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.",
115
172
  },
116
173
  force: forceFlag,
174
+ dryRun: dryRunFlag,
175
+ json: jsonFlag,
117
176
  },
118
177
  positional: {
119
178
  kind: "tuple",
@@ -122,28 +181,53 @@ Available integrations: ${integrationChoices.join(", ")}`,
122
181
  parse: parseString,
123
182
  placeholder: "directory",
124
183
  brief: "Project directory to create.",
184
+ optional: true,
125
185
  },
126
186
  ],
127
187
  },
128
188
  },
129
- async func(flags, directory) {
130
- const integrations = uniqueValues([
131
- ...(flags.integration ?? []),
132
- ...(flags.integrations ?? []),
133
- ]);
134
- const result = await createProject({
135
- name: directory,
136
- template: flags.template,
137
- preset: flags.preset,
138
- features: uniqueValues([
139
- ...(flags.feature ?? []),
140
- ...(flags.features ?? []),
141
- ]),
142
- packageManager: flags.packageManager,
143
- integrations,
144
- force: Boolean(flags.force),
145
- });
146
- 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
+ };
147
231
  },
148
232
  });
149
233
  const routesCommand = buildCommand({
@@ -151,11 +235,14 @@ const routesCommand = buildCommand({
151
235
  brief: "Inspect registered Beignet routes.",
152
236
  },
153
237
  parameters: {
154
- flags: jsonFlagParameters,
238
+ flags: routesFlagParameters,
155
239
  },
156
- async func(flags) {
157
- const result = await inspectApp();
158
- 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
+ };
159
246
  },
160
247
  });
161
248
  const doctorCommand = buildCommand({
@@ -177,19 +264,35 @@ const doctorCommand = buildCommand({
177
264
  withNegated: false,
178
265
  brief: "Apply low-risk fixes before reporting.",
179
266
  },
267
+ cwd: cwdFlag,
268
+ format: formatFlag,
180
269
  },
181
270
  },
182
- async func(flags) {
183
- const fixes = flags.fix
184
- ? await applyDoctorFixes({ strict: Boolean(flags.strict) })
185
- : [];
186
- const result = await inspectApp({ strict: Boolean(flags.strict) });
187
- result.fixes = fixes;
188
- writeOutput(this, flags.json ? JSON.stringify(result, null, 2) : formatDoctor(result));
189
- if (result.diagnostics.some((diagnostic) => diagnostic.severity === "error" ||
190
- (flags.strict && diagnostic.severity === "warning"))) {
191
- this.process.exitCode = 1;
192
- }
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
+ };
193
296
  },
194
297
  });
195
298
  const lintCommand = buildCommand({
@@ -197,237 +300,380 @@ const lintCommand = buildCommand({
197
300
  brief: "Check Beignet dependency direction conventions.",
198
301
  },
199
302
  parameters: {
200
- flags: jsonFlagParameters,
303
+ flags: lintFlagParameters,
201
304
  },
202
- async func(flags) {
203
- const result = await lintApp();
204
- writeOutput(this, flags.json ? JSON.stringify(result, null, 2) : formatLint(result));
205
- if (result.diagnostics.length > 0) {
206
- this.process.exitCode = 1;
207
- }
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
+ };
208
319
  },
209
320
  });
210
- const makeResourceCommand = buildCommand({
321
+ function databaseCommand(command) {
322
+ return buildCommand({
323
+ docs: {
324
+ brief: `Run the app's db:${command} script.`,
325
+ },
326
+ parameters: {
327
+ flags: dbFlagParameters,
328
+ },
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
+ };
344
+ },
345
+ });
346
+ }
347
+ const dbRoutes = buildRouteMap({
211
348
  docs: {
212
- brief: "Generate a feature resource.",
213
- },
214
- parameters: {
215
- flags: makeFlagParameters,
216
- positional: namePositional,
349
+ brief: "Run Beignet database lifecycle commands.",
217
350
  },
218
- async func(flags, name) {
219
- const result = await makeResource({
220
- name,
221
- force: Boolean(flags.force),
222
- dryRun: Boolean(flags.dryRun),
223
- });
224
- writeOutput(this, flags.json
225
- ? JSON.stringify(result, null, 2)
226
- : makeResourceNextSteps(result));
351
+ routes: {
352
+ generate: databaseCommand("generate"),
353
+ migrate: databaseCommand("migrate"),
354
+ reset: databaseCommand("reset"),
355
+ seed: databaseCommand("seed"),
227
356
  },
228
357
  });
229
- const makeContractCommand = buildCommand({
358
+ const taskRunCommand = buildCommand({
230
359
  docs: {
231
- brief: "Generate a contract group.",
360
+ brief: "Run an app-owned operational task.",
232
361
  },
233
362
  parameters: {
234
- flags: makeFlagParameters,
363
+ flags: taskRunFlagParameters,
235
364
  positional: namePositional,
236
365
  },
237
- async func(flags, name) {
238
- const result = await makeContract({
239
- name,
240
- force: Boolean(flags.force),
241
- dryRun: Boolean(flags.dryRun),
242
- });
243
- writeOutput(this, flags.json
244
- ? JSON.stringify(result, null, 2)
245
- : makeContractNextSteps(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
+ };
246
378
  },
247
379
  });
248
- const makeUseCaseCommand = buildCommand({
380
+ const taskRoutes = buildRouteMap({
249
381
  docs: {
250
- brief: "Generate a use case.",
251
- },
252
- parameters: {
253
- flags: makeFlagParameters,
254
- positional: namePositional,
382
+ brief: "Run Beignet app operational tasks.",
255
383
  },
256
- async func(flags, name) {
257
- const result = await makeUseCase({
258
- name,
259
- force: Boolean(flags.force),
260
- dryRun: Boolean(flags.dryRun),
261
- });
262
- writeOutput(this, flags.json
263
- ? JSON.stringify(result, null, 2)
264
- : makeUseCaseNextSteps(result));
384
+ routes: {
385
+ run: taskRunCommand,
265
386
  },
266
387
  });
267
- const makeTestCommand = buildCommand({
388
+ const outboxDrainCommand = buildCommand({
268
389
  docs: {
269
- brief: "Generate a use case test.",
390
+ brief: "Run one app-owned outbox drain pass.",
270
391
  },
271
392
  parameters: {
272
- flags: makeFlagParameters,
273
- positional: namePositional,
393
+ flags: outboxDrainFlagParameters,
274
394
  },
275
- async func(flags, name) {
276
- const result = await makeTest({
277
- name,
278
- force: Boolean(flags.force),
279
- dryRun: Boolean(flags.dryRun),
280
- });
281
- writeOutput(this, flags.json ? JSON.stringify(result, null, 2) : makeTestNextSteps(result));
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
+ };
282
406
  },
283
407
  });
284
- const makePortCommand = buildCommand({
408
+ const outboxRoutes = buildRouteMap({
285
409
  docs: {
286
- brief: "Generate an application port.",
410
+ brief: "Run Beignet outbox operations.",
411
+ },
412
+ routes: {
413
+ drain: outboxDrainCommand,
414
+ },
415
+ });
416
+ const scheduleRunCommand = buildCommand({
417
+ docs: {
418
+ brief: "Run an app-owned schedule.",
287
419
  },
288
420
  parameters: {
289
- flags: makeFlagParameters,
421
+ flags: scheduleRunFlagParameters,
290
422
  positional: namePositional,
291
423
  },
292
- async func(flags, name) {
293
- const result = await makePort({
294
- name,
295
- force: Boolean(flags.force),
296
- dryRun: Boolean(flags.dryRun),
297
- });
298
- 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
+ };
441
+ },
442
+ });
443
+ const scheduleRoutes = buildRouteMap({
444
+ docs: {
445
+ brief: "Run Beignet schedule operations.",
446
+ },
447
+ routes: {
448
+ run: scheduleRunCommand,
299
449
  },
300
450
  });
301
- const makeAdapterCommand = buildCommand({
451
+ const completionShellFlagParameters = {
452
+ shell: {
453
+ kind: "enum",
454
+ values: completionShellChoices,
455
+ optional: true,
456
+ brief: "Shell to manage completions for. Defaults to $SHELL.",
457
+ },
458
+ json: jsonFlag,
459
+ };
460
+ const completionInstallCommand = buildCommand({
302
461
  docs: {
303
- brief: "Generate a port adapter.",
462
+ brief: "Install beignet shell completions for bash or zsh.",
304
463
  },
305
464
  parameters: {
306
- flags: makeFlagParameters,
307
- positional: namePositional,
465
+ flags: completionShellFlagParameters,
308
466
  },
309
- async func(flags, name) {
310
- const result = await makeAdapter({
311
- name,
312
- force: Boolean(flags.force),
313
- dryRun: Boolean(flags.dryRun),
314
- });
315
- writeOutput(this, flags.json
316
- ? JSON.stringify(result, null, 2)
317
- : makeAdapterNextSteps(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
+ };
318
477
  },
319
478
  });
320
- const makePolicyCommand = buildCommand({
479
+ const completionUninstallCommand = buildCommand({
321
480
  docs: {
322
- brief: "Generate an authorization policy.",
481
+ brief: "Remove beignet shell completions for bash or zsh.",
323
482
  },
324
483
  parameters: {
325
- flags: makeFlagParameters,
326
- positional: namePositional,
484
+ flags: completionShellFlagParameters,
485
+ },
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
+ };
496
+ },
497
+ });
498
+ const completionRoutes = buildRouteMap({
499
+ docs: {
500
+ brief: "Manage beignet shell completions.",
327
501
  },
328
- async func(flags, name) {
329
- const result = await makePolicy({
330
- name,
331
- force: Boolean(flags.force),
332
- dryRun: Boolean(flags.dryRun),
333
- });
334
- writeOutput(this, flags.json
335
- ? JSON.stringify(result, null, 2)
336
- : makePolicyNextSteps(result));
502
+ routes: {
503
+ install: completionInstallCommand,
504
+ uninstall: completionUninstallCommand,
337
505
  },
338
506
  });
339
- const makeEventCommand = buildCommand({
507
+ const makeResourceCommand = buildCommand({
340
508
  docs: {
341
- brief: "Generate a feature event.",
509
+ brief: "Generate a CRUD-shaped resource slice.",
342
510
  },
343
511
  parameters: {
344
- 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
+ },
345
539
  positional: namePositional,
346
540
  },
347
- async func(flags, name) {
348
- const result = await makeEvent({
349
- name,
350
- force: Boolean(flags.force),
351
- dryRun: Boolean(flags.dryRun),
352
- });
353
- writeOutput(this, flags.json ? JSON.stringify(result, null, 2) : makeEventNextSteps(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
+ };
354
557
  },
355
558
  });
356
- const makeJobCommand = buildCommand({
559
+ const makeFeatureCommand = buildCommand({
357
560
  docs: {
358
- brief: "Generate a feature job.",
561
+ brief: "Generate a contract-first feature slice.",
359
562
  },
360
563
  parameters: {
361
- 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
+ },
362
574
  positional: namePositional,
363
575
  },
364
- async func(flags, name) {
365
- const result = await makeJob({
366
- name,
367
- force: Boolean(flags.force),
368
- dryRun: Boolean(flags.dryRun),
369
- });
370
- writeOutput(this, flags.json ? JSON.stringify(result, null, 2) : makeJobNextSteps(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
+ };
371
589
  },
372
590
  });
373
- const makeListenerFlagParameters = {
374
- ...makeFlagParameters,
375
- event: parsedStringFlag("Event to listen to, for example posts/published."),
376
- };
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
+ }
377
615
  const makeListenerCommand = buildCommand({
378
616
  docs: {
379
617
  brief: "Generate a feature event listener.",
380
618
  },
381
619
  parameters: {
382
- flags: makeListenerFlagParameters,
620
+ flags: {
621
+ ...makeFlagParameters,
622
+ event: parsedStringFlag("Event to listen to, for example posts/published."),
623
+ },
383
624
  positional: namePositional,
384
625
  },
385
- async func(flags, name) {
386
- if (!flags.event) {
387
- throw new Error("beignet make listener requires --event feature/name, for example --event posts/published.");
388
- }
389
- const result = await makeListener({
390
- name,
391
- event: flags.event,
392
- force: Boolean(flags.force),
393
- dryRun: Boolean(flags.dryRun),
394
- });
395
- writeOutput(this, flags.json
396
- ? JSON.stringify(result, null, 2)
397
- : 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
+ };
398
642
  },
399
643
  });
400
- const makeScheduleFlagParameters = {
401
- ...makeFlagParameters,
402
- cron: parsedStringFlag("Cron expression. Defaults to 0 9 * * *."),
403
- timezone: parsedStringFlag("IANA timezone, for example America/Chicago."),
404
- route: {
405
- kind: "boolean",
406
- optional: true,
407
- withNegated: false,
408
- brief: "Generate a Next.js cron route for this schedule.",
409
- },
410
- };
411
644
  const makeScheduleCommand = buildCommand({
412
645
  docs: {
413
646
  brief: "Generate a feature schedule.",
414
647
  },
415
648
  parameters: {
416
- flags: makeScheduleFlagParameters,
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
+ },
417
660
  positional: namePositional,
418
661
  },
419
- async func(flags, name) {
420
- const result = await makeSchedule({
421
- name,
422
- cron: flags.cron,
423
- timezone: flags.timezone,
424
- route: Boolean(flags.route),
425
- force: Boolean(flags.force),
426
- dryRun: Boolean(flags.dryRun),
427
- });
428
- writeOutput(this, flags.json
429
- ? JSON.stringify(result, null, 2)
430
- : makeScheduleNextSteps(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
+ };
431
677
  },
432
678
  });
433
679
  const makeRoutes = buildRouteMap({
@@ -435,17 +681,23 @@ const makeRoutes = buildRouteMap({
435
681
  brief: "Generate Beignet app files.",
436
682
  },
437
683
  routes: {
438
- adapter: makeAdapterCommand,
439
- contract: makeContractCommand,
440
- event: makeEventCommand,
441
- job: makeJobCommand,
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),
688
+ feature: makeFeatureCommand,
689
+ job: simpleMakeCommand("makeJob", "Generate a feature job.", makeJobNextSteps),
690
+ notification: simpleMakeCommand("makeNotification", "Generate a feature notification.", makeNotificationNextSteps),
442
691
  listener: makeListenerCommand,
443
- policy: makePolicyCommand,
444
- port: makePortCommand,
692
+ policy: simpleMakeCommand("makePolicy", "Generate an authorization policy.", makePolicyNextSteps),
693
+ port: simpleMakeCommand("makePort", "Generate an application port.", makePortNextSteps),
445
694
  resource: makeResourceCommand,
446
695
  schedule: makeScheduleCommand,
447
- test: makeTestCommand,
448
- 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),
449
701
  },
450
702
  });
451
703
  const rootRoutes = buildRouteMap({
@@ -453,18 +705,26 @@ const rootRoutes = buildRouteMap({
453
705
  brief: "Beignet CLI",
454
706
  fullDescription: `Create apps, generate framework files, inspect routes, and check Beignet conventions.
455
707
 
456
- create-beignet <directory> is equivalent to beignet create <directory>.`,
708
+ Run npm create beignet@latest (or bun create beignet) to scaffold a new app.`,
457
709
  },
458
710
  routes: {
711
+ completion: completionRoutes,
459
712
  create: createCommand,
713
+ db: dbRoutes,
460
714
  doctor: doctorCommand,
461
715
  lint: lintCommand,
462
716
  make: makeRoutes,
717
+ outbox: outboxRoutes,
463
718
  routes: routesCommand,
719
+ schedule: scheduleRoutes,
720
+ task: taskRoutes,
464
721
  },
465
722
  });
466
723
  const cli = buildApplication(rootRoutes, {
467
724
  name: "beignet",
725
+ versionInfo: {
726
+ currentVersion: getCliVersion(),
727
+ },
468
728
  scanner: {
469
729
  caseStyle: "allow-kebab-for-camel",
470
730
  },
@@ -478,7 +738,7 @@ const cli = buildApplication(rootRoutes, {
478
738
  commandErrorResult: (error) => error.message,
479
739
  },
480
740
  },
481
- determineExitCode: () => 1,
741
+ determineExitCode: () => 2,
482
742
  });
483
743
  function uniqueValues(values) {
484
744
  return [...new Set(values)];
@@ -489,9 +749,45 @@ function writeOutput(context, output) {
489
749
  function formatCliException(error) {
490
750
  return error instanceof Error ? error.message : String(error);
491
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
+ }
492
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
+ }
493
788
  const pm = result.packageManager;
494
789
  const run = pm === "npm" ? "npm run" : `${pm} run`;
790
+ const cli = packageRunnerFromPackageManager(pm);
495
791
  const envFileStep = result.files.includes(".env.example")
496
792
  ? " cp .env.example .env.local\n"
497
793
  : "";
@@ -499,218 +795,232 @@ function nextSteps(result, options = {}) {
499
795
  const envStep = envRequiredIntegrations.length > 0
500
796
  ? " Fill .env.local for selected provider integrations before starting the app.\n"
501
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
+ : "";
502
811
  return `Created ${result.name} at ${result.targetDir}
503
812
 
504
813
  Next steps:
505
814
  cd ${result.targetDir}
506
815
  ${pm} install
507
- ${envFileStep}${envStep}\
508
- ${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
+ }
509
843
  }
510
844
  function isEnvRequiredProviderIntegration(integration) {
511
845
  return (integration === "inngest" ||
512
846
  integration === "resend" ||
513
847
  integration === "upstash-rate-limit");
514
848
  }
515
- function makeResourceNextSteps(result) {
516
- const changedFiles = [...result.createdFiles, ...result.updatedFiles]
517
- .map((file) => ` ${file}`)
518
- .join("\n");
519
- const skippedFiles = result.skippedFiles
520
- .map((file) => ` ${file}`)
521
- .join("\n");
522
- 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}
523
860
 
524
861
  Changed files:
525
862
  ${changedFiles || " none"}
526
863
  ${skippedFiles ? `\nSkipped identical files:\n${skippedFiles}` : ""}
527
864
 
528
865
  Next steps:
529
- Review the generated schemas and repository fields.
530
- Run your app's typecheck and test commands.`;
866
+ ${steps.map((step) => ` ${step}`).join("\n")}`;
531
867
  }
532
- function makeContractNextSteps(result) {
533
- const changedFiles = [...result.createdFiles, ...result.updatedFiles]
534
- .map((file) => ` ${file}`)
535
- .join("\n");
536
- const skippedFiles = result.skippedFiles
537
- .map((file) => ` ${file}`)
538
- .join("\n");
539
- return `${result.dryRun ? "Would create" : "Created"} ${result.name} contract in ${result.targetDir}
540
-
541
- Changed files:
542
- ${changedFiles || " none"}
543
- ${skippedFiles ? `\nSkipped identical files:\n${skippedFiles}` : ""}
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
+ ]);
875
+ }
876
+ function databaseCommandNextSteps(result) {
877
+ const command = [result.runner, ...result.args].join(" ");
878
+ const prefix = result.dryRun ? "Would run" : "Ran";
879
+ return `${prefix} ${result.script} in ${result.cwd}
544
880
 
545
- Next steps:
546
- Add route handlers for the generated contracts.
547
- Register the exported contract list with OpenAPI if this app publishes docs.`;
881
+ Command:
882
+ ${command}`;
883
+ }
884
+ function makeFeatureNextSteps(result) {
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
+ ]);
891
+ }
892
+ function makeContractNextSteps(result) {
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
+ ]);
548
903
  }
549
904
  function makeUseCaseNextSteps(result) {
550
- const changedFiles = [...result.createdFiles, ...result.updatedFiles]
551
- .map((file) => ` ${file}`)
552
- .join("\n");
553
- const skippedFiles = result.skippedFiles
554
- .map((file) => ` ${file}`)
555
- .join("\n");
556
- return `${result.dryRun ? "Would create" : "Created"} ${result.name} use case in ${result.targetDir}
557
-
558
- Changed files:
559
- ${changedFiles || " none"}
560
- ${skippedFiles ? `\nSkipped identical files:\n${skippedFiles}` : ""}
561
-
562
- Next steps:
563
- Replace the starter input and output schemas with domain-specific shapes.
564
- 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
+ ]);
565
909
  }
566
910
  function makeTestNextSteps(result) {
567
- const changedFiles = [...result.createdFiles, ...result.updatedFiles]
568
- .map((file) => ` ${file}`)
569
- .join("\n");
570
- const skippedFiles = result.skippedFiles
571
- .map((file) => ` ${file}`)
572
- .join("\n");
573
- return `${result.dryRun ? "Would create" : "Created"} ${result.name} test in ${result.targetDir}
574
-
575
- Changed files:
576
- ${changedFiles || " none"}
577
- ${skippedFiles ? `\nSkipped identical files:\n${skippedFiles}` : ""}
578
-
579
- Next steps:
580
- Replace the starter input, context, and assertion with behavior-specific coverage.
581
- 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
+ ]);
582
915
  }
583
916
  function makePortNextSteps(result) {
584
- const changedFiles = [...result.createdFiles, ...result.updatedFiles]
585
- .map((file) => ` ${file}`)
586
- .join("\n");
587
- const skippedFiles = result.skippedFiles
588
- .map((file) => ` ${file}`)
589
- .join("\n");
590
- return `${result.dryRun ? "Would create" : "Created"} ${result.name} port in ${result.targetDir}
591
-
592
- Changed files:
593
- ${changedFiles || " none"}
594
- ${skippedFiles ? `\nSkipped identical files:\n${skippedFiles}` : ""}
595
-
596
- Next steps:
597
- Replace the starter execute method with domain-specific operations.
598
- 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
+ ]);
599
921
  }
600
922
  function makePolicyNextSteps(result) {
601
- const changedFiles = [...result.createdFiles, ...result.updatedFiles]
602
- .map((file) => ` ${file}`)
603
- .join("\n");
604
- const skippedFiles = result.skippedFiles
605
- .map((file) => ` ${file}`)
606
- .join("\n");
607
- return `${result.dryRun ? "Would create" : "Created"} ${result.name} policy in ${result.targetDir}
608
-
609
- Changed files:
610
- ${changedFiles || " none"}
611
- ${skippedFiles ? `\nSkipped identical files:\n${skippedFiles}` : ""}
612
-
613
- Next steps:
614
- Replace the starter abilities with domain-specific authorization rules.
615
- 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
+ ]);
616
927
  }
617
928
  function makeAdapterNextSteps(result) {
618
- const changedFiles = [...result.createdFiles, ...result.updatedFiles]
619
- .map((file) => ` ${file}`)
620
- .join("\n");
621
- const skippedFiles = result.skippedFiles
622
- .map((file) => ` ${file}`)
623
- .join("\n");
624
- return `${result.dryRun ? "Would create" : "Created"} ${result.name} port adapter in ${result.targetDir}
625
-
626
- Changed files:
627
- ${changedFiles || " none"}
628
- ${skippedFiles ? `\nSkipped identical files:\n${skippedFiles}` : ""}
629
-
630
- Next steps:
631
- Replace the generated throwing implementation with real infrastructure code.
632
- 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
+ ]);
633
933
  }
634
934
  function makeEventNextSteps(result) {
635
- const changedFiles = [...result.createdFiles, ...result.updatedFiles]
636
- .map((file) => ` ${file}`)
637
- .join("\n");
638
- const skippedFiles = result.skippedFiles
639
- .map((file) => ` ${file}`)
640
- .join("\n");
641
- return `${result.dryRun ? "Would create" : "Created"} ${result.name} event in ${result.targetDir}
642
-
643
- Changed files:
644
- ${changedFiles || " none"}
645
- ${skippedFiles ? `\nSkipped identical files:\n${skippedFiles}` : ""}
646
-
647
- Next steps:
648
- Replace the starter payload schema with the domain fact shape.
649
- 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
+ ]);
650
939
  }
651
940
  function makeJobNextSteps(result) {
652
- const changedFiles = [...result.createdFiles, ...result.updatedFiles]
653
- .map((file) => ` ${file}`)
654
- .join("\n");
655
- const skippedFiles = result.skippedFiles
656
- .map((file) => ` ${file}`)
657
- .join("\n");
658
- return `${result.dryRun ? "Would create" : "Created"} ${result.name} job in ${result.targetDir}
659
-
660
- Changed files:
661
- ${changedFiles || " none"}
662
- ${skippedFiles ? `\nSkipped identical files:\n${skippedFiles}` : ""}
663
-
664
- Next steps:
665
- Replace the starter payload and handler with the real background work.
666
- 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
+ ]);
945
+ }
946
+ function makeFactoryNextSteps(result) {
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
+ ]);
951
+ }
952
+ function makeSeedNextSteps(result) {
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
+ ]);
957
+ }
958
+ function makeNotificationNextSteps(result) {
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
+ ]);
667
963
  }
668
964
  function makeListenerNextSteps(result) {
669
- const changedFiles = [...result.createdFiles, ...result.updatedFiles]
670
- .map((file) => ` ${file}`)
671
- .join("\n");
672
- const skippedFiles = result.skippedFiles
673
- .map((file) => ` ${file}`)
674
- .join("\n");
675
- return `${result.dryRun ? "Would create" : "Created"} ${result.name} listener in ${result.targetDir}
676
-
677
- Changed files:
678
- ${changedFiles || " none"}
679
- ${skippedFiles ? `\nSkipped identical files:\n${skippedFiles}` : ""}
680
-
681
- Next steps:
682
- Replace the starter handler with the event reaction.
683
- 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
+ ]);
684
969
  }
685
970
  function makeScheduleNextSteps(result) {
686
- const changedFiles = [...result.createdFiles, ...result.updatedFiles]
687
- .map((file) => ` ${file}`)
688
- .join("\n");
689
- const skippedFiles = result.skippedFiles
690
- .map((file) => ` ${file}`)
691
- .join("\n");
692
- return `${result.dryRun ? "Would create" : "Created"} ${result.name} schedule in ${result.targetDir}
693
-
694
- Changed files:
695
- ${changedFiles || " none"}
696
- ${skippedFiles ? `\nSkipped identical files:\n${skippedFiles}` : ""}
697
-
698
- Next steps:
699
- Replace the starter payload and handler with the scheduled workflow.
700
- 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
+ ]);
975
+ }
976
+ function makeUploadNextSteps(result) {
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
+ ]);
701
981
  }
702
982
  export async function main(inputs = process.argv.slice(2), context = { process }) {
703
983
  if (inputs.length === 0) {
704
984
  await run(cli, ["--help"], context);
705
- 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);
706
993
  return;
707
994
  }
708
995
  await run(cli, inputs, context);
709
996
  normalizeExitCode(context.process);
710
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
+ ]);
711
1015
  function normalizeExitCode(proc) {
712
- if (typeof proc.exitCode === "number" &&
713
- (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) {
714
1024
  proc.exitCode = 1;
715
1025
  }
716
1026
  }
@@ -729,7 +1039,7 @@ function isCliEntrypoint() {
729
1039
  if (isCliEntrypoint()) {
730
1040
  main().catch((error) => {
731
1041
  console.error(error instanceof Error ? error.message : String(error));
732
- process.exitCode = 1;
1042
+ process.exitCode = 2;
733
1043
  });
734
1044
  }
735
1045
  //# sourceMappingURL=index.js.map