@beignet/cli 0.0.47 → 0.0.48
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 +12 -0
- package/README.md +50 -21
- package/dist/check.d.ts +8 -1
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +18 -6
- package/dist/check.js.map +1 -1
- package/dist/db.d.ts +38 -0
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +52 -0
- package/dist/db.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +73 -6
- package/dist/index.js.map +1 -1
- package/dist/inspect.d.ts.map +1 -1
- package/dist/inspect.js +39 -2
- package/dist/inspect.js.map +1 -1
- package/dist/lib.d.ts +2 -2
- package/dist/lib.d.ts.map +1 -1
- package/dist/lib.js +1 -1
- package/dist/lib.js.map +1 -1
- package/dist/make/inbox.js +1 -1
- package/dist/make/inbox.js.map +1 -1
- package/dist/make/payments.js +1 -1
- package/dist/make/payments.js.map +1 -1
- package/dist/make/tenancy.js +1 -1
- package/dist/make/tenancy.js.map +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +39 -2
- package/dist/mcp.js.map +1 -1
- package/dist/preflight.d.ts +11 -3
- package/dist/preflight.d.ts.map +1 -1
- package/dist/preflight.js +139 -16
- package/dist/preflight.js.map +1 -1
- package/dist/provider-add.d.ts.map +1 -1
- package/dist/provider-add.js +7 -2
- package/dist/provider-add.js.map +1 -1
- package/dist/provider-audit.d.ts +13 -0
- package/dist/provider-audit.d.ts.map +1 -1
- package/dist/provider-audit.js +122 -8
- package/dist/provider-audit.js.map +1 -1
- package/dist/templates/agents.d.ts.map +1 -1
- package/dist/templates/agents.js +13 -7
- package/dist/templates/agents.js.map +1 -1
- package/dist/templates/base.d.ts +1 -0
- package/dist/templates/base.d.ts.map +1 -1
- package/dist/templates/base.js +11 -5
- package/dist/templates/base.js.map +1 -1
- package/dist/templates/db/index.d.ts +1 -0
- package/dist/templates/db/index.d.ts.map +1 -1
- package/dist/templates/db/index.js.map +1 -1
- package/dist/templates/db/migration-status.d.ts +9 -0
- package/dist/templates/db/migration-status.d.ts.map +1 -0
- package/dist/templates/db/migration-status.js +117 -0
- package/dist/templates/db/migration-status.js.map +1 -0
- package/dist/templates/db/mysql.d.ts.map +1 -1
- package/dist/templates/db/mysql.js +23 -0
- package/dist/templates/db/mysql.js.map +1 -1
- package/dist/templates/db/postgres.d.ts.map +1 -1
- package/dist/templates/db/postgres.js +21 -0
- package/dist/templates/db/postgres.js.map +1 -1
- package/dist/templates/db/sqlite.d.ts.map +1 -1
- package/dist/templates/db/sqlite.js +53 -0
- package/dist/templates/db/sqlite.js.map +1 -1
- package/dist/templates/index.d.ts.map +1 -1
- package/dist/templates/index.js +5 -0
- package/dist/templates/index.js.map +1 -1
- package/dist/templates/shared.d.ts.map +1 -1
- package/dist/templates/shared.js +1 -0
- package/dist/templates/shared.js.map +1 -1
- package/package.json +2 -2
- package/skills/app-structure/SKILL.md +11 -7
- package/src/check.ts +35 -7
- package/src/db.ts +97 -0
- package/src/index.ts +108 -7
- package/src/inspect.ts +76 -1
- package/src/lib.ts +9 -1
- package/src/make/inbox.ts +1 -1
- package/src/make/payments.ts +1 -1
- package/src/make/tenancy.ts +1 -1
- package/src/mcp.ts +56 -1
- package/src/preflight.ts +195 -16
- package/src/provider-add.ts +7 -2
- package/src/provider-audit.ts +210 -18
- package/src/templates/agents.ts +13 -7
- package/src/templates/base.ts +12 -3
- package/src/templates/db/index.ts +1 -0
- package/src/templates/db/migration-status.ts +126 -0
- package/src/templates/db/mysql.ts +23 -0
- package/src/templates/db/postgres.ts +21 -0
- package/src/templates/db/sqlite.ts +53 -0
- package/src/templates/index.ts +5 -0
- package/src/templates/shared.ts +1 -0
package/src/provider-audit.ts
CHANGED
|
@@ -18,6 +18,7 @@ export type ProviderPackageJson = {
|
|
|
18
18
|
dependencies?: Record<string, string>;
|
|
19
19
|
devDependencies?: Record<string, string>;
|
|
20
20
|
peerDependencies?: Record<string, string>;
|
|
21
|
+
scripts?: Record<string, string>;
|
|
21
22
|
};
|
|
22
23
|
|
|
23
24
|
export type ProviderDoctorVariantRule = {
|
|
@@ -25,6 +26,7 @@ export type ProviderDoctorVariantRule = {
|
|
|
25
26
|
displayName: string;
|
|
26
27
|
tokens: readonly string[];
|
|
27
28
|
requiredEnv: readonly string[];
|
|
29
|
+
requiredEnvAlternatives: readonly (readonly string[])[];
|
|
28
30
|
requiredTables: readonly string[];
|
|
29
31
|
registrationSeverity?: "warning" | "hint";
|
|
30
32
|
};
|
|
@@ -35,6 +37,7 @@ export type ProviderDoctorRule = {
|
|
|
35
37
|
tokens: readonly string[];
|
|
36
38
|
appPorts?: readonly { name: string; type: string }[];
|
|
37
39
|
requiredEnv?: readonly string[];
|
|
40
|
+
requiredEnvAlternatives?: readonly (readonly string[])[];
|
|
38
41
|
requiredTables?: readonly string[];
|
|
39
42
|
watchers?: readonly string[];
|
|
40
43
|
registrationSeverity?: "warning" | "hint";
|
|
@@ -94,6 +97,14 @@ export type ProviderRequirementAudit = {
|
|
|
94
97
|
status: ProviderAuditStatus;
|
|
95
98
|
required: string[];
|
|
96
99
|
missing: string[];
|
|
100
|
+
alternatives?: string[][];
|
|
101
|
+
alternativeGroups?: Array<{
|
|
102
|
+
name: string;
|
|
103
|
+
displayName: string;
|
|
104
|
+
status: "present" | "missing";
|
|
105
|
+
alternatives: string[][];
|
|
106
|
+
missing: string[];
|
|
107
|
+
}>;
|
|
97
108
|
};
|
|
98
109
|
|
|
99
110
|
export type ProviderAppPortsAudit = {
|
|
@@ -282,6 +293,36 @@ export async function providerDoctorRulesForInstalledPackages(
|
|
|
282
293
|
return rules.filter((rule): rule is ProviderDoctorRule => Boolean(rule));
|
|
283
294
|
}
|
|
284
295
|
|
|
296
|
+
export async function detectedProviderVariantsForRules(
|
|
297
|
+
targetDir: string,
|
|
298
|
+
rules: readonly ProviderDoctorRule[],
|
|
299
|
+
): Promise<Map<string, ProviderDoctorVariantRule[]>> {
|
|
300
|
+
const variantRules = rules.filter(
|
|
301
|
+
(
|
|
302
|
+
rule,
|
|
303
|
+
): rule is ProviderDoctorRule & {
|
|
304
|
+
variants: readonly ProviderDoctorVariantRule[];
|
|
305
|
+
} => rule.variants !== undefined,
|
|
306
|
+
);
|
|
307
|
+
if (variantRules.length === 0) return new Map();
|
|
308
|
+
|
|
309
|
+
const files = await listFiles(targetDir);
|
|
310
|
+
const config = await loadBeignetConfig(targetDir, files);
|
|
311
|
+
const providerEntries = await readProviderListEntries(
|
|
312
|
+
targetDir,
|
|
313
|
+
files,
|
|
314
|
+
config,
|
|
315
|
+
new Map(),
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
return new Map(
|
|
319
|
+
variantRules.map((rule) => [
|
|
320
|
+
rule.packageName,
|
|
321
|
+
detectedProviderVariants(rule.variants, providerEntries),
|
|
322
|
+
]),
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
285
326
|
export async function readProviderPackageMetadataSource(
|
|
286
327
|
targetDir: string,
|
|
287
328
|
packageName: string,
|
|
@@ -314,6 +355,7 @@ export function providerDoctorRuleFromMetadata(
|
|
|
314
355
|
tokens: registration?.tokens ?? [],
|
|
315
356
|
appPorts: metadata.appPorts ?? [],
|
|
316
357
|
requiredEnv: metadata.requiredEnv ?? [],
|
|
358
|
+
requiredEnvAlternatives: metadata.requiredEnvAlternatives ?? [],
|
|
317
359
|
requiredTables: metadata.requiredTables ?? [],
|
|
318
360
|
watchers: metadata.watchers ?? [],
|
|
319
361
|
registrationSeverity: registrationSeverityFromMetadata(registration),
|
|
@@ -322,6 +364,7 @@ export function providerDoctorRuleFromMetadata(
|
|
|
322
364
|
displayName: variant.displayName ?? variant.name,
|
|
323
365
|
tokens: variant.registration?.tokens ?? [],
|
|
324
366
|
requiredEnv: variant.requiredEnv ?? [],
|
|
367
|
+
requiredEnvAlternatives: variant.requiredEnvAlternatives ?? [],
|
|
325
368
|
requiredTables: variant.requiredTables ?? [],
|
|
326
369
|
registrationSeverity: registrationSeverityFromMetadata(
|
|
327
370
|
variant.registration,
|
|
@@ -1061,6 +1104,15 @@ function configuredProviderRequiredEnv(
|
|
|
1061
1104
|
return envVars.filter((envVar) => !ignored.has(envVar));
|
|
1062
1105
|
}
|
|
1063
1106
|
|
|
1107
|
+
function configuredProviderRequiredEnvAlternatives(
|
|
1108
|
+
alternatives: readonly (readonly string[])[],
|
|
1109
|
+
config: ResolvedBeignetConfig,
|
|
1110
|
+
): string[][] {
|
|
1111
|
+
return alternatives.map((alternative) =>
|
|
1112
|
+
configuredProviderRequiredEnv(alternative, config),
|
|
1113
|
+
);
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1064
1116
|
function configuredProviderRequiredTables(
|
|
1065
1117
|
tableNames: readonly string[],
|
|
1066
1118
|
config: ResolvedBeignetConfig,
|
|
@@ -1187,19 +1239,23 @@ async function providerAuditEntryForRule(options: {
|
|
|
1187
1239
|
const requiredEnv =
|
|
1188
1240
|
options.rule.variants !== undefined
|
|
1189
1241
|
? aggregateVariantRequirement(variants, "requiredEnv")
|
|
1190
|
-
: await
|
|
1191
|
-
configuredProviderRequiredEnv(
|
|
1242
|
+
: await providerEnvRequirementAudit({
|
|
1243
|
+
required: configuredProviderRequiredEnv(
|
|
1192
1244
|
options.rule.requiredEnv ?? [],
|
|
1193
1245
|
options.config,
|
|
1194
1246
|
),
|
|
1195
|
-
(
|
|
1247
|
+
alternatives: configuredProviderRequiredEnvAlternatives(
|
|
1248
|
+
options.rule.requiredEnvAlternatives ?? [],
|
|
1249
|
+
options.config,
|
|
1250
|
+
),
|
|
1251
|
+
exists: (envVar) =>
|
|
1196
1252
|
providerRequiredEnvExists(
|
|
1197
1253
|
options.targetDir,
|
|
1198
1254
|
options.configFiles,
|
|
1199
1255
|
envVar,
|
|
1200
1256
|
options.sourceCache,
|
|
1201
1257
|
),
|
|
1202
|
-
);
|
|
1258
|
+
});
|
|
1203
1259
|
|
|
1204
1260
|
const requiredTables =
|
|
1205
1261
|
options.rule.variants !== undefined
|
|
@@ -1288,12 +1344,19 @@ async function providerVariantAudit(options: {
|
|
|
1288
1344
|
options.variant.requiredEnv,
|
|
1289
1345
|
options.config,
|
|
1290
1346
|
);
|
|
1347
|
+
const requiredEnvAlternatives = configuredProviderRequiredEnvAlternatives(
|
|
1348
|
+
options.variant.requiredEnvAlternatives,
|
|
1349
|
+
options.config,
|
|
1350
|
+
);
|
|
1291
1351
|
if (!options.registered) {
|
|
1292
1352
|
return {
|
|
1293
1353
|
name: options.variant.name,
|
|
1294
1354
|
displayName: options.variant.displayName,
|
|
1295
1355
|
registration: "inactive",
|
|
1296
|
-
requiredEnv:
|
|
1356
|
+
requiredEnv:
|
|
1357
|
+
requiredEnvAlternatives.length > 0
|
|
1358
|
+
? inactiveAlternativeRequirement(requiredEnvAlternatives)
|
|
1359
|
+
: inactiveRequirement(requiredEnv),
|
|
1297
1360
|
requiredTables: inactiveRequirement(options.variant.requiredTables),
|
|
1298
1361
|
};
|
|
1299
1362
|
}
|
|
@@ -1306,14 +1369,17 @@ async function providerVariantAudit(options: {
|
|
|
1306
1369
|
options.variant.registrationSeverity,
|
|
1307
1370
|
options.providerEntries,
|
|
1308
1371
|
),
|
|
1309
|
-
requiredEnv: await
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1372
|
+
requiredEnv: await providerEnvRequirementAudit({
|
|
1373
|
+
required: requiredEnv,
|
|
1374
|
+
alternatives: requiredEnvAlternatives,
|
|
1375
|
+
exists: (envVar) =>
|
|
1376
|
+
providerRequiredEnvExists(
|
|
1377
|
+
options.targetDir,
|
|
1378
|
+
options.configFiles,
|
|
1379
|
+
envVar,
|
|
1380
|
+
options.sourceCache,
|
|
1381
|
+
),
|
|
1382
|
+
}),
|
|
1317
1383
|
requiredTables: await requirementAudit(
|
|
1318
1384
|
configuredProviderRequiredTables(
|
|
1319
1385
|
options.variant.requiredTables,
|
|
@@ -1448,6 +1514,34 @@ async function requirementAudit(
|
|
|
1448
1514
|
};
|
|
1449
1515
|
}
|
|
1450
1516
|
|
|
1517
|
+
async function providerEnvRequirementAudit(options: {
|
|
1518
|
+
required: readonly string[];
|
|
1519
|
+
alternatives: readonly (readonly string[])[];
|
|
1520
|
+
exists: (requirement: string) => Promise<boolean>;
|
|
1521
|
+
}): Promise<ProviderRequirementAudit> {
|
|
1522
|
+
if (options.alternatives.length === 0) {
|
|
1523
|
+
return requirementAudit(options.required, options.exists);
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
const missingAlternatives: string[][] = [];
|
|
1527
|
+
for (const alternative of options.alternatives) {
|
|
1528
|
+
const missing: string[] = [];
|
|
1529
|
+
for (const requirement of alternative) {
|
|
1530
|
+
if (await options.exists(requirement)) continue;
|
|
1531
|
+
missing.push(requirement);
|
|
1532
|
+
}
|
|
1533
|
+
missingAlternatives.push(missing);
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
const present = missingAlternatives.some((missing) => missing.length === 0);
|
|
1537
|
+
return {
|
|
1538
|
+
status: present ? "present" : "missing",
|
|
1539
|
+
required: unique(options.alternatives.flat()),
|
|
1540
|
+
missing: present ? [] : unique(missingAlternatives.flat()),
|
|
1541
|
+
alternatives: options.alternatives.map((alternative) => [...alternative]),
|
|
1542
|
+
};
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1451
1545
|
function inactiveRequirement(
|
|
1452
1546
|
required: readonly string[],
|
|
1453
1547
|
): ProviderRequirementAudit {
|
|
@@ -1458,6 +1552,17 @@ function inactiveRequirement(
|
|
|
1458
1552
|
};
|
|
1459
1553
|
}
|
|
1460
1554
|
|
|
1555
|
+
function inactiveAlternativeRequirement(
|
|
1556
|
+
alternatives: readonly (readonly string[])[],
|
|
1557
|
+
): ProviderRequirementAudit {
|
|
1558
|
+
return {
|
|
1559
|
+
status: "inactive",
|
|
1560
|
+
required: unique(alternatives.flat()),
|
|
1561
|
+
missing: [],
|
|
1562
|
+
alternatives: alternatives.map((alternative) => [...alternative]),
|
|
1563
|
+
};
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1461
1566
|
function emptyRequirement(): ProviderRequirementAudit {
|
|
1462
1567
|
return { status: "none", required: [], missing: [] };
|
|
1463
1568
|
}
|
|
@@ -1467,21 +1572,57 @@ function aggregateVariantRequirement(
|
|
|
1467
1572
|
key: "requiredEnv" | "requiredTables",
|
|
1468
1573
|
): ProviderRequirementAudit {
|
|
1469
1574
|
const active = variants
|
|
1470
|
-
.map((variant) => variant[key])
|
|
1471
|
-
.filter((requirement) => requirement.status !== "inactive");
|
|
1575
|
+
.map((variant) => ({ variant, requirement: variant[key] }))
|
|
1576
|
+
.filter(({ requirement }) => requirement.status !== "inactive");
|
|
1472
1577
|
if (active.length === 0)
|
|
1473
1578
|
return { status: "inactive", required: [], missing: [] };
|
|
1474
1579
|
|
|
1475
1580
|
const required = unique(
|
|
1476
|
-
active.flatMap((requirement) => requirement.required),
|
|
1581
|
+
active.flatMap(({ requirement }) => requirement.required),
|
|
1582
|
+
);
|
|
1583
|
+
const missing = unique(
|
|
1584
|
+
active.flatMap(({ requirement }) => requirement.missing),
|
|
1477
1585
|
);
|
|
1478
|
-
const missing = unique(active.flatMap((requirement) => requirement.missing));
|
|
1479
1586
|
if (required.length === 0) return emptyRequirement();
|
|
1480
1587
|
|
|
1588
|
+
const hasAlternativeRequirements = active.some(
|
|
1589
|
+
({ requirement }) =>
|
|
1590
|
+
requirement.alternatives !== undefined ||
|
|
1591
|
+
requirement.alternativeGroups !== undefined,
|
|
1592
|
+
);
|
|
1593
|
+
const alternativeGroups = hasAlternativeRequirements
|
|
1594
|
+
? active.flatMap(({ variant, requirement }) => {
|
|
1595
|
+
if (requirement.status === "none") return [];
|
|
1596
|
+
if (
|
|
1597
|
+
requirement.status !== "present" &&
|
|
1598
|
+
requirement.status !== "missing"
|
|
1599
|
+
) {
|
|
1600
|
+
return [];
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
return [
|
|
1604
|
+
{
|
|
1605
|
+
name: variant.name,
|
|
1606
|
+
displayName: variant.displayName,
|
|
1607
|
+
status: requirement.status,
|
|
1608
|
+
alternatives: (
|
|
1609
|
+
requirement.alternatives ?? [requirement.required]
|
|
1610
|
+
).map((alternative) => [...alternative]),
|
|
1611
|
+
missing: [...requirement.missing],
|
|
1612
|
+
},
|
|
1613
|
+
];
|
|
1614
|
+
})
|
|
1615
|
+
: [];
|
|
1616
|
+
|
|
1481
1617
|
return {
|
|
1482
1618
|
status: missing.length === 0 ? "present" : "missing",
|
|
1483
1619
|
required,
|
|
1484
1620
|
missing,
|
|
1621
|
+
...(alternativeGroups.length === 1
|
|
1622
|
+
? { alternatives: alternativeGroups[0]?.alternatives ?? [] }
|
|
1623
|
+
: alternativeGroups.length > 0
|
|
1624
|
+
? { alternativeGroups }
|
|
1625
|
+
: {}),
|
|
1485
1626
|
};
|
|
1486
1627
|
}
|
|
1487
1628
|
|
|
@@ -1520,7 +1661,17 @@ function providerAuditNotes(input: {
|
|
|
1520
1661
|
notes.push("optional provider not registered");
|
|
1521
1662
|
}
|
|
1522
1663
|
if (input.requiredEnv.missing.length > 0) {
|
|
1523
|
-
notes.push(
|
|
1664
|
+
notes.push(
|
|
1665
|
+
input.requiredEnv.alternativeGroups
|
|
1666
|
+
? `missing env configuration groups: ${formatProviderEnvAlternativeGroups(
|
|
1667
|
+
input.requiredEnv.alternativeGroups,
|
|
1668
|
+
)}`
|
|
1669
|
+
: input.requiredEnv.alternatives
|
|
1670
|
+
? `missing env alternative: ${formatProviderEnvAlternatives(
|
|
1671
|
+
input.requiredEnv.alternatives,
|
|
1672
|
+
)}`
|
|
1673
|
+
: `missing env: ${input.requiredEnv.missing.join(", ")}`,
|
|
1674
|
+
);
|
|
1524
1675
|
}
|
|
1525
1676
|
if (input.requiredTables.missing.length > 0) {
|
|
1526
1677
|
notes.push(`missing tables: ${input.requiredTables.missing.join(", ")}`);
|
|
@@ -1863,6 +2014,21 @@ function escapeRegExp(value: string): string {
|
|
|
1863
2014
|
function requirementSummary(requirement: ProviderRequirementAudit): string {
|
|
1864
2015
|
if (requirement.status === "none") return "none";
|
|
1865
2016
|
if (requirement.status === "inactive") return "inactive";
|
|
2017
|
+
if (requirement.alternativeGroups) {
|
|
2018
|
+
const count = requirement.alternativeGroups.length;
|
|
2019
|
+
const missing = requirement.alternativeGroups.filter(
|
|
2020
|
+
(group) => group.status === "missing",
|
|
2021
|
+
).length;
|
|
2022
|
+
return requirement.status === "present"
|
|
2023
|
+
? `${count} variant groups ok`
|
|
2024
|
+
: `${missing}/${count} variant groups incomplete`;
|
|
2025
|
+
}
|
|
2026
|
+
if (requirement.alternatives) {
|
|
2027
|
+
if (requirement.status === "present") {
|
|
2028
|
+
return "alternative ok";
|
|
2029
|
+
}
|
|
2030
|
+
return `${requirement.alternatives.length} alternatives missing`;
|
|
2031
|
+
}
|
|
1866
2032
|
if (requirement.status === "present") {
|
|
1867
2033
|
return `${requirement.required.length} ok`;
|
|
1868
2034
|
}
|
|
@@ -1879,6 +2045,32 @@ function unique(values: readonly string[]): string[] {
|
|
|
1879
2045
|
return [...new Set(values)];
|
|
1880
2046
|
}
|
|
1881
2047
|
|
|
2048
|
+
export function formatProviderEnvAlternatives(
|
|
2049
|
+
alternatives: readonly (readonly string[])[],
|
|
2050
|
+
): string {
|
|
2051
|
+
return alternatives
|
|
2052
|
+
.map((alternative) =>
|
|
2053
|
+
alternative.length === 1
|
|
2054
|
+
? alternative[0]
|
|
2055
|
+
: `(${alternative.join(" + ")})`,
|
|
2056
|
+
)
|
|
2057
|
+
.join(" or ");
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
function formatProviderEnvAlternativeGroups(
|
|
2061
|
+
groups: NonNullable<ProviderRequirementAudit["alternativeGroups"]>,
|
|
2062
|
+
): string {
|
|
2063
|
+
return groups
|
|
2064
|
+
.filter((group) => group.status === "missing")
|
|
2065
|
+
.map(
|
|
2066
|
+
(group) =>
|
|
2067
|
+
`${group.displayName}: ${formatProviderEnvAlternatives(
|
|
2068
|
+
group.alternatives,
|
|
2069
|
+
)}`,
|
|
2070
|
+
)
|
|
2071
|
+
.join(" and ");
|
|
2072
|
+
}
|
|
2073
|
+
|
|
1882
2074
|
function table(rows: string[][]): string {
|
|
1883
2075
|
const widths = rows[0].map((_, columnIndex) =>
|
|
1884
2076
|
Math.max(...rows.map((row) => row[columnIndex].length)),
|
package/src/templates/agents.ts
CHANGED
|
@@ -79,7 +79,8 @@ array anchors are unambiguous. It also syncs missing default Beignet
|
|
|
79
79
|
provider-table exports when \`infra/db/schema/beignet.ts\` is missing or still
|
|
80
80
|
matches \`${cli} db schema sync\` output and the schema index has no custom
|
|
81
81
|
named re-export for that file. Run \`${cli} db generate\` and
|
|
82
|
-
\`${cli} db migrate\` after accepting that source
|
|
82
|
+
\`${cli} db migrate\`, then \`${cli} db status\` after accepting that source
|
|
83
|
+
repair. Custom or ambiguous
|
|
83
84
|
code stays diagnostic-only.
|
|
84
85
|
|
|
85
86
|
## Prefer generators
|
|
@@ -89,10 +90,12 @@ files — prefer it over hand-writing them. See \`${cli} make --help\` for the
|
|
|
89
90
|
artifact list. Use \`${cli} make feature <name> --recipe full-slice\` when you
|
|
90
91
|
need a richer reference slice with policy, client helpers, workflow artifacts,
|
|
91
92
|
events, listener registration, jobs, and outbox wiring. After changing the Drizzle schema in
|
|
92
|
-
\`infra/db/schema/\`, run \`${cli} db generate
|
|
93
|
+
\`infra/db/schema/\`, run \`${cli} db generate\`, \`${cli} db migrate\`, then
|
|
94
|
+
\`${cli} db status\`.
|
|
93
95
|
When MCP is available, use \`db_schema_sync\` for Beignet provider table
|
|
94
96
|
re-exports and \`db\` for \`generate\`, \`migrate\`, \`seed\`, or \`reset\`
|
|
95
|
-
instead of falling back to a shell. Use \`
|
|
97
|
+
instead of falling back to a shell. Use the read-only \`db_status\` tool before
|
|
98
|
+
deploying. Use \`task_run\`, \`schedule_run\`,
|
|
96
99
|
\`outbox_inspect\`, and \`outbox_run\` for registered operational workflows
|
|
97
100
|
and outbox recovery.
|
|
98
101
|
|
|
@@ -175,7 +178,7 @@ skill-loading block.
|
|
|
175
178
|
\`.mcp.json\` registers the app-local \`@beignet/cli\` bin at
|
|
176
179
|
\`./node_modules/.bin/beignet mcp\`, which exposes app context, validation,
|
|
177
180
|
generation, and operations as structured tools named exactly: \`app_map\`,
|
|
178
|
-
\`explain\`, \`check\`, \`db\`, \`db_schema_sync\`, \`task_run\`,
|
|
181
|
+
\`explain\`, \`check\`, \`db\`, \`db_status\`, \`db_schema_sync\`, \`task_run\`,
|
|
179
182
|
\`schedule_run\`, \`outbox_inspect\`, \`outbox_run\`, \`routes\`, \`doctor\`,
|
|
180
183
|
\`doctor_fix_plan\`, \`doctor_fix\`, \`lint\`, \`make\`, \`provider_add\`.
|
|
181
184
|
It also publishes \`beignet://app/guidance\` and focused
|
|
@@ -190,11 +193,14 @@ relationships, conventions, findings, and suggested files for any mapped
|
|
|
190
193
|
concept or diagnostic; its kinds match the app map. Use \`check\` after edits
|
|
191
194
|
to run the complete validation loop; it returns the same versioned result as
|
|
192
195
|
\`${cli} check --json\`, bounds failure output, and cancels the active package
|
|
193
|
-
script when the MCP request is cancelled.
|
|
194
|
-
|
|
196
|
+
script when the MCP request is cancelled. Pass \`preflightConnect: true\` to
|
|
197
|
+
include migration status and dependency health checks; \`connectTimeoutMs\`
|
|
198
|
+
sets their per-check timeout. It does not apply Beignet fixes, but app-owned
|
|
199
|
+
scripts retain their normal side effects. Use \`db_schema_sync\` to
|
|
195
200
|
preview or apply idempotent provider-table schema re-exports, then call \`db\`
|
|
196
201
|
with \`generate\` and \`migrate\`; database output is bounded, commands time
|
|
197
|
-
out, and cancellation stops the active process tree.
|
|
202
|
+
out, and cancellation stops the active process tree. Use \`db_status\` for a
|
|
203
|
+
read-only current, pending, or failed migration report. Treat \`seed\` and
|
|
198
204
|
especially \`reset\` as app-owned mutations. Lifecycle \`dryRun\` validates
|
|
199
205
|
and reports the script without executing it; it does not simulate SQL or data
|
|
200
206
|
changes. Use \`task_run\` and \`schedule_run\` for registered operational
|
package/src/templates/base.ts
CHANGED
|
@@ -103,6 +103,7 @@ export function packageJson(ctx: TemplateContext): string {
|
|
|
103
103
|
"db:generate": "drizzle-kit generate",
|
|
104
104
|
"db:migrate": "drizzle-kit migrate",
|
|
105
105
|
"db:reset": currentGeneratedPackageScripts["db:reset"],
|
|
106
|
+
"db:status": currentGeneratedPackageScripts["db:status"],
|
|
106
107
|
};
|
|
107
108
|
|
|
108
109
|
return json({
|
|
@@ -254,9 +255,10 @@ ${startDatabase}## Prepare the database
|
|
|
254
255
|
|
|
255
256
|
\`\`\`bash
|
|
256
257
|
${cli} db migrate
|
|
258
|
+
${cli} db status
|
|
257
259
|
\`\`\`
|
|
258
260
|
|
|
259
|
-
The starter ships its initial Drizzle migration in \`drizzle/\`, so the first run only applies it. After you change \`infra/db/schema/\`, run \`${cli} db generate\` and \`${cli} db migrate\` together
|
|
261
|
+
The starter ships its initial Drizzle migration in \`drizzle/\`, so the first run only applies it. After you change \`infra/db/schema/\`, run \`${cli} db generate\` and \`${cli} db migrate\` together, then verify the target database with \`${cli} db status\`.
|
|
260
262
|
|
|
261
263
|
## Start the app
|
|
262
264
|
|
|
@@ -303,10 +305,11 @@ ${start}
|
|
|
303
305
|
${cli} make feature projects
|
|
304
306
|
${cli} db generate
|
|
305
307
|
${cli} db migrate
|
|
308
|
+
${cli} db status
|
|
306
309
|
${cli} check
|
|
307
310
|
\`\`\`
|
|
308
311
|
|
|
309
|
-
\`make feature\` creates a contract-to-test vertical slice with Drizzle schema and repository files, so regenerate and
|
|
312
|
+
\`make feature\` creates a contract-to-test vertical slice with Drizzle schema and repository files, so regenerate, migrate, and verify the database before running the app against the new feature.
|
|
310
313
|
Use \`${cli} make feature projects --recipe full-slice\` when you want a richer reference slice with policy, feature client helpers, workflow artifacts, events, listener registration, jobs, and outbox wiring.
|
|
311
314
|
|
|
312
315
|
## App map
|
|
@@ -352,7 +355,8 @@ ${optionalAuthRedirects}
|
|
|
352
355
|
" db migrate` against it before starting the app."
|
|
353
356
|
}
|
|
354
357
|
- Size the process-wide database client for the deployment. Postgres and MySQL use the generated pool-limit environment variable; count web, worker, preview, and task processes against the database connection limit, and keep writable SQLite files on one host.
|
|
355
|
-
- Run \`${cli} db generate
|
|
358
|
+
- Run \`${cli} db generate\`, \`${cli} db migrate\`, and \`${cli} db status\` after changing the Drizzle schema.
|
|
359
|
+
- Run \`${cli} db migrate\` in one serialized migration job per environment, then \`${cli} db status\` and \`${cli} preflight --connect\` before shifting traffic. Do not migrate independently from every web replica.
|
|
356
360
|
- Run \`${cli} db reset\` to rebuild a local ${
|
|
357
361
|
ctx.database === "sqlite"
|
|
358
362
|
? "SQLite"
|
|
@@ -429,6 +433,7 @@ export function providersDoc(ctx: TemplateContext): string {
|
|
|
429
433
|
"- Peer dependencies: `@upstash/ratelimit` and `@upstash/redis`",
|
|
430
434
|
"- The starter wires `createUpstashRateLimitProvider()` in `server/providers.ts` and adds `rateLimit: RateLimitPort` to `AppPorts`.",
|
|
431
435
|
"- `.env.example` ships dev placeholders so the app boots; set real `UPSTASH_REDIS_REST_URL` and `UPSTASH_REDIS_REST_TOKEN` values before rate limiting requests.",
|
|
436
|
+
"- Rate-limit decisions time out after 5000ms and fail closed by default. Opt into `UPSTASH_TIMEOUT_POLICY=fail-open` only when availability outweighs enforcement across this provider during an outage.",
|
|
432
437
|
"",
|
|
433
438
|
);
|
|
434
439
|
}
|
|
@@ -510,6 +515,8 @@ export function envExample(ctx: TemplateContext): string {
|
|
|
510
515
|
"UPSTASH_REDIS_REST_URL=https://dev-placeholder.upstash.io",
|
|
511
516
|
"UPSTASH_REDIS_REST_TOKEN=dev-placeholder-change-me",
|
|
512
517
|
"UPSTASH_PREFIX=beignet:ratelimit",
|
|
518
|
+
"# UPSTASH_TIMEOUT_MS=5000",
|
|
519
|
+
"# UPSTASH_TIMEOUT_POLICY=fail-closed",
|
|
513
520
|
"",
|
|
514
521
|
);
|
|
515
522
|
}
|
|
@@ -518,6 +525,8 @@ export function envExample(ctx: TemplateContext): string {
|
|
|
518
525
|
}
|
|
519
526
|
|
|
520
527
|
const files = {
|
|
528
|
+
gitattributes: `**/drizzle/*.sql text eol=lf
|
|
529
|
+
`,
|
|
521
530
|
gitignore: `.next
|
|
522
531
|
node_modules
|
|
523
532
|
dist
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
type MigrationStatusTemplateOptions = {
|
|
2
|
+
imports: string;
|
|
3
|
+
open: string;
|
|
4
|
+
loadApplied: string;
|
|
5
|
+
close: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export function migrationStatusTemplate(
|
|
9
|
+
options: MigrationStatusTemplateOptions,
|
|
10
|
+
): string {
|
|
11
|
+
return `${options.imports}
|
|
12
|
+
import { readFile } from "node:fs/promises";
|
|
13
|
+
import { readMigrationFiles } from "drizzle-orm/migrator";
|
|
14
|
+
|
|
15
|
+
type AppliedMigration = {
|
|
16
|
+
hash: string;
|
|
17
|
+
createdAt: number;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type MigrationJournal = {
|
|
21
|
+
entries: Array<{ tag: string; when: number }>;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
${options.open}
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
${indent(options.loadApplied, 1)}
|
|
28
|
+
process.exitCode = await reportMigrationStatus(applied);
|
|
29
|
+
} finally {
|
|
30
|
+
${indent(options.close, 1)}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function reportMigrationStatus(
|
|
34
|
+
applied: AppliedMigration[],
|
|
35
|
+
): Promise<number> {
|
|
36
|
+
const migrationsFolder = "drizzle";
|
|
37
|
+
const journal = JSON.parse(
|
|
38
|
+
await readFile(\`\${migrationsFolder}/meta/_journal.json\`, "utf8"),
|
|
39
|
+
) as MigrationJournal;
|
|
40
|
+
const files = readMigrationFiles({ migrationsFolder });
|
|
41
|
+
|
|
42
|
+
if (journal.entries.length !== files.length) {
|
|
43
|
+
console.error(
|
|
44
|
+
\`Migration history is invalid: journal has \${journal.entries.length} entries but \${files.length} migration files were read.\`,
|
|
45
|
+
);
|
|
46
|
+
return 1;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const local = files.map((migration, index) => ({
|
|
50
|
+
tag: journal.entries[index]?.tag ?? \`migration-\${index}\`,
|
|
51
|
+
createdAt: migration.folderMillis,
|
|
52
|
+
hash: migration.hash,
|
|
53
|
+
}));
|
|
54
|
+
const localByCreatedAt = new Map(
|
|
55
|
+
local.map((migration) => [migration.createdAt, migration]),
|
|
56
|
+
);
|
|
57
|
+
const appliedByCreatedAt = new Map<number, AppliedMigration>();
|
|
58
|
+
|
|
59
|
+
for (const migration of applied) {
|
|
60
|
+
if (appliedByCreatedAt.has(migration.createdAt)) {
|
|
61
|
+
console.error(
|
|
62
|
+
\`Migration history has duplicate applied timestamp \${migration.createdAt}.\`,
|
|
63
|
+
);
|
|
64
|
+
return 1;
|
|
65
|
+
}
|
|
66
|
+
appliedByCreatedAt.set(migration.createdAt, migration);
|
|
67
|
+
|
|
68
|
+
const localMigration = localByCreatedAt.get(migration.createdAt);
|
|
69
|
+
if (!localMigration) {
|
|
70
|
+
console.error(
|
|
71
|
+
\`Applied migration \${migration.createdAt} is not present in the checked-in migration journal.\`,
|
|
72
|
+
);
|
|
73
|
+
return 1;
|
|
74
|
+
}
|
|
75
|
+
if (localMigration.hash !== migration.hash) {
|
|
76
|
+
console.error(
|
|
77
|
+
\`Applied migration \${localMigration.tag} no longer matches its checked-in SQL.\`,
|
|
78
|
+
);
|
|
79
|
+
return 1;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const latestAppliedAt = applied.reduce(
|
|
84
|
+
(latest, migration) => Math.max(latest, migration.createdAt),
|
|
85
|
+
0,
|
|
86
|
+
);
|
|
87
|
+
for (const migration of local) {
|
|
88
|
+
if (
|
|
89
|
+
migration.createdAt <= latestAppliedAt &&
|
|
90
|
+
!appliedByCreatedAt.has(migration.createdAt)
|
|
91
|
+
) {
|
|
92
|
+
console.error(
|
|
93
|
+
\`Checked-in migration \${migration.tag} predates the latest applied migration but is absent from database history.\`,
|
|
94
|
+
);
|
|
95
|
+
return 1;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const pending = local.filter(
|
|
100
|
+
(migration) => migration.createdAt > latestAppliedAt,
|
|
101
|
+
);
|
|
102
|
+
if (pending.length > 0) {
|
|
103
|
+
console.error(
|
|
104
|
+
\`\${pending.length} pending database migration(s):\\n\${pending
|
|
105
|
+
.map((migration) => \`- \${migration.tag}\`)
|
|
106
|
+
.join("\\n")}\`,
|
|
107
|
+
);
|
|
108
|
+
return 2;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
console.log(
|
|
112
|
+
\`Database migrations are current (\${applied.length} applied, 0 pending).\`,
|
|
113
|
+
);
|
|
114
|
+
return 0;
|
|
115
|
+
}
|
|
116
|
+
`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function indent(source: string, depth: number): string {
|
|
120
|
+
const prefix = "\t".repeat(depth);
|
|
121
|
+
return source
|
|
122
|
+
.trim()
|
|
123
|
+
.split("\n")
|
|
124
|
+
.map((line) => `${prefix}${line}`)
|
|
125
|
+
.join("\n");
|
|
126
|
+
}
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
import { databaseLocalUrl, databaseStartCommand } from "../../choices.js";
|
|
26
26
|
import type { TemplateContext } from "../shared.js";
|
|
27
27
|
import type { DbTemplateFiles } from "./index.js";
|
|
28
|
+
import { migrationStatusTemplate } from "./migration-status.js";
|
|
28
29
|
|
|
29
30
|
export function mysqlDbFiles(ctx: TemplateContext): DbTemplateFiles {
|
|
30
31
|
return {
|
|
@@ -271,6 +272,28 @@ async function prepare(pool: Pool): Promise<void> {
|
|
|
271
272
|
await migrate(drizzle(pool), { migrationsFolder: "drizzle" });
|
|
272
273
|
}
|
|
273
274
|
`,
|
|
275
|
+
dbMigrationStatus: migrationStatusTemplate({
|
|
276
|
+
imports: `import mysql, { type RowDataPacket } from "mysql2/promise";
|
|
277
|
+
import { env } from "@/lib/env";`,
|
|
278
|
+
open: `const pool = mysql.createPool({ uri: env.MYSQL_DB_URL });`,
|
|
279
|
+
loadApplied: `const [tables] = await pool.query<RowDataPacket[]>(
|
|
280
|
+
"SELECT table_name FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = '__drizzle_migrations' LIMIT 1",
|
|
281
|
+
);
|
|
282
|
+
const applied: AppliedMigration[] =
|
|
283
|
+
tables.length === 0
|
|
284
|
+
? []
|
|
285
|
+
: (
|
|
286
|
+
(
|
|
287
|
+
await pool.query<RowDataPacket[]>(
|
|
288
|
+
"SELECT hash, created_at FROM __drizzle_migrations ORDER BY created_at",
|
|
289
|
+
)
|
|
290
|
+
)[0]
|
|
291
|
+
).map((row) => ({
|
|
292
|
+
hash: String(row.hash),
|
|
293
|
+
createdAt: Number(row.created_at),
|
|
294
|
+
}));`,
|
|
295
|
+
close: "await pool.end();",
|
|
296
|
+
}),
|
|
274
297
|
dbProvider: `import "@beignet/core/server-only";
|
|
275
298
|
import { createProvider } from "@beignet/core/providers";
|
|
276
299
|
import {
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
import { databaseLocalUrl } from "../../choices.js";
|
|
23
23
|
import type { TemplateContext } from "../shared.js";
|
|
24
24
|
import type { DbTemplateFiles } from "./index.js";
|
|
25
|
+
import { migrationStatusTemplate } from "./migration-status.js";
|
|
25
26
|
|
|
26
27
|
export function postgresDbFiles(ctx: TemplateContext): DbTemplateFiles {
|
|
27
28
|
return {
|
|
@@ -259,6 +260,26 @@ async function prepare(pool: Pool): Promise<void> {
|
|
|
259
260
|
await migrate(drizzle(pool), { migrationsFolder: "drizzle" });
|
|
260
261
|
}
|
|
261
262
|
`,
|
|
263
|
+
dbMigrationStatus: migrationStatusTemplate({
|
|
264
|
+
imports: `import pg from "pg";
|
|
265
|
+
import { env } from "@/lib/env";`,
|
|
266
|
+
open: `const pool = new pg.Pool({ connectionString: env.POSTGRES_DB_URL });`,
|
|
267
|
+
loadApplied: `const table = await pool.query<{ name: string | null }>(
|
|
268
|
+
"SELECT to_regclass('drizzle.__drizzle_migrations')::text AS name",
|
|
269
|
+
);
|
|
270
|
+
const applied: AppliedMigration[] =
|
|
271
|
+
table.rows[0]?.name == null
|
|
272
|
+
? []
|
|
273
|
+
: (
|
|
274
|
+
await pool.query<{ hash: string; created_at: string }>(
|
|
275
|
+
'SELECT hash, created_at FROM "drizzle"."__drizzle_migrations" ORDER BY created_at',
|
|
276
|
+
)
|
|
277
|
+
).rows.map((row) => ({
|
|
278
|
+
hash: row.hash,
|
|
279
|
+
createdAt: Number(row.created_at),
|
|
280
|
+
}));`,
|
|
281
|
+
close: "await pool.end();",
|
|
282
|
+
}),
|
|
262
283
|
dbProvider: `import "@beignet/core/server-only";
|
|
263
284
|
import { createProvider } from "@beignet/core/providers";
|
|
264
285
|
import {
|