@beignet/cli 0.0.3 → 0.0.5

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