@beignet/cli 0.0.25 → 0.0.27

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 (80) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +32 -11
  3. package/dist/choices.d.ts +8 -0
  4. package/dist/choices.d.ts.map +1 -1
  5. package/dist/choices.js +14 -0
  6. package/dist/choices.js.map +1 -1
  7. package/dist/db.js +2 -2
  8. package/dist/db.js.map +1 -1
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +109 -13
  11. package/dist/index.js.map +1 -1
  12. package/dist/inspect.js +51 -3
  13. package/dist/inspect.js.map +1 -1
  14. package/dist/make/inbox.d.ts +63 -0
  15. package/dist/make/inbox.d.ts.map +1 -0
  16. package/dist/make/inbox.js +1747 -0
  17. package/dist/make/inbox.js.map +1 -0
  18. package/dist/make/payments.d.ts +61 -0
  19. package/dist/make/payments.d.ts.map +1 -0
  20. package/dist/make/payments.js +1900 -0
  21. package/dist/make/payments.js.map +1 -0
  22. package/dist/make/shared.d.ts +575 -0
  23. package/dist/make/shared.d.ts.map +1 -0
  24. package/dist/make/shared.js +2166 -0
  25. package/dist/make/shared.js.map +1 -0
  26. package/dist/make/tenancy.d.ts +101 -0
  27. package/dist/make/tenancy.d.ts.map +1 -0
  28. package/dist/make/tenancy.js +4409 -0
  29. package/dist/make/tenancy.js.map +1 -0
  30. package/dist/make.d.ts +8 -44
  31. package/dist/make.d.ts.map +1 -1
  32. package/dist/make.js +71 -3659
  33. package/dist/make.js.map +1 -1
  34. package/dist/mcp.d.ts.map +1 -1
  35. package/dist/mcp.js +44 -3
  36. package/dist/mcp.js.map +1 -1
  37. package/dist/provider-add.d.ts +19 -0
  38. package/dist/provider-add.d.ts.map +1 -0
  39. package/dist/provider-add.js +691 -0
  40. package/dist/provider-add.js.map +1 -0
  41. package/dist/registry-edits.d.ts +9 -0
  42. package/dist/registry-edits.d.ts.map +1 -1
  43. package/dist/registry-edits.js +32 -0
  44. package/dist/registry-edits.js.map +1 -1
  45. package/dist/task.d.ts +2 -0
  46. package/dist/task.d.ts.map +1 -1
  47. package/dist/task.js +2 -0
  48. package/dist/task.js.map +1 -1
  49. package/dist/templates/agents.js +4 -4
  50. package/dist/templates/agents.js.map +1 -1
  51. package/dist/templates/base.d.ts.map +1 -1
  52. package/dist/templates/base.js +8 -1
  53. package/dist/templates/base.js.map +1 -1
  54. package/dist/templates/server.d.ts.map +1 -1
  55. package/dist/templates/server.js +44 -37
  56. package/dist/templates/server.js.map +1 -1
  57. package/dist/templates/shared.d.ts +5 -0
  58. package/dist/templates/shared.d.ts.map +1 -1
  59. package/dist/templates/shared.js +5 -0
  60. package/dist/templates/shared.js.map +1 -1
  61. package/package.json +2 -2
  62. package/skills/app-structure/SKILL.md +6 -1
  63. package/src/choices.ts +28 -0
  64. package/src/db.ts +2 -2
  65. package/src/index.ts +170 -13
  66. package/src/inspect.ts +68 -2
  67. package/src/make/inbox.ts +2015 -0
  68. package/src/make/payments.ts +2182 -0
  69. package/src/make/shared.ts +3550 -0
  70. package/src/make/tenancy.ts +4809 -0
  71. package/src/make.ts +380 -5289
  72. package/src/mcp.ts +65 -2
  73. package/src/provider-add.ts +926 -0
  74. package/src/registry-edits.ts +47 -0
  75. package/src/task.ts +8 -10
  76. package/src/templates/agents.ts +4 -4
  77. package/src/templates/base.ts +8 -1
  78. package/src/templates/server.ts +44 -37
  79. package/src/templates/shared.ts +5 -0
  80. package/src/test-helpers/generated-app.ts +168 -0
@@ -0,0 +1,2166 @@
1
+ import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { clientIndexPath, directoryPath, normalizePath, } from "../config.js";
4
+ import { appendToArrayExpression, appendToNamedArray, arrayInitializerInfo, identifiersFromArrayExpression, insertAfterImports, matchingDelimiterIndex, } from "../registry-edits.js";
5
+ export const drizzleDialects = {
6
+ sqlite: {
7
+ subpath: "sqlite",
8
+ dbTypeName: "DrizzleSqliteDatabase",
9
+ columnModule: "drizzle-orm/sqlite-core",
10
+ tableFunction: "sqliteTable",
11
+ schemaImports: ["integer", "sqliteTable", "text"],
12
+ supportsReturning: true,
13
+ idColumn: (columnName) => `text("${columnName}")`,
14
+ timestampColumn: (columnName) => `text("${columnName}")`,
15
+ integerColumn: (columnName) => `integer("${columnName}")`,
16
+ },
17
+ postgres: {
18
+ subpath: "postgres",
19
+ dbTypeName: "DrizzlePostgresDatabase",
20
+ columnModule: "drizzle-orm/pg-core",
21
+ tableFunction: "pgTable",
22
+ schemaImports: ["integer", "pgTable", "text"],
23
+ supportsReturning: true,
24
+ idColumn: (columnName) => `text("${columnName}")`,
25
+ timestampColumn: (columnName) => `text("${columnName}")`,
26
+ integerColumn: (columnName) => `integer("${columnName}")`,
27
+ },
28
+ mysql: {
29
+ subpath: "mysql",
30
+ dbTypeName: "DrizzleMysqlDatabase",
31
+ columnModule: "drizzle-orm/mysql-core",
32
+ tableFunction: "mysqlTable",
33
+ schemaImports: ["int", "mysqlTable", "text", "varchar"],
34
+ supportsReturning: false,
35
+ idColumn: (columnName) => `varchar("${columnName}", { length: 36 })`,
36
+ timestampColumn: (columnName) => `varchar("${columnName}", { length: 32 })`,
37
+ integerColumn: (columnName) => `int("${columnName}")`,
38
+ },
39
+ };
40
+ export function mergeMakeResults(left, right) {
41
+ return {
42
+ ...left,
43
+ files: uniqueStrings([...left.files, ...right.files]),
44
+ createdFiles: uniqueStrings([...left.createdFiles, ...right.createdFiles]),
45
+ updatedFiles: uniqueStrings([...left.updatedFiles, ...right.updatedFiles]),
46
+ skippedFiles: uniqueStrings([...left.skippedFiles, ...right.skippedFiles]),
47
+ };
48
+ }
49
+ export function mergeMakeResultInto(target, source) {
50
+ target.files = uniqueStrings([...target.files, ...source.files]);
51
+ target.createdFiles = uniqueStrings([
52
+ ...target.createdFiles,
53
+ ...source.createdFiles,
54
+ ]);
55
+ target.updatedFiles = uniqueStrings([
56
+ ...target.updatedFiles,
57
+ ...source.updatedFiles,
58
+ ]);
59
+ target.skippedFiles = uniqueStrings([
60
+ ...target.skippedFiles,
61
+ ...source.skippedFiles,
62
+ ]);
63
+ }
64
+ export function uniqueStrings(values) {
65
+ return [...new Set(values)];
66
+ }
67
+ export function eventBusPortWiring(command) {
68
+ return {
69
+ command,
70
+ portKey: "eventBus",
71
+ portType: "EventBusPort",
72
+ portTypeModule: "@beignet/core/ports",
73
+ providerFactory: "createInMemoryEventBusProvider",
74
+ providerModule: "@beignet/provider-event-bus-memory",
75
+ dependency: "@beignet/provider-event-bus-memory",
76
+ };
77
+ }
78
+ export function notificationPortWirings(command) {
79
+ return [
80
+ {
81
+ command,
82
+ portKey: "mailer",
83
+ portType: "MailerPort",
84
+ portTypeModule: "@beignet/core/mail",
85
+ providerFactory: "createMemoryMailerProvider",
86
+ providerModule: "@beignet/core/mail",
87
+ },
88
+ {
89
+ command,
90
+ portKey: "notifications",
91
+ portType: "NotificationPort",
92
+ portTypeModule: "@beignet/core/notifications",
93
+ providerFactory: "createInlineNotificationsProvider",
94
+ providerModule: "@beignet/core/notifications",
95
+ },
96
+ ];
97
+ }
98
+ export function paymentsPortWiring(command) {
99
+ return {
100
+ command,
101
+ portKey: "payments",
102
+ portType: "PaymentsPort",
103
+ portTypeModule: "@beignet/core/payments",
104
+ providerFactory: "createMemoryPaymentsProvider",
105
+ providerModule: "@beignet/core/payments",
106
+ };
107
+ }
108
+ export function storagePortWiring(command) {
109
+ return {
110
+ command,
111
+ portKey: "storage",
112
+ portType: "StoragePort",
113
+ portTypeModule: "@beignet/core/ports",
114
+ providerFactory: "createLocalStorageProvider",
115
+ providerModule: "@beignet/provider-storage-local",
116
+ dependency: "@beignet/provider-storage-local",
117
+ };
118
+ }
119
+ export function providersFilePath(config) {
120
+ return `${directoryPath(path.dirname(config.paths.server))}/providers.ts`;
121
+ }
122
+ /**
123
+ * Wire provider-backed app ports a generator depends on.
124
+ *
125
+ * Each wiring is independent and skipped silently when its port key already
126
+ * appears in the ports file, so apps that wired their own adapter (or scaffolds
127
+ * whose integrations already contribute the port) are left untouched. All
128
+ * anchors are computed before any file is written; an anchor miss aborts with a
129
+ * copy-pasteable manual instruction so the generator never leaves partial
130
+ * wiring behind. Callers validate anchors with `dryRun: true` before writing
131
+ * artifact files.
132
+ */
133
+ export async function wirePortProviders(targetDir, config, wirings, options) {
134
+ const portsFile = config.paths.ports;
135
+ const infrastructureFile = config.paths.infrastructurePorts;
136
+ const providersFile = providersFilePath(config);
137
+ const manualFor = (wiring) => `Wire the ${wiring.portKey} port manually: add ${wiring.portKey}: ${wiring.portType}; to AppPorts in ${portsFile}, defer "${wiring.portKey}" in ${infrastructureFile}, and register ${wiring.providerFactory}() from ${wiring.providerModule} in ${providersFile}.`;
138
+ const portsOriginal = await readOptionalFile(path.join(targetDir, portsFile));
139
+ if (portsOriginal === undefined) {
140
+ if (wirings.length === 0)
141
+ return { updatedFiles: [] };
142
+ throw new Error(`Could not find the generated ports file ${portsFile}. ${manualFor(wirings[0])}`);
143
+ }
144
+ // A port key that already appears in the ports file means the app wired its
145
+ // own adapter or an integration already contributes the port.
146
+ const active = wirings.filter((wiring) => !new RegExp(`\\b${wiring.portKey}\\b`).test(portsOriginal));
147
+ if (active.length === 0)
148
+ return { updatedFiles: [] };
149
+ let portsNext = portsOriginal;
150
+ for (const wiring of active) {
151
+ portsNext = addNamedTypeImport(portsNext, wiring.portType, wiring.portTypeModule);
152
+ portsNext = insertTypeProperty(portsNext, "AppPorts", `${wiring.portKey}: ${wiring.portType};`, `Could not find AppPorts in ${portsFile}. ${manualFor(wiring)} Or restore the generated ports file before running beignet ${wiring.command}.`);
153
+ }
154
+ const infrastructureOriginal = await readOptionalFile(path.join(targetDir, infrastructureFile));
155
+ if (infrastructureOriginal === undefined) {
156
+ throw new Error(`Could not find the generated infrastructure ports file ${infrastructureFile}. ${manualFor(active[0])}`);
157
+ }
158
+ let infrastructureNext = infrastructureOriginal;
159
+ for (const wiring of active) {
160
+ const appended = appendDeferredPortKey(infrastructureNext, wiring.portKey);
161
+ if (appended === infrastructureNext &&
162
+ !new RegExp(`["']${wiring.portKey}["']`).test(infrastructureNext)) {
163
+ throw new Error(`Could not find the deferred port list in ${infrastructureFile}. ${manualFor(wiring)} Or restore the generated infrastructure ports file before running beignet ${wiring.command}.`);
164
+ }
165
+ infrastructureNext = appended;
166
+ }
167
+ const providersOriginal = await readOptionalFile(path.join(targetDir, providersFile));
168
+ if (providersOriginal === undefined) {
169
+ throw new Error(`Could not find the generated providers file ${providersFile}. ${manualFor(active[0])}`);
170
+ }
171
+ let providersNext = providersOriginal;
172
+ for (const wiring of active) {
173
+ if (new RegExp(`\\b${wiring.providerFactory}\\b`).test(providersNext)) {
174
+ continue;
175
+ }
176
+ const appended = appendToNamedArray(providersNext, "providers", `${wiring.providerFactory}()`, `import { ${wiring.providerFactory} } from "${wiring.providerModule}";`);
177
+ if (appended.kind === "missing") {
178
+ throw new Error(`Could not find the exported providers array in ${providersFile}. ${manualFor(wiring)} Or restore the generated providers file before running beignet ${wiring.command}.`);
179
+ }
180
+ if (appended.kind === "updated")
181
+ providersNext = appended.source;
182
+ }
183
+ const dependencies = active.filter((wiring) => wiring.dependency !== undefined);
184
+ let packageJsonNext;
185
+ if (dependencies.length > 0) {
186
+ const packageJsonOriginal = await readFile(path.join(targetDir, "package.json"), "utf8");
187
+ const packageJson = JSON.parse(packageJsonOriginal);
188
+ packageJson.dependencies = packageJson.dependencies ?? {};
189
+ for (const wiring of dependencies) {
190
+ packageJson.dependencies[wiring.dependency] ??=
191
+ beignetDependencyVersion(packageJson);
192
+ }
193
+ const next = `${JSON.stringify(packageJson, null, "\t")}\n`;
194
+ if (next !== packageJsonOriginal)
195
+ packageJsonNext = next;
196
+ }
197
+ const plannedFiles = [];
198
+ if (portsNext !== portsOriginal) {
199
+ plannedFiles.push({ path: portsFile, content: portsNext });
200
+ }
201
+ if (infrastructureNext !== infrastructureOriginal) {
202
+ plannedFiles.push({
203
+ path: infrastructureFile,
204
+ content: infrastructureNext,
205
+ });
206
+ }
207
+ if (providersNext !== providersOriginal) {
208
+ plannedFiles.push({ path: providersFile, content: providersNext });
209
+ }
210
+ if (packageJsonNext !== undefined) {
211
+ plannedFiles.push({ path: "package.json", content: packageJsonNext });
212
+ }
213
+ if (!options.dryRun) {
214
+ for (const file of plannedFiles) {
215
+ await writeFile(path.join(targetDir, file.path), file.content);
216
+ }
217
+ }
218
+ return { updatedFiles: plannedFiles.map((file) => file.path) };
219
+ }
220
+ /**
221
+ * Record port provider wiring updates on a make result.
222
+ */
223
+ export async function applyPortProviderWiring(result, targetDir, config, wirings, options) {
224
+ const wired = await wirePortProviders(targetDir, config, wirings, options);
225
+ for (const file of wired.updatedFiles) {
226
+ if (!result.updatedFiles.includes(file))
227
+ result.updatedFiles.push(file);
228
+ if (!result.files.includes(file))
229
+ result.files.push(file);
230
+ }
231
+ }
232
+ export async function assertStandardApp(targetDir, config) {
233
+ const requiredFiles = [
234
+ config.paths.appContext,
235
+ config.paths.infrastructurePorts,
236
+ config.paths.ports,
237
+ config.paths.server,
238
+ config.paths.useCaseBuilder,
239
+ ];
240
+ for (const file of requiredFiles) {
241
+ try {
242
+ await stat(path.join(targetDir, file));
243
+ }
244
+ catch {
245
+ throw new Error(`beignet make resource expects a standard Beignet app. Missing ${file}.`);
246
+ }
247
+ }
248
+ }
249
+ export async function assertUseCaseApp(targetDir, config) {
250
+ try {
251
+ await stat(path.join(targetDir, config.paths.useCaseBuilder));
252
+ }
253
+ catch {
254
+ throw new Error(`beignet make use-case expects a standard Beignet app. Missing ${config.paths.useCaseBuilder}.`);
255
+ }
256
+ }
257
+ export async function assertTestApp(targetDir, names, config) {
258
+ const requiredFiles = [
259
+ config.paths.appContext,
260
+ config.paths.infrastructurePorts,
261
+ useCaseFilePath(names, config),
262
+ ];
263
+ for (const file of requiredFiles) {
264
+ try {
265
+ await stat(path.join(targetDir, file));
266
+ }
267
+ catch {
268
+ throw new Error(`beignet make test expects a standard Beignet app with the target use case. Missing ${file}.`);
269
+ }
270
+ }
271
+ }
272
+ export async function assertPortApp(targetDir, config) {
273
+ const requiredFiles = [config.paths.ports, config.paths.infrastructurePorts];
274
+ for (const file of requiredFiles) {
275
+ try {
276
+ await stat(path.join(targetDir, file));
277
+ }
278
+ catch {
279
+ throw new Error(`beignet make port expects a standard Beignet app. Missing ${file}.`);
280
+ }
281
+ }
282
+ }
283
+ export async function assertAdapterApp(targetDir, names, config) {
284
+ const requiredFiles = [
285
+ config.paths.infrastructurePorts,
286
+ portFilePath(names, config),
287
+ ];
288
+ for (const file of requiredFiles) {
289
+ try {
290
+ await stat(path.join(targetDir, file));
291
+ }
292
+ catch {
293
+ throw new Error(`beignet make adapter expects an existing port in a standard Beignet app. Missing ${file}.`);
294
+ }
295
+ }
296
+ const infrastructurePorts = await readFile(path.join(targetDir, config.paths.infrastructurePorts), "utf8");
297
+ if (!infrastructurePorts.includes(portStubEntry(names)) &&
298
+ !infrastructurePorts.includes(adapterEntry(names))) {
299
+ throw new Error(`Could not find the generated ${names.interfaceName} infrastructure stub in ${config.paths.infrastructurePorts}. Wire ${names.adapterFactoryName}() manually or restore the generated stub before running make adapter.`);
300
+ }
301
+ }
302
+ export async function assertPolicyApp(targetDir, config) {
303
+ try {
304
+ await stat(path.join(targetDir, config.paths.appContext));
305
+ }
306
+ catch {
307
+ throw new Error(`beignet make policy expects a standard Beignet app. Missing ${config.paths.appContext}.`);
308
+ }
309
+ }
310
+ export async function assertFeatureArtifactApp(targetDir, config, artifact, options = {}) {
311
+ const requiredFiles = [
312
+ config.paths.appContext,
313
+ ...(options.requireServer ? [config.paths.server] : []),
314
+ ];
315
+ for (const file of requiredFiles) {
316
+ try {
317
+ await stat(path.join(targetDir, file));
318
+ }
319
+ catch {
320
+ throw new Error(`beignet make ${artifact} expects a standard Beignet app. Missing ${file}.`);
321
+ }
322
+ }
323
+ }
324
+ export async function assertFeatureUiApp(targetDir, config) {
325
+ const clientPath = clientIndexPath(config);
326
+ const requiredFiles = [config.paths.appContext, clientPath];
327
+ for (const file of requiredFiles) {
328
+ try {
329
+ await stat(path.join(targetDir, file));
330
+ }
331
+ catch {
332
+ throw new Error(`beignet make feature --with ui expects a standard Beignet app with React Query client helpers. Missing ${file}.`);
333
+ }
334
+ }
335
+ }
336
+ export async function assertServerRuntimeApp(targetDir, config, artifact) {
337
+ const requiredFiles = [
338
+ config.paths.appContext,
339
+ config.paths.server,
340
+ config.paths.infrastructurePorts,
341
+ ];
342
+ for (const file of requiredFiles) {
343
+ try {
344
+ await stat(path.join(targetDir, file));
345
+ }
346
+ catch {
347
+ throw new Error(`beignet make ${artifact} expects a standard Beignet app with a server runtime. Missing ${file}.`);
348
+ }
349
+ }
350
+ }
351
+ export async function assertPersistenceArtifactApp(targetDir, names, config, artifact) {
352
+ const requiredFiles = [
353
+ config.paths.appContext,
354
+ config.paths.ports,
355
+ resourcePortFilePath(featureResourceNames(names), config),
356
+ ];
357
+ for (const file of requiredFiles) {
358
+ try {
359
+ await stat(path.join(targetDir, file));
360
+ }
361
+ catch {
362
+ throw new Error(`beignet make ${artifact} expects a feature with a repository port. Missing ${file}. Run beignet make feature ${names.feature.kebab} first, or add the repository port before generating ${artifact}s.`);
363
+ }
364
+ }
365
+ }
366
+ export async function writeGeneratedFile(targetDir, file, options) {
367
+ const plannedFile = await planGeneratedFile(targetDir, file, {
368
+ force: options.force,
369
+ });
370
+ if (!options.dryRun) {
371
+ await writePlannedGeneratedFile(targetDir, plannedFile);
372
+ }
373
+ return plannedFile.result;
374
+ }
375
+ export async function planGeneratedFiles(targetDir, files, options) {
376
+ const plannedFiles = [];
377
+ for (const file of files) {
378
+ plannedFiles.push(await planGeneratedFile(targetDir, file, options));
379
+ }
380
+ return plannedFiles;
381
+ }
382
+ export async function planGeneratedFile(targetDir, file, options) {
383
+ const destination = path.join(targetDir, file.path);
384
+ const existing = await readOptionalFile(destination);
385
+ if (existing === file.content) {
386
+ return { ...file, result: "skipped" };
387
+ }
388
+ if (existing !== undefined && !options.force) {
389
+ throw new Error(`File ${file.path} already exists with different content. Pass --force to overwrite it.`);
390
+ }
391
+ return { ...file, result: existing === undefined ? "created" : "updated" };
392
+ }
393
+ export async function writePlannedGeneratedFile(targetDir, file) {
394
+ if (file.result === "skipped")
395
+ return;
396
+ const destination = path.join(targetDir, file.path);
397
+ await mkdir(path.dirname(destination), { recursive: true });
398
+ await writeFile(destination, file.content);
399
+ }
400
+ export async function fileExists(filePath) {
401
+ try {
402
+ await stat(filePath);
403
+ return true;
404
+ }
405
+ catch {
406
+ return false;
407
+ }
408
+ }
409
+ export async function readOptionalFile(filePath) {
410
+ try {
411
+ return await readFile(filePath, "utf8");
412
+ }
413
+ catch {
414
+ return undefined;
415
+ }
416
+ }
417
+ export const capabilityBuilders = {
418
+ listeners: {
419
+ factoryName: "createListeners",
420
+ defineName: "defineListener",
421
+ module: "@beignet/core/events",
422
+ },
423
+ jobs: {
424
+ factoryName: "createJobs",
425
+ defineName: "defineJob",
426
+ module: "@beignet/core/jobs",
427
+ },
428
+ schedules: {
429
+ factoryName: "createSchedules",
430
+ defineName: "defineSchedule",
431
+ module: "@beignet/core/schedules",
432
+ },
433
+ notifications: {
434
+ factoryName: "createNotifications",
435
+ defineName: "defineNotification",
436
+ module: "@beignet/core/notifications",
437
+ },
438
+ tasks: {
439
+ factoryName: "createTasks",
440
+ defineName: "defineTask",
441
+ module: "@beignet/core/tasks",
442
+ },
443
+ };
444
+ export function capabilityBuilderPath(capability, config) {
445
+ if (capability === "listeners")
446
+ return config.paths.listenersBuilder;
447
+ if (capability === "jobs")
448
+ return config.paths.jobsBuilder;
449
+ if (capability === "schedules")
450
+ return config.paths.schedulesBuilder;
451
+ if (capability === "notifications")
452
+ return config.paths.notificationsBuilder;
453
+ return config.paths.tasksBuilder;
454
+ }
455
+ export function capabilityBuilderFile(capability, config) {
456
+ const builder = capabilityBuilders[capability];
457
+ return {
458
+ path: capabilityBuilderPath(capability, config),
459
+ content: `import { ${builder.factoryName} } from "${builder.module}";
460
+ import type { AppContext } from "${aliasModule(config.paths.appContext)}";
461
+
462
+ export const { ${builder.defineName} } = ${builder.factoryName}<AppContext>();
463
+ `,
464
+ };
465
+ }
466
+ export async function missingCapabilityBuilderFiles(targetDir, capability, config) {
467
+ const file = capabilityBuilderFile(capability, config);
468
+ if (await fileExists(path.join(targetDir, file.path))) {
469
+ return [];
470
+ }
471
+ return [file];
472
+ }
473
+ export async function updatePackageDependencies(targetDir, dependencies, options) {
474
+ const filePath = path.join(targetDir, "package.json");
475
+ const original = await readFile(filePath, "utf8");
476
+ const packageJson = JSON.parse(original);
477
+ packageJson.dependencies = packageJson.dependencies ?? {};
478
+ for (const [name, version] of Object.entries(dependencies)) {
479
+ packageJson.dependencies[name] ??= version(packageJson);
480
+ }
481
+ const next = `${JSON.stringify(packageJson, null, "\t")}\n`;
482
+ if (next === original)
483
+ return false;
484
+ if (!options.dryRun)
485
+ await writeFile(filePath, next);
486
+ return true;
487
+ }
488
+ export async function updatePackageScripts(targetDir, scripts, options) {
489
+ const filePath = path.join(targetDir, "package.json");
490
+ const original = await readFile(filePath, "utf8");
491
+ const packageJson = JSON.parse(original);
492
+ packageJson.scripts = packageJson.scripts ?? {};
493
+ for (const [name, command] of Object.entries(scripts)) {
494
+ packageJson.scripts[name] ??= command;
495
+ }
496
+ const next = `${JSON.stringify(packageJson, null, "\t")}\n`;
497
+ if (next === original)
498
+ return false;
499
+ if (!options.dryRun)
500
+ await writeFile(filePath, next);
501
+ return true;
502
+ }
503
+ export function beignetDependencyVersion(packageJson) {
504
+ return (packageJson.dependencies?.["@beignet/core"] ??
505
+ packageJson.dependencies?.["@beignet/next"] ??
506
+ "*");
507
+ }
508
+ export function tanstackReactQueryDependencyVersion(packageJson) {
509
+ return packageJson.dependencies?.["@tanstack/react-query"] ?? "^5.100.7";
510
+ }
511
+ export async function updatePackageJson(targetDir, options) {
512
+ const filePath = path.join(targetDir, "package.json");
513
+ const original = await readFile(filePath, "utf8");
514
+ const packageJson = JSON.parse(original);
515
+ packageJson.scripts = packageJson.scripts ?? {};
516
+ packageJson.scripts.test ??= "bun test";
517
+ if (options.webTestingDependency) {
518
+ packageJson.devDependencies = packageJson.devDependencies ?? {};
519
+ packageJson.devDependencies["@beignet/web"] ??=
520
+ packageJson.dependencies?.["@beignet/core"] ??
521
+ packageJson.dependencies?.["@beignet/next"] ??
522
+ "*";
523
+ }
524
+ const next = `${JSON.stringify(packageJson, null, "\t")}\n`;
525
+ if (!options.dryRun)
526
+ await writeFile(filePath, next);
527
+ return next !== original;
528
+ }
529
+ export async function updatePortsIndex(targetDir, names, config, options) {
530
+ const filePath = path.join(targetDir, config.paths.ports);
531
+ const original = await readFile(filePath, "utf8");
532
+ let next = original;
533
+ const repositoryPath = resourcePortFilePath(names, config);
534
+ const importLine = `import type { ${names.singularPascal}Repository } from "${relativeModule(config.paths.ports, repositoryPath)}";`;
535
+ if (!next.includes(importLine)) {
536
+ next = insertAfterImports(next, importLine);
537
+ }
538
+ if (options.generationOptions?.auth) {
539
+ const policyPath = path.join(config.paths.features, names.pluralKebab, "policy.ts");
540
+ const policyImportLine = `import type { AuthorizationContext as ${names.singularPascal}AuthorizationContext, ${names.singularCamel}Policy } from "${relativeModule(config.paths.ports, policyPath)}";`;
541
+ if (!next.includes(policyImportLine)) {
542
+ next = insertAfterImports(next, policyImportLine);
543
+ }
544
+ next = appendUnionTypeMember(next, "AppAuthorizationContext", `${names.singularPascal}AuthorizationContext`);
545
+ next = appendTupleTypeMember(next, "AppGate", `typeof ${names.singularCamel}Policy`);
546
+ next = appendGatePolicyMember(next, `typeof ${names.singularCamel}Policy`);
547
+ }
548
+ const entry = `\t${names.pluralCamel}: ${names.singularPascal}Repository;`;
549
+ if (!next.includes(entry.trim())) {
550
+ next = insertTypeProperty(next, "AppPorts", entry.trim(), `Could not find AppPorts in ${config.paths.ports}. Add ${entry.trim()} manually, or restore the generated ports file before running make resource.`);
551
+ }
552
+ const transactionPorts = typeBodySource(next, "AppTransactionPorts");
553
+ if (transactionPorts !== undefined &&
554
+ !transactionPorts.includes(entry.trim())) {
555
+ next = replaceTransactionPortsRequired(next, entry, `Could not find AppTransactionPorts in ${config.paths.ports}. Add ${entry.trim()} manually, or restore the generated ports file before running make resource.`);
556
+ }
557
+ if (next === original)
558
+ return false;
559
+ if (!options.dryRun)
560
+ await writeFile(filePath, next);
561
+ return true;
562
+ }
563
+ export async function updatePortIndex(targetDir, names, config, options) {
564
+ const filePath = path.join(targetDir, config.paths.ports);
565
+ const original = await readFile(filePath, "utf8");
566
+ let next = original;
567
+ const portPath = path.join(path.dirname(config.paths.ports), `${names.kebab}.ts`);
568
+ const importLine = `import type { ${names.interfaceName} } from "${relativeModule(config.paths.ports, portPath)}";`;
569
+ if (!next.includes(importLine)) {
570
+ next = insertAfterImports(next, importLine);
571
+ }
572
+ const entry = `\t${names.camel}: ${names.interfaceName};`;
573
+ if (!next.includes(entry.trim())) {
574
+ next = insertTypeProperty(next, "AppPorts", entry.trim(), `Could not find AppPorts in ${config.paths.ports}. Add ${entry.trim()} manually, or restore the generated ports file before running make port.`);
575
+ }
576
+ if (next === original)
577
+ return false;
578
+ if (!options.dryRun)
579
+ await writeFile(filePath, next);
580
+ return true;
581
+ }
582
+ export async function updateInfrastructurePortStub(targetDir, names, config, options) {
583
+ const filePath = path.join(targetDir, config.paths.infrastructurePorts);
584
+ const original = await readFile(filePath, "utf8");
585
+ let next = original;
586
+ const portPath = portFilePath(names, config);
587
+ const importLine = `import type { ${names.interfaceName} } from "${relativeModule(config.paths.infrastructurePorts, portPath)}";`;
588
+ if (!next.includes(importLine)) {
589
+ next = insertAfterImports(next, importLine);
590
+ }
591
+ const entry = portStubEntry(names);
592
+ const definePortsBody = definePortsBodySource(next);
593
+ if (definePortsBody === undefined ||
594
+ !hasTopLevelObjectProperty(definePortsBody, names.camel)) {
595
+ next = insertDefinePortsProperty(next, entry.trim(), `Could not find definePorts({ in ${config.paths.infrastructurePorts}. Add the ${names.interfaceName} stub manually, or restore the generated infrastructure ports file before running make port.`);
596
+ }
597
+ if (next === original)
598
+ return false;
599
+ if (!options.dryRun)
600
+ await writeFile(filePath, next);
601
+ return true;
602
+ }
603
+ export async function updateInfrastructureAdapterWiring(targetDir, names, config, options) {
604
+ const filePath = path.join(targetDir, config.paths.infrastructurePorts);
605
+ const original = await readFile(filePath, "utf8");
606
+ let next = original;
607
+ const adapterPath = adapterFilePath(names, config);
608
+ const adapterImportLine = `import { ${names.adapterFactoryName} } from "${relativeModule(config.paths.infrastructurePorts, adapterPath)}";`;
609
+ if (!next.includes(adapterImportLine)) {
610
+ next = insertAfterImports(next, adapterImportLine);
611
+ }
612
+ const portPath = portFilePath(names, config);
613
+ const portImportLine = `import type { ${names.interfaceName} } from "${relativeModule(config.paths.infrastructurePorts, portPath)}";`;
614
+ if (next.includes(portImportLine)) {
615
+ next = next.replace(`${portImportLine}\n`, "");
616
+ }
617
+ const adapter = adapterEntry(names);
618
+ const stub = portStubEntry(names);
619
+ if (next.includes(stub)) {
620
+ next = next.replace(stub, adapter);
621
+ }
622
+ else if (!next.includes(adapter)) {
623
+ throw new Error(`Could not find the generated ${names.interfaceName} infrastructure stub in ${config.paths.infrastructurePorts}. Wire ${names.adapterFactoryName}() manually or restore the generated stub before running make adapter.`);
624
+ }
625
+ if (next === original)
626
+ return false;
627
+ if (!options.dryRun)
628
+ await writeFile(filePath, next);
629
+ return true;
630
+ }
631
+ export async function updateInfrastructurePorts(targetDir, names, config, options) {
632
+ const filePath = path.join(targetDir, config.paths.infrastructurePorts);
633
+ const original = await readFile(filePath, "utf8");
634
+ let next = original;
635
+ const repositoryPath = path.join(path.dirname(config.paths.infrastructurePorts), names.pluralKebab, `in-memory-${names.singularKebab}-repository.ts`);
636
+ const importLine = `import { createInMemory${names.singularPascal}Repository } from "${relativeModule(config.paths.infrastructurePorts, repositoryPath)}";`;
637
+ if (!next.includes(importLine)) {
638
+ next = insertAfterImports(next, importLine);
639
+ }
640
+ const repositoryConst = `const ${names.pluralCamel} = createInMemory${names.singularPascal}Repository();`;
641
+ if (!next.includes(repositoryConst)) {
642
+ next = replaceRequired(next, /(\nexport const appPorts = definePorts(?:<[^>]+>)?\((?:\)\()?\{)/, `\n${repositoryConst}\n$1`, `Could not find definePorts({ in ${config.paths.infrastructurePorts}. Add ${repositoryConst} manually, or restore the generated infrastructure ports file before running make resource.`);
643
+ }
644
+ const entry = `\t${names.pluralCamel},`;
645
+ const definePortsBody = definePortsBodySource(next);
646
+ if (definePortsBody === undefined ||
647
+ !hasTopLevelObjectProperty(definePortsBody, names.pluralCamel)) {
648
+ next = insertDefinePortsProperty(next, entry.trim(), `Could not find definePorts({ in ${config.paths.infrastructurePorts}. Add ${entry.trim()} to appPorts manually, or restore the generated infrastructure ports file before running make resource.`);
649
+ }
650
+ const transactionEntry = `\t\t${names.pluralCamel},`;
651
+ const uowBody = noopUnitOfWorkBodySource(next);
652
+ if (uowBody !== undefined &&
653
+ !new RegExp(`\\b${names.pluralCamel}\\b\\s*,`).test(uowBody)) {
654
+ next = insertNoopUnitOfWorkProperty(next, transactionEntry.trim(), `Could not find the generated Unit of Work port list in ${config.paths.infrastructurePorts}. Add ${transactionEntry.trim()} manually, or restore the generated infrastructure ports file before running make resource.`);
655
+ }
656
+ if (next === original)
657
+ return false;
658
+ if (!options.dryRun)
659
+ await writeFile(filePath, next);
660
+ return true;
661
+ }
662
+ export async function updateInfrastructureGatePolicies(targetDir, names, config, options) {
663
+ const filePath = path.join(targetDir, config.paths.infrastructurePorts);
664
+ const original = await readFile(filePath, "utf8");
665
+ let next = original;
666
+ const policyPath = path.join(config.paths.features, names.pluralKebab, "policy.ts");
667
+ const importLine = `import { ${names.singularCamel}Policy } from "${relativeModule(config.paths.infrastructurePorts, policyPath)}";`;
668
+ if (!next.includes(importLine)) {
669
+ next = insertAfterImports(next, importLine);
670
+ }
671
+ next = appendCreateGatePolicy(next, `${names.singularCamel}Policy`, `Could not find createGate({ policies: [...] }) in ${config.paths.infrastructurePorts}. Add ${names.singularCamel}Policy to the gate policies manually, or restore the generated infrastructure ports file before running make resource --auth.`);
672
+ if (next === original)
673
+ return false;
674
+ if (!options.dryRun)
675
+ await writeFile(filePath, next);
676
+ return true;
677
+ }
678
+ export async function updateDrizzleSchemaIndex(targetDir, names, config, options) {
679
+ const filePath = path.join(targetDir, drizzleSchemaIndexPath(config));
680
+ if (!(await fileExists(filePath)))
681
+ return false;
682
+ const original = await readFile(filePath, "utf8");
683
+ const exportLine = `export { ${names.pluralCamel} } from "./${names.pluralKebab}";`;
684
+ if (original.includes(exportLine))
685
+ return false;
686
+ const next = `${original.trimEnd()}\n${exportLine}\n`;
687
+ if (!options.dryRun)
688
+ await writeFile(filePath, next);
689
+ return true;
690
+ }
691
+ export async function updateDrizzleRepositories(targetDir, names, config, options) {
692
+ const filePath = path.join(targetDir, drizzleRepositoriesPath(config));
693
+ if (!(await fileExists(filePath)))
694
+ return false;
695
+ const original = await readFile(filePath, "utf8");
696
+ let next = original;
697
+ const repositoryPath = drizzleResourceRepositoryFilePath(names, config);
698
+ const importLine = `import { createDrizzle${names.singularPascal}Repository } from "${relativeModule(drizzleRepositoriesPath(config), repositoryPath)}";`;
699
+ const repositoriesBody = createRepositoriesReturnBodySource(next);
700
+ const entry = `\t\t${names.pluralCamel}: createDrizzle${names.singularPascal}Repository(db),`;
701
+ if (repositoriesBody === undefined) {
702
+ throw new Error(`Could not find return { in createRepositories in ${drizzleRepositoriesPath(config)}. Add ${entry.trim()} manually, or restore the generated repositories file before running make resource.`);
703
+ }
704
+ if (!next.includes(importLine)) {
705
+ next = insertAfterImports(next, importLine);
706
+ }
707
+ if (!hasTopLevelObjectProperty(repositoriesBody, names.pluralCamel)) {
708
+ next = insertCreateRepositoriesProperty(next, entry.trim(), `Could not find return { in createRepositories in ${drizzleRepositoriesPath(config)}. Add ${entry.trim()} manually, or restore the generated repositories file before running make resource.`);
709
+ }
710
+ if (next === original)
711
+ return false;
712
+ if (!options.dryRun)
713
+ await writeFile(filePath, next);
714
+ return true;
715
+ }
716
+ /**
717
+ * Add port keys to the database provider's `Pick<AppPorts, ...>` provided-port
718
+ * union. Shared by the payments, tenancy, and inbox generators.
719
+ */
720
+ export async function updateDatabaseProviderPick(targetDir, config, keys, options) {
721
+ const file = path.join(infrastructureDir(config), "db/provider.ts");
722
+ const filePath = path.join(targetDir, file);
723
+ if (!(await fileExists(filePath)))
724
+ return false;
725
+ const original = await readFile(filePath, "utf8");
726
+ let next = original;
727
+ for (const key of keys) {
728
+ next = appendPickStringLiteralMember(next, "AppPorts", key);
729
+ }
730
+ if (next === original)
731
+ return false;
732
+ if (!options.dryRun)
733
+ await writeFile(filePath, next);
734
+ return true;
735
+ }
736
+ /**
737
+ * Add tables to the `tables` array in the database reset entrypoint so
738
+ * `beignet db reset` truncates generator-owned tables too.
739
+ */
740
+ export async function updateDatabaseResetTables(targetDir, config, tableNames, options) {
741
+ const file = path.join(infrastructureDir(config), "db/reset.ts");
742
+ const filePath = path.join(targetDir, file);
743
+ if (!(await fileExists(filePath)))
744
+ return false;
745
+ const original = await readFile(filePath, "utf8");
746
+ const missingTables = tableNames.filter((table) => !new RegExp(`["']${table}["']`).test(original));
747
+ if (missingTables.length === 0)
748
+ return false;
749
+ const tables = arrayInitializerInfo(original, "tables");
750
+ if (!tables)
751
+ return false;
752
+ const nextArray = appendToArrayExpression(tables.text, missingTables.map((table) => `"${table}"`));
753
+ const next = `${original.slice(0, tables.start)}${nextArray}${original.slice(tables.end)}`;
754
+ if (next === original)
755
+ return false;
756
+ if (!options.dryRun)
757
+ await writeFile(filePath, next);
758
+ return true;
759
+ }
760
+ /**
761
+ * Intersect the transaction-ports repositories dependency with
762
+ * `Pick<AppTransactionPorts, ...>` for generator-owned repository keys, so
763
+ * transaction-scoped use cases see the new ports.
764
+ */
765
+ export async function updateTransactionPortsPick(targetDir, config, keys, generator, options) {
766
+ const file = path.join(infrastructureDir(config), "db/transaction-ports.ts");
767
+ const filePath = path.join(targetDir, file);
768
+ if (!(await fileExists(filePath)))
769
+ return false;
770
+ const original = await readFile(filePath, "utf8");
771
+ const firstKey = keys[0];
772
+ if (firstKey !== undefined &&
773
+ new RegExp(`Pick<AppTransactionPorts,\\s*["']${firstKey}["']`).test(original)) {
774
+ return false;
775
+ }
776
+ const keyList = keys.join(", ");
777
+ const repositoryProperty = /repositories:\s*([^;\n]+);/.exec(original);
778
+ const repositoryType = repositoryProperty?.[1]?.trim();
779
+ if (!repositoryProperty || !repositoryType) {
780
+ throw new Error(`Could not find repositories: ... in ${file}. Add ${keyList} to the transaction repository dependency manually, or restore the generated transaction ports file before running ${generator}.`);
781
+ }
782
+ const pick = keys.map((key) => `"${key}"`).join(" | ");
783
+ const next = original.replace(repositoryProperty[0], `repositories: ${repositoryType} & Pick<AppTransactionPorts, ${pick}>;`);
784
+ if (next === original)
785
+ return false;
786
+ if (!options.dryRun)
787
+ await writeFile(filePath, next);
788
+ return true;
789
+ }
790
+ /**
791
+ * Insert missing entries into the app's `defineErrors({ ... })` catalog.
792
+ * Entries are skipped when their name already appears anywhere in the file.
793
+ */
794
+ export async function appendSharedErrorEntries(targetDir, config, entries, generator, options) {
795
+ const file = resourceSharedErrorsPath(config);
796
+ const filePath = path.join(targetDir, file);
797
+ if (!(await fileExists(filePath)))
798
+ return false;
799
+ const original = await readFile(filePath, "utf8");
800
+ const missingEntries = entries.filter(({ name }) => !new RegExp(`\\b${name}\\b`).test(original));
801
+ if (missingEntries.length === 0)
802
+ return false;
803
+ const expectedErrorNames = missingEntries
804
+ .map(({ name }) => name)
805
+ .join(" and ");
806
+ const defineErrorsStart = original.indexOf("defineErrors(");
807
+ const openBrace = defineErrorsStart === -1 ? -1 : original.indexOf("{", defineErrorsStart);
808
+ if (openBrace === -1) {
809
+ throw new Error(`Could not find defineErrors({ in ${file}. Add ${expectedErrorNames} manually, or restore the generated shared error catalog before running ${generator}.`);
810
+ }
811
+ const closeBrace = matchingDelimiterIndex(original, openBrace, "{", "}");
812
+ if (closeBrace === -1) {
813
+ throw new Error(`Could not update ${file} because the error catalog object is malformed. Add ${expectedErrorNames} manually, or restore the generated shared error catalog before running ${generator}.`);
814
+ }
815
+ const next = insertBeforeClosingBrace(original, openBrace, closeBrace, missingEntries.map(({ entry }) => entry).join("\n"));
816
+ if (next === original)
817
+ return false;
818
+ if (!options.dryRun)
819
+ await writeFile(filePath, next);
820
+ return true;
821
+ }
822
+ /**
823
+ * Register contract exports with an array-literal `createOpenAPIHandler([...])`
824
+ * route. Apps that pass `contracts` derived from routes pick up new route
825
+ * groups automatically and need no edit.
826
+ */
827
+ export async function updateOpenApiRouteContracts(targetDir, config, registration, options) {
828
+ const filePath = path.join(targetDir, config.paths.openapiRoute);
829
+ if (!(await fileExists(filePath)))
830
+ return false;
831
+ const original = await readFile(filePath, "utf8");
832
+ const firstArg = firstCreateOpenAPIHandlerArgInfo(original);
833
+ if (!firstArg?.text.startsWith("["))
834
+ return false;
835
+ const listedContracts = identifiersFromArrayExpression(firstArg.text);
836
+ const missingContracts = registration.contractNames.filter((contractName) => !listedContracts.has(contractName));
837
+ if (missingContracts.length === 0)
838
+ return false;
839
+ const importLine = `import { ${registration.contractNames.join(", ")} } from "${registration.contractsModule}";`;
840
+ const nextArray = appendToArrayExpression(firstArg.text, missingContracts);
841
+ let next = `${original.slice(0, firstArg.start)}${nextArray}${original.slice(firstArg.end)}`;
842
+ if (!next.includes(importLine)) {
843
+ next = insertAfterImports(next, importLine);
844
+ }
845
+ if (next === original)
846
+ return false;
847
+ if (!options.dryRun)
848
+ await writeFile(filePath, next);
849
+ return true;
850
+ }
851
+ /**
852
+ * Append a schema module's table exports to the Drizzle schema index. The
853
+ * guard is the module specifier, so a hand-edited export list still skips.
854
+ */
855
+ export async function updateDrizzleSchemaIndexExports(targetDir, config, registration, options) {
856
+ const filePath = path.join(targetDir, drizzleSchemaIndexPath(config));
857
+ if (!(await fileExists(filePath)))
858
+ return false;
859
+ const original = await readFile(filePath, "utf8");
860
+ if (original.includes(`from "${registration.module}"`))
861
+ return false;
862
+ const exportLine = `export { ${registration.exports.join(", ")} } from "${registration.module}";`;
863
+ const next = `${original.trimEnd()}\n${exportLine}\n`;
864
+ if (!options.dryRun)
865
+ await writeFile(filePath, next);
866
+ return true;
867
+ }
868
+ /**
869
+ * Detect the React Query client the full starter shell ships. The API-only
870
+ * starter keeps `client/index.ts` but drops the `rq` helper, so its presence
871
+ * decides whether a generator emits UI files.
872
+ */
873
+ export async function detectUiShell(targetDir, config) {
874
+ const clientIndex = await readOptionalFile(path.join(targetDir, clientIndexPath(config)));
875
+ return clientIndex !== undefined && /\bexport const rq\b/.test(clientIndex);
876
+ }
877
+ export async function updateInfrastructureDeferredPorts(targetDir, names, config, options) {
878
+ const filePath = path.join(targetDir, config.paths.infrastructurePorts);
879
+ const original = await readFile(filePath, "utf8");
880
+ // Drizzle resources are provided by the app database provider at startup,
881
+ // so deferred-form ports files defer the new repository key. Identity-form
882
+ // ports files are left untouched.
883
+ const next = appendDeferredPortKey(original, names.pluralCamel);
884
+ if (next === original)
885
+ return false;
886
+ if (!options.dryRun)
887
+ await writeFile(filePath, next);
888
+ return true;
889
+ }
890
+ export async function updateServerTransactionPorts(targetDir, names, config, options) {
891
+ const filePath = path.join(targetDir, config.paths.server);
892
+ const original = await readFile(filePath, "utf8");
893
+ let next = original;
894
+ const transactionPortsBody = drizzleTransactionPortsBodySource(next);
895
+ if (transactionPortsBody === undefined ||
896
+ hasTopLevelObjectProperty(transactionPortsBody, names.pluralCamel)) {
897
+ return false;
898
+ }
899
+ if (!/\bappPorts\b/.test(next)) {
900
+ throw new Error(`Could not find appPorts in ${config.paths.server}. Add ${names.pluralCamel}: appPorts.${names.pluralCamel} to createTransactionPorts manually, or restore the generated server file before running make resource.`);
901
+ }
902
+ const entry = `\t\t\t\t\t\t${names.pluralCamel}: appPorts.${names.pluralCamel},`;
903
+ next = insertDrizzleTransactionPortsProperty(next, entry.trim(), `Could not find createTransactionPorts({ in ${config.paths.server}. Add ${entry.trim()} manually, or restore the generated server file before running make resource.`);
904
+ if (next === original)
905
+ return false;
906
+ if (!options.dryRun)
907
+ await writeFile(filePath, next);
908
+ return true;
909
+ }
910
+ export async function updateOpenApiRoute(targetDir, names, config, options) {
911
+ return updateOpenApiRouteContracts(targetDir, config, {
912
+ contractNames: resourceContractNames(names, options.mode),
913
+ contractsModule: aliasModule(resourceContractFilePath(names, config)),
914
+ }, options);
915
+ }
916
+ export async function updateSharedErrors(targetDir, names, config, options) {
917
+ if (options.mode !== "resource")
918
+ return false;
919
+ return appendSharedErrorEntries(targetDir, config, [
920
+ {
921
+ name: `${names.singularPascal}NotFound`,
922
+ entry: `${names.singularPascal}NotFound: {
923
+ \t\tcode: "${constantCase(names.singularKebab)}_NOT_FOUND",
924
+ \t\tstatus: 404,
925
+ \t\tmessage: "${names.singularPascal} not found",
926
+ \t},`,
927
+ },
928
+ {
929
+ name: `${names.singularPascal}Conflict`,
930
+ entry: `${names.singularPascal}Conflict: {
931
+ \t\tcode: "${constantCase(names.singularKebab)}_CONFLICT",
932
+ \t\tstatus: 409,
933
+ \t\tmessage: "${names.singularPascal} was changed by another request",
934
+ \t},`,
935
+ },
936
+ ], "make resource", options);
937
+ }
938
+ export async function updateServerRoutes(targetDir, names, config, options) {
939
+ const filePath = path.join(targetDir, config.paths.server);
940
+ const original = await readFile(filePath, "utf8");
941
+ const routeRegistryPath = routeRegistryFileFromServerSource(original, config.paths.server);
942
+ if (routeRegistryPath) {
943
+ await assertImportedRoutesFileExists(targetDir, routeRegistryPath);
944
+ return updateDefineRoutesFile(targetDir, routeRegistryPath, names, config, options);
945
+ }
946
+ return updateDefineRoutesFile(targetDir, config.paths.server, names, config, options);
947
+ }
948
+ export async function updateDefineRoutesFile(targetDir, file, names, config, options) {
949
+ const filePath = path.join(targetDir, file);
950
+ const original = await readFile(filePath, "utf8");
951
+ let next = original;
952
+ const routeGroupName = `${names.singularCamel}Routes`;
953
+ const routeGroupPath = path.join(config.paths.features, names.pluralKebab, "routes.ts");
954
+ const importLine = `import { ${routeGroupName} } from "${relativeModule(file, routeGroupPath)}";`;
955
+ if (!next.includes(importLine)) {
956
+ next = insertAfterImports(next, importLine);
957
+ }
958
+ if (!routeGroupIsRegistered(next, routeGroupName)) {
959
+ const firstArg = firstDefineRoutesArgInfo(next);
960
+ if (!firstArg?.text.startsWith("[")) {
961
+ throw new Error(`Could not find a central defineRoutes([...]) registration in ${file}. Import ${routeGroupName} and add it to defineRoutes manually.`);
962
+ }
963
+ const nextArray = appendToArrayExpression(firstArg.text, [routeGroupName]);
964
+ next = `${next.slice(0, firstArg.start)}${nextArray}${next.slice(firstArg.end)}`;
965
+ }
966
+ if (!routeGroupIsRegistered(next, routeGroupName)) {
967
+ throw new Error(`Could not find a central defineRoutes([...]) registration in ${file}. Import ${routeGroupName} and add it to defineRoutes manually.`);
968
+ }
969
+ if (next === original)
970
+ return undefined;
971
+ if (!options.dryRun)
972
+ await writeFile(filePath, next);
973
+ return file;
974
+ }
975
+ export function routeGroupIsRegistered(source, routeGroupName) {
976
+ const firstArg = firstDefineRoutesArgInfo(source);
977
+ if (!firstArg?.text.startsWith("["))
978
+ return false;
979
+ return identifiersFromArrayExpression(firstArg.text).has(routeGroupName);
980
+ }
981
+ export function routeRegistryFileFromServerSource(source, serverFile) {
982
+ const serverOptions = firstCreateNextServerArgInfo(source);
983
+ if (!serverOptions)
984
+ return undefined;
985
+ const routeIdentifier = routeOptionIdentifier(serverOptions.text);
986
+ if (!routeIdentifier)
987
+ return undefined;
988
+ const imported = parseNamedImportSources(source).get(routeIdentifier);
989
+ if (!imported)
990
+ return undefined;
991
+ return resolveImportFile(imported.sourcePath, serverFile);
992
+ }
993
+ export function routeOptionIdentifier(source) {
994
+ const explicitMatch = /(?:^|[,{])\s*routes\s*:\s*([A-Za-z_$][\w$]*)/m.exec(source);
995
+ if (explicitMatch)
996
+ return explicitMatch[1];
997
+ if (/(?:^|[,{])\s*routes\s*(?:[,}])/m.test(source)) {
998
+ return "routes";
999
+ }
1000
+ return undefined;
1001
+ }
1002
+ export function parseNamedImportSources(source) {
1003
+ const imports = new Map();
1004
+ const importRegex = /import\s+\{([^}]+)\}\s+from\s+["']([^"']+)["']/g;
1005
+ for (const match of source.matchAll(importRegex)) {
1006
+ for (const member of match[1].split(",")) {
1007
+ const parsed = parseImportMember(member);
1008
+ if (!parsed)
1009
+ continue;
1010
+ imports.set(parsed.localName, {
1011
+ importedName: parsed.importedName,
1012
+ sourcePath: match[2],
1013
+ });
1014
+ }
1015
+ }
1016
+ return imports;
1017
+ }
1018
+ export function parseImportMember(member) {
1019
+ const trimmed = member.trim();
1020
+ if (!trimmed)
1021
+ return undefined;
1022
+ const aliasMatch = /^([A-Za-z_$][\w$]*)\s+as\s+([A-Za-z_$][\w$]*)$/.exec(trimmed);
1023
+ if (aliasMatch) {
1024
+ return {
1025
+ importedName: aliasMatch[1],
1026
+ localName: aliasMatch[2],
1027
+ };
1028
+ }
1029
+ if (/^[A-Za-z_$][\w$]*$/.test(trimmed)) {
1030
+ return {
1031
+ importedName: trimmed,
1032
+ localName: trimmed,
1033
+ };
1034
+ }
1035
+ return undefined;
1036
+ }
1037
+ export function resolveImportFile(sourcePath, importerFile) {
1038
+ if (sourcePath.startsWith("@/")) {
1039
+ return `${sourcePath.slice(2)}.ts`;
1040
+ }
1041
+ if (sourcePath.startsWith(".")) {
1042
+ return `${normalizePath(path.join(path.dirname(importerFile), sourcePath))}.ts`;
1043
+ }
1044
+ return undefined;
1045
+ }
1046
+ export async function assertImportedRoutesFileExists(targetDir, file) {
1047
+ try {
1048
+ await stat(path.join(targetDir, file));
1049
+ }
1050
+ catch {
1051
+ throw new Error(`Could not find the route registry ${file}. Restore the imported routes file or add the new route group manually.`);
1052
+ }
1053
+ }
1054
+ export function firstDefineRoutesArgInfo(source) {
1055
+ const match = /defineRoutes(?:<[^>]+>)?\(/.exec(source);
1056
+ if (!match)
1057
+ return undefined;
1058
+ return firstCallArgInfo(source, match.index + match[0].length);
1059
+ }
1060
+ export function firstCreateNextServerArgInfo(source) {
1061
+ const match = /createNextServer(?:<[^>]+>)?\(/.exec(source);
1062
+ if (!match)
1063
+ return undefined;
1064
+ return firstCallArgInfo(source, match.index + match[0].length);
1065
+ }
1066
+ export function firstCreateOpenAPIHandlerArgInfo(source) {
1067
+ const start = source.indexOf("createOpenAPIHandler(");
1068
+ if (start === -1)
1069
+ return undefined;
1070
+ return firstCallArgInfo(source, start + "createOpenAPIHandler(".length);
1071
+ }
1072
+ export function firstCallArgInfo(source, argStart) {
1073
+ let depth = 0;
1074
+ let inString;
1075
+ let escaped = false;
1076
+ for (let index = argStart; index < source.length; index++) {
1077
+ const char = source[index];
1078
+ if (inString) {
1079
+ if (escaped) {
1080
+ escaped = false;
1081
+ }
1082
+ else if (char === "\\") {
1083
+ escaped = true;
1084
+ }
1085
+ else if (char === inString) {
1086
+ inString = undefined;
1087
+ }
1088
+ continue;
1089
+ }
1090
+ if (char === '"' || char === "'" || char === "`") {
1091
+ inString = char;
1092
+ continue;
1093
+ }
1094
+ if (char === "[" || char === "(" || char === "{") {
1095
+ depth++;
1096
+ continue;
1097
+ }
1098
+ if (char === "]" || char === ")" || char === "}") {
1099
+ if (depth === 0) {
1100
+ return trimmedSlice(source, argStart, index);
1101
+ }
1102
+ depth--;
1103
+ continue;
1104
+ }
1105
+ if (char === "," && depth === 0) {
1106
+ return trimmedSlice(source, argStart, index);
1107
+ }
1108
+ }
1109
+ return undefined;
1110
+ }
1111
+ export function trimmedSlice(source, start, end) {
1112
+ let trimmedStart = start;
1113
+ let trimmedEnd = end;
1114
+ while (trimmedStart < trimmedEnd && /\s/.test(source[trimmedStart])) {
1115
+ trimmedStart++;
1116
+ }
1117
+ while (trimmedEnd > trimmedStart && /\s/.test(source[trimmedEnd - 1])) {
1118
+ trimmedEnd--;
1119
+ }
1120
+ return {
1121
+ text: source.slice(trimmedStart, trimmedEnd),
1122
+ start: trimmedStart,
1123
+ end: trimmedEnd,
1124
+ };
1125
+ }
1126
+ export function replaceRequired(source, searchValue, replaceValue, message) {
1127
+ const next = source.replace(searchValue, replaceValue);
1128
+ if (next === source)
1129
+ throw new Error(message);
1130
+ return next;
1131
+ }
1132
+ export function insertTypeProperty(source, typeName, property, message) {
1133
+ const match = new RegExp(`export\\s+type\\s+${typeName}\\b`).exec(source);
1134
+ if (!match)
1135
+ throw new Error(message);
1136
+ const openBrace = source.indexOf("{", match.index);
1137
+ if (openBrace === -1)
1138
+ throw new Error(message);
1139
+ const closeBrace = matchingBraceIndex(source, openBrace);
1140
+ if (closeBrace === -1)
1141
+ throw new Error(message);
1142
+ return insertBeforeClosingBrace(source, openBrace, closeBrace, property);
1143
+ }
1144
+ export function typeBodySource(source, typeName) {
1145
+ const match = new RegExp(`export\\s+type\\s+${typeName}\\b`).exec(source);
1146
+ if (!match)
1147
+ return undefined;
1148
+ const openBrace = source.indexOf("{", match.index);
1149
+ if (openBrace === -1)
1150
+ return undefined;
1151
+ const closeBrace = matchingBraceIndex(source, openBrace);
1152
+ if (closeBrace === -1)
1153
+ return undefined;
1154
+ return source.slice(openBrace + 1, closeBrace);
1155
+ }
1156
+ export function appendUnionTypeMember(source, typeName, member) {
1157
+ const match = new RegExp(`(export\\s+type\\s+${typeName}\\s*=)([\\s\\S]*?);`).exec(source);
1158
+ if (!match)
1159
+ return source;
1160
+ const declaration = match[0];
1161
+ if (new RegExp(`\\b${member}\\b`).test(declaration))
1162
+ return source;
1163
+ const nextDeclaration = declaration.replace(/;\s*$/, ` | ${member};`);
1164
+ return `${source.slice(0, match.index)}${nextDeclaration}${source.slice(match.index + declaration.length)}`;
1165
+ }
1166
+ export function appendPickStringLiteralMember(source, typeName, member) {
1167
+ const match = new RegExp(`Pick<\\s*${typeName}\\s*,([\\s\\S]*?)>`).exec(source);
1168
+ if (!match)
1169
+ return source;
1170
+ const literal = `"${member}"`;
1171
+ if (match[1].includes(literal))
1172
+ return source;
1173
+ const union = match[1];
1174
+ const indent = union.match(/\n([\t ]*)\|/)?.[1] ?? "\t";
1175
+ const nextUnion = union.includes("\n")
1176
+ ? `${union.replace(/\s*$/, "")}\n${indent}| ${literal}\n`
1177
+ : `${union.trimEnd()} | ${literal}`;
1178
+ return `${source.slice(0, match.index)}Pick<${typeName},${nextUnion}>${source.slice(match.index + match[0].length)}`;
1179
+ }
1180
+ export function appendTupleTypeMember(source, typeName, member) {
1181
+ const match = new RegExp(`export\\s+type\\s+${typeName}\\s*=\\s*`).exec(source);
1182
+ if (!match)
1183
+ return source;
1184
+ const tupleStart = source.indexOf("[", match.index);
1185
+ if (tupleStart === -1)
1186
+ return source;
1187
+ const tupleEnd = matchingDelimiterIndex(source, tupleStart, "[", "]");
1188
+ if (tupleEnd === -1)
1189
+ return source;
1190
+ const tuple = source.slice(tupleStart, tupleEnd + 1);
1191
+ if (tuple.includes(member))
1192
+ return source;
1193
+ const nextTuple = appendToArrayExpression(tuple, [member]);
1194
+ return `${source.slice(0, tupleStart)}${nextTuple}${source.slice(tupleEnd + 1)}`;
1195
+ }
1196
+ export function appendGatePolicyMember(source, member) {
1197
+ const gateMatch = /\bGatePort\s*</.exec(source);
1198
+ if (!gateMatch)
1199
+ return source;
1200
+ const tupleStart = source.indexOf("[", gateMatch.index);
1201
+ if (tupleStart === -1)
1202
+ return source;
1203
+ const tupleEnd = matchingDelimiterIndex(source, tupleStart, "[", "]");
1204
+ if (tupleEnd === -1)
1205
+ return source;
1206
+ const tuple = source.slice(tupleStart, tupleEnd + 1);
1207
+ if (tuple.includes(member))
1208
+ return source;
1209
+ const nextTuple = appendToArrayExpression(tuple, [member]);
1210
+ return `${source.slice(0, tupleStart)}${nextTuple}${source.slice(tupleEnd + 1)}`;
1211
+ }
1212
+ export function appendCreateGatePolicy(source, policy, failureMessage) {
1213
+ const policyMatch = /\bpolicies\s*:\s*\[/.exec(source);
1214
+ if (!policyMatch) {
1215
+ throw new Error(failureMessage);
1216
+ }
1217
+ const arrayStart = source.indexOf("[", policyMatch.index);
1218
+ if (arrayStart === -1) {
1219
+ throw new Error(failureMessage);
1220
+ }
1221
+ const arrayEnd = matchingDelimiterIndex(source, arrayStart, "[", "]");
1222
+ if (arrayEnd === -1) {
1223
+ throw new Error(failureMessage);
1224
+ }
1225
+ const policies = source.slice(arrayStart, arrayEnd + 1);
1226
+ if (policies.includes(policy))
1227
+ return source;
1228
+ const nextPolicies = appendToArrayExpression(policies, [policy]);
1229
+ return `${source.slice(0, arrayStart)}${nextPolicies}${source.slice(arrayEnd + 1)}`;
1230
+ }
1231
+ export function insertDefinePortsProperty(source, property, message) {
1232
+ const body = definePortsBodyRange(source);
1233
+ if (!body)
1234
+ throw new Error(message);
1235
+ return insertBeforeClosingBrace(source, body.openBrace, body.closeBrace, property);
1236
+ }
1237
+ export function insertNoopUnitOfWorkProperty(source, property, message) {
1238
+ const body = noopUnitOfWorkBodyRange(source);
1239
+ if (!body)
1240
+ throw new Error(message);
1241
+ return insertBeforeClosingBrace(source, body.openBrace, body.closeBrace, property);
1242
+ }
1243
+ export function insertDrizzleTransactionPortsProperty(source, property, message) {
1244
+ const body = drizzleTransactionPortsBodyRange(source);
1245
+ if (!body)
1246
+ throw new Error(message);
1247
+ return insertBeforeClosingBrace(source, body.openBrace, body.closeBrace, property);
1248
+ }
1249
+ export function insertCreateRepositoriesProperty(source, property, message) {
1250
+ const body = createRepositoriesReturnBodyRange(source);
1251
+ if (!body)
1252
+ throw new Error(message);
1253
+ return insertBeforeClosingBrace(source, body.openBrace, body.closeBrace, property);
1254
+ }
1255
+ export function noopUnitOfWorkBodySource(source) {
1256
+ const body = noopUnitOfWorkBodyRange(source);
1257
+ if (!body)
1258
+ return undefined;
1259
+ return source.slice(body.openBrace + 1, body.closeBrace);
1260
+ }
1261
+ export function drizzleTransactionPortsBodySource(source) {
1262
+ const body = drizzleTransactionPortsBodyRange(source);
1263
+ if (!body)
1264
+ return undefined;
1265
+ return source.slice(body.openBrace + 1, body.closeBrace);
1266
+ }
1267
+ export function createRepositoriesReturnBodySource(source) {
1268
+ const body = createRepositoriesReturnBodyRange(source);
1269
+ if (!body)
1270
+ return undefined;
1271
+ return source.slice(body.openBrace + 1, body.closeBrace);
1272
+ }
1273
+ export function definePortsBodySource(source) {
1274
+ const body = definePortsBodyRange(source);
1275
+ if (!body)
1276
+ return undefined;
1277
+ return source.slice(body.openBrace + 1, body.closeBrace);
1278
+ }
1279
+ export function definePortsBodyRange(source) {
1280
+ const outer = definePortsOuterBodyRange(source);
1281
+ if (!outer)
1282
+ return undefined;
1283
+ // The curried `definePorts<AppPorts>()({ bound, deferred })` form binds app
1284
+ // ports inside `bound`; generated bindings belong there.
1285
+ return (topLevelBoundObjectRange(source, outer.openBrace, outer.closeBrace) ?? outer);
1286
+ }
1287
+ export function definePortsOuterBodyRange(source) {
1288
+ const match = /definePorts(?:<[^>]+>)?\(\s*(?:\)\s*\(\s*)?\{/.exec(source);
1289
+ if (!match)
1290
+ return undefined;
1291
+ const openBrace = source.indexOf("{", match.index);
1292
+ const closeBrace = matchingBraceIndex(source, openBrace);
1293
+ if (closeBrace === -1)
1294
+ return undefined;
1295
+ return { openBrace, closeBrace };
1296
+ }
1297
+ export function appendDeferredPortKey(source, portKey) {
1298
+ const outer = definePortsOuterBodyRange(source);
1299
+ if (!outer)
1300
+ return source;
1301
+ const body = source.slice(outer.openBrace + 1, outer.closeBrace);
1302
+ if (!hasTopLevelObjectProperty(body, "deferred"))
1303
+ return source;
1304
+ const deferredMatch = /\bdeferred\s*:\s*\[/.exec(body);
1305
+ if (!deferredMatch)
1306
+ return source;
1307
+ const arrayStart = source.indexOf("[", outer.openBrace + 1 + deferredMatch.index);
1308
+ const arrayEnd = matchingDelimiterIndex(source, arrayStart, "[", "]");
1309
+ if (arrayEnd === -1)
1310
+ return source;
1311
+ const tuple = source.slice(arrayStart, arrayEnd + 1);
1312
+ if (new RegExp(`["']${portKey}["']`).test(tuple))
1313
+ return source;
1314
+ const nextTuple = appendToArrayExpression(tuple, [`"${portKey}"`]);
1315
+ return `${source.slice(0, arrayStart)}${nextTuple}${source.slice(arrayEnd + 1)}`;
1316
+ }
1317
+ /**
1318
+ * Add a name to an `import type { ... } from "<module>";` statement, creating
1319
+ * the import when the module is not imported yet. Names are inserted in sorted
1320
+ * position and single-line versus multiline formatting is preserved.
1321
+ */
1322
+ export function addNamedTypeImport(source, name, moduleName) {
1323
+ const escapedModule = moduleName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1324
+ const importPattern = new RegExp(`import type \\{([^}]*)\\} from "${escapedModule}";`);
1325
+ const match = importPattern.exec(source);
1326
+ if (!match) {
1327
+ return insertAfterImports(source, `import type { ${name} } from "${moduleName}";`);
1328
+ }
1329
+ const names = match[1]
1330
+ .split(",")
1331
+ .map((entry) => entry.trim())
1332
+ .filter(Boolean);
1333
+ if (names.includes(name))
1334
+ return source;
1335
+ const insertIndex = names.findIndex((existing) => existing.localeCompare(name) > 0);
1336
+ if (insertIndex === -1)
1337
+ names.push(name);
1338
+ else
1339
+ names.splice(insertIndex, 0, name);
1340
+ const indent = match[1].match(/\n([\t ]+)/)?.[1] ?? "\t";
1341
+ const replacement = match[1].includes("\n")
1342
+ ? `import type {\n${indent}${names.join(`,\n${indent}`)},\n} from "${moduleName}";`
1343
+ : `import type { ${names.join(", ")} } from "${moduleName}";`;
1344
+ return `${source.slice(0, match.index)}${replacement}${source.slice(match.index + match[0].length)}`;
1345
+ }
1346
+ export function addNamedImport(source, name, moduleName) {
1347
+ const escapedModule = moduleName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1348
+ const importPattern = new RegExp(`import \\{([^}]*)\\} from "${escapedModule}";`);
1349
+ const match = importPattern.exec(source);
1350
+ if (!match) {
1351
+ return insertAfterImports(source, `import { ${name} } from "${moduleName}";`);
1352
+ }
1353
+ const names = match[1]
1354
+ .split(",")
1355
+ .map((entry) => entry.trim())
1356
+ .filter(Boolean);
1357
+ if (names.includes(name))
1358
+ return source;
1359
+ const insertIndex = names.findIndex((existing) => existing.localeCompare(name) > 0);
1360
+ if (insertIndex === -1)
1361
+ names.push(name);
1362
+ else
1363
+ names.splice(insertIndex, 0, name);
1364
+ const indent = match[1].match(/\n([\t ]+)/)?.[1] ?? "\t";
1365
+ const replacement = match[1].includes("\n")
1366
+ ? `import {\n${indent}${names.join(`,\n${indent}`)},\n} from "${moduleName}";`
1367
+ : `import { ${names.join(", ")} } from "${moduleName}";`;
1368
+ return `${source.slice(0, match.index)}${replacement}${source.slice(match.index + match[0].length)}`;
1369
+ }
1370
+ export function topLevelBoundObjectRange(source, openBrace, closeBrace) {
1371
+ const body = source.slice(openBrace + 1, closeBrace);
1372
+ if (!hasTopLevelObjectProperty(body, "bound"))
1373
+ return undefined;
1374
+ const boundMatch = /\bbound\s*:\s*\{/.exec(body);
1375
+ if (!boundMatch)
1376
+ return undefined;
1377
+ const boundOpen = source.indexOf("{", openBrace + 1 + boundMatch.index);
1378
+ const boundClose = matchingBraceIndex(source, boundOpen);
1379
+ if (boundClose === -1)
1380
+ return undefined;
1381
+ return { openBrace: boundOpen, closeBrace: boundClose };
1382
+ }
1383
+ /**
1384
+ * Whether the named type declares a top-level property, including optional
1385
+ * (`name?:`) properties.
1386
+ */
1387
+ export function typeHasTopLevelProperty(source, typeName, propertyName) {
1388
+ const body = typeBodySource(source, typeName);
1389
+ return body !== undefined && hasTopLevelObjectProperty(body, propertyName);
1390
+ }
1391
+ export function hasTopLevelObjectProperty(source, propertyName) {
1392
+ let depth = 0;
1393
+ let inString;
1394
+ let escaped = false;
1395
+ let lineComment = false;
1396
+ let blockComment = false;
1397
+ for (let index = 0; index < source.length; index++) {
1398
+ const char = source[index];
1399
+ const next = source[index + 1];
1400
+ if (lineComment) {
1401
+ if (char === "\n")
1402
+ lineComment = false;
1403
+ continue;
1404
+ }
1405
+ if (blockComment) {
1406
+ if (char === "*" && next === "/") {
1407
+ blockComment = false;
1408
+ index++;
1409
+ }
1410
+ continue;
1411
+ }
1412
+ if (inString) {
1413
+ if (escaped) {
1414
+ escaped = false;
1415
+ }
1416
+ else if (char === "\\") {
1417
+ escaped = true;
1418
+ }
1419
+ else if (char === inString) {
1420
+ inString = undefined;
1421
+ }
1422
+ continue;
1423
+ }
1424
+ if (char === "/" && next === "/") {
1425
+ lineComment = true;
1426
+ index++;
1427
+ continue;
1428
+ }
1429
+ if (char === "/" && next === "*") {
1430
+ blockComment = true;
1431
+ index++;
1432
+ continue;
1433
+ }
1434
+ if (char === '"' || char === "'" || char === "`") {
1435
+ inString = char;
1436
+ continue;
1437
+ }
1438
+ if (char === "{" || char === "[" || char === "(") {
1439
+ depth++;
1440
+ continue;
1441
+ }
1442
+ if (char === "}" || char === "]" || char === ")") {
1443
+ depth = Math.max(0, depth - 1);
1444
+ continue;
1445
+ }
1446
+ if (depth !== 0 || !identifierStartPattern.test(char)) {
1447
+ continue;
1448
+ }
1449
+ const property = identifierAt(source, index);
1450
+ if (property !== propertyName) {
1451
+ index += property.length - 1;
1452
+ continue;
1453
+ }
1454
+ let cursor = index + property.length;
1455
+ while (/\s/.test(source[cursor] ?? ""))
1456
+ cursor++;
1457
+ // Optional properties (`name?:`) count as present too.
1458
+ if (source[cursor] === "?") {
1459
+ cursor++;
1460
+ while (/\s/.test(source[cursor] ?? ""))
1461
+ cursor++;
1462
+ }
1463
+ if (source[cursor] === ":" || source[cursor] === ",")
1464
+ return true;
1465
+ index = cursor;
1466
+ }
1467
+ return false;
1468
+ }
1469
+ export const identifierStartPattern = /[$A-Z_a-z]/;
1470
+ export const identifierPartPattern = /[$\w]/;
1471
+ export function identifierAt(source, index) {
1472
+ let cursor = index;
1473
+ while (identifierPartPattern.test(source[cursor] ?? "")) {
1474
+ cursor++;
1475
+ }
1476
+ return source.slice(index, cursor);
1477
+ }
1478
+ export function noopUnitOfWorkBodyRange(source) {
1479
+ const match = /createNoopUnitOfWork\(\s*\(\)\s*=>\s*\(\s*\{/.exec(source);
1480
+ if (!match)
1481
+ return undefined;
1482
+ const openBrace = source.indexOf("{", match.index);
1483
+ const closeBrace = matchingBraceIndex(source, openBrace);
1484
+ if (closeBrace === -1)
1485
+ return undefined;
1486
+ return { openBrace, closeBrace };
1487
+ }
1488
+ export function drizzleTransactionPortsBodyRange(source) {
1489
+ const match = /createTransactionPorts\s*:\s*\([^)]*\)\s*=>\s*\(\s*\{/.exec(source);
1490
+ if (!match)
1491
+ return undefined;
1492
+ const openBrace = source.indexOf("{", match.index);
1493
+ const closeBrace = matchingBraceIndex(source, openBrace);
1494
+ if (closeBrace === -1)
1495
+ return undefined;
1496
+ return { openBrace, closeBrace };
1497
+ }
1498
+ export function createRepositoriesReturnBodyRange(source) {
1499
+ const functionBody = createRepositoriesFunctionBodyRange(source);
1500
+ if (!functionBody)
1501
+ return undefined;
1502
+ const functionSource = source.slice(functionBody.openBrace + 1, functionBody.closeBrace);
1503
+ const match = /return\s*\{\s*/.exec(functionSource);
1504
+ if (!match)
1505
+ return undefined;
1506
+ const openBrace = source.indexOf("{", functionBody.openBrace + 1 + match.index);
1507
+ if (openBrace === -1)
1508
+ return undefined;
1509
+ const closeBrace = matchingDelimiterIndex(source, openBrace, "{", "}");
1510
+ if (closeBrace === -1 || closeBrace > functionBody.closeBrace) {
1511
+ return undefined;
1512
+ }
1513
+ return { openBrace, closeBrace };
1514
+ }
1515
+ export function createRepositoriesFunctionBodyRange(source) {
1516
+ const match = /(?:export\s+)?function\s+createRepositories\b/.exec(source);
1517
+ if (!match)
1518
+ return undefined;
1519
+ const openParen = source.indexOf("(", match.index);
1520
+ if (openParen === -1)
1521
+ return undefined;
1522
+ const closeParen = matchingDelimiterIndex(source, openParen, "(", ")");
1523
+ if (closeParen === -1)
1524
+ return undefined;
1525
+ const bodyMatch = /(?:\s*:[^{]+)?\s*\{/.exec(source.slice(closeParen + 1));
1526
+ if (!bodyMatch)
1527
+ return undefined;
1528
+ const openBrace = closeParen + 1 + bodyMatch.index + bodyMatch[0].lastIndexOf("{");
1529
+ const closeBrace = matchingDelimiterIndex(source, openBrace, "{", "}");
1530
+ if (closeBrace === -1)
1531
+ return undefined;
1532
+ return { openBrace, closeBrace };
1533
+ }
1534
+ export function insertBeforeClosingBrace(source, openBrace, closeBrace, property) {
1535
+ const body = source.slice(openBrace + 1, closeBrace);
1536
+ const propertyIndent = detectPropertyIndent(body);
1537
+ const closingIndent = source.slice(0, closeBrace).match(/\n([\t ]*)$/)?.[1] ?? "";
1538
+ const trimmedBody = body.replace(/\s*$/, "");
1539
+ const nextBody = `${trimmedBody}\n${propertyIndent}${property}\n${closingIndent}`;
1540
+ return `${source.slice(0, openBrace + 1)}${nextBody}${source.slice(closeBrace)}`;
1541
+ }
1542
+ export function detectPropertyIndent(body) {
1543
+ return (body
1544
+ .split("\n")
1545
+ .find((line) => line.trim())
1546
+ ?.match(/^[\t ]*/)?.[0] ?? "\t");
1547
+ }
1548
+ export function matchingBraceIndex(source, openBrace) {
1549
+ return matchingDelimiterIndex(source, openBrace, "{", "}");
1550
+ }
1551
+ export function replaceTransactionPortsRequired(source, entry, message) {
1552
+ return insertTypeProperty(source, "AppTransactionPorts", entry.trim(), message);
1553
+ }
1554
+ export function aliasModule(filePath) {
1555
+ return `@/${modulePath(filePath)}`;
1556
+ }
1557
+ export function relativeModule(fromFile, toFile) {
1558
+ const fromDir = path.posix.dirname(normalizePath(fromFile));
1559
+ const relative = path.posix.relative(fromDir, normalizePath(toFile));
1560
+ const specifier = modulePath(relative);
1561
+ if (specifier.startsWith("../"))
1562
+ return specifier;
1563
+ if (specifier === "..")
1564
+ return specifier;
1565
+ return specifier.startsWith(".") ? specifier : `./${specifier}`;
1566
+ }
1567
+ export function modulePath(filePath) {
1568
+ return directoryPath(filePath)
1569
+ .replace(/\.(?:[cm]?[jt]sx?)$/, "")
1570
+ .replace(/\/index$/, "");
1571
+ }
1572
+ export function resourceNames(input) {
1573
+ const pluralKebab = normalizeResourceName(input);
1574
+ const singularKebab = singularize(pluralKebab);
1575
+ const names = {
1576
+ input,
1577
+ pluralKebab,
1578
+ singularKebab,
1579
+ pluralCamel: camelCase(pluralKebab),
1580
+ singularCamel: camelCase(singularKebab),
1581
+ singularPascal: pascalCase(singularKebab),
1582
+ pluralPascal: pascalCase(pluralKebab),
1583
+ };
1584
+ assertGeneratedIdentifiers(input, "Resource name", [
1585
+ names.pluralCamel,
1586
+ names.singularCamel,
1587
+ names.singularPascal,
1588
+ names.pluralPascal,
1589
+ ]);
1590
+ return names;
1591
+ }
1592
+ export function featureUiNames(input) {
1593
+ const names = resourceNames(input);
1594
+ const resolvedNames = {
1595
+ ...names,
1596
+ componentExportName: `${names.pluralPascal}Panel`,
1597
+ componentFileName: `${names.pluralKebab}-panel`,
1598
+ };
1599
+ assertGeneratedIdentifiers(input, "Feature UI name", [
1600
+ resolvedNames.componentExportName,
1601
+ ]);
1602
+ return resolvedNames;
1603
+ }
1604
+ export function featureResourceNames(names) {
1605
+ return resourceNames(names.feature.kebab);
1606
+ }
1607
+ export function identifierNames(input, label) {
1608
+ const kebab = normalizeName(input, label);
1609
+ return {
1610
+ input,
1611
+ kebab,
1612
+ camel: camelCase(kebab),
1613
+ pascal: pascalCase(kebab),
1614
+ };
1615
+ }
1616
+ export function portNames(input) {
1617
+ const names = identifierNames(input, "Port name");
1618
+ const resolvedNames = {
1619
+ ...names,
1620
+ interfaceName: `${names.pascal}Port`,
1621
+ inputName: `Execute${names.pascal}Input`,
1622
+ adapterFactoryName: `create${names.pascal}Adapter`,
1623
+ };
1624
+ assertGeneratedIdentifiers(input, "Port name", [
1625
+ resolvedNames.camel,
1626
+ resolvedNames.pascal,
1627
+ resolvedNames.interfaceName,
1628
+ resolvedNames.inputName,
1629
+ resolvedNames.adapterFactoryName,
1630
+ ]);
1631
+ return resolvedNames;
1632
+ }
1633
+ export function featureArtifactNames(input, label) {
1634
+ const parts = artifactNameParts(input, label);
1635
+ const feature = identifierNames(parts[0], `${label} feature`);
1636
+ const artifact = identifierNames(parts[1], `${label} name`);
1637
+ const featureSingularKebab = singularize(feature.kebab);
1638
+ const names = {
1639
+ input,
1640
+ feature,
1641
+ featureSingularKebab,
1642
+ featureSingularCamel: camelCase(featureSingularKebab),
1643
+ featureSingularPascal: pascalCase(featureSingularKebab),
1644
+ artifact,
1645
+ stableName: `${feature.kebab}.${artifact.kebab}`,
1646
+ };
1647
+ assertGeneratedIdentifiers(parts[0], `${label} feature`, [
1648
+ names.feature.camel,
1649
+ names.feature.pascal,
1650
+ names.featureSingularCamel,
1651
+ names.featureSingularPascal,
1652
+ ]);
1653
+ assertGeneratedIdentifiers(parts[1], `${label} name`, [
1654
+ names.artifact.camel,
1655
+ names.artifact.pascal,
1656
+ ]);
1657
+ return names;
1658
+ }
1659
+ export function seedNames(input) {
1660
+ const names = featureArtifactNames(input, "Seed");
1661
+ const seedExportName = `${names.artifact.camel}Seed`;
1662
+ assertGeneratedIdentifiers(input, "Seed name", [seedExportName]);
1663
+ return {
1664
+ ...names,
1665
+ seedName: names.stableName,
1666
+ seedExportName,
1667
+ };
1668
+ }
1669
+ export function artifactNameParts(input, label) {
1670
+ const parts = input
1671
+ .trim()
1672
+ .split("/")
1673
+ .map((part) => part.trim())
1674
+ .filter(Boolean);
1675
+ if (parts.length !== 2) {
1676
+ throw new Error(`${label} name must use feature/name format, for example posts/published.`);
1677
+ }
1678
+ return [parts[0], parts[1]];
1679
+ }
1680
+ export function portStubEntry(names) {
1681
+ return `\t${names.camel}: {\n\t\tasync execute() {\n\t\t\tthrow new Error("${names.interfaceName} is not implemented.");\n\t\t},\n\t} satisfies ${names.interfaceName},`;
1682
+ }
1683
+ export function adapterEntry(names) {
1684
+ return `\t${names.camel}: ${names.adapterFactoryName}(),`;
1685
+ }
1686
+ export function normalizeResourceName(input) {
1687
+ return normalizeName(input, "Resource name");
1688
+ }
1689
+ export function normalizeName(input, label) {
1690
+ const normalized = input
1691
+ .trim()
1692
+ .toLowerCase()
1693
+ .replaceAll(/[^a-z0-9]+/g, "-")
1694
+ .replaceAll(/^-+|-+$/g, "");
1695
+ if (!normalized) {
1696
+ throw new Error(`${label} is required.`);
1697
+ }
1698
+ return normalized;
1699
+ }
1700
+ export function assertGeneratedIdentifiers(input, label, identifiers) {
1701
+ for (const identifier of new Set(identifiers)) {
1702
+ if (isValidGeneratedIdentifier(identifier)) {
1703
+ continue;
1704
+ }
1705
+ throw new Error(`${label} "${input}" generates invalid JavaScript identifier "${identifier}". Use a name that starts with a letter and is not a reserved word.`);
1706
+ }
1707
+ }
1708
+ export function isValidGeneratedIdentifier(identifier) {
1709
+ return (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(identifier) &&
1710
+ !reservedJavaScriptIdentifiers.has(identifier));
1711
+ }
1712
+ export const reservedJavaScriptIdentifiers = new Set([
1713
+ "arguments",
1714
+ "await",
1715
+ "break",
1716
+ "case",
1717
+ "catch",
1718
+ "class",
1719
+ "const",
1720
+ "continue",
1721
+ "debugger",
1722
+ "default",
1723
+ "delete",
1724
+ "do",
1725
+ "else",
1726
+ "enum",
1727
+ "eval",
1728
+ "export",
1729
+ "extends",
1730
+ "false",
1731
+ "finally",
1732
+ "for",
1733
+ "function",
1734
+ "if",
1735
+ "implements",
1736
+ "import",
1737
+ "in",
1738
+ "instanceof",
1739
+ "interface",
1740
+ "let",
1741
+ "new",
1742
+ "null",
1743
+ "package",
1744
+ "private",
1745
+ "protected",
1746
+ "public",
1747
+ "return",
1748
+ "static",
1749
+ "super",
1750
+ "switch",
1751
+ "this",
1752
+ "throw",
1753
+ "true",
1754
+ "try",
1755
+ "typeof",
1756
+ "var",
1757
+ "void",
1758
+ "while",
1759
+ "with",
1760
+ "yield",
1761
+ ]);
1762
+ export function singularize(value) {
1763
+ if (value.endsWith("ies") && value.length > 3) {
1764
+ return `${value.slice(0, -3)}y`;
1765
+ }
1766
+ if (value.endsWith("ses") && value.length > 3) {
1767
+ return value.slice(0, -2);
1768
+ }
1769
+ if (value.endsWith("s") && value.length > 1) {
1770
+ return value.slice(0, -1);
1771
+ }
1772
+ return value;
1773
+ }
1774
+ export function camelCase(value) {
1775
+ return value.replaceAll(/-([a-z0-9])/g, (_, char) => char.toUpperCase());
1776
+ }
1777
+ export function pascalCase(value) {
1778
+ const camel = camelCase(value);
1779
+ return `${camel.charAt(0).toUpperCase()}${camel.slice(1)}`;
1780
+ }
1781
+ export function constantCase(value) {
1782
+ return value.replaceAll("-", "_").toUpperCase();
1783
+ }
1784
+ export function portFilePath(names, config) {
1785
+ return path.join(path.dirname(config.paths.ports), `${names.kebab}.ts`);
1786
+ }
1787
+ export function adapterFilePath(names, config) {
1788
+ return path.join(path.dirname(config.paths.infrastructurePorts), names.kebab, `${names.kebab}-adapter.ts`);
1789
+ }
1790
+ export function infrastructureDir(config) {
1791
+ return path.dirname(config.paths.infrastructurePorts);
1792
+ }
1793
+ export function drizzleSchemaIndexPath(config) {
1794
+ return path.join(infrastructureDir(config), "db", "schema", "index.ts");
1795
+ }
1796
+ export function drizzleRepositoriesPath(config) {
1797
+ return path.join(infrastructureDir(config), "db", "repositories.ts");
1798
+ }
1799
+ export function drizzleResourceRepositoryFilePath(names, config) {
1800
+ return path.join(infrastructureDir(config), names.pluralKebab, `drizzle-${names.singularKebab}-repository.ts`);
1801
+ }
1802
+ export function resourceSharedErrorsPath(config) {
1803
+ return path.join(config.paths.features, "shared", "errors.ts");
1804
+ }
1805
+ export function resourceContractNames(names, mode) {
1806
+ const contractNames = [
1807
+ `list${names.pluralPascal}`,
1808
+ `create${names.singularPascal}`,
1809
+ ];
1810
+ if (mode === "resource") {
1811
+ contractNames.push(`get${names.singularPascal}`, `update${names.singularPascal}`, `delete${names.singularPascal}`);
1812
+ }
1813
+ return contractNames;
1814
+ }
1815
+ export async function detectResourcePersistence(targetDir, config) {
1816
+ return (await fileExists(path.join(targetDir, drizzleRepositoriesPath(config))))
1817
+ ? "drizzle"
1818
+ : "memory";
1819
+ }
1820
+ export const databaseNames = ["sqlite", "postgres", "mysql"];
1821
+ /**
1822
+ * Detect the SQL dialect of a Drizzle app from its generated wiring.
1823
+ *
1824
+ * Precedence:
1825
+ * 1. The provider import in the Drizzle repositories file
1826
+ * (`@beignet/provider-db-drizzle/<dialect>`), resolved through the
1827
+ * configured infrastructure paths so custom layouts keep working.
1828
+ * 2. The provider registration tokens in `server/providers.ts`
1829
+ * (`drizzle<Dialect>Provider` / `createDrizzle<Dialect>Provider`).
1830
+ * 3. `"sqlite"`, the starter default.
1831
+ *
1832
+ * Self-healing by design: there is no config field to drift, the detector
1833
+ * reads the same files the app boots from.
1834
+ */
1835
+ export async function detectResourceDatabase(targetDir, config) {
1836
+ const repositories = await readOptionalFile(path.join(targetDir, drizzleRepositoriesPath(config)));
1837
+ const providerImport = repositories?.match(/from\s+["']@beignet\/provider-db-drizzle\/(sqlite|postgres|mysql)["']/);
1838
+ if (providerImport)
1839
+ return providerImport[1];
1840
+ const providers = await readOptionalFile(path.join(targetDir, providersFilePath(config)));
1841
+ if (providers !== undefined) {
1842
+ for (const database of databaseNames) {
1843
+ const dialectPascal = database.charAt(0).toUpperCase() + database.slice(1);
1844
+ const tokens = new RegExp(`\\b(?:drizzle${dialectPascal}Provider|createDrizzle${dialectPascal}Provider)\\b`);
1845
+ if (tokens.test(providers))
1846
+ return database;
1847
+ }
1848
+ }
1849
+ return "sqlite";
1850
+ }
1851
+ export function resourceFeatureDir(names, config) {
1852
+ return path.join(config.paths.features, names.pluralKebab);
1853
+ }
1854
+ export function featureUiDir(names, config) {
1855
+ return path.join(resourceFeatureDir(names, config), "components");
1856
+ }
1857
+ export function featureUiClientDir(names, config) {
1858
+ return path.join(resourceFeatureDir(names, config), "client");
1859
+ }
1860
+ export function featureUiClientQueriesFilePath(names, config) {
1861
+ return path.join(featureUiClientDir(names, config), "queries.ts");
1862
+ }
1863
+ export function featureUiComponentFilePath(names, config) {
1864
+ return path.join(featureUiDir(names, config), `${names.componentFileName}.tsx`);
1865
+ }
1866
+ export function featureUiIndexFilePath(names, config) {
1867
+ return path.join(featureUiDir(names, config), "index.ts");
1868
+ }
1869
+ export function usesFeatureOwnedContracts(config) {
1870
+ return (directoryPath(config.paths.contracts) ===
1871
+ directoryPath(config.paths.features));
1872
+ }
1873
+ export function usesFeatureOwnedUseCases(config) {
1874
+ return (directoryPath(config.paths.useCases) ===
1875
+ directoryPath(config.paths.features));
1876
+ }
1877
+ export function usesFeatureOwnedPolicies(config) {
1878
+ return (directoryPath(config.paths.policies) ===
1879
+ directoryPath(config.paths.features));
1880
+ }
1881
+ export function usesFeatureOwnedTests(config) {
1882
+ return (usesFeatureOwnedUseCases(config) &&
1883
+ directoryPath(config.paths.tests) === "tests");
1884
+ }
1885
+ export function resourceContractFilePath(names, config) {
1886
+ if (usesFeatureOwnedContracts(config)) {
1887
+ return path.join(resourceFeatureDir(names, config), "contracts.ts");
1888
+ }
1889
+ return path.join(config.paths.contracts, `${names.pluralKebab}.ts`);
1890
+ }
1891
+ export function useCaseFeatureDir(feature, config) {
1892
+ if (usesFeatureOwnedUseCases(config)) {
1893
+ return path.join(config.paths.features, feature, "use-cases");
1894
+ }
1895
+ return path.join(config.paths.useCases, feature);
1896
+ }
1897
+ export function resourcePortFilePath(names, config) {
1898
+ if (usesFeatureOwnedContracts(config)) {
1899
+ return path.join(resourceFeatureDir(names, config), "ports.ts");
1900
+ }
1901
+ return path.join(path.dirname(config.paths.ports), `${names.singularKebab}-repository.ts`);
1902
+ }
1903
+ export function featureArtifactDir(names, kind, config) {
1904
+ const featureDir = path.join(config.paths.features, names.feature.kebab);
1905
+ if (kind === "events") {
1906
+ return path.join(featureDir, "domain", "events");
1907
+ }
1908
+ if (kind === "factories") {
1909
+ return path.join(featureDir, "tests", "factories");
1910
+ }
1911
+ return path.join(featureDir, kind);
1912
+ }
1913
+ export function eventFilePath(names, config) {
1914
+ return path.join(featureArtifactDir(names, "events", config), `${names.artifact.kebab}.ts`);
1915
+ }
1916
+ export function jobFilePath(names, config) {
1917
+ return path.join(featureArtifactDir(names, "jobs", config), `${names.artifact.kebab}.ts`);
1918
+ }
1919
+ export function taskFilePath(names, config) {
1920
+ return path.join(featureArtifactDir(names, "tasks", config), `${names.artifact.kebab}.ts`);
1921
+ }
1922
+ export function factoryFilePath(names, config) {
1923
+ return path.join(featureArtifactDir(names, "factories", config), `${names.artifact.kebab}.ts`);
1924
+ }
1925
+ export function seedFilePath(names, config) {
1926
+ return path.join(featureArtifactDir(names, "seeds", config), `${names.artifact.kebab}.ts`);
1927
+ }
1928
+ export function notificationFilePath(names, config) {
1929
+ return path.join(featureArtifactDir(names, "notifications", config), `${names.artifact.kebab}.ts`);
1930
+ }
1931
+ export function listenerFilePath(names, config) {
1932
+ return path.join(featureArtifactDir(names, "listeners", config), `${names.artifact.kebab}.ts`);
1933
+ }
1934
+ export function scheduleFilePath(names, config) {
1935
+ return path.join(featureArtifactDir(names, "schedules", config), `${names.artifact.kebab}.ts`);
1936
+ }
1937
+ export function uploadFilePath(names, config) {
1938
+ return path.join(featureArtifactDir(names, "uploads", config), `${names.artifact.kebab}.ts`);
1939
+ }
1940
+ /**
1941
+ * The app-owned seed entrypoint lives beside the server runtime because it
1942
+ * boots the app through `getServer()`; `beignet lint` forbids that import
1943
+ * from `infra/`.
1944
+ */
1945
+ export function seedEntrypointPath(config) {
1946
+ return path.join(path.dirname(config.paths.server), "seed.ts");
1947
+ }
1948
+ export function useCaseFilePath(names, config) {
1949
+ return path.join(useCaseFeatureDir(names.feature.kebab, config), `${names.action.kebab}.ts`);
1950
+ }
1951
+ export function featureArtifactIndexFile(kind, names, config) {
1952
+ const artifactPath = kind === "event"
1953
+ ? eventFilePath(names, config)
1954
+ : kind === "job"
1955
+ ? jobFilePath(names, config)
1956
+ : kind === "task"
1957
+ ? taskFilePath(names, config)
1958
+ : kind === "factory"
1959
+ ? factoryFilePath(names, config)
1960
+ : kind === "seed"
1961
+ ? seedFilePath(names, config)
1962
+ : kind === "notification"
1963
+ ? notificationFilePath(names, config)
1964
+ : kind === "listener"
1965
+ ? listenerFilePath(names, config)
1966
+ : kind === "schedule"
1967
+ ? scheduleFilePath(names, config)
1968
+ : uploadFilePath(names, config);
1969
+ const registrySuffix = kind === "event"
1970
+ ? "Events"
1971
+ : kind === "job"
1972
+ ? "Jobs"
1973
+ : kind === "task"
1974
+ ? "Tasks"
1975
+ : kind === "factory"
1976
+ ? "Factories"
1977
+ : kind === "seed"
1978
+ ? "Seeds"
1979
+ : kind === "notification"
1980
+ ? "Notifications"
1981
+ : kind === "listener"
1982
+ ? "Listeners"
1983
+ : kind === "schedule"
1984
+ ? "Schedules"
1985
+ : "Uploads";
1986
+ const importName = kind === "event"
1987
+ ? names.eventExportName
1988
+ : kind === "job"
1989
+ ? names.jobExportName
1990
+ : kind === "task"
1991
+ ? names.taskExportName
1992
+ : kind === "factory"
1993
+ ? names.factoryExportName
1994
+ : kind === "seed"
1995
+ ? names.seedExportName
1996
+ : kind === "notification"
1997
+ ? names.notificationExportName
1998
+ : kind === "listener"
1999
+ ? names.listenerExportName
2000
+ : kind === "schedule"
2001
+ ? names.scheduleExportName
2002
+ : names.uploadExportName;
2003
+ return {
2004
+ path: path.join(path.dirname(artifactPath), "index.ts"),
2005
+ kind,
2006
+ importName,
2007
+ artifactPath,
2008
+ registryName: `${names.featureSingularCamel}${registrySuffix}`,
2009
+ registryEntryName: names.artifact.camel,
2010
+ };
2011
+ }
2012
+ export async function updateFeatureArtifactIndex(targetDir, file, options) {
2013
+ const destination = path.join(targetDir, file.path);
2014
+ const existing = await readOptionalFile(destination);
2015
+ const specifier = relativeModule(file.path, file.artifactPath);
2016
+ const importLine = `import { ${file.importName} } from "${specifier}";`;
2017
+ const defineUploadsImport = `import { defineUploads } from "@beignet/core/uploads";`;
2018
+ const exportLine = `export { ${file.importName} } from "${specifier}";`;
2019
+ if (existing === undefined) {
2020
+ const content = file.kind === "upload"
2021
+ ? `${defineUploadsImport}
2022
+ ${importLine}
2023
+
2024
+ ${exportLine}
2025
+
2026
+ export const ${file.registryName} = defineUploads({
2027
+ \t${file.registryEntryName}: ${file.importName},
2028
+ });
2029
+ `
2030
+ : `${importLine}
2031
+
2032
+ ${exportLine}
2033
+
2034
+ export const ${file.registryName} = [${file.importName}] as const;
2035
+ `;
2036
+ if (!options.dryRun) {
2037
+ await mkdir(path.dirname(destination), { recursive: true });
2038
+ await writeFile(destination, content);
2039
+ }
2040
+ return "created";
2041
+ }
2042
+ let next = existing;
2043
+ if (!next.includes(importLine)) {
2044
+ next = insertAfterImports(next, importLine);
2045
+ }
2046
+ if (!next.includes(exportLine)) {
2047
+ const registryExport = new RegExp(`export\\s+const\\s+${file.registryName}\\b`).exec(next);
2048
+ next = registryExport
2049
+ ? `${next.slice(0, registryExport.index).trimEnd()}\n${exportLine}\n\n${next.slice(registryExport.index)}`
2050
+ : `${next.trimEnd()}\n${exportLine}\n`;
2051
+ }
2052
+ if (file.kind === "upload") {
2053
+ const registry = defineUploadsInitializerInfo(next, file.registryName);
2054
+ const arrayRegistry = arrayInitializerInfo(next, file.registryName);
2055
+ if (registry) {
2056
+ if (!next.includes(defineUploadsImport)) {
2057
+ next = insertAfterImports(next, defineUploadsImport);
2058
+ }
2059
+ if (!registry.text.includes(`${file.registryEntryName}:`)) {
2060
+ next = `${next.slice(0, registry.end)}\t${file.registryEntryName}: ${file.importName},\n${next.slice(registry.end)}`;
2061
+ }
2062
+ }
2063
+ else if (arrayRegistry) {
2064
+ if (!identifiersFromArrayExpression(arrayRegistry.text).has(file.importName)) {
2065
+ const nextArray = appendToArrayExpression(arrayRegistry.text, [
2066
+ file.importName,
2067
+ ]);
2068
+ next = `${next.slice(0, arrayRegistry.start)}${nextArray}${next.slice(arrayRegistry.end)}`;
2069
+ }
2070
+ }
2071
+ else {
2072
+ if (!next.includes(defineUploadsImport)) {
2073
+ next = insertAfterImports(next, defineUploadsImport);
2074
+ }
2075
+ next = `${next.trimEnd()}\n\nexport const ${file.registryName} = defineUploads({\n\t${file.registryEntryName}: ${file.importName},\n});\n`;
2076
+ }
2077
+ }
2078
+ else {
2079
+ const registry = arrayInitializerInfo(next, file.registryName);
2080
+ if (!registry) {
2081
+ next = `${next.trimEnd()}\n\nexport const ${file.registryName} = [${file.importName}] as const;\n`;
2082
+ }
2083
+ else if (!identifiersFromArrayExpression(registry.text).has(file.importName)) {
2084
+ const nextArray = appendToArrayExpression(registry.text, [
2085
+ file.importName,
2086
+ ]);
2087
+ next = `${next.slice(0, registry.start)}${nextArray}${next.slice(registry.end)}`;
2088
+ }
2089
+ }
2090
+ if (next === existing)
2091
+ return "skipped";
2092
+ if (!options.dryRun)
2093
+ await writeFile(destination, next);
2094
+ return "updated";
2095
+ }
2096
+ export async function updateSeedEntrypoint(targetDir, names, config, options) {
2097
+ const file = seedEntrypointPath(config);
2098
+ const destination = path.join(targetDir, file);
2099
+ const existing = await readOptionalFile(destination);
2100
+ const registryName = `${names.featureSingularCamel}Seeds`;
2101
+ const importLine = `import { ${registryName} } from "${aliasModule(path.join(featureArtifactDir(names, "seeds", config), "index.ts"))}";`;
2102
+ const registryEntry = `...${registryName}`;
2103
+ if (existing === undefined) {
2104
+ const content = seedEntrypointFile(names, config);
2105
+ if (!options.dryRun) {
2106
+ await mkdir(path.dirname(destination), { recursive: true });
2107
+ await writeFile(destination, content);
2108
+ }
2109
+ return "created";
2110
+ }
2111
+ let next = existing;
2112
+ if (!next.includes(importLine)) {
2113
+ next = insertAfterImports(next, importLine);
2114
+ }
2115
+ const registry = arrayInitializerInfo(next, "seeds");
2116
+ if (!registry) {
2117
+ next = `${next.trimEnd()}\n\nconst seeds = [${registryEntry}] as const;\n`;
2118
+ }
2119
+ else if (!identifiersFromArrayExpression(registry.text).has(registryName)) {
2120
+ const nextArray = appendToArrayExpression(registry.text, [registryEntry]);
2121
+ next = `${next.slice(0, registry.start)}${nextArray}${next.slice(registry.end)}`;
2122
+ }
2123
+ if (next === existing)
2124
+ return "skipped";
2125
+ if (!options.dryRun)
2126
+ await writeFile(destination, next);
2127
+ return "updated";
2128
+ }
2129
+ export function defineUploadsInitializerInfo(source, constName) {
2130
+ const match = new RegExp(`\\bconst\\s+${constName}\\s*=\\s*defineUploads\\s*\\(`).exec(source);
2131
+ if (!match)
2132
+ return undefined;
2133
+ const openBrace = source.indexOf("{", match.index);
2134
+ if (openBrace === -1)
2135
+ return undefined;
2136
+ const closeBrace = matchingDelimiterIndex(source, openBrace, "{", "}");
2137
+ if (closeBrace === -1)
2138
+ return undefined;
2139
+ return {
2140
+ text: source.slice(openBrace, closeBrace + 1),
2141
+ start: openBrace,
2142
+ end: closeBrace,
2143
+ };
2144
+ }
2145
+ export function seedEntrypointFile(names, config) {
2146
+ const registryName = `${names.featureSingularCamel}Seeds`;
2147
+ return `import { createServiceActor } from "@beignet/core/ports";
2148
+ import { runSeeds } from "@beignet/core/testing";
2149
+ import { ${registryName} } from "${aliasModule(path.join(featureArtifactDir(names, "seeds", config), "index.ts"))}";
2150
+ import { getServer } from "${relativeModule(seedEntrypointPath(config), config.paths.server)}";
2151
+
2152
+ const seeds = [...${registryName}] as const;
2153
+ const server = await getServer();
2154
+
2155
+ const ctx = await server.createServiceContext({
2156
+ \tactor: createServiceActor("beignet-seed"),
2157
+ });
2158
+
2159
+ try {
2160
+ \tawait runSeeds({ ctx, seeds });
2161
+ } finally {
2162
+ \tawait server.stop();
2163
+ }
2164
+ `;
2165
+ }
2166
+ //# sourceMappingURL=shared.js.map