@beignet/cli 0.0.48 → 0.0.49
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 +34 -0
- package/README.md +76 -21
- package/dist/analysis/contract-factories.d.ts +17 -0
- package/dist/analysis/contract-factories.d.ts.map +1 -0
- package/dist/analysis/contract-factories.js +176 -0
- package/dist/analysis/contract-factories.js.map +1 -0
- package/dist/analysis/layers.d.ts +5 -0
- package/dist/analysis/layers.d.ts.map +1 -0
- package/dist/analysis/layers.js +172 -0
- package/dist/analysis/layers.js.map +1 -0
- package/dist/analysis/port-wiring.d.ts +23 -0
- package/dist/analysis/port-wiring.d.ts.map +1 -0
- package/dist/analysis/port-wiring.js +379 -0
- package/dist/analysis/port-wiring.js.map +1 -0
- package/dist/analysis/source-index.d.ts +5 -0
- package/dist/analysis/source-index.d.ts.map +1 -1
- package/dist/analysis/source-index.js +171 -42
- package/dist/analysis/source-index.js.map +1 -1
- package/dist/analysis/workspace.d.ts +10 -4
- package/dist/analysis/workspace.d.ts.map +1 -1
- package/dist/analysis/workspace.js +66 -6
- package/dist/analysis/workspace.js.map +1 -1
- package/dist/app-map.d.ts.map +1 -1
- package/dist/app-map.js +311 -92
- package/dist/app-map.js.map +1 -1
- package/dist/check.js +37 -3
- package/dist/check.js.map +1 -1
- package/dist/explain.js +4 -4
- package/dist/explain.js.map +1 -1
- package/dist/inspect.d.ts.map +1 -1
- package/dist/inspect.js +415 -168
- package/dist/inspect.js.map +1 -1
- package/dist/lint.d.ts.map +1 -1
- package/dist/lint.js +309 -487
- package/dist/lint.js.map +1 -1
- package/dist/make/inbox.d.ts.map +1 -1
- package/dist/make/inbox.js +11 -4
- package/dist/make/inbox.js.map +1 -1
- package/dist/make/payments.js +2 -2
- package/dist/make.d.ts.map +1 -1
- package/dist/make.js +144 -8
- package/dist/make.js.map +1 -1
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +19 -19
- package/dist/mcp.js.map +1 -1
- package/dist/provider-add.d.ts.map +1 -1
- package/dist/provider-add.js +142 -7
- package/dist/provider-add.js.map +1 -1
- package/dist/registry-edits.d.ts +7 -0
- package/dist/registry-edits.d.ts.map +1 -1
- package/dist/registry-edits.js +237 -53
- package/dist/registry-edits.js.map +1 -1
- package/dist/templates/agents.d.ts.map +1 -1
- package/dist/templates/agents.js +16 -0
- package/dist/templates/agents.js.map +1 -1
- package/dist/templates/base.d.ts.map +1 -1
- package/dist/templates/base.js +9 -1
- package/dist/templates/base.js.map +1 -1
- package/dist/templates/server.d.ts.map +1 -1
- package/dist/templates/server.js +2 -0
- package/dist/templates/server.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 +3 -2
- package/dist/templates/shared.js.map +1 -1
- package/package.json +7 -6
- package/skills/app-structure/SKILL.md +46 -5
- package/src/analysis/contract-factories.ts +256 -0
- package/src/analysis/layers.ts +266 -0
- package/src/analysis/port-wiring.ts +506 -0
- package/src/analysis/source-index.ts +225 -57
- package/src/analysis/workspace.ts +115 -6
- package/src/app-map.ts +418 -109
- package/src/check.ts +51 -3
- package/src/explain.ts +4 -4
- package/src/inspect.ts +583 -215
- package/src/lint.ts +393 -643
- package/src/make/inbox.ts +11 -4
- package/src/make/payments.ts +2 -2
- package/src/make.ts +225 -8
- package/src/mcp.ts +19 -22
- package/src/provider-add.ts +186 -7
- package/src/registry-edits.ts +313 -63
- package/src/templates/agents.ts +16 -0
- package/src/templates/base.ts +9 -1
- package/src/templates/server.ts +2 -0
- package/src/templates/shared.ts +3 -2
package/src/provider-add.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import ts from "typescript";
|
|
3
4
|
import {
|
|
4
5
|
type ProviderPresetName,
|
|
5
6
|
providerPresetChoices,
|
|
@@ -164,7 +165,10 @@ const presets: Record<ProviderPresetName, ProviderPreset> = {
|
|
|
164
165
|
"Add a worker entrypoint that runs createBullMQJobWorker(...) in a worker process; dispatched jobs do not execute without one. See the jobs docs page.",
|
|
165
166
|
"Run beignet provider audit and beignet doctor --strict.",
|
|
166
167
|
],
|
|
167
|
-
conflictingProviderEntries: [
|
|
168
|
+
conflictingProviderEntries: [
|
|
169
|
+
"createInngestJobsProvider",
|
|
170
|
+
"inlineJobsProvider",
|
|
171
|
+
],
|
|
168
172
|
},
|
|
169
173
|
"jobs-inngest": {
|
|
170
174
|
name: "jobs-inngest",
|
|
@@ -215,7 +219,10 @@ const presets: Record<ProviderPresetName, ProviderPreset> = {
|
|
|
215
219
|
"For non-Next runtimes, call createAppInngestFunctions(...) after creating the app server, then mount the result with Inngest's host adapter.",
|
|
216
220
|
"Run beignet provider audit and beignet doctor --strict.",
|
|
217
221
|
],
|
|
218
|
-
conflictingProviderEntries: [
|
|
222
|
+
conflictingProviderEntries: [
|
|
223
|
+
"createBullMQJobsProvider",
|
|
224
|
+
"inlineJobsProvider",
|
|
225
|
+
],
|
|
219
226
|
},
|
|
220
227
|
"mail-resend": {
|
|
221
228
|
name: "mail-resend",
|
|
@@ -276,7 +283,14 @@ const presets: Record<ProviderPresetName, ProviderPreset> = {
|
|
|
276
283
|
importName: "MailerPort",
|
|
277
284
|
importFrom: "@beignet/core/mail",
|
|
278
285
|
},
|
|
279
|
-
envVars: [
|
|
286
|
+
envVars: [
|
|
287
|
+
"MAIL_HOST",
|
|
288
|
+
"MAIL_PORT",
|
|
289
|
+
"MAIL_USER",
|
|
290
|
+
"MAIL_PASS",
|
|
291
|
+
"MAIL_FROM",
|
|
292
|
+
"MAIL_REQUIRE_TLS",
|
|
293
|
+
],
|
|
280
294
|
envExample: [
|
|
281
295
|
"# SMTP mail provider",
|
|
282
296
|
"MAIL_HOST=smtp.example.com",
|
|
@@ -284,6 +298,7 @@ const presets: Record<ProviderPresetName, ProviderPreset> = {
|
|
|
284
298
|
"MAIL_USER=dev-placeholder-change-me",
|
|
285
299
|
"MAIL_PASS=dev-placeholder-change-me",
|
|
286
300
|
"MAIL_FROM=Beignet App <noreply@example.com>",
|
|
301
|
+
"MAIL_REQUIRE_TLS=true",
|
|
287
302
|
"",
|
|
288
303
|
].join("\n"),
|
|
289
304
|
docs: [
|
|
@@ -293,12 +308,13 @@ const presets: Record<ProviderPresetName, ProviderPreset> = {
|
|
|
293
308
|
"- Peer dependency: `nodemailer`",
|
|
294
309
|
"- The preset wires `createSmtpMailProvider()` in `server/providers.ts` and defers `mailer` in `infra/port-wiring.ts`.",
|
|
295
310
|
"- `.env.example` ships dev placeholders so the app boots; set real `MAIL_*` values before sending mail.",
|
|
311
|
+
"- Encrypted transport is required by default: port 465 uses implicit TLS and other ports must negotiate STARTTLS.",
|
|
296
312
|
"- Use `ctx.ports.mailer` in app code; `ctx.ports.smtp` is available as an escape hatch.",
|
|
297
313
|
"",
|
|
298
314
|
].join("\n"),
|
|
299
315
|
nextSteps: [
|
|
300
316
|
"Run your package manager install command.",
|
|
301
|
-
"Set MAIL_HOST, MAIL_PORT, MAIL_USER, MAIL_PASS, and MAIL_FROM in .env.local or your deployment environment.",
|
|
317
|
+
"Set MAIL_HOST, MAIL_PORT, MAIL_USER, MAIL_PASS, and MAIL_FROM in .env.local or your deployment environment; keep MAIL_REQUIRE_TLS=true outside trusted local capture services.",
|
|
302
318
|
"Run beignet provider audit and beignet doctor --strict.",
|
|
303
319
|
],
|
|
304
320
|
conflictingProviderEntries: [
|
|
@@ -561,6 +577,7 @@ const presets: Record<ProviderPresetName, ProviderPreset> = {
|
|
|
561
577
|
dependencies: (packageJson) => ({
|
|
562
578
|
"@beignet/provider-storage-s3": beignetDependencyVersion(packageJson),
|
|
563
579
|
"@aws-sdk/client-s3": externalVersions.awsSdkS3,
|
|
580
|
+
"@aws-sdk/lib-storage": externalVersions.awsSdkLibStorage,
|
|
564
581
|
"@aws-sdk/s3-request-presigner": externalVersions.awsSdkS3,
|
|
565
582
|
}),
|
|
566
583
|
appPort: {
|
|
@@ -601,7 +618,7 @@ const presets: Record<ProviderPresetName, ProviderPreset> = {
|
|
|
601
618
|
"## S3-compatible storage",
|
|
602
619
|
"",
|
|
603
620
|
"- Package: `@beignet/provider-storage-s3`",
|
|
604
|
-
"- Peer dependencies: `@aws-sdk/client-s3
|
|
621
|
+
"- Peer dependencies: `@aws-sdk/client-s3`, `@aws-sdk/lib-storage`, and `@aws-sdk/s3-request-presigner`",
|
|
605
622
|
"- The preset wires `createS3StorageProvider()` in `server/providers.ts` and defers `storage` in `infra/port-wiring.ts`.",
|
|
606
623
|
"- Set `STORAGE_S3_BUCKET` before booting the app; set endpoint and credentials when using an S3-compatible host outside AWS defaults.",
|
|
607
624
|
"- Use `ctx.ports.storage` in app code; `ctx.ports.s3Storage` is available as an escape hatch.",
|
|
@@ -1179,9 +1196,13 @@ function updatePackageJson(
|
|
|
1179
1196
|
}
|
|
1180
1197
|
|
|
1181
1198
|
function updateServerProviders(source: string, preset: ProviderPreset): string {
|
|
1182
|
-
|
|
1199
|
+
const withoutGeneratedFallback =
|
|
1200
|
+
preset.appPort.key === "jobs"
|
|
1201
|
+
? removeGeneratedInlineJobsProvider(source)
|
|
1202
|
+
: source;
|
|
1203
|
+
assertNoConflictingProviders(withoutGeneratedFallback, preset);
|
|
1183
1204
|
|
|
1184
|
-
let next =
|
|
1205
|
+
let next = withoutGeneratedFallback;
|
|
1185
1206
|
for (const importLine of preset.additionalImports ?? []) {
|
|
1186
1207
|
if (!next.includes(importLine)) next = insertAfterImports(next, importLine);
|
|
1187
1208
|
}
|
|
@@ -1205,6 +1226,164 @@ function updateServerProviders(source: string, preset: ProviderPreset): string {
|
|
|
1205
1226
|
return result.kind === "updated" ? result.source : next;
|
|
1206
1227
|
}
|
|
1207
1228
|
|
|
1229
|
+
function removeGeneratedInlineJobsProvider(source: string): string {
|
|
1230
|
+
const marker = "// beignet:generated-inline-jobs-provider";
|
|
1231
|
+
const sourceFile = ts.createSourceFile(
|
|
1232
|
+
"providers.ts",
|
|
1233
|
+
source,
|
|
1234
|
+
ts.ScriptTarget.Latest,
|
|
1235
|
+
true,
|
|
1236
|
+
ts.ScriptKind.TS,
|
|
1237
|
+
);
|
|
1238
|
+
const statement = sourceFile.statements.find((candidate) => {
|
|
1239
|
+
if (!ts.isVariableStatement(candidate)) return false;
|
|
1240
|
+
if (candidate.declarationList.declarations.length !== 1) return false;
|
|
1241
|
+
const statementStart = candidate.getStart(sourceFile);
|
|
1242
|
+
const markerStart = source.lastIndexOf(marker, statementStart);
|
|
1243
|
+
if (
|
|
1244
|
+
markerStart < 0 ||
|
|
1245
|
+
source.slice(markerStart + marker.length, statementStart).trim() !== ""
|
|
1246
|
+
) {
|
|
1247
|
+
return false;
|
|
1248
|
+
}
|
|
1249
|
+
const declaration = candidate.declarationList.declarations.find(
|
|
1250
|
+
(item) =>
|
|
1251
|
+
ts.isIdentifier(item.name) && item.name.text === "inlineJobsProvider",
|
|
1252
|
+
);
|
|
1253
|
+
if (!declaration?.initializer) return false;
|
|
1254
|
+
const text = declaration.initializer.getText(sourceFile);
|
|
1255
|
+
return (
|
|
1256
|
+
/\bcreateProvider\b/.test(text) &&
|
|
1257
|
+
/\bcreateInlineJobDispatcher\b/.test(text) &&
|
|
1258
|
+
/\bname\s*:\s*["']inline-jobs["']/.test(text)
|
|
1259
|
+
);
|
|
1260
|
+
});
|
|
1261
|
+
if (!statement) return source;
|
|
1262
|
+
|
|
1263
|
+
const statementStart = statement.getStart(sourceFile);
|
|
1264
|
+
const markerStart = source.lastIndexOf(marker, statementStart);
|
|
1265
|
+
const removeStart =
|
|
1266
|
+
markerStart >= 0 &&
|
|
1267
|
+
source.slice(markerStart + marker.length, statementStart).trim() === ""
|
|
1268
|
+
? markerStart
|
|
1269
|
+
: statementStart;
|
|
1270
|
+
let next = `${source.slice(0, removeStart)}${source.slice(statement.end)}`;
|
|
1271
|
+
const registry = arrayInitializerInfo(next, "providers");
|
|
1272
|
+
if (!registry) {
|
|
1273
|
+
throw new Error(
|
|
1274
|
+
"Could not remove the generated inline jobs provider because `export const providers = [...]` is missing.",
|
|
1275
|
+
);
|
|
1276
|
+
}
|
|
1277
|
+
const lines = registry.text.split(/\r?\n/);
|
|
1278
|
+
const entryIndex = lines.findIndex(
|
|
1279
|
+
(line) =>
|
|
1280
|
+
line.trim() === "inlineJobsProvider," ||
|
|
1281
|
+
line.trim() === "inlineJobsProvider",
|
|
1282
|
+
);
|
|
1283
|
+
let nextRegistry: string | undefined;
|
|
1284
|
+
if (entryIndex >= 0) {
|
|
1285
|
+
lines.splice(entryIndex, 1);
|
|
1286
|
+
nextRegistry = lines.join(registry.text.includes("\r\n") ? "\r\n" : "\n");
|
|
1287
|
+
} else if (!registry.text.includes("\n")) {
|
|
1288
|
+
const beforeFollowingEntry = /\binlineJobsProvider\s*,\s*/;
|
|
1289
|
+
const afterPreviousEntry = /,\s*inlineJobsProvider\b/;
|
|
1290
|
+
const onlyEntry = /^\[\s*inlineJobsProvider\s*\]$/;
|
|
1291
|
+
if (onlyEntry.test(registry.text)) nextRegistry = "[]";
|
|
1292
|
+
else if (beforeFollowingEntry.test(registry.text)) {
|
|
1293
|
+
nextRegistry = registry.text.replace(beforeFollowingEntry, "");
|
|
1294
|
+
} else if (afterPreviousEntry.test(registry.text)) {
|
|
1295
|
+
nextRegistry = registry.text.replace(afterPreviousEntry, "");
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
if (nextRegistry === undefined) {
|
|
1299
|
+
throw new Error(
|
|
1300
|
+
"Could not remove the generated inline jobs provider from `export const providers = [...]`.",
|
|
1301
|
+
);
|
|
1302
|
+
}
|
|
1303
|
+
next = `${next.slice(0, registry.start)}${nextRegistry}${next.slice(registry.end)}`;
|
|
1304
|
+
|
|
1305
|
+
return removeNamedProviderImport(
|
|
1306
|
+
next,
|
|
1307
|
+
"createInlineJobDispatcher",
|
|
1308
|
+
"@beignet/core/jobs",
|
|
1309
|
+
);
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
function removeNamedProviderImport(
|
|
1313
|
+
source: string,
|
|
1314
|
+
name: string,
|
|
1315
|
+
moduleName: string,
|
|
1316
|
+
): string {
|
|
1317
|
+
const escapedModule = moduleName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1318
|
+
const pattern = new RegExp(
|
|
1319
|
+
`import \\{([^}]*)\\} from ["']${escapedModule}["'];`,
|
|
1320
|
+
);
|
|
1321
|
+
const match = pattern.exec(source);
|
|
1322
|
+
if (!match) return source;
|
|
1323
|
+
const names = match[1]
|
|
1324
|
+
.split(",")
|
|
1325
|
+
.map((entry) => entry.trim())
|
|
1326
|
+
.filter(Boolean);
|
|
1327
|
+
if (!names.includes(name)) return source;
|
|
1328
|
+
if (hasIdentifierReferenceOutsideImport(source, name, moduleName)) {
|
|
1329
|
+
return source;
|
|
1330
|
+
}
|
|
1331
|
+
const remaining = names.filter((entry) => entry !== name);
|
|
1332
|
+
if (remaining.length === 0) {
|
|
1333
|
+
const lineBreak = source.startsWith("\r\n", match.index + match[0].length)
|
|
1334
|
+
? "\r\n"
|
|
1335
|
+
: source[match.index + match[0].length] === "\n"
|
|
1336
|
+
? "\n"
|
|
1337
|
+
: "";
|
|
1338
|
+
return `${source.slice(0, match.index)}${source.slice(match.index + match[0].length + lineBreak.length)}`;
|
|
1339
|
+
}
|
|
1340
|
+
const indent = match[1].match(/\n([\t ]+)/)?.[1] ?? "\t";
|
|
1341
|
+
const replacement = match[1].includes("\n")
|
|
1342
|
+
? `import {\n${indent}${remaining.join(`,\n${indent}`)},\n} from "${moduleName}";`
|
|
1343
|
+
: `import { ${remaining.join(", ")} } from "${moduleName}";`;
|
|
1344
|
+
return `${source.slice(0, match.index)}${replacement}${source.slice(match.index + match[0].length)}`;
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
function hasIdentifierReferenceOutsideImport(
|
|
1348
|
+
source: string,
|
|
1349
|
+
name: string,
|
|
1350
|
+
moduleName: string,
|
|
1351
|
+
): boolean {
|
|
1352
|
+
const sourceFile = ts.createSourceFile(
|
|
1353
|
+
"providers.ts",
|
|
1354
|
+
source,
|
|
1355
|
+
ts.ScriptTarget.Latest,
|
|
1356
|
+
true,
|
|
1357
|
+
ts.ScriptKind.TS,
|
|
1358
|
+
);
|
|
1359
|
+
const importDeclaration = sourceFile.statements.find(
|
|
1360
|
+
(statement): statement is ts.ImportDeclaration =>
|
|
1361
|
+
ts.isImportDeclaration(statement) &&
|
|
1362
|
+
ts.isStringLiteral(statement.moduleSpecifier) &&
|
|
1363
|
+
statement.moduleSpecifier.text === moduleName &&
|
|
1364
|
+
Boolean(
|
|
1365
|
+
statement.importClause?.namedBindings &&
|
|
1366
|
+
ts.isNamedImports(statement.importClause.namedBindings) &&
|
|
1367
|
+
statement.importClause.namedBindings.elements.some(
|
|
1368
|
+
(element) => !element.propertyName && element.name.text === name,
|
|
1369
|
+
),
|
|
1370
|
+
),
|
|
1371
|
+
);
|
|
1372
|
+
if (!importDeclaration) return true;
|
|
1373
|
+
|
|
1374
|
+
let referenced = false;
|
|
1375
|
+
const visit = (node: ts.Node): void => {
|
|
1376
|
+
if (node === importDeclaration || referenced) return;
|
|
1377
|
+
if (ts.isIdentifier(node) && node.text === name) {
|
|
1378
|
+
referenced = true;
|
|
1379
|
+
return;
|
|
1380
|
+
}
|
|
1381
|
+
ts.forEachChild(node, visit);
|
|
1382
|
+
};
|
|
1383
|
+
ts.forEachChild(sourceFile, visit);
|
|
1384
|
+
return referenced;
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1208
1387
|
function assertNoConflictingProviders(
|
|
1209
1388
|
source: string,
|
|
1210
1389
|
preset: ProviderPreset,
|
package/src/registry-edits.ts
CHANGED
|
@@ -308,24 +308,324 @@ export function outboxRegistryValueInfo(
|
|
|
308
308
|
source: string,
|
|
309
309
|
property: "events" | "jobs",
|
|
310
310
|
): InitializerInfo | undefined {
|
|
311
|
-
const
|
|
312
|
-
if (!
|
|
311
|
+
const match = outboxRegistryPropertyAnalysis(source, property);
|
|
312
|
+
if (!match?.value || match.indeterminate) return undefined;
|
|
313
|
+
const start = match.value.getStart(match.sourceFile);
|
|
314
|
+
return {
|
|
315
|
+
text: source.slice(start, match.value.end),
|
|
316
|
+
start,
|
|
317
|
+
end: match.value.end,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export type OutboxRegistryEntryState =
|
|
322
|
+
| "absent"
|
|
323
|
+
| "empty"
|
|
324
|
+
| "present"
|
|
325
|
+
| "indeterminate";
|
|
326
|
+
|
|
327
|
+
/** Static state of one outbox registry property. */
|
|
328
|
+
export function outboxRegistryEntryState(
|
|
329
|
+
source: string,
|
|
330
|
+
property: "events" | "jobs",
|
|
331
|
+
): OutboxRegistryEntryState {
|
|
332
|
+
const match = outboxRegistryPropertyAnalysis(source, property);
|
|
333
|
+
if (!match) return "absent";
|
|
334
|
+
if (match.indeterminate) return "indeterminate";
|
|
335
|
+
if (!match.value) return "empty";
|
|
336
|
+
return resolvedRegistryExpressions(match.value, match.sourceFile).length > 0
|
|
337
|
+
? "present"
|
|
338
|
+
: "empty";
|
|
339
|
+
}
|
|
313
340
|
|
|
314
|
-
|
|
315
|
-
|
|
341
|
+
/** Whether a registry property is non-empty or cannot be proven empty. */
|
|
342
|
+
export function outboxRegistryHasEntries(
|
|
343
|
+
source: string,
|
|
344
|
+
property: "events" | "jobs",
|
|
345
|
+
): boolean {
|
|
346
|
+
const state = outboxRegistryEntryState(source, property);
|
|
347
|
+
return state === "present" || state === "indeterminate";
|
|
348
|
+
}
|
|
316
349
|
|
|
317
|
-
|
|
318
|
-
|
|
350
|
+
/** Identifiers referenced by a registry property after resolving local array aliases. */
|
|
351
|
+
export function outboxRegistryIdentifiers(
|
|
352
|
+
source: string,
|
|
353
|
+
property: "events" | "jobs",
|
|
354
|
+
): Set<string> {
|
|
355
|
+
const match = outboxRegistryPropertyAnalysis(source, property);
|
|
356
|
+
if (!match?.value || match.indeterminate) return new Set();
|
|
357
|
+
|
|
358
|
+
const identifiers = new Set<string>();
|
|
359
|
+
for (const expression of resolvedRegistryExpressions(
|
|
360
|
+
match.value,
|
|
361
|
+
match.sourceFile,
|
|
362
|
+
)) {
|
|
363
|
+
const start = expression.getStart(match.sourceFile);
|
|
364
|
+
for (const identifier of identifiersFromArrayExpression(
|
|
365
|
+
source.slice(start, expression.end),
|
|
366
|
+
)) {
|
|
367
|
+
identifiers.add(identifier);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return identifiers;
|
|
371
|
+
}
|
|
319
372
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
373
|
+
type OutboxRegistryPropertyAnalysis = {
|
|
374
|
+
sourceFile: ts.SourceFile;
|
|
375
|
+
value?: ts.Expression;
|
|
376
|
+
indeterminate: boolean;
|
|
377
|
+
};
|
|
323
378
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
379
|
+
function outboxRegistryPropertyAnalysis(
|
|
380
|
+
source: string,
|
|
381
|
+
property: "events" | "jobs",
|
|
382
|
+
): OutboxRegistryPropertyAnalysis | undefined {
|
|
383
|
+
const sourceFile = ts.createSourceFile(
|
|
384
|
+
"outbox-registry.ts",
|
|
385
|
+
source,
|
|
386
|
+
ts.ScriptTarget.Latest,
|
|
387
|
+
true,
|
|
388
|
+
ts.ScriptKind.TS,
|
|
389
|
+
);
|
|
390
|
+
const initializers = registryInitializers(sourceFile);
|
|
391
|
+
const selected = selectOutboxRegistryCall(sourceFile);
|
|
392
|
+
if (!selected.call) {
|
|
393
|
+
return selected.indeterminate
|
|
394
|
+
? { sourceFile, indeterminate: true }
|
|
395
|
+
: undefined;
|
|
396
|
+
}
|
|
327
397
|
|
|
328
|
-
|
|
398
|
+
const firstArgument = selected.call.arguments[0];
|
|
399
|
+
const object = firstArgument
|
|
400
|
+
? resolveRegistryExpression(firstArgument, initializers)
|
|
401
|
+
: undefined;
|
|
402
|
+
if (!object || !ts.isObjectLiteralExpression(object)) {
|
|
403
|
+
return { sourceFile, indeterminate: true };
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
let value: ts.Expression | undefined;
|
|
407
|
+
let indeterminate = false;
|
|
408
|
+
for (const member of object.properties) {
|
|
409
|
+
if (ts.isSpreadAssignment(member)) {
|
|
410
|
+
value = undefined;
|
|
411
|
+
indeterminate = true;
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
const name = member.name ? registryPropertyName(member.name) : undefined;
|
|
415
|
+
if (!name) {
|
|
416
|
+
value = undefined;
|
|
417
|
+
indeterminate = true;
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
if (name !== property) continue;
|
|
421
|
+
if (
|
|
422
|
+
ts.isPropertyAssignment(member) ||
|
|
423
|
+
ts.isShorthandPropertyAssignment(member)
|
|
424
|
+
) {
|
|
425
|
+
value = ts.isPropertyAssignment(member)
|
|
426
|
+
? member.initializer
|
|
427
|
+
: member.name;
|
|
428
|
+
indeterminate = false;
|
|
429
|
+
} else {
|
|
430
|
+
value = undefined;
|
|
431
|
+
indeterminate = true;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
if (value && !isStaticRegistryValue(value, initializers)) {
|
|
435
|
+
return { sourceFile, indeterminate: true };
|
|
436
|
+
}
|
|
437
|
+
return { sourceFile, value, indeterminate };
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function isStaticRegistryValue(
|
|
441
|
+
expression: ts.Expression,
|
|
442
|
+
initializers: Map<string, ts.Expression>,
|
|
443
|
+
visited = new Set<string>(),
|
|
444
|
+
): boolean {
|
|
445
|
+
const current = unwrapRegistryExpression(expression);
|
|
446
|
+
if (ts.isArrayLiteralExpression(current)) return true;
|
|
447
|
+
if (!ts.isIdentifier(current)) return false;
|
|
448
|
+
|
|
449
|
+
const initializer = initializers.get(current.text);
|
|
450
|
+
if (!initializer) return true;
|
|
451
|
+
if (visited.has(current.text)) return false;
|
|
452
|
+
|
|
453
|
+
const nextVisited = new Set(visited);
|
|
454
|
+
nextVisited.add(current.text);
|
|
455
|
+
return isStaticRegistryValue(initializer, initializers, nextVisited);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function selectOutboxRegistryCall(sourceFile: ts.SourceFile): {
|
|
459
|
+
call?: ts.CallExpression;
|
|
460
|
+
indeterminate: boolean;
|
|
461
|
+
} {
|
|
462
|
+
const exportedNames = locallyExportedRegistryNames(sourceFile);
|
|
463
|
+
const candidates: {
|
|
464
|
+
name?: string;
|
|
465
|
+
exported: boolean;
|
|
466
|
+
calls: ts.CallExpression[];
|
|
467
|
+
}[] = [];
|
|
468
|
+
|
|
469
|
+
for (const statement of sourceFile.statements) {
|
|
470
|
+
if (!ts.isVariableStatement(statement)) continue;
|
|
471
|
+
const statementExported = Boolean(
|
|
472
|
+
statement.modifiers?.some(
|
|
473
|
+
(modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword,
|
|
474
|
+
),
|
|
475
|
+
);
|
|
476
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
477
|
+
if (!declaration.initializer) continue;
|
|
478
|
+
const calls = outboxRegistryCalls(declaration.initializer);
|
|
479
|
+
if (calls.length === 0) continue;
|
|
480
|
+
const name = ts.isIdentifier(declaration.name)
|
|
481
|
+
? declaration.name.text
|
|
482
|
+
: undefined;
|
|
483
|
+
candidates.push({
|
|
484
|
+
...(name ? { name } : {}),
|
|
485
|
+
exported: statementExported || (name ? exportedNames.has(name) : false),
|
|
486
|
+
calls,
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const canonical = candidates.filter(
|
|
492
|
+
(candidate) => candidate.name === "outboxRegistry" && candidate.exported,
|
|
493
|
+
);
|
|
494
|
+
const exported = candidates.filter((candidate) => candidate.exported);
|
|
495
|
+
const selected =
|
|
496
|
+
canonical.length > 0
|
|
497
|
+
? canonical
|
|
498
|
+
: exported.length > 0
|
|
499
|
+
? exported
|
|
500
|
+
: candidates;
|
|
501
|
+
if (selected.length === 1 && selected[0].calls.length === 1) {
|
|
502
|
+
return { call: selected[0].calls[0], indeterminate: false };
|
|
503
|
+
}
|
|
504
|
+
if (selected.length > 0) return { indeterminate: true };
|
|
505
|
+
|
|
506
|
+
const fallback = outboxRegistryCalls(sourceFile);
|
|
507
|
+
return fallback.length === 1
|
|
508
|
+
? { call: fallback[0], indeterminate: false }
|
|
509
|
+
: { indeterminate: fallback.length > 0 };
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function locallyExportedRegistryNames(sourceFile: ts.SourceFile): Set<string> {
|
|
513
|
+
const names = new Set<string>();
|
|
514
|
+
for (const statement of sourceFile.statements) {
|
|
515
|
+
if (
|
|
516
|
+
!ts.isExportDeclaration(statement) ||
|
|
517
|
+
!statement.exportClause ||
|
|
518
|
+
!ts.isNamedExports(statement.exportClause) ||
|
|
519
|
+
statement.moduleSpecifier
|
|
520
|
+
) {
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
for (const element of statement.exportClause.elements) {
|
|
524
|
+
names.add(element.propertyName?.text ?? element.name.text);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
return names;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function outboxRegistryCalls(node: ts.Node): ts.CallExpression[] {
|
|
531
|
+
const calls: ts.CallExpression[] = [];
|
|
532
|
+
const visit = (child: ts.Node): void => {
|
|
533
|
+
if (
|
|
534
|
+
ts.isCallExpression(child) &&
|
|
535
|
+
((ts.isIdentifier(child.expression) &&
|
|
536
|
+
child.expression.text === "defineOutboxRegistry") ||
|
|
537
|
+
(ts.isPropertyAccessExpression(child.expression) &&
|
|
538
|
+
child.expression.name.text === "defineOutboxRegistry"))
|
|
539
|
+
) {
|
|
540
|
+
calls.push(child);
|
|
541
|
+
}
|
|
542
|
+
ts.forEachChild(child, visit);
|
|
543
|
+
};
|
|
544
|
+
visit(node);
|
|
545
|
+
return calls;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function resolvedRegistryExpressions(
|
|
549
|
+
expression: ts.Expression,
|
|
550
|
+
sourceFile: ts.SourceFile,
|
|
551
|
+
): ts.Expression[] {
|
|
552
|
+
const initializers = registryInitializers(sourceFile);
|
|
553
|
+
|
|
554
|
+
const resolve = (
|
|
555
|
+
candidate: ts.Expression,
|
|
556
|
+
visited: Set<string>,
|
|
557
|
+
): ts.Expression[] => {
|
|
558
|
+
const current = unwrapRegistryExpression(candidate);
|
|
559
|
+
if (ts.isArrayLiteralExpression(current)) {
|
|
560
|
+
return current.elements.flatMap((element) =>
|
|
561
|
+
ts.isSpreadElement(element)
|
|
562
|
+
? resolve(element.expression, new Set(visited))
|
|
563
|
+
: ts.isExpression(element)
|
|
564
|
+
? [element]
|
|
565
|
+
: [],
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
if (ts.isIdentifier(current) && !visited.has(current.text)) {
|
|
569
|
+
const initializer = initializers.get(current.text);
|
|
570
|
+
if (initializer) {
|
|
571
|
+
const nextVisited = new Set(visited);
|
|
572
|
+
nextVisited.add(current.text);
|
|
573
|
+
return resolve(initializer, nextVisited);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
return [current];
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
return resolve(expression, new Set());
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function registryInitializers(
|
|
583
|
+
sourceFile: ts.SourceFile,
|
|
584
|
+
): Map<string, ts.Expression> {
|
|
585
|
+
const initializers = new Map<string, ts.Expression>();
|
|
586
|
+
for (const statement of sourceFile.statements) {
|
|
587
|
+
if (!ts.isVariableStatement(statement)) continue;
|
|
588
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
589
|
+
if (ts.isIdentifier(declaration.name) && declaration.initializer) {
|
|
590
|
+
initializers.set(declaration.name.text, declaration.initializer);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
return initializers;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function resolveRegistryExpression(
|
|
598
|
+
expression: ts.Expression,
|
|
599
|
+
initializers: Map<string, ts.Expression>,
|
|
600
|
+
visited = new Set<string>(),
|
|
601
|
+
): ts.Expression {
|
|
602
|
+
const current = unwrapRegistryExpression(expression);
|
|
603
|
+
if (!ts.isIdentifier(current) || visited.has(current.text)) return current;
|
|
604
|
+
const initializer = initializers.get(current.text);
|
|
605
|
+
if (!initializer) return current;
|
|
606
|
+
const nextVisited = new Set(visited);
|
|
607
|
+
nextVisited.add(current.text);
|
|
608
|
+
return resolveRegistryExpression(initializer, initializers, nextVisited);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function unwrapRegistryExpression(expression: ts.Expression): ts.Expression {
|
|
612
|
+
let current = expression;
|
|
613
|
+
while (
|
|
614
|
+
ts.isParenthesizedExpression(current) ||
|
|
615
|
+
ts.isAsExpression(current) ||
|
|
616
|
+
ts.isSatisfiesExpression(current) ||
|
|
617
|
+
ts.isTypeAssertionExpression(current) ||
|
|
618
|
+
ts.isNonNullExpression(current)
|
|
619
|
+
) {
|
|
620
|
+
current = current.expression;
|
|
621
|
+
}
|
|
622
|
+
return current;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function registryPropertyName(name: ts.PropertyName): string | undefined {
|
|
626
|
+
return ts.isIdentifier(name) || ts.isStringLiteralLike(name)
|
|
627
|
+
? name.text
|
|
628
|
+
: undefined;
|
|
329
629
|
}
|
|
330
630
|
|
|
331
631
|
/**
|
|
@@ -390,53 +690,3 @@ function appendEntry(
|
|
|
390
690
|
if (next === source) return { kind: "unchanged" };
|
|
391
691
|
return { kind: "updated", source: next };
|
|
392
692
|
}
|
|
393
|
-
|
|
394
|
-
function topLevelValueSlice(
|
|
395
|
-
source: string,
|
|
396
|
-
valueStart: number,
|
|
397
|
-
hardEnd: number,
|
|
398
|
-
): InitializerInfo | undefined {
|
|
399
|
-
let start = valueStart;
|
|
400
|
-
while (start < hardEnd && /\s/.test(source[start])) start++;
|
|
401
|
-
if (start >= hardEnd) return undefined;
|
|
402
|
-
|
|
403
|
-
let depth = 0;
|
|
404
|
-
|
|
405
|
-
for (const index of codeCharacterIndexes(source, start, hardEnd + 1)) {
|
|
406
|
-
const char = source[index];
|
|
407
|
-
|
|
408
|
-
if (char === "[" || char === "(" || char === "{") {
|
|
409
|
-
depth++;
|
|
410
|
-
continue;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
if (char === "]" || char === ")" || char === "}") {
|
|
414
|
-
if (depth === 0) {
|
|
415
|
-
return trimmedInfo(source, start, index);
|
|
416
|
-
}
|
|
417
|
-
depth--;
|
|
418
|
-
continue;
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
if (char === "," && depth === 0) {
|
|
422
|
-
return trimmedInfo(source, start, index);
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
return undefined;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
function trimmedInfo(
|
|
430
|
-
source: string,
|
|
431
|
-
start: number,
|
|
432
|
-
end: number,
|
|
433
|
-
): InitializerInfo {
|
|
434
|
-
let trimmedEnd = end;
|
|
435
|
-
while (trimmedEnd > start && /\s/.test(source[trimmedEnd - 1])) trimmedEnd--;
|
|
436
|
-
|
|
437
|
-
return {
|
|
438
|
-
text: source.slice(start, trimmedEnd),
|
|
439
|
-
start,
|
|
440
|
-
end: trimmedEnd,
|
|
441
|
-
};
|
|
442
|
-
}
|
package/src/templates/agents.ts
CHANGED
|
@@ -41,6 +41,9 @@ https://beignetjs.com (agent-friendly index at https://beignetjs.com/llms.txt).
|
|
|
41
41
|
README.md covers setup and the app map; this file covers what is not
|
|
42
42
|
discoverable from the code.
|
|
43
43
|
|
|
44
|
+
This app requires Node.js 22.12 or newer. Bun 1.3.14 or newer is supported for
|
|
45
|
+
Beignet commands.
|
|
46
|
+
|
|
44
47
|
## Registration is explicit
|
|
45
48
|
|
|
46
49
|
Hand-writing an artifact file does not wire it into the app. Non-registration
|
|
@@ -54,6 +57,11 @@ is a silent failure — the file exists but never runs:
|
|
|
54
57
|
- Tasks must be added to \`defineTasks([...])\` in \`server/tasks.ts\`.
|
|
55
58
|
- Outbox events and jobs must be registered in \`defineOutboxRegistry({...})\`
|
|
56
59
|
in \`server/outbox.ts\`.
|
|
60
|
+
- An outbox registry containing jobs also requires \`jobs: JobDispatcherPort\`
|
|
61
|
+
to be bound directly or deferred to a provider. \`${cli} make job\` installs
|
|
62
|
+
an app-owned inline dispatcher only when no dispatcher is already wired;
|
|
63
|
+
adding an Inngest or BullMQ preset later replaces only that marked generated
|
|
64
|
+
fallback and rejects unmarked custom inline wiring as a conflict.
|
|
57
65
|
- Listeners must be added to the \`listeners\` array in \`server/listeners.ts\`
|
|
58
66
|
and wired through a \`registerListeners(...)\` call in server provider wiring.
|
|
59
67
|
- Apps using Inngest must also spread feature job registries into
|
|
@@ -106,6 +114,7 @@ apps have reimplemented by accident:
|
|
|
106
114
|
|
|
107
115
|
| Need | Use |
|
|
108
116
|
| --- | --- |${capabilityClientRow}
|
|
117
|
+
| Query parameters on an HTTP contract | Pass both the Standard Schema and an explicit \`defineQueryTransport({ ... })\` to \`.query(schema, transport)\`. Use \`query.string()\`, \`query.number()\`, \`query.integer()\`, \`query.boolean()\`, \`query.dateTime()\`, \`query.date()\`, repeated scalar arrays, or one flat \`deepObject\`; never make schema coercion define the URL format. |
|
|
109
118
|
| 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. |
|
|
110
119
|
| 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. |
|
|
111
120
|
| 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. |
|
|
@@ -133,6 +142,13 @@ formatting. The individual commands (\`${lint}\`, \`${cli} lint\`,
|
|
|
133
142
|
\`${cli} doctor --strict\`, \`${test}\`, \`${typecheck}\`) still work when you
|
|
134
143
|
need one check alone.
|
|
135
144
|
|
|
145
|
+
Beignet's architecture lint parses TypeScript imports and re-exports,
|
|
146
|
+
\`require()\`, import assignments, and literal dynamic imports, then follows
|
|
147
|
+
local value imports through feature-root helpers. When lint cannot verify a
|
|
148
|
+
computed module reference inside a constrained layer, use a string-literal
|
|
149
|
+
specifier or move runtime loading behind an allowed boundary. Move near-miss
|
|
150
|
+
feature layers to the canonical path named in the diagnostic.
|
|
151
|
+
|
|
136
152
|
Before exploring unfamiliar code, run \`${cli} map\` for the human inventory
|
|
137
153
|
or \`${cli} map --json --feature <name>\` for an evidence-backed graph scoped
|
|
138
154
|
to one feature. The graph honors the app's complete TypeScript \`baseUrl\` and
|