@beignet/cli 0.0.26 → 0.0.28
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.
- package/CHANGELOG.md +26 -0
- package/README.md +59 -15
- package/dist/check.d.ts +61 -0
- package/dist/check.d.ts.map +1 -0
- package/dist/check.js +164 -0
- package/dist/check.js.map +1 -0
- package/dist/choices.d.ts +8 -0
- package/dist/choices.d.ts.map +1 -1
- package/dist/choices.js +14 -0
- package/dist/choices.js.map +1 -1
- package/dist/db.d.ts +8 -0
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +4 -4
- package/dist/db.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +156 -21
- package/dist/index.js.map +1 -1
- package/dist/inspect.d.ts.map +1 -1
- package/dist/inspect.js +121 -3
- package/dist/inspect.js.map +1 -1
- package/dist/make/inbox.d.ts +63 -0
- package/dist/make/inbox.d.ts.map +1 -0
- package/dist/make/inbox.js +1747 -0
- package/dist/make/inbox.js.map +1 -0
- package/dist/make/payments.d.ts +61 -0
- package/dist/make/payments.d.ts.map +1 -0
- package/dist/make/payments.js +1900 -0
- package/dist/make/payments.js.map +1 -0
- package/dist/make/shared.d.ts +592 -0
- package/dist/make/shared.d.ts.map +1 -0
- package/dist/make/shared.js +2233 -0
- package/dist/make/shared.js.map +1 -0
- package/dist/make/tenancy.d.ts +101 -0
- package/dist/make/tenancy.d.ts.map +1 -0
- package/dist/make/tenancy.js +4409 -0
- package/dist/make/tenancy.js.map +1 -0
- package/dist/make.d.ts +8 -44
- package/dist/make.d.ts.map +1 -1
- package/dist/make.js +57 -3689
- package/dist/make.js.map +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +45 -4
- package/dist/mcp.js.map +1 -1
- package/dist/provider-add.d.ts +19 -0
- package/dist/provider-add.d.ts.map +1 -0
- package/dist/provider-add.js +691 -0
- package/dist/provider-add.js.map +1 -0
- package/dist/provider-audit.d.ts.map +1 -1
- package/dist/provider-audit.js +77 -1
- package/dist/provider-audit.js.map +1 -1
- package/dist/registry-edits.d.ts +9 -0
- package/dist/registry-edits.d.ts.map +1 -1
- package/dist/registry-edits.js +32 -0
- package/dist/registry-edits.js.map +1 -1
- package/dist/task.d.ts +2 -0
- package/dist/task.d.ts.map +1 -1
- package/dist/task.js +2 -0
- package/dist/task.js.map +1 -1
- package/dist/templates/agents.d.ts.map +1 -1
- package/dist/templates/agents.js +29 -11
- package/dist/templates/agents.js.map +1 -1
- package/dist/templates/base.d.ts.map +1 -1
- package/dist/templates/base.js +42 -12
- package/dist/templates/base.js.map +1 -1
- package/dist/templates/db/sqlite.d.ts.map +1 -1
- package/dist/templates/db/sqlite.js +7 -1
- package/dist/templates/db/sqlite.js.map +1 -1
- package/dist/templates/server.d.ts.map +1 -1
- package/dist/templates/server.js +44 -37
- package/dist/templates/server.js.map +1 -1
- package/dist/templates/shared.d.ts +5 -0
- package/dist/templates/shared.d.ts.map +1 -1
- package/dist/templates/shared.js +5 -0
- package/dist/templates/shared.js.map +1 -1
- package/package.json +2 -2
- package/skills/app-structure/SKILL.md +1 -1
- package/src/check.ts +246 -0
- package/src/choices.ts +28 -0
- package/src/db.ts +4 -4
- package/src/index.ts +232 -21
- package/src/inspect.ts +168 -2
- package/src/make/inbox.ts +2015 -0
- package/src/make/payments.ts +2182 -0
- package/src/make/shared.ts +3638 -0
- package/src/make/tenancy.ts +4809 -0
- package/src/make.ts +388 -5355
- package/src/mcp.ts +66 -3
- package/src/provider-add.ts +926 -0
- package/src/provider-audit.ts +95 -3
- package/src/registry-edits.ts +47 -0
- package/src/task.ts +8 -10
- package/src/templates/agents.ts +29 -11
- package/src/templates/base.ts +43 -12
- package/src/templates/db/sqlite.ts +7 -1
- package/src/templates/server.ts +44 -37
- package/src/templates/shared.ts +5 -0
- package/src/test-helpers/generated-app.ts +168 -0
package/src/index.ts
CHANGED
|
@@ -26,12 +26,15 @@ import {
|
|
|
26
26
|
makeFeatureAddonChoices,
|
|
27
27
|
makeFeatureRecipeChoices,
|
|
28
28
|
type PackageManager,
|
|
29
|
+
type ProviderPresetName,
|
|
29
30
|
packageManagerChoices,
|
|
31
|
+
providerPresetChoices,
|
|
30
32
|
type TemplateName,
|
|
31
33
|
templateChoices,
|
|
32
34
|
} from "./choices.js";
|
|
33
35
|
// Type-only: the runtime module stays behind the create command's lazy loader.
|
|
34
36
|
import type { CreateSelections } from "./create-prompts.js";
|
|
37
|
+
import type { AddProviderPresetResult } from "./provider-add.js";
|
|
35
38
|
import { getCliVersion } from "./version.js";
|
|
36
39
|
|
|
37
40
|
type CliContext = CommandContext & {
|
|
@@ -102,6 +105,12 @@ type LintFlags = {
|
|
|
102
105
|
format?: OutputFormat;
|
|
103
106
|
};
|
|
104
107
|
|
|
108
|
+
type CheckFlags = {
|
|
109
|
+
json?: boolean;
|
|
110
|
+
fix?: boolean;
|
|
111
|
+
cwd?: string;
|
|
112
|
+
};
|
|
113
|
+
|
|
105
114
|
type DbFlags = {
|
|
106
115
|
json?: boolean;
|
|
107
116
|
dryRun?: boolean;
|
|
@@ -131,6 +140,7 @@ type DbSchemaGenerateFlags = DbFlags & {
|
|
|
131
140
|
type TaskRunFlags = {
|
|
132
141
|
json?: boolean;
|
|
133
142
|
input?: string;
|
|
143
|
+
tenant?: string;
|
|
134
144
|
module?: string;
|
|
135
145
|
};
|
|
136
146
|
|
|
@@ -168,7 +178,24 @@ type ProviderAuditFlags = {
|
|
|
168
178
|
cwd?: string;
|
|
169
179
|
};
|
|
170
180
|
|
|
181
|
+
type ProviderAddFlags = {
|
|
182
|
+
json?: boolean;
|
|
183
|
+
dryRun?: boolean;
|
|
184
|
+
cwd?: string;
|
|
185
|
+
};
|
|
186
|
+
|
|
171
187
|
const parseString = (input: string): string => input;
|
|
188
|
+
const parseProviderPreset = (input: string): ProviderPresetName => {
|
|
189
|
+
if ((providerPresetChoices as readonly string[]).includes(input)) {
|
|
190
|
+
return input as ProviderPresetName;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
throw new Error(
|
|
194
|
+
`Unknown provider preset "${input}". Available presets: ${providerPresetChoices.join(
|
|
195
|
+
", ",
|
|
196
|
+
)}.`,
|
|
197
|
+
);
|
|
198
|
+
};
|
|
172
199
|
const parsePositiveInteger = (input: string): number => {
|
|
173
200
|
const value = Number(input);
|
|
174
201
|
if (!Number.isInteger(value) || value <= 0) {
|
|
@@ -240,6 +267,12 @@ const providerAuditFlagParameters = {
|
|
|
240
267
|
cwd: cwdFlag,
|
|
241
268
|
} satisfies FlagParametersForType<ProviderAuditFlags, CliContext>;
|
|
242
269
|
|
|
270
|
+
const providerAddFlagParameters = {
|
|
271
|
+
json: jsonFlag,
|
|
272
|
+
dryRun: dryRunFlag,
|
|
273
|
+
cwd: cwdFlag,
|
|
274
|
+
} satisfies FlagParametersForType<ProviderAddFlags, CliContext>;
|
|
275
|
+
|
|
243
276
|
const dbFlagParameters = {
|
|
244
277
|
json: jsonFlag,
|
|
245
278
|
dryRun: dryRunFlag,
|
|
@@ -270,6 +303,9 @@ const dbSchemaGenerateFlagParameters = {
|
|
|
270
303
|
const taskRunFlagParameters = {
|
|
271
304
|
json: jsonFlag,
|
|
272
305
|
input: parsedStringFlag("JSON input passed to the task schema."),
|
|
306
|
+
tenant: parsedStringFlag(
|
|
307
|
+
"Tenant id or slug passed to the app's createTaskContext, separate from task input.",
|
|
308
|
+
),
|
|
273
309
|
module: parsedStringFlag(
|
|
274
310
|
"Task registry module. Defaults to server/tasks.ts.",
|
|
275
311
|
),
|
|
@@ -317,6 +353,19 @@ const namePositional = {
|
|
|
317
353
|
],
|
|
318
354
|
} as const;
|
|
319
355
|
|
|
356
|
+
const providerPresetPositional = {
|
|
357
|
+
kind: "tuple",
|
|
358
|
+
parameters: [
|
|
359
|
+
{
|
|
360
|
+
parse: parseProviderPreset,
|
|
361
|
+
placeholder: "preset",
|
|
362
|
+
brief: `Provider preset to add. Available: ${providerPresetChoices.join(
|
|
363
|
+
", ",
|
|
364
|
+
)}.`,
|
|
365
|
+
},
|
|
366
|
+
],
|
|
367
|
+
} as const;
|
|
368
|
+
|
|
320
369
|
function resolveOutputFormat(flags: {
|
|
321
370
|
json?: boolean;
|
|
322
371
|
format?: OutputFormat;
|
|
@@ -558,6 +607,55 @@ const doctorCommand = buildCommand<DoctorFlags, [], CliContext>({
|
|
|
558
607
|
},
|
|
559
608
|
});
|
|
560
609
|
|
|
610
|
+
const checkCommand = buildCommand<CheckFlags, [], CliContext>({
|
|
611
|
+
docs: {
|
|
612
|
+
brief: "Run the full app validation loop as one command.",
|
|
613
|
+
fullDescription:
|
|
614
|
+
"Runs beignet lint, beignet doctor --strict, and the app's lint, typecheck, and test package scripts through the detected package manager. Every step runs even when an earlier one fails, so one run reports everything. Missing package scripts are skipped, not failed.",
|
|
615
|
+
},
|
|
616
|
+
parameters: {
|
|
617
|
+
flags: {
|
|
618
|
+
json: jsonFlag,
|
|
619
|
+
fix: {
|
|
620
|
+
kind: "boolean",
|
|
621
|
+
optional: true,
|
|
622
|
+
withNegated: false,
|
|
623
|
+
brief: "Apply low-risk doctor fixes before checking.",
|
|
624
|
+
},
|
|
625
|
+
cwd: cwdFlag,
|
|
626
|
+
} satisfies FlagParametersForType<CheckFlags, CliContext>,
|
|
627
|
+
},
|
|
628
|
+
loader: async () => {
|
|
629
|
+
const { checkApp, formatCheckStep, formatCheckSummary } = await import(
|
|
630
|
+
"./check.js"
|
|
631
|
+
);
|
|
632
|
+
|
|
633
|
+
return async function runCheck(this: CliContext, flags: CheckFlags) {
|
|
634
|
+
const color = useColor();
|
|
635
|
+
const result = await checkApp({
|
|
636
|
+
cwd: flags.cwd,
|
|
637
|
+
fix: flags.fix,
|
|
638
|
+
color,
|
|
639
|
+
...(flags.json
|
|
640
|
+
? {}
|
|
641
|
+
: {
|
|
642
|
+
onStep: (step) =>
|
|
643
|
+
writeOutput(this, formatCheckStep(step, { color })),
|
|
644
|
+
}),
|
|
645
|
+
});
|
|
646
|
+
writeOutput(
|
|
647
|
+
this,
|
|
648
|
+
flags.json
|
|
649
|
+
? JSON.stringify(result, null, 2)
|
|
650
|
+
: `\n${formatCheckSummary(result, { color })}`,
|
|
651
|
+
);
|
|
652
|
+
if (!result.ok) {
|
|
653
|
+
this.process.exitCode = 1;
|
|
654
|
+
}
|
|
655
|
+
};
|
|
656
|
+
},
|
|
657
|
+
});
|
|
658
|
+
|
|
561
659
|
const lintCommand = buildCommand<LintFlags, [], CliContext>({
|
|
562
660
|
docs: {
|
|
563
661
|
brief: "Check Beignet dependency direction conventions.",
|
|
@@ -639,11 +737,50 @@ const providerAuditCommand = buildCommand<ProviderAuditFlags, [], CliContext>({
|
|
|
639
737
|
},
|
|
640
738
|
});
|
|
641
739
|
|
|
740
|
+
const providerAddCommand = buildCommand<
|
|
741
|
+
ProviderAddFlags,
|
|
742
|
+
[ProviderPresetName],
|
|
743
|
+
CliContext
|
|
744
|
+
>({
|
|
745
|
+
docs: {
|
|
746
|
+
brief: "Add a provider setup preset to a Beignet app.",
|
|
747
|
+
fullDescription: `Updates an existing Beignet app with provider package dependencies, server/provider registration, app port wiring, .env.example entries, and local setup notes.
|
|
748
|
+
|
|
749
|
+
Available presets: ${providerPresetChoices.join(", ")}`,
|
|
750
|
+
},
|
|
751
|
+
parameters: {
|
|
752
|
+
flags: providerAddFlagParameters,
|
|
753
|
+
positional: providerPresetPositional,
|
|
754
|
+
},
|
|
755
|
+
loader: async () => {
|
|
756
|
+
const { addProviderPreset } = await import("./provider-add.js");
|
|
757
|
+
|
|
758
|
+
return async function runProviderAdd(
|
|
759
|
+
this: CliContext,
|
|
760
|
+
flags: ProviderAddFlags,
|
|
761
|
+
name: ProviderPresetName,
|
|
762
|
+
) {
|
|
763
|
+
const result = await addProviderPreset({
|
|
764
|
+
name,
|
|
765
|
+
cwd: flags.cwd,
|
|
766
|
+
dryRun: Boolean(flags.dryRun),
|
|
767
|
+
});
|
|
768
|
+
writeOutput(
|
|
769
|
+
this,
|
|
770
|
+
flags.json
|
|
771
|
+
? JSON.stringify(result, null, 2)
|
|
772
|
+
: providerAddNextSteps(result),
|
|
773
|
+
);
|
|
774
|
+
};
|
|
775
|
+
},
|
|
776
|
+
});
|
|
777
|
+
|
|
642
778
|
const providerRoutes = buildRouteMap({
|
|
643
779
|
docs: {
|
|
644
|
-
brief: "
|
|
780
|
+
brief: "Manage Beignet provider setup.",
|
|
645
781
|
},
|
|
646
782
|
routes: {
|
|
783
|
+
add: providerAddCommand,
|
|
647
784
|
audit: providerAuditCommand,
|
|
648
785
|
},
|
|
649
786
|
});
|
|
@@ -760,6 +897,7 @@ const taskRunCommand = buildCommand<TaskRunFlags, [string], CliContext>({
|
|
|
760
897
|
const result = await runAppTask({
|
|
761
898
|
name,
|
|
762
899
|
input: flags.input,
|
|
900
|
+
tenant: flags.tenant,
|
|
763
901
|
modulePath: flags.module,
|
|
764
902
|
});
|
|
765
903
|
|
|
@@ -1215,7 +1353,9 @@ const makePaymentsCommand = buildCommand<MakeFlags, [], CliContext>({
|
|
|
1215
1353
|
flags: makeFlagParameters,
|
|
1216
1354
|
},
|
|
1217
1355
|
loader: async () => {
|
|
1218
|
-
const { makePayments } = await import(
|
|
1356
|
+
const { makePayments, makePaymentsNextStepLines } = await import(
|
|
1357
|
+
"./make.js"
|
|
1358
|
+
);
|
|
1219
1359
|
|
|
1220
1360
|
return async function runMakePayments(this: CliContext, flags: MakeFlags) {
|
|
1221
1361
|
const result = await makePayments({
|
|
@@ -1226,7 +1366,69 @@ const makePaymentsCommand = buildCommand<MakeFlags, [], CliContext>({
|
|
|
1226
1366
|
this,
|
|
1227
1367
|
flags.json
|
|
1228
1368
|
? JSON.stringify(result, null, 2)
|
|
1229
|
-
:
|
|
1369
|
+
: makeNextSteps(
|
|
1370
|
+
result,
|
|
1371
|
+
"billing slice",
|
|
1372
|
+
makePaymentsNextStepLines(result),
|
|
1373
|
+
),
|
|
1374
|
+
);
|
|
1375
|
+
};
|
|
1376
|
+
},
|
|
1377
|
+
});
|
|
1378
|
+
|
|
1379
|
+
const makeTenancyCommand = buildCommand<MakeFlags, [], CliContext>({
|
|
1380
|
+
docs: {
|
|
1381
|
+
brief: "Generate a workspace tenancy slice.",
|
|
1382
|
+
},
|
|
1383
|
+
parameters: {
|
|
1384
|
+
flags: makeFlagParameters,
|
|
1385
|
+
},
|
|
1386
|
+
loader: async () => {
|
|
1387
|
+
const { makeTenancy, makeTenancyNextStepLines } = await import("./make.js");
|
|
1388
|
+
|
|
1389
|
+
return async function runMakeTenancy(this: CliContext, flags: MakeFlags) {
|
|
1390
|
+
const result = await makeTenancy({
|
|
1391
|
+
force: Boolean(flags.force),
|
|
1392
|
+
dryRun: Boolean(flags.dryRun),
|
|
1393
|
+
});
|
|
1394
|
+
writeOutput(
|
|
1395
|
+
this,
|
|
1396
|
+
flags.json
|
|
1397
|
+
? JSON.stringify(result, null, 2)
|
|
1398
|
+
: makeNextSteps(
|
|
1399
|
+
result,
|
|
1400
|
+
"workspace slice",
|
|
1401
|
+
makeTenancyNextStepLines(),
|
|
1402
|
+
),
|
|
1403
|
+
);
|
|
1404
|
+
};
|
|
1405
|
+
},
|
|
1406
|
+
});
|
|
1407
|
+
|
|
1408
|
+
const makeInboxCommand = buildCommand<MakeFlags, [], CliContext>({
|
|
1409
|
+
docs: {
|
|
1410
|
+
brief: "Generate an in-app inbox notifications slice.",
|
|
1411
|
+
},
|
|
1412
|
+
parameters: {
|
|
1413
|
+
flags: makeFlagParameters,
|
|
1414
|
+
},
|
|
1415
|
+
loader: async () => {
|
|
1416
|
+
const { makeInbox, makeInboxNextStepLines } = await import("./make.js");
|
|
1417
|
+
|
|
1418
|
+
return async function runMakeInbox(this: CliContext, flags: MakeFlags) {
|
|
1419
|
+
const result = await makeInbox({
|
|
1420
|
+
force: Boolean(flags.force),
|
|
1421
|
+
dryRun: Boolean(flags.dryRun),
|
|
1422
|
+
});
|
|
1423
|
+
writeOutput(
|
|
1424
|
+
this,
|
|
1425
|
+
flags.json
|
|
1426
|
+
? JSON.stringify(result, null, 2)
|
|
1427
|
+
: makeNextSteps(
|
|
1428
|
+
result,
|
|
1429
|
+
"notifications slice",
|
|
1430
|
+
makeInboxNextStepLines(),
|
|
1431
|
+
),
|
|
1230
1432
|
);
|
|
1231
1433
|
};
|
|
1232
1434
|
},
|
|
@@ -1283,6 +1485,7 @@ const makeRoutes = buildRouteMap({
|
|
|
1283
1485
|
makeFactoryNextSteps,
|
|
1284
1486
|
),
|
|
1285
1487
|
feature: makeFeatureCommand,
|
|
1488
|
+
inbox: makeInboxCommand,
|
|
1286
1489
|
job: simpleMakeCommand(
|
|
1287
1490
|
"makeJob",
|
|
1288
1491
|
"Generate a feature job.",
|
|
@@ -1318,6 +1521,7 @@ const makeRoutes = buildRouteMap({
|
|
|
1318
1521
|
"Generate a feature operational task.",
|
|
1319
1522
|
makeTaskNextSteps,
|
|
1320
1523
|
),
|
|
1524
|
+
tenancy: makeTenancyCommand,
|
|
1321
1525
|
test: simpleMakeCommand(
|
|
1322
1526
|
"makeTest",
|
|
1323
1527
|
"Generate a use case test.",
|
|
@@ -1344,6 +1548,7 @@ const rootRoutes = buildRouteMap({
|
|
|
1344
1548
|
Run npm create beignet@latest (or bun create beignet) to scaffold a new app.`,
|
|
1345
1549
|
},
|
|
1346
1550
|
routes: {
|
|
1551
|
+
check: checkCommand,
|
|
1347
1552
|
completion: completionRoutes,
|
|
1348
1553
|
create: createCommand,
|
|
1349
1554
|
db: dbRoutes,
|
|
@@ -1357,6 +1562,10 @@ Run npm create beignet@latest (or bun create beignet) to scaffold a new app.`,
|
|
|
1357
1562
|
schedule: scheduleRoutes,
|
|
1358
1563
|
task: taskRoutes,
|
|
1359
1564
|
},
|
|
1565
|
+
aliases: {
|
|
1566
|
+
// The singular form is a common near-miss for the providers commands.
|
|
1567
|
+
provider: "providers",
|
|
1568
|
+
},
|
|
1360
1569
|
});
|
|
1361
1570
|
|
|
1362
1571
|
const cli = buildApplication(rootRoutes, {
|
|
@@ -1432,6 +1641,7 @@ type DatabaseSchemaGenerateNextStepsResult = {
|
|
|
1432
1641
|
type AppTaskRunNextStepsResult = {
|
|
1433
1642
|
name: string;
|
|
1434
1643
|
durationMs: number;
|
|
1644
|
+
tenant?: string;
|
|
1435
1645
|
output: unknown;
|
|
1436
1646
|
};
|
|
1437
1647
|
|
|
@@ -1463,7 +1673,9 @@ function appTaskRunNextSteps(result: AppTaskRunNextStepsResult): string {
|
|
|
1463
1673
|
? "undefined"
|
|
1464
1674
|
: JSON.stringify(result.output, null, 2);
|
|
1465
1675
|
|
|
1466
|
-
|
|
1676
|
+
const tenant = result.tenant ? `\nTenant: ${result.tenant}` : "";
|
|
1677
|
+
|
|
1678
|
+
return `Ran task ${result.name} in ${result.durationMs}ms${tenant}
|
|
1467
1679
|
|
|
1468
1680
|
Output:
|
|
1469
1681
|
${output}`;
|
|
@@ -1545,19 +1757,13 @@ ${openStep}
|
|
|
1545
1757
|
|
|
1546
1758
|
Inspect the app:
|
|
1547
1759
|
${cli} routes
|
|
1548
|
-
${
|
|
1549
|
-
${cli} lint
|
|
1550
|
-
${cli} doctor
|
|
1760
|
+
${cli} check
|
|
1551
1761
|
|
|
1552
1762
|
Generate a feature:
|
|
1553
1763
|
${cli} make feature projects
|
|
1554
1764
|
${cli} db generate
|
|
1555
1765
|
${cli} db migrate
|
|
1556
|
-
${
|
|
1557
|
-
${run} lint
|
|
1558
|
-
${run} typecheck
|
|
1559
|
-
${cli} lint
|
|
1560
|
-
${cli} doctor`;
|
|
1766
|
+
${cli} check`;
|
|
1561
1767
|
}
|
|
1562
1768
|
|
|
1563
1769
|
function packageRunnerFromPackageManager(pm: PackageManager): string {
|
|
@@ -1617,6 +1823,20 @@ Next steps:
|
|
|
1617
1823
|
${steps.map((step) => ` ${step}`).join("\n")}`;
|
|
1618
1824
|
}
|
|
1619
1825
|
|
|
1826
|
+
function providerAddNextSteps(result: AddProviderPresetResult): string {
|
|
1827
|
+
const { changedFiles, skippedFiles } = changedFileLines(result);
|
|
1828
|
+
const prefix = result.dryRun ? "Would add" : "Added";
|
|
1829
|
+
|
|
1830
|
+
return `${prefix} ${result.name} provider preset in ${result.targetDir}
|
|
1831
|
+
|
|
1832
|
+
Changed files:
|
|
1833
|
+
${changedFiles || " none"}
|
|
1834
|
+
${skippedFiles ? `\nSkipped identical files:\n${skippedFiles}` : ""}
|
|
1835
|
+
|
|
1836
|
+
Next steps:
|
|
1837
|
+
${result.nextSteps.map((step) => ` ${step}`).join("\n")}`;
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1620
1840
|
function makeResourceNextSteps(result: MakeNextStepsResult): string {
|
|
1621
1841
|
return makeNextSteps(result, "resource", [
|
|
1622
1842
|
"Use make resource when the feature maps cleanly to a REST resource with repository-backed persistence.",
|
|
@@ -1754,15 +1974,6 @@ function makeNotificationNextSteps(result: MakeNextStepsResult): string {
|
|
|
1754
1974
|
]);
|
|
1755
1975
|
}
|
|
1756
1976
|
|
|
1757
|
-
function makePaymentsNextSteps(result: MakeNextStepsResult): string {
|
|
1758
|
-
return makeNextSteps(result, "billing slice", [
|
|
1759
|
-
"Set BILLING_TEAM_PRICE_ID in .env.local to the provider price ID for your team plan.",
|
|
1760
|
-
"Run beignet db generate and beignet db migrate so billing_accounts exists before handling webhooks.",
|
|
1761
|
-
"Use createMemoryPaymentsProvider() for local development, then swap server/providers.ts to stripePaymentsProvider when STRIPE_SECRET_KEY and STRIPE_WEBHOOK_SECRET are configured.",
|
|
1762
|
-
"Run your app's test, lint, and typecheck commands, then beignet lint and beignet doctor.",
|
|
1763
|
-
]);
|
|
1764
|
-
}
|
|
1765
|
-
|
|
1766
1977
|
function makeOutboxNextSteps(result: MakeNextStepsResult): string {
|
|
1767
1978
|
return makeNextSteps(result, "outbox runtime", [
|
|
1768
1979
|
"Set CRON_SECRET in your deployment and call /api/cron/outbox/drain from a scheduler with Authorization: Bearer $CRON_SECRET.",
|
package/src/inspect.ts
CHANGED
|
@@ -19,6 +19,10 @@ import {
|
|
|
19
19
|
formatGithubAnnotation,
|
|
20
20
|
type GithubAnnotationSeverity,
|
|
21
21
|
} from "./github-annotations.js";
|
|
22
|
+
import {
|
|
23
|
+
providersFilePath,
|
|
24
|
+
wireListenersProviderSource,
|
|
25
|
+
} from "./make/shared.js";
|
|
22
26
|
import {
|
|
23
27
|
detectedProviderVariants,
|
|
24
28
|
diagnosticPackageJsonFile,
|
|
@@ -269,6 +273,14 @@ export async function applyDoctorFixes(
|
|
|
269
273
|
...(await fixUnregisteredOutboxEntries(targetDir, files, drift, config)),
|
|
270
274
|
);
|
|
271
275
|
|
|
276
|
+
const listenerFix = await fixUnregisteredListeners(
|
|
277
|
+
targetDir,
|
|
278
|
+
files,
|
|
279
|
+
drift,
|
|
280
|
+
config,
|
|
281
|
+
);
|
|
282
|
+
if (listenerFix) fixes.push(listenerFix);
|
|
283
|
+
|
|
272
284
|
const openApiFix = await fixDirectOpenApiArrayDrift(
|
|
273
285
|
targetDir,
|
|
274
286
|
files,
|
|
@@ -986,6 +998,24 @@ function parseRouteExports(
|
|
|
986
998
|
const imports = parseNamedImports(source, config);
|
|
987
999
|
const exportRegex =
|
|
988
1000
|
/export const\s+(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s*=\s*([^;\n]+)/g;
|
|
1001
|
+
const apiRouteExportRegex =
|
|
1002
|
+
/export const\s*\{([^}]+)\}\s*=\s*createApiRoute\s*\(/g;
|
|
1003
|
+
|
|
1004
|
+
for (const match of source.matchAll(apiRouteExportRegex)) {
|
|
1005
|
+
for (const member of match[1].split(",")) {
|
|
1006
|
+
const parts = member.split(":");
|
|
1007
|
+
const method = (parts[1] ?? parts[0])?.trim();
|
|
1008
|
+
if (!method || !isHttpMethod(method)) continue;
|
|
1009
|
+
|
|
1010
|
+
exports.push({
|
|
1011
|
+
method,
|
|
1012
|
+
handlerFile,
|
|
1013
|
+
contractRef: routePath,
|
|
1014
|
+
catchAllPrefix: catchAllRoutePrefix(routePath),
|
|
1015
|
+
source: "next-route",
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
989
1019
|
|
|
990
1020
|
for (const match of source.matchAll(exportRegex)) {
|
|
991
1021
|
const method = match[1] as HttpMethod;
|
|
@@ -1384,7 +1414,7 @@ async function inspectDiagnostics(
|
|
|
1384
1414
|
convention,
|
|
1385
1415
|
contracts,
|
|
1386
1416
|
)),
|
|
1387
|
-
...(await inspectServerlessFootguns(targetDir, files)),
|
|
1417
|
+
...(await inspectServerlessFootguns(targetDir, files, config, convention)),
|
|
1388
1418
|
...(await inspectProductionReadiness(targetDir, files, config, convention)),
|
|
1389
1419
|
...(await inspectFeatureArtifactDrift(
|
|
1390
1420
|
targetDir,
|
|
@@ -3773,6 +3803,8 @@ function isWorkflowTestFile(file: string): boolean {
|
|
|
3773
3803
|
async function inspectServerlessFootguns(
|
|
3774
3804
|
targetDir: string,
|
|
3775
3805
|
files: string[],
|
|
3806
|
+
config: ResolvedBeignetConfig,
|
|
3807
|
+
convention: InspectConvention,
|
|
3776
3808
|
): Promise<InspectDiagnostic[]> {
|
|
3777
3809
|
const diagnostics: InspectDiagnostic[] = [];
|
|
3778
3810
|
|
|
@@ -3792,6 +3824,19 @@ async function inspectServerlessFootguns(
|
|
|
3792
3824
|
/(^|\/)[^/]*provider[^/]*\.ts$/.test(file);
|
|
3793
3825
|
const source = await readFile(path.join(targetDir, file), "utf8");
|
|
3794
3826
|
|
|
3827
|
+
if (
|
|
3828
|
+
convention.resourceGenerator &&
|
|
3829
|
+
isNextRouteFile(file, config) &&
|
|
3830
|
+
importsEagerServer(source, file, files, config)
|
|
3831
|
+
) {
|
|
3832
|
+
diagnostics.push({
|
|
3833
|
+
severity: "warning",
|
|
3834
|
+
code: "BEIGNET_NEXT_ROUTE_EAGER_SERVER_IMPORT",
|
|
3835
|
+
file,
|
|
3836
|
+
message: `${file} imports the eager server singleton. Next.js imports route modules during production builds, so use createNextServerLoader(...) in ${config.paths.server} and pass getServer to createApiRoute(...), createHealthRoute(...), or the focused route helper instead.`,
|
|
3837
|
+
});
|
|
3838
|
+
}
|
|
3839
|
+
|
|
3795
3840
|
if (
|
|
3796
3841
|
isInfraOrServerFile &&
|
|
3797
3842
|
isProviderFile &&
|
|
@@ -3850,6 +3895,37 @@ async function inspectServerlessFootguns(
|
|
|
3850
3895
|
return diagnostics;
|
|
3851
3896
|
}
|
|
3852
3897
|
|
|
3898
|
+
function isNextRouteFile(file: string, config: ResolvedBeignetConfig): boolean {
|
|
3899
|
+
const routesPath = directoryPath(config.paths.routes);
|
|
3900
|
+
|
|
3901
|
+
return (
|
|
3902
|
+
file.startsWith(`${routesPath}/`) &&
|
|
3903
|
+
file.endsWith("/route.ts") &&
|
|
3904
|
+
!file.endsWith(".test.ts")
|
|
3905
|
+
);
|
|
3906
|
+
}
|
|
3907
|
+
|
|
3908
|
+
function importsEagerServer(
|
|
3909
|
+
source: string,
|
|
3910
|
+
importerFile: string,
|
|
3911
|
+
files: string[],
|
|
3912
|
+
config: ResolvedBeignetConfig,
|
|
3913
|
+
): boolean {
|
|
3914
|
+
const importRegex = /import\s+\{([^}]+)\}\s+from\s+["']([^"']+)["']/g;
|
|
3915
|
+
|
|
3916
|
+
for (const match of source.matchAll(importRegex)) {
|
|
3917
|
+
const members = match[1]
|
|
3918
|
+
.split(",")
|
|
3919
|
+
.map((member) => parseImportMember(member));
|
|
3920
|
+
if (!members.some((member) => member?.importedName === "server")) continue;
|
|
3921
|
+
|
|
3922
|
+
const sourceFile = sourceFileFromImport(match[2], importerFile, files);
|
|
3923
|
+
if (sourceFile === config.paths.server) return true;
|
|
3924
|
+
}
|
|
3925
|
+
|
|
3926
|
+
return false;
|
|
3927
|
+
}
|
|
3928
|
+
|
|
3853
3929
|
function hasOutboxRegistry(files: string[]): boolean {
|
|
3854
3930
|
return files.some(isOutboxRegistryFile);
|
|
3855
3931
|
}
|
|
@@ -4076,7 +4152,9 @@ async function inspectDatabaseLifecycleDrift(
|
|
|
4076
4152
|
{
|
|
4077
4153
|
script: "db:seed",
|
|
4078
4154
|
command: "seed",
|
|
4079
|
-
|
|
4155
|
+
// The seed entrypoint lives beside the server runtime because it boots
|
|
4156
|
+
// the app through getServer(), which infra/ must not import.
|
|
4157
|
+
file: `${directoryPath(path.dirname(config.paths.server))}/seed.ts`,
|
|
4080
4158
|
code: "BEIGNET_DB_SEED_ENTRYPOINT_MISSING",
|
|
4081
4159
|
},
|
|
4082
4160
|
{
|
|
@@ -5396,6 +5474,94 @@ async function fixUnregisteredTasks(
|
|
|
5396
5474
|
});
|
|
5397
5475
|
}
|
|
5398
5476
|
|
|
5477
|
+
/**
|
|
5478
|
+
* Wire fully unregistered feature listener registries into the providers
|
|
5479
|
+
* file with the same `<registry>Provider` block `beignet make listener`
|
|
5480
|
+
* generates, so both write paths stay byte-compatible.
|
|
5481
|
+
*
|
|
5482
|
+
* Bails without writing when the providers file or its exported array is
|
|
5483
|
+
* missing or customized, when the app has no eventBus port to register
|
|
5484
|
+
* against, or when the registry name is already imported from another file.
|
|
5485
|
+
* The diagnostic stays in every bail case.
|
|
5486
|
+
*/
|
|
5487
|
+
async function fixUnregisteredListeners(
|
|
5488
|
+
targetDir: string,
|
|
5489
|
+
files: string[],
|
|
5490
|
+
drift: WorkflowRegistrationDrift,
|
|
5491
|
+
config: ResolvedBeignetConfig,
|
|
5492
|
+
): Promise<InspectFix | undefined> {
|
|
5493
|
+
const candidates = drift.listeners.unregistered.filter(
|
|
5494
|
+
(entry) => entry.fullyUnregistered,
|
|
5495
|
+
);
|
|
5496
|
+
if (candidates.length === 0) return undefined;
|
|
5497
|
+
|
|
5498
|
+
const providersFile = providersFilePath(config);
|
|
5499
|
+
const contextFile = normalizePath(
|
|
5500
|
+
path.join(path.dirname(config.paths.server), "context.ts"),
|
|
5501
|
+
);
|
|
5502
|
+
const wiringDependencies = [
|
|
5503
|
+
providersFile,
|
|
5504
|
+
contextFile,
|
|
5505
|
+
config.paths.appContext,
|
|
5506
|
+
config.paths.ports,
|
|
5507
|
+
];
|
|
5508
|
+
if (wiringDependencies.some((file) => !files.includes(file))) {
|
|
5509
|
+
return undefined;
|
|
5510
|
+
}
|
|
5511
|
+
|
|
5512
|
+
// The generated wiring registers listeners on ports.eventBus; without the
|
|
5513
|
+
// port the fix would produce code that cannot typecheck.
|
|
5514
|
+
const portsSource = await readFile(
|
|
5515
|
+
path.join(targetDir, config.paths.ports),
|
|
5516
|
+
"utf8",
|
|
5517
|
+
);
|
|
5518
|
+
if (!/\beventBus\s*[:?]/.test(portsSource)) return undefined;
|
|
5519
|
+
|
|
5520
|
+
const providersPath = path.join(targetDir, providersFile);
|
|
5521
|
+
const original = await readFile(providersPath, "utf8");
|
|
5522
|
+
let next = original;
|
|
5523
|
+
const registeredNames: string[] = [];
|
|
5524
|
+
|
|
5525
|
+
for (const { registry } of candidates) {
|
|
5526
|
+
const imported = parseNamedImportSources(next).get(registry.registryName);
|
|
5527
|
+
if (imported) {
|
|
5528
|
+
const importedFile = sourceFileFromImport(
|
|
5529
|
+
imported.sourcePath,
|
|
5530
|
+
providersFile,
|
|
5531
|
+
files,
|
|
5532
|
+
);
|
|
5533
|
+
if (importedFile !== registry.indexFile) return undefined;
|
|
5534
|
+
}
|
|
5535
|
+
|
|
5536
|
+
const result = wireListenersProviderSource(next, {
|
|
5537
|
+
registryName: registry.registryName,
|
|
5538
|
+
featureKebab: listenerFeatureKebab(registry.indexFile),
|
|
5539
|
+
listenerModule: `@/${modulePath(registry.indexFile)}`,
|
|
5540
|
+
config,
|
|
5541
|
+
});
|
|
5542
|
+
if (result.kind === "missing") return undefined;
|
|
5543
|
+
if (result.kind === "updated") {
|
|
5544
|
+
next = result.source;
|
|
5545
|
+
registeredNames.push(registry.registryName);
|
|
5546
|
+
}
|
|
5547
|
+
}
|
|
5548
|
+
|
|
5549
|
+
if (next === original || registeredNames.length === 0) return undefined;
|
|
5550
|
+
|
|
5551
|
+
await writeFile(providersPath, next);
|
|
5552
|
+
return {
|
|
5553
|
+
code: "BEIGNET_LISTENER_UNREGISTERED",
|
|
5554
|
+
file: providersFile,
|
|
5555
|
+
message: `Registered ${registeredNames.join(", ")} with registerListeners(...) in ${providersFile}.`,
|
|
5556
|
+
};
|
|
5557
|
+
}
|
|
5558
|
+
|
|
5559
|
+
function listenerFeatureKebab(indexFile: string): string {
|
|
5560
|
+
return path.posix.basename(
|
|
5561
|
+
path.posix.dirname(path.posix.dirname(normalizePath(indexFile))),
|
|
5562
|
+
);
|
|
5563
|
+
}
|
|
5564
|
+
|
|
5399
5565
|
async function fixUnregisteredOutboxEntries(
|
|
5400
5566
|
targetDir: string,
|
|
5401
5567
|
files: string[],
|