@beignet/cli 0.0.48 → 0.0.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/README.md +117 -22
  3. package/dist/analysis/contract-factories.d.ts +17 -0
  4. package/dist/analysis/contract-factories.d.ts.map +1 -0
  5. package/dist/analysis/contract-factories.js +176 -0
  6. package/dist/analysis/contract-factories.js.map +1 -0
  7. package/dist/analysis/layers.d.ts +5 -0
  8. package/dist/analysis/layers.d.ts.map +1 -0
  9. package/dist/analysis/layers.js +172 -0
  10. package/dist/analysis/layers.js.map +1 -0
  11. package/dist/analysis/port-wiring.d.ts +23 -0
  12. package/dist/analysis/port-wiring.d.ts.map +1 -0
  13. package/dist/analysis/port-wiring.js +379 -0
  14. package/dist/analysis/port-wiring.js.map +1 -0
  15. package/dist/analysis/source-index.d.ts +5 -0
  16. package/dist/analysis/source-index.d.ts.map +1 -1
  17. package/dist/analysis/source-index.js +171 -42
  18. package/dist/analysis/source-index.js.map +1 -1
  19. package/dist/analysis/workspace.d.ts +11 -4
  20. package/dist/analysis/workspace.d.ts.map +1 -1
  21. package/dist/analysis/workspace.js +78 -8
  22. package/dist/analysis/workspace.js.map +1 -1
  23. package/dist/app-map-changes.d.ts +103 -0
  24. package/dist/app-map-changes.d.ts.map +1 -0
  25. package/dist/app-map-changes.js +949 -0
  26. package/dist/app-map-changes.js.map +1 -0
  27. package/dist/app-map.d.ts.map +1 -1
  28. package/dist/app-map.js +311 -92
  29. package/dist/app-map.js.map +1 -1
  30. package/dist/check.js +37 -3
  31. package/dist/check.js.map +1 -1
  32. package/dist/explain.js +4 -4
  33. package/dist/explain.js.map +1 -1
  34. package/dist/git-changes.d.ts +30 -0
  35. package/dist/git-changes.d.ts.map +1 -0
  36. package/dist/git-changes.js +367 -0
  37. package/dist/git-changes.js.map +1 -0
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +26 -1
  40. package/dist/index.js.map +1 -1
  41. package/dist/inspect.d.ts.map +1 -1
  42. package/dist/inspect.js +706 -186
  43. package/dist/inspect.js.map +1 -1
  44. package/dist/lib.d.ts +3 -0
  45. package/dist/lib.d.ts.map +1 -1
  46. package/dist/lib.js +1 -0
  47. package/dist/lib.js.map +1 -1
  48. package/dist/lint.d.ts.map +1 -1
  49. package/dist/lint.js +309 -487
  50. package/dist/lint.js.map +1 -1
  51. package/dist/make/inbox.d.ts.map +1 -1
  52. package/dist/make/inbox.js +11 -4
  53. package/dist/make/inbox.js.map +1 -1
  54. package/dist/make/payments.js +2 -2
  55. package/dist/make/shared.d.ts.map +1 -1
  56. package/dist/make/shared.js +31 -27
  57. package/dist/make/shared.js.map +1 -1
  58. package/dist/make.d.ts.map +1 -1
  59. package/dist/make.js +144 -8
  60. package/dist/make.js.map +1 -1
  61. package/dist/mcp.d.ts +1 -1
  62. package/dist/mcp.d.ts.map +1 -1
  63. package/dist/mcp.js +43 -21
  64. package/dist/mcp.js.map +1 -1
  65. package/dist/provider-add.d.ts.map +1 -1
  66. package/dist/provider-add.js +142 -7
  67. package/dist/provider-add.js.map +1 -1
  68. package/dist/registry-edits.d.ts +7 -0
  69. package/dist/registry-edits.d.ts.map +1 -1
  70. package/dist/registry-edits.js +237 -53
  71. package/dist/registry-edits.js.map +1 -1
  72. package/dist/templates/agents.d.ts.map +1 -1
  73. package/dist/templates/agents.js +37 -1
  74. package/dist/templates/agents.js.map +1 -1
  75. package/dist/templates/base.d.ts.map +1 -1
  76. package/dist/templates/base.js +13 -2
  77. package/dist/templates/base.js.map +1 -1
  78. package/dist/templates/server.d.ts.map +1 -1
  79. package/dist/templates/server.js +2 -0
  80. package/dist/templates/server.js.map +1 -1
  81. package/dist/templates/shared.d.ts +1 -0
  82. package/dist/templates/shared.d.ts.map +1 -1
  83. package/dist/templates/shared.js +3 -2
  84. package/dist/templates/shared.js.map +1 -1
  85. package/package.json +7 -6
  86. package/skills/app-structure/SKILL.md +71 -7
  87. package/src/analysis/contract-factories.ts +256 -0
  88. package/src/analysis/layers.ts +266 -0
  89. package/src/analysis/port-wiring.ts +506 -0
  90. package/src/analysis/source-index.ts +225 -57
  91. package/src/analysis/workspace.ts +134 -9
  92. package/src/app-map-changes.ts +1462 -0
  93. package/src/app-map.ts +418 -109
  94. package/src/check.ts +51 -3
  95. package/src/explain.ts +4 -4
  96. package/src/git-changes.ts +511 -0
  97. package/src/index.ts +39 -1
  98. package/src/inspect.ts +1005 -238
  99. package/src/lib.ts +21 -0
  100. package/src/lint.ts +393 -643
  101. package/src/make/inbox.ts +11 -4
  102. package/src/make/payments.ts +2 -2
  103. package/src/make/shared.ts +31 -27
  104. package/src/make.ts +225 -8
  105. package/src/mcp.ts +51 -24
  106. package/src/provider-add.ts +186 -7
  107. package/src/registry-edits.ts +313 -63
  108. package/src/templates/agents.ts +37 -1
  109. package/src/templates/base.ts +13 -2
  110. package/src/templates/server.ts +2 -0
  111. package/src/templates/shared.ts +3 -2
@@ -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: ["createInngestJobsProvider"],
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: ["createBullMQJobsProvider"],
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: ["MAIL_HOST", "MAIL_PORT", "MAIL_USER", "MAIL_PASS", "MAIL_FROM"],
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` and `@aws-sdk/s3-request-presigner`",
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
- assertNoConflictingProviders(source, preset);
1199
+ const withoutGeneratedFallback =
1200
+ preset.appPort.key === "jobs"
1201
+ ? removeGeneratedInlineJobsProvider(source)
1202
+ : source;
1203
+ assertNoConflictingProviders(withoutGeneratedFallback, preset);
1183
1204
 
1184
- let next = source;
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,
@@ -308,24 +308,324 @@ export function outboxRegistryValueInfo(
308
308
  source: string,
309
309
  property: "events" | "jobs",
310
310
  ): InitializerInfo | undefined {
311
- const call = /\bdefineOutboxRegistry\s*(?:<[^>]*>\s*)?\(/.exec(source);
312
- if (!call) return undefined;
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
- const openBrace = source.indexOf("{", call.index);
315
- if (openBrace === -1) return undefined;
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
- const closeBrace = matchingDelimiterIndex(source, openBrace, "{", "}");
318
- if (closeBrace === -1) return undefined;
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
- const objectText = source.slice(openBrace, closeBrace + 1);
321
- const propertyMatch = new RegExp(`\\b${property}\\s*:`).exec(objectText);
322
- if (!propertyMatch) return undefined;
373
+ type OutboxRegistryPropertyAnalysis = {
374
+ sourceFile: ts.SourceFile;
375
+ value?: ts.Expression;
376
+ indeterminate: boolean;
377
+ };
323
378
 
324
- const valueStart = openBrace + propertyMatch.index + propertyMatch[0].length;
325
- const value = topLevelValueSlice(source, valueStart, closeBrace);
326
- if (!value) return undefined;
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
- return value;
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
- }