@beignet/cli 0.0.27 → 0.0.29
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 +19 -0
- package/README.md +31 -6
- 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 +2 -2
- package/dist/choices.d.ts.map +1 -1
- package/dist/choices.js +1 -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 +2 -2
- package/dist/db.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +47 -8
- package/dist/index.js.map +1 -1
- package/dist/inspect.d.ts.map +1 -1
- package/dist/inspect.js +70 -0
- package/dist/inspect.js.map +1 -1
- package/dist/make/shared.d.ts +17 -0
- package/dist/make/shared.d.ts.map +1 -1
- package/dist/make/shared.js +67 -0
- package/dist/make/shared.js.map +1 -1
- package/dist/make.d.ts.map +1 -1
- package/dist/make.js +11 -55
- package/dist/make.js.map +1 -1
- package/dist/mcp.js +1 -1
- package/dist/mcp.js.map +1 -1
- package/dist/provider-add.d.ts.map +1 -1
- package/dist/provider-add.js +50 -0
- package/dist/provider-add.js.map +1 -1
- 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/templates/agents.d.ts.map +1 -1
- package/dist/templates/agents.js +27 -9
- 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/shared.d.ts +1 -0
- 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/src/check.ts +246 -0
- package/src/choices.ts +3 -1
- package/src/db.ts +2 -2
- package/src/index.ts +62 -8
- package/src/inspect.ts +100 -0
- package/src/make/shared.ts +88 -0
- package/src/make.ts +10 -68
- package/src/mcp.ts +1 -1
- package/src/provider-add.ts +51 -0
- package/src/provider-audit.ts +95 -3
- package/src/templates/agents.ts +27 -9
- package/src/templates/base.ts +43 -12
- package/src/templates/db/sqlite.ts +7 -1
- package/src/templates/shared.ts +1 -0
package/src/make/shared.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
type ResolvedBeignetConfig,
|
|
8
8
|
} from "../config.js";
|
|
9
9
|
import {
|
|
10
|
+
type AppendResult,
|
|
10
11
|
appendToArrayExpression,
|
|
11
12
|
appendToNamedArray,
|
|
12
13
|
arrayInitializerInfo,
|
|
@@ -349,6 +350,93 @@ export function providersFilePath(config: ResolvedBeignetConfig): string {
|
|
|
349
350
|
return `${directoryPath(path.dirname(config.paths.server))}/providers.ts`;
|
|
350
351
|
}
|
|
351
352
|
|
|
353
|
+
/**
|
|
354
|
+
* Wire a feature listener registry into the generated providers file by
|
|
355
|
+
* adding a `<registry>Provider` lifecycle provider whose setup calls
|
|
356
|
+
* `registerListeners(...)` and registering it in the providers array.
|
|
357
|
+
*
|
|
358
|
+
* Shared by `beignet make listener` and `beignet doctor --fix` so both write
|
|
359
|
+
* paths produce byte-identical wiring. Returns `missing` when the exported
|
|
360
|
+
* providers array anchor cannot be found; callers decide whether that is a
|
|
361
|
+
* hard error (make) or a keep-the-diagnostic bail (doctor).
|
|
362
|
+
*/
|
|
363
|
+
export function wireListenersProviderSource(
|
|
364
|
+
source: string,
|
|
365
|
+
options: {
|
|
366
|
+
registryName: string;
|
|
367
|
+
featureKebab: string;
|
|
368
|
+
listenerModule: string;
|
|
369
|
+
config: ResolvedBeignetConfig;
|
|
370
|
+
},
|
|
371
|
+
): AppendResult {
|
|
372
|
+
const { registryName, featureKebab, listenerModule, config } = options;
|
|
373
|
+
const providerName = `${registryName}Provider`;
|
|
374
|
+
let next = source;
|
|
375
|
+
next = addNamedImport(next, "registerListeners", "@beignet/core/events");
|
|
376
|
+
next = addNamedImport(next, "createServiceActor", "@beignet/core/ports");
|
|
377
|
+
next = addNamedImport(next, "createProvider", "@beignet/core/providers");
|
|
378
|
+
next = addNamedTypeImport(
|
|
379
|
+
next,
|
|
380
|
+
"AppContext",
|
|
381
|
+
aliasModule(config.paths.appContext),
|
|
382
|
+
);
|
|
383
|
+
next = addNamedTypeImport(
|
|
384
|
+
next,
|
|
385
|
+
"AppServiceContextInput",
|
|
386
|
+
aliasModule(path.join(path.dirname(config.paths.server), "context.ts")),
|
|
387
|
+
);
|
|
388
|
+
next = addNamedTypeImport(next, "AppPorts", aliasModule(config.paths.ports));
|
|
389
|
+
|
|
390
|
+
const listenerImport = `import { ${registryName} } from "${listenerModule}";`;
|
|
391
|
+
if (!next.includes(listenerImport)) {
|
|
392
|
+
next = insertAfterImports(next, listenerImport);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (!new RegExp(`\\bconst\\s+${providerName}\\b`).test(next)) {
|
|
396
|
+
const providerDefinition = `const ${providerName} = createProvider<
|
|
397
|
+
\tPick<AppPorts, "eventBus" | "logger">,
|
|
398
|
+
\tAppContext,
|
|
399
|
+
\tAppServiceContextInput
|
|
400
|
+
>()({
|
|
401
|
+
\tname: "${featureKebab}-listeners",
|
|
402
|
+
\tsetup({ ports, createServiceContext }) {
|
|
403
|
+
\t\tconst unregister = registerListeners(ports.eventBus, ${registryName}, {
|
|
404
|
+
\t\t\tctx: () =>
|
|
405
|
+
\t\t\t\tcreateServiceContext({
|
|
406
|
+
\t\t\t\t\tactor: createServiceActor("beignet-listener"),
|
|
407
|
+
\t\t\t\t}),
|
|
408
|
+
\t\t\tonError(error, listener) {
|
|
409
|
+
\t\t\t\tports.logger.error("Event listener failed", {
|
|
410
|
+
\t\t\t\t\terror,
|
|
411
|
+
\t\t\t\t\tlistenerName: listener.name,
|
|
412
|
+
\t\t\t\t});
|
|
413
|
+
\t\t\t},
|
|
414
|
+
\t\t});
|
|
415
|
+
|
|
416
|
+
\t\treturn {
|
|
417
|
+
\t\t\tstop() {
|
|
418
|
+
\t\t\t\tunregister();
|
|
419
|
+
\t\t\t},
|
|
420
|
+
\t\t};
|
|
421
|
+
\t},
|
|
422
|
+
});
|
|
423
|
+
`;
|
|
424
|
+
const withProvider = next.replace(
|
|
425
|
+
/\nexport const providers = \[/,
|
|
426
|
+
`\n${providerDefinition}\nexport const providers = [`,
|
|
427
|
+
);
|
|
428
|
+
if (withProvider === next) return { kind: "missing" };
|
|
429
|
+
next = withProvider;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const appended = appendToNamedArray(next, "providers", providerName);
|
|
433
|
+
if (appended.kind === "missing") return { kind: "missing" };
|
|
434
|
+
if (appended.kind === "updated") next = appended.source;
|
|
435
|
+
|
|
436
|
+
if (next === source) return { kind: "unchanged" };
|
|
437
|
+
return { kind: "updated", source: next };
|
|
438
|
+
}
|
|
439
|
+
|
|
352
440
|
/**
|
|
353
441
|
* Wire provider-backed app ports a generator depends on.
|
|
354
442
|
*
|
package/src/make.ts
CHANGED
|
@@ -131,6 +131,7 @@ import {
|
|
|
131
131
|
usesFeatureOwnedPolicies,
|
|
132
132
|
usesFeatureOwnedTests,
|
|
133
133
|
type WriteGeneratedFileResult,
|
|
134
|
+
wireListenersProviderSource,
|
|
134
135
|
wirePortProviders,
|
|
135
136
|
writeGeneratedFile,
|
|
136
137
|
writePlannedGeneratedFile,
|
|
@@ -1392,79 +1393,20 @@ async function updateListenerProviderWiring(
|
|
|
1392
1393
|
}
|
|
1393
1394
|
|
|
1394
1395
|
const listenerIndex = featureArtifactIndexFile("listener", names, config);
|
|
1395
|
-
const
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
"AppContext",
|
|
1403
|
-
aliasModule(config.paths.appContext),
|
|
1404
|
-
);
|
|
1405
|
-
next = addNamedTypeImport(
|
|
1406
|
-
next,
|
|
1407
|
-
"AppServiceContextInput",
|
|
1408
|
-
aliasModule(path.join(path.dirname(config.paths.server), "context.ts")),
|
|
1409
|
-
);
|
|
1410
|
-
next = addNamedTypeImport(next, "AppPorts", aliasModule(config.paths.ports));
|
|
1411
|
-
|
|
1412
|
-
const listenerImport = `import { ${listenerIndex.registryName} } from "${aliasModule(listenerIndex.path)}";`;
|
|
1413
|
-
if (!next.includes(listenerImport)) {
|
|
1414
|
-
next = insertAfterImports(next, listenerImport);
|
|
1415
|
-
}
|
|
1416
|
-
|
|
1417
|
-
if (!new RegExp(`\\bconst\\s+${providerName}\\b`).test(next)) {
|
|
1418
|
-
const providerDefinition = `const ${providerName} = createProvider<
|
|
1419
|
-
\tPick<AppPorts, "eventBus" | "logger">,
|
|
1420
|
-
\tAppContext,
|
|
1421
|
-
\tAppServiceContextInput
|
|
1422
|
-
>()({
|
|
1423
|
-
\tname: "${names.feature.kebab}-listeners",
|
|
1424
|
-
\tsetup({ ports, createServiceContext }) {
|
|
1425
|
-
\t\tconst unregister = registerListeners(ports.eventBus, ${listenerIndex.registryName}, {
|
|
1426
|
-
\t\t\tctx: () =>
|
|
1427
|
-
\t\t\t\tcreateServiceContext({
|
|
1428
|
-
\t\t\t\t\tactor: createServiceActor("beignet-listener"),
|
|
1429
|
-
\t\t\t\t}),
|
|
1430
|
-
\t\t\tonError(error, listener) {
|
|
1431
|
-
\t\t\t\tports.logger.error("Event listener failed", {
|
|
1432
|
-
\t\t\t\t\terror,
|
|
1433
|
-
\t\t\t\t\tlistenerName: listener.name,
|
|
1434
|
-
\t\t\t\t});
|
|
1435
|
-
\t\t\t},
|
|
1436
|
-
\t\t});
|
|
1437
|
-
|
|
1438
|
-
\t\treturn {
|
|
1439
|
-
\t\t\tstop() {
|
|
1440
|
-
\t\t\t\tunregister();
|
|
1441
|
-
\t\t\t},
|
|
1442
|
-
\t\t};
|
|
1443
|
-
\t},
|
|
1444
|
-
});
|
|
1445
|
-
`;
|
|
1446
|
-
const withProvider = next.replace(
|
|
1447
|
-
/\nexport const providers = \[/,
|
|
1448
|
-
`\n${providerDefinition}\nexport const providers = [`,
|
|
1449
|
-
);
|
|
1450
|
-
if (withProvider === next) {
|
|
1451
|
-
throw new Error(
|
|
1452
|
-
`Could not find the exported providers array in ${providersFile}. Register ${listenerIndex.registryName} manually with registerListeners(...), or restore the generated providers file before running make listener.`,
|
|
1453
|
-
);
|
|
1454
|
-
}
|
|
1455
|
-
next = withProvider;
|
|
1456
|
-
}
|
|
1457
|
-
|
|
1458
|
-
const appended = appendToNamedArray(next, "providers", providerName);
|
|
1459
|
-
if (appended.kind === "missing") {
|
|
1396
|
+
const result = wireListenersProviderSource(original, {
|
|
1397
|
+
registryName: listenerIndex.registryName,
|
|
1398
|
+
featureKebab: names.feature.kebab,
|
|
1399
|
+
listenerModule: aliasModule(listenerIndex.path),
|
|
1400
|
+
config,
|
|
1401
|
+
});
|
|
1402
|
+
if (result.kind === "missing") {
|
|
1460
1403
|
throw new Error(
|
|
1461
1404
|
`Could not find the exported providers array in ${providersFile}. Register ${listenerIndex.registryName} manually with registerListeners(...), or restore the generated providers file before running make listener.`,
|
|
1462
1405
|
);
|
|
1463
1406
|
}
|
|
1464
|
-
if (appended.kind === "updated") next = appended.source;
|
|
1465
1407
|
|
|
1466
|
-
if (
|
|
1467
|
-
if (!options.dryRun) await writeFile(filePath,
|
|
1408
|
+
if (result.kind === "unchanged") return "skipped";
|
|
1409
|
+
if (!options.dryRun) await writeFile(filePath, result.source);
|
|
1468
1410
|
return "updated";
|
|
1469
1411
|
}
|
|
1470
1412
|
|
package/src/mcp.ts
CHANGED
|
@@ -380,7 +380,7 @@ export function buildBeignetMcpServer(options: McpServerOptions): McpServer {
|
|
|
380
380
|
"doctor_fix",
|
|
381
381
|
{
|
|
382
382
|
description:
|
|
383
|
-
"Apply low-risk doctor fixes, then re-check the app. Repairs route-group, schedule, task,
|
|
383
|
+
"Apply low-risk doctor fixes, then re-check the app. Repairs route-group, schedule, task, outbox, and listener registration drift. Returns applied fixes and remaining diagnostics as JSON.",
|
|
384
384
|
inputSchema: {
|
|
385
385
|
strict: z
|
|
386
386
|
.boolean()
|
package/src/provider-add.ts
CHANGED
|
@@ -396,6 +396,57 @@ const presets: Record<ProviderPresetName, ProviderPreset> = {
|
|
|
396
396
|
conflictingProviderEntries: [
|
|
397
397
|
"localStorageProvider",
|
|
398
398
|
"createLocalStorageProvider",
|
|
399
|
+
"vercelBlobStorageProvider",
|
|
400
|
+
"createVercelBlobStorageProvider",
|
|
401
|
+
],
|
|
402
|
+
},
|
|
403
|
+
"vercel-blob-storage": {
|
|
404
|
+
name: "vercel-blob-storage",
|
|
405
|
+
displayName: "Vercel Blob storage",
|
|
406
|
+
importName: "vercelBlobStorageProvider",
|
|
407
|
+
importFrom: "@beignet/provider-storage-vercel-blob",
|
|
408
|
+
providerEntry: "vercelBlobStorageProvider",
|
|
409
|
+
dependencies: (packageJson) => ({
|
|
410
|
+
"@beignet/provider-storage-vercel-blob":
|
|
411
|
+
beignetDependencyVersion(packageJson),
|
|
412
|
+
"@vercel/blob": externalVersions.vercelBlob,
|
|
413
|
+
}),
|
|
414
|
+
appPort: {
|
|
415
|
+
key: "storage",
|
|
416
|
+
typeName: "StoragePort",
|
|
417
|
+
importName: "StoragePort",
|
|
418
|
+
importFrom: "@beignet/core/storage",
|
|
419
|
+
},
|
|
420
|
+
envVars: ["BLOB_READ_WRITE_TOKEN"],
|
|
421
|
+
envExample: [
|
|
422
|
+
"# Vercel Blob storage provider. Vercel sets BLOB_READ_WRITE_TOKEN",
|
|
423
|
+
"# automatically on deployments with a connected Blob store.",
|
|
424
|
+
"# BLOB_READ_WRITE_TOKEN=vercel_blob_rw_...",
|
|
425
|
+
"# BLOB_ACCESS=private",
|
|
426
|
+
"# BLOB_KEY_PREFIX=",
|
|
427
|
+
"",
|
|
428
|
+
].join("\n"),
|
|
429
|
+
docs: [
|
|
430
|
+
"## Vercel Blob storage",
|
|
431
|
+
"",
|
|
432
|
+
"- Package: `@beignet/provider-storage-vercel-blob`",
|
|
433
|
+
"- Peer dependency: `@vercel/blob`",
|
|
434
|
+
"- The preset wires `vercelBlobStorageProvider` in `server/providers.ts` and defers `storage` in `infra/app-ports.ts`.",
|
|
435
|
+
"- On Vercel, connect a Blob store to the project and `BLOB_READ_WRITE_TOKEN` is set automatically; set it manually elsewhere.",
|
|
436
|
+
"- The store is uniform-visibility (`BLOB_ACCESS`, default `private`): Vercel Blob does not report per-object access back, so mixed visibility needs a second store.",
|
|
437
|
+
"- Use `ctx.ports.storage` in app code; `ctx.ports.vercelBlob` is available as an escape hatch.",
|
|
438
|
+
"",
|
|
439
|
+
].join("\n"),
|
|
440
|
+
nextSteps: [
|
|
441
|
+
"Run your package manager install command.",
|
|
442
|
+
"Connect a Vercel Blob store to the project (or set BLOB_READ_WRITE_TOKEN manually).",
|
|
443
|
+
"Run beignet providers audit and beignet doctor --strict.",
|
|
444
|
+
],
|
|
445
|
+
conflictingProviderEntries: [
|
|
446
|
+
"localStorageProvider",
|
|
447
|
+
"createLocalStorageProvider",
|
|
448
|
+
"s3StorageProvider",
|
|
449
|
+
"createS3StorageProvider",
|
|
399
450
|
],
|
|
400
451
|
},
|
|
401
452
|
"upstash-rate-limit": {
|
package/src/provider-audit.ts
CHANGED
|
@@ -365,9 +365,101 @@ export async function readProviderListEntries(
|
|
|
365
365
|
)
|
|
366
366
|
).join("\n");
|
|
367
367
|
const providerListSource = extractProviderListSource(providerSource);
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
368
|
+
const entries =
|
|
369
|
+
providerListSource === undefined
|
|
370
|
+
? []
|
|
371
|
+
: providerListEntries(providerListSource);
|
|
372
|
+
return expandProviderEntryIdentifiers(entries, providerSource);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Resolve identifiers referenced by provider list entries to their `const`
|
|
377
|
+
* initializers in the same providers module.
|
|
378
|
+
*
|
|
379
|
+
* Registration detection matches provider tokens against entry text, so an
|
|
380
|
+
* environment-switched provider extracted to a named constant —
|
|
381
|
+
* `const storage = env.TOKEN ? blobProvider : createLocalStorageProvider()`
|
|
382
|
+
* registered as `[storage]` — must contribute its initializer text too, not
|
|
383
|
+
* just the identifier. Resolution follows chains through a seen-set so
|
|
384
|
+
* self-referential constants cannot loop.
|
|
385
|
+
*/
|
|
386
|
+
function expandProviderEntryIdentifiers(
|
|
387
|
+
entries: string[],
|
|
388
|
+
source: string,
|
|
389
|
+
): string[] {
|
|
390
|
+
const expanded = [...entries];
|
|
391
|
+
const seen = new Set<string>();
|
|
392
|
+
const queue = [...entries];
|
|
393
|
+
|
|
394
|
+
while (queue.length > 0) {
|
|
395
|
+
const entry = queue.pop() as string;
|
|
396
|
+
for (const match of entry.matchAll(/\b[A-Za-z_$][\w$]*\b/g)) {
|
|
397
|
+
const name = match[0];
|
|
398
|
+
if (seen.has(name)) continue;
|
|
399
|
+
seen.add(name);
|
|
400
|
+
|
|
401
|
+
const initializer = constInitializerText(source, name);
|
|
402
|
+
if (initializer !== undefined) {
|
|
403
|
+
expanded.push(initializer);
|
|
404
|
+
queue.push(initializer);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return expanded;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Extract the initializer text of `const <name> = ...;`, ending at the first
|
|
414
|
+
* top-level semicolon outside strings and brackets.
|
|
415
|
+
*/
|
|
416
|
+
function constInitializerText(
|
|
417
|
+
source: string,
|
|
418
|
+
name: string,
|
|
419
|
+
): string | undefined {
|
|
420
|
+
const declaration = new RegExp(`\\bconst\\s+${name}\\s*=`).exec(source);
|
|
421
|
+
if (!declaration) return undefined;
|
|
422
|
+
|
|
423
|
+
const start = declaration.index + declaration[0].length;
|
|
424
|
+
let depth = 0;
|
|
425
|
+
let quote: '"' | "'" | "`" | undefined;
|
|
426
|
+
let escaped = false;
|
|
427
|
+
|
|
428
|
+
for (let index = start; index < source.length; index += 1) {
|
|
429
|
+
const char = source[index];
|
|
430
|
+
|
|
431
|
+
if (quote) {
|
|
432
|
+
if (escaped) {
|
|
433
|
+
escaped = false;
|
|
434
|
+
} else if (char === "\\") {
|
|
435
|
+
escaped = true;
|
|
436
|
+
} else if (char === quote) {
|
|
437
|
+
quote = undefined;
|
|
438
|
+
}
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (char === '"' || char === "'" || char === "`") {
|
|
443
|
+
quote = char;
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (char === "(" || char === "[" || char === "{") {
|
|
448
|
+
depth += 1;
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (char === ")" || char === "]" || char === "}") {
|
|
453
|
+
depth -= 1;
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (char === ";" && depth === 0) {
|
|
458
|
+
return source.slice(start, index).trim();
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
return source.slice(start).trim() || undefined;
|
|
371
463
|
}
|
|
372
464
|
|
|
373
465
|
export function registrationTokenHint(tokens: readonly string[]): string {
|
package/src/templates/agents.ts
CHANGED
|
@@ -25,6 +25,9 @@ export function agentsMd(ctx: TemplateContext): string {
|
|
|
25
25
|
? "npm run typecheck"
|
|
26
26
|
: `${ctx.packageManager} run typecheck`;
|
|
27
27
|
const intent = intentRunner(ctx);
|
|
28
|
+
const capabilityClientRow = ctx.api
|
|
29
|
+
? ""
|
|
30
|
+
: "\n| Query cache keys and invalidation | `rq(contract).invalidate(queryClient, params?)`, `.key(...)`, `.filter(...)` from `client/` (`@beignet/react-query`). Thin named wrappers in `features/<feature>/client/` are the convention — hand-built key arrays are not. |";
|
|
28
31
|
const specificSkills = ctx.api
|
|
29
32
|
? "`@beignet/next#routes-server`, `@beignet/provider-db-drizzle#database-provider`, `@beignet/provider-auth-better-auth#auth-provider`, or `@beignet/cli#app-structure`"
|
|
30
33
|
: "`@beignet/next#routes-server`, `@beignet/react-query#client`, `@beignet/react-hook-form#forms`, `@beignet/provider-db-drizzle#database-provider`, `@beignet/provider-auth-better-auth#auth-provider`, or `@beignet/cli#app-structure`";
|
|
@@ -56,8 +59,7 @@ use, so their absence is fine. \`${cli} make event\`, \`${cli} make job\`,
|
|
|
56
59
|
\`${cli} make seed\`, and \`${cli} make upload\` create or update their
|
|
57
60
|
required app entrypoints.
|
|
58
61
|
\`${cli} doctor\` detects registration drift. \`${cli} doctor --fix\` repairs
|
|
59
|
-
route-group, schedule, task,
|
|
60
|
-
report-only and must be fixed by hand.
|
|
62
|
+
route-group, schedule, task, outbox, and listener registration.
|
|
61
63
|
|
|
62
64
|
## Prefer generators
|
|
63
65
|
|
|
@@ -68,20 +70,36 @@ need a richer reference slice with policy, client helpers, workflow artifacts,
|
|
|
68
70
|
events, listener registration, jobs, and outbox wiring. After changing the Drizzle schema in
|
|
69
71
|
\`infra/db/schema/\`, run \`${cli} db generate\` then \`${cli} db migrate\`.
|
|
70
72
|
|
|
73
|
+
## The framework already solves these
|
|
74
|
+
|
|
75
|
+
Check this table before hand-rolling. Every row is an existing API that
|
|
76
|
+
apps have reimplemented by accident:
|
|
77
|
+
|
|
78
|
+
| Need | Use |
|
|
79
|
+
| --- | --- |${capabilityClientRow}
|
|
80
|
+
| App context outside HTTP — agents, queues, scripts, backfills | \`server.createServiceContext(input)\`, or \`server.runServiceContext(input, fn)\` in one-off scripts. Never hand-assemble a context or call \`gate.attach(...)\` — the server owns gate attachment. |
|
|
81
|
+
| Ports outside a request — auth callbacks, module-level helpers | \`const { ports } = await getServer()\` (dynamic \`import("@/server")\` breaks module cycles). Do not construct parallel provider clients or fall back to \`console.*\` when \`ports.logger\` exists. |
|
|
82
|
+
| Routes that cannot be contracts — webhooks, third-party callbacks, streaming | \`createWebhookRoute\`, \`createPaymentWebhookRoute\`, \`createScheduleRoute\`, \`createOutboxDrainRoute\` from \`@beignet/next\`; \`server.rawRoute(...)\` for anything else. All run the hooks pipeline — never hand-enforce rate limits in a route body. |
|
|
83
|
+
| Rate limiting or idempotency on a route | Declare \`metadata.rateLimit\` / \`metadata.idempotency\` on the contract (or the \`pipeline\` option on raw routes); hooks enforce it. |
|
|
84
|
+
| Route-level tests that exercise real hooks | \`createTestApp\` / \`createTestRequester\` from \`@beignet/web/testing\`. Bind the rate-limit and idempotency ports in the test app when asserting 429s or replay. |
|
|
85
|
+
| Environment configuration | \`lib/env.ts\` (\`createEnv\`), never ad-hoc \`process.env\` reads in app code. |
|
|
86
|
+
| Request feels slow | Open devtools; every request span breaks down into per-stage bars (context creation is the usual serverless culprit). |
|
|
87
|
+
|
|
71
88
|
## Validation loop
|
|
72
89
|
|
|
73
90
|
Run after every change:
|
|
74
91
|
|
|
75
92
|
\`\`\`bash
|
|
76
|
-
${
|
|
77
|
-
${cli} lint
|
|
78
|
-
${cli} doctor --strict
|
|
79
|
-
${test}
|
|
80
|
-
${typecheck}
|
|
93
|
+
${cli} check
|
|
81
94
|
\`\`\`
|
|
82
95
|
|
|
83
|
-
\`${lint
|
|
84
|
-
\`${cli}
|
|
96
|
+
It runs \`${cli} lint\` (Beignet's dependency-direction lint),
|
|
97
|
+
\`${cli} doctor --strict\`, and the app's \`lint\` (Biome), \`typecheck\`, and
|
|
98
|
+
\`test\` scripts in one pass, reporting every failure. Add \`--fix\` to apply
|
|
99
|
+
doctor's low-risk registration fixes first. Use \`${format}\` to apply
|
|
100
|
+
formatting. The individual commands (\`${lint}\`, \`${cli} lint\`,
|
|
101
|
+
\`${cli} doctor --strict\`, \`${test}\`, \`${typecheck}\`) still work when you
|
|
102
|
+
need one check alone.
|
|
85
103
|
|
|
86
104
|
## Package skills
|
|
87
105
|
|
package/src/templates/base.ts
CHANGED
|
@@ -209,6 +209,41 @@ The starter's shipped tests use in-memory ports and need no database server. Dat
|
|
|
209
209
|
`
|
|
210
210
|
: "";
|
|
211
211
|
|
|
212
|
+
const optionalAuthRedirects = ctx.api
|
|
213
|
+
? ""
|
|
214
|
+
: `## Optional faster auth redirects
|
|
215
|
+
|
|
216
|
+
The starter keeps real auth enforcement in \`app/(app)/layout.tsx\`,
|
|
217
|
+
\`server/context.ts\`, route hooks, and protected use cases. After your product
|
|
218
|
+
routes settle, you can add a root \`proxy.ts\` to redirect requests that do not
|
|
219
|
+
have a Better Auth session cookie before rendering the protected app shell:
|
|
220
|
+
|
|
221
|
+
\`\`\`ts
|
|
222
|
+
import { getSessionCookie } from "better-auth/cookies";
|
|
223
|
+
import { NextResponse, type NextRequest } from "next/server";
|
|
224
|
+
|
|
225
|
+
export function proxy(request: NextRequest) {
|
|
226
|
+
const sessionCookie = getSessionCookie(request);
|
|
227
|
+
|
|
228
|
+
if (!sessionCookie) {
|
|
229
|
+
return NextResponse.redirect(new URL("/sign-in", request.url));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return NextResponse.next();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export const config = {
|
|
236
|
+
matcher: ["/dashboard/:path*", "/todos/:path*", "/settings/:path*"],
|
|
237
|
+
};
|
|
238
|
+
\`\`\`
|
|
239
|
+
|
|
240
|
+
This is only an optimistic UX gate. A session cookie can be absent, stale, or
|
|
241
|
+
manually created, so keep the layout/session checks and Beignet auth
|
|
242
|
+
hooks/use-case helpers. Update the matcher to match your app's protected URLs;
|
|
243
|
+
route groups such as \`app/(app)\` are not part of the URL.
|
|
244
|
+
|
|
245
|
+
`;
|
|
246
|
+
|
|
212
247
|
return `# ${ctx.name}
|
|
213
248
|
|
|
214
249
|
Beignet app scaffolded with \`@beignet/cli\`.
|
|
@@ -241,15 +276,14 @@ ${firstOpen}
|
|
|
241
276
|
\`\`\`bash
|
|
242
277
|
# in another terminal
|
|
243
278
|
${cli} routes
|
|
244
|
-
${
|
|
245
|
-
${cli} lint
|
|
246
|
-
${cli} doctor
|
|
247
|
-
${test}
|
|
248
|
-
${typecheck}
|
|
279
|
+
${cli} check
|
|
249
280
|
\`\`\`
|
|
250
281
|
|
|
251
|
-
\`routes\` shows the contracts Beignet can inspect.
|
|
252
|
-
|
|
282
|
+
\`routes\` shows the contracts Beignet can inspect. \`check\` runs the whole
|
|
283
|
+
validation loop in one pass: \`${cli} lint\` (dependency direction),
|
|
284
|
+
\`${cli} doctor --strict\` (route, OpenAPI, and resource drift), and the
|
|
285
|
+
\`lint\`, \`typecheck\`, and \`test\` package scripts. Use \`${format}\` to
|
|
286
|
+
apply Biome formatting.
|
|
253
287
|
${testingNotes}
|
|
254
288
|
## Coding agents
|
|
255
289
|
|
|
@@ -272,11 +306,7 @@ ${start}
|
|
|
272
306
|
${cli} make feature projects
|
|
273
307
|
${cli} db generate
|
|
274
308
|
${cli} db migrate
|
|
275
|
-
${
|
|
276
|
-
${lint}
|
|
277
|
-
${typecheck}
|
|
278
|
-
${cli} lint
|
|
279
|
-
${cli} doctor
|
|
309
|
+
${cli} check
|
|
280
310
|
\`\`\`
|
|
281
311
|
|
|
282
312
|
\`make feature\` creates a contract-to-test vertical slice with Drizzle schema and repository files, so regenerate and migrate the database before running the app against the new feature.
|
|
@@ -306,6 +336,7 @@ Use \`${cli} make feature projects --recipe full-slice\` when you want a richer
|
|
|
306
336
|
- \`lib/auth.ts\` exposes \`requireUser(ctx)\` for protected use cases.
|
|
307
337
|
- \`lib/better-auth.ts\` owns Better Auth setup and keeps provider-specific auth details outside use cases.
|
|
308
338
|
${shellMap}
|
|
339
|
+
${optionalAuthRedirects}
|
|
309
340
|
## Before deploying
|
|
310
341
|
|
|
311
342
|
- ${
|
|
@@ -225,7 +225,13 @@ export function ensureDatabaseReady(client: Client): Promise<void> {
|
|
|
225
225
|
}
|
|
226
226
|
|
|
227
227
|
async function prepare(client: Client): Promise<void> {
|
|
228
|
-
|
|
228
|
+
// Remote libsql (Turso) runs over HTTP with no file locking and rejects
|
|
229
|
+
// the lock-wait pragma, so it only applies to local file databases.
|
|
230
|
+
if (client.protocol === "file") {
|
|
231
|
+
await client.execute("PRAGMA busy_timeout = 5000");
|
|
232
|
+
}
|
|
233
|
+
// SQLite leaves foreign keys off per connection unless asked.
|
|
234
|
+
await client.execute("PRAGMA foreign_keys = ON");
|
|
229
235
|
|
|
230
236
|
if (process.env.NODE_ENV === "production") {
|
|
231
237
|
const tables = await client.execute(
|