@holo-js/cli 0.1.2 → 0.1.4

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 (56) hide show
  1. package/dist/bin/holo.mjs +533 -4616
  2. package/dist/broadcast-CSSARTSA.mjs +84 -0
  3. package/dist/broadcast-YSIJCL3R.mjs +85 -0
  4. package/dist/cache-4G6QGIZO.mjs +66 -0
  5. package/dist/cache-OWQY4E7W.mjs +67 -0
  6. package/dist/cache-migrations-NATT5WPQ.mjs +154 -0
  7. package/dist/cache-migrations-RVEA6CEU.mjs +155 -0
  8. package/dist/chunk-3OTCSFDG.mjs +849 -0
  9. package/dist/chunk-66FHW725.mjs +465 -0
  10. package/dist/chunk-BWW5TDFI.mjs +4 -0
  11. package/dist/chunk-CUL4RJTG.mjs +22 -0
  12. package/dist/chunk-D4GG556Y.mjs +23 -0
  13. package/dist/chunk-D7O4SU6N.mjs +2 -0
  14. package/dist/chunk-DMH2B4UQ.mjs +343 -0
  15. package/dist/chunk-ET7UXHHQ.mjs +166 -0
  16. package/dist/chunk-EUIVXVJL.mjs +25 -0
  17. package/dist/chunk-G5ADO27Q.mjs +463 -0
  18. package/dist/chunk-GSQ3HTRO.mjs +165 -0
  19. package/dist/chunk-H7TJ4FB3.mjs +848 -0
  20. package/dist/chunk-ICJR7TS4.mjs +66 -0
  21. package/dist/chunk-JX2ZH6XY.mjs +270 -0
  22. package/dist/chunk-M7J3YTHR.mjs +26 -0
  23. package/dist/chunk-MZXN2YMI.mjs +3236 -0
  24. package/dist/chunk-Q5F6C2D4.mjs +65 -0
  25. package/dist/chunk-QFUSWV3J.mjs +3237 -0
  26. package/dist/chunk-QYLSMF7V.mjs +539 -0
  27. package/dist/chunk-S7P7EBM3.mjs +787 -0
  28. package/dist/chunk-SRWJU3A5.mjs +11 -0
  29. package/dist/chunk-URK7C3VQ.mjs +538 -0
  30. package/dist/chunk-VT5IDQG6.mjs +788 -0
  31. package/dist/chunk-XUYKPU5Q.mjs +272 -0
  32. package/dist/chunk-ZLRO7HXY.mjs +342 -0
  33. package/dist/chunk-ZXDU7RHU.mjs +9 -0
  34. package/dist/config-DMWBMMGD.mjs +26 -0
  35. package/dist/config-LS5USBRB.mjs +25 -0
  36. package/dist/dev-KQFT7RHR.mjs +43 -0
  37. package/dist/dev-LZ3O2E3U.mjs +42 -0
  38. package/dist/discovery-GBLAUTXS.mjs +28 -0
  39. package/dist/discovery-R733D2PO.mjs +29 -0
  40. package/dist/generators-DSN4GWJI.mjs +425 -0
  41. package/dist/generators-WX45BI4U.mjs +426 -0
  42. package/dist/index.d.ts +1 -0
  43. package/dist/index.mjs +536 -4618
  44. package/dist/queue-6OG7VJ34.mjs +626 -0
  45. package/dist/queue-FV35LLPR.mjs +625 -0
  46. package/dist/queue-migrations-NK2EYX3J.mjs +163 -0
  47. package/dist/queue-migrations-SSIYKK5S.mjs +162 -0
  48. package/dist/runtime-4BV3JODY.mjs +56 -0
  49. package/dist/runtime-ANBO7VQM.mjs +33 -0
  50. package/dist/runtime-EFZ5H5IL.mjs +55 -0
  51. package/dist/runtime-OOSJ5JBY.mjs +32 -0
  52. package/dist/scaffold-7OTDH4UR.mjs +121 -0
  53. package/dist/scaffold-DRKBGS2K.mjs +120 -0
  54. package/dist/security-ATKDC26E.mjs +68 -0
  55. package/dist/security-R7VH6W5H.mjs +69 -0
  56. package/package.json +12 -11
@@ -0,0 +1,3237 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ loadProjectConfig,
4
+ resolveGeneratedSchemaPath
5
+ } from "./chunk-ET7UXHHQ.mjs";
6
+ import {
7
+ loadGeneratedProjectRegistry,
8
+ relativeImportPath
9
+ } from "./chunk-3OTCSFDG.mjs";
10
+ import {
11
+ AUTH_CONFIG_FILE_NAMES,
12
+ AUTH_SOCIAL_PROVIDER_PACKAGE_NAMES,
13
+ BROADCAST_CONFIG_FILE_NAMES,
14
+ CACHE_CONFIG_FILE_NAMES,
15
+ DB_DRIVER_PACKAGE_NAMES,
16
+ MAIL_CONFIG_FILE_NAMES,
17
+ NOTIFICATIONS_CONFIG_FILE_NAMES,
18
+ QUEUE_CONFIG_FILE_NAMES,
19
+ REDIS_CONFIG_FILE_NAMES,
20
+ SECURITY_CONFIG_FILE_NAMES,
21
+ SESSION_CONFIG_FILE_NAMES,
22
+ SUPPORTED_AUTH_SOCIAL_PROVIDERS,
23
+ isSupportedCacheInstallerDriver,
24
+ isSupportedQueueInstallerDriver,
25
+ normalizeScaffoldOptionalPackages,
26
+ pathExists,
27
+ readTextFile,
28
+ resolveFirstExistingPath,
29
+ sanitizePackageName,
30
+ writeTextFile
31
+ } from "./chunk-66FHW725.mjs";
32
+
33
+ // src/project/scaffold.ts
34
+ import { appendFile, mkdir, readdir, writeFile } from "fs/promises";
35
+ import { extname, resolve } from "path";
36
+ import {
37
+ loadConfigDirectory,
38
+ holoStorageDefaults
39
+ } from "@holo-js/config";
40
+ import {
41
+ normalizeHoloProjectConfig,
42
+ renderGeneratedSchemaPlaceholder,
43
+ createMigrationFileName
44
+ } from "@holo-js/db";
45
+
46
+ // package.json
47
+ var package_default = {
48
+ name: "@holo-js/cli",
49
+ version: "0.1.3",
50
+ description: "Holo-JS Framework - project creation, discovery, and operational CLI",
51
+ type: "module",
52
+ license: "MIT",
53
+ bin: {
54
+ holo: "./dist/bin/holo.mjs",
55
+ "holo-js": "./dist/bin/holo.mjs"
56
+ },
57
+ exports: {
58
+ ".": {
59
+ types: "./dist/index.d.ts",
60
+ import: "./dist/index.mjs"
61
+ }
62
+ },
63
+ main: "./dist/index.mjs",
64
+ types: "./dist/index.d.ts",
65
+ files: [
66
+ "dist"
67
+ ],
68
+ scripts: {
69
+ build: "tsup",
70
+ stub: "tsup --watch",
71
+ typecheck: "tsc -p tsconfig.json --noEmit",
72
+ test: "vitest --run",
73
+ "test:integration": "HOLO_CLI_INCLUDE_INTEGRATION=1 vitest --run tests/cli.test.ts"
74
+ },
75
+ dependencies: {
76
+ "@holo-js/config": "^0.1.3",
77
+ "@holo-js/core": "^0.1.3",
78
+ "@holo-js/db": "^0.1.3",
79
+ esbuild: "^0.27.4"
80
+ },
81
+ devDependencies: {
82
+ "@holo-js/events": "^0.1.3",
83
+ "@holo-js/queue": "^0.1.3",
84
+ "@holo-js/queue-db": "^0.1.3",
85
+ "@types/node": "^22.10.2",
86
+ tsup: "^8.3.5",
87
+ typescript: "^5.7.2",
88
+ vitest: "^2.1.8"
89
+ },
90
+ engines: {
91
+ node: ">=20.11.0"
92
+ }
93
+ };
94
+
95
+ // src/metadata.ts
96
+ var HOLO_PACKAGE_VERSION = package_default.version;
97
+ var ESBUILD_PACKAGE_VERSION = "^0.27.4";
98
+ var HOLO_PACKAGE_RANGE = `^${HOLO_PACKAGE_VERSION}`;
99
+ var SCAFFOLD_PACKAGE_MANAGER_VERSIONS = Object.freeze({
100
+ bun: "bun@1.3.9",
101
+ npm: "npm@latest",
102
+ pnpm: "pnpm@latest",
103
+ yarn: "yarn@stable"
104
+ });
105
+ var SCAFFOLD_FRAMEWORK_VERSIONS = Object.freeze({
106
+ nuxt: "^4.0.0",
107
+ next: "^16.0.0",
108
+ sveltekit: "^2.0.0"
109
+ });
110
+ var SCAFFOLD_FRAMEWORK_ADAPTER_VERSIONS = Object.freeze({
111
+ nuxt: HOLO_PACKAGE_RANGE,
112
+ next: HOLO_PACKAGE_RANGE,
113
+ sveltekit: HOLO_PACKAGE_RANGE
114
+ });
115
+ var SCAFFOLD_FRAMEWORK_RUNTIME_VERSIONS = Object.freeze({
116
+ nuxt: {
117
+ "@holo-js/storage": HOLO_PACKAGE_RANGE
118
+ },
119
+ next: {
120
+ "@holo-js/storage": HOLO_PACKAGE_RANGE
121
+ },
122
+ sveltekit: {
123
+ "@holo-js/storage": HOLO_PACKAGE_RANGE
124
+ }
125
+ });
126
+
127
+ // src/project/scaffold.ts
128
+ var IOREDIS_PACKAGE_VERSION = "^5.4.2";
129
+ var AUTH_MIGRATION_SLUGS = [
130
+ "create_users",
131
+ "create_sessions",
132
+ "create_auth_identities",
133
+ "create_personal_access_tokens",
134
+ "create_password_reset_tokens",
135
+ "create_email_verification_tokens"
136
+ ];
137
+ function renderStorageConfig() {
138
+ return [
139
+ "import { defineStorageConfig, env } from '@holo-js/config'",
140
+ "",
141
+ "export default defineStorageConfig({",
142
+ ` defaultDisk: env('STORAGE_DEFAULT_DISK', '${holoStorageDefaults.defaultDisk}'),`,
143
+ ` routePrefix: env('STORAGE_ROUTE_PREFIX', '${holoStorageDefaults.routePrefix}'),`,
144
+ " disks: {",
145
+ " local: {",
146
+ " driver: 'local',",
147
+ " root: './storage/app',",
148
+ " },",
149
+ " public: {",
150
+ " driver: 'public',",
151
+ " root: './storage/app/public',",
152
+ " visibility: 'public',",
153
+ " },",
154
+ " },",
155
+ "})",
156
+ ""
157
+ ].join("\n");
158
+ }
159
+ function renderMediaConfig() {
160
+ return [
161
+ "import { defineMediaConfig } from '@holo-js/config'",
162
+ "",
163
+ "export default defineMediaConfig({})",
164
+ ""
165
+ ].join("\n");
166
+ }
167
+ function renderQueueConfig(options = {}) {
168
+ const driver = options.driver ?? "sync";
169
+ const defaultDatabaseConnection = options.defaultDatabaseConnection?.trim() || "default";
170
+ if (driver === "redis") {
171
+ return [
172
+ "import { defineQueueConfig, env } from '@holo-js/config'",
173
+ "",
174
+ "export default defineQueueConfig({",
175
+ " default: 'redis',",
176
+ " failed: false,",
177
+ " connections: {",
178
+ " redis: {",
179
+ " driver: 'redis',",
180
+ " connection: 'default',",
181
+ " queue: 'default',",
182
+ " retryAfter: 90,",
183
+ " blockFor: 5,",
184
+ " },",
185
+ " },",
186
+ "})",
187
+ ""
188
+ ].join("\n");
189
+ }
190
+ if (driver === "database") {
191
+ return [
192
+ "import { defineQueueConfig } from '@holo-js/config'",
193
+ "",
194
+ "export default defineQueueConfig({",
195
+ " default: 'database',",
196
+ " failed: {",
197
+ " driver: 'database',",
198
+ ` connection: '${defaultDatabaseConnection}',`,
199
+ " table: 'failed_jobs',",
200
+ " },",
201
+ " connections: {",
202
+ " database: {",
203
+ " driver: 'database',",
204
+ ` connection: '${defaultDatabaseConnection}',`,
205
+ " table: 'jobs',",
206
+ " queue: 'default',",
207
+ " retryAfter: 90,",
208
+ " sleep: 1,",
209
+ " },",
210
+ " },",
211
+ "})",
212
+ ""
213
+ ].join("\n");
214
+ }
215
+ return [
216
+ "import { defineQueueConfig } from '@holo-js/config'",
217
+ "",
218
+ "export default defineQueueConfig({",
219
+ " default: 'sync',",
220
+ " failed: false,",
221
+ " connections: {",
222
+ " sync: {",
223
+ " driver: 'sync',",
224
+ " queue: 'default',",
225
+ " },",
226
+ " },",
227
+ "})",
228
+ ""
229
+ ].join("\n");
230
+ }
231
+ function renderCacheConfig(driver = "file", defaultDatabaseConnection = "default", defaultRedisConnection = "default") {
232
+ const lines = [
233
+ "import { defineCacheConfig, env } from '@holo-js/config'",
234
+ "",
235
+ "export default defineCacheConfig({",
236
+ ` default: '${driver}',`,
237
+ " prefix: env('CACHE_PREFIX', ''),",
238
+ " drivers: {",
239
+ " file: {",
240
+ " driver: 'file',",
241
+ " path: './storage/framework/cache/data',",
242
+ " },",
243
+ " memory: {",
244
+ " driver: 'memory',",
245
+ " maxEntries: 1000,",
246
+ " },"
247
+ ];
248
+ if (driver === "redis") {
249
+ lines.push(
250
+ " redis: {",
251
+ " driver: 'redis',",
252
+ ` connection: '${defaultRedisConnection}',`,
253
+ " prefix: 'cache:',",
254
+ " },"
255
+ );
256
+ }
257
+ if (driver === "database") {
258
+ lines.push(
259
+ " database: {",
260
+ " driver: 'database',",
261
+ ` connection: '${defaultDatabaseConnection}',`,
262
+ " table: 'cache',",
263
+ " lockTable: 'cache_locks',",
264
+ " },"
265
+ );
266
+ }
267
+ lines.push(
268
+ " },",
269
+ "})",
270
+ ""
271
+ );
272
+ return lines.join("\n");
273
+ }
274
+ function renderRedisConfig() {
275
+ return [
276
+ "import { defineRedisConfig, env } from '@holo-js/config'",
277
+ "",
278
+ "export default defineRedisConfig({",
279
+ " default: 'default',",
280
+ " connections: {",
281
+ " default: {",
282
+ " url: env('REDIS_URL') || undefined,",
283
+ " host: env('REDIS_HOST', '127.0.0.1'),",
284
+ " port: env('REDIS_PORT', 6379),",
285
+ " username: env('REDIS_USERNAME'),",
286
+ " password: env('REDIS_PASSWORD'),",
287
+ " db: env('REDIS_DB', 0),",
288
+ " },",
289
+ " },",
290
+ "})",
291
+ ""
292
+ ].join("\n");
293
+ }
294
+ async function ensureRedisConfigFile(projectRoot) {
295
+ const redisConfigPath = await resolveFirstExistingPath(projectRoot, REDIS_CONFIG_FILE_NAMES) ?? resolve(projectRoot, "config/redis.ts");
296
+ const redisConfigExists = await pathExists(redisConfigPath);
297
+ if (!redisConfigExists) {
298
+ await writeTextFile(redisConfigPath, renderRedisConfig());
299
+ }
300
+ return !redisConfigExists;
301
+ }
302
+ function renderNotificationsConfig() {
303
+ return [
304
+ "import { defineNotificationsConfig } from '@holo-js/config'",
305
+ "",
306
+ "export default defineNotificationsConfig({",
307
+ " table: 'notifications',",
308
+ " queue: {",
309
+ " afterCommit: false,",
310
+ " },",
311
+ "})",
312
+ ""
313
+ ].join("\n");
314
+ }
315
+ function renderMailConfig() {
316
+ return [
317
+ "import { defineMailConfig, env } from '@holo-js/config'",
318
+ "",
319
+ "export default defineMailConfig({",
320
+ " default: env('MAIL_MAILER', 'preview'),",
321
+ " from: {",
322
+ " email: env('MAIL_FROM_ADDRESS', 'hello@app.test'),",
323
+ " name: env('MAIL_FROM_NAME', 'Holo App'),",
324
+ " },",
325
+ " preview: {",
326
+ " allowedEnvironments: ['development'],",
327
+ " },",
328
+ " mailers: {",
329
+ " preview: {",
330
+ " driver: 'preview',",
331
+ " },",
332
+ " log: {",
333
+ " driver: 'log',",
334
+ " },",
335
+ " fake: {",
336
+ " driver: 'fake',",
337
+ " },",
338
+ " smtp: {",
339
+ " driver: 'smtp',",
340
+ " host: env('MAIL_HOST', '127.0.0.1'),",
341
+ " port: env('MAIL_PORT', 1025),",
342
+ " secure: env<boolean>('MAIL_SECURE', false),",
343
+ " },",
344
+ " },",
345
+ "})",
346
+ ""
347
+ ].join("\n");
348
+ }
349
+ function renderSecurityConfig() {
350
+ return [
351
+ `import { defineSecurityConfig, limit } from '@holo-js/security'`,
352
+ "",
353
+ "export default defineSecurityConfig({",
354
+ " csrf: {",
355
+ " enabled: true,",
356
+ " field: '_token',",
357
+ " header: 'X-CSRF-TOKEN',",
358
+ " cookie: 'XSRF-TOKEN',",
359
+ " except: [],",
360
+ " },",
361
+ " rateLimit: {",
362
+ " driver: 'file',",
363
+ " file: {",
364
+ " path: './storage/framework/rate-limits',",
365
+ " },",
366
+ " redis: {",
367
+ " connection: 'default',",
368
+ " prefix: 'holo:rate-limit:',",
369
+ " },",
370
+ " limiters: {",
371
+ " login: limit.perMinute(5).define(),",
372
+ " register: limit.perHour(10).define(),",
373
+ " },",
374
+ " },",
375
+ "})",
376
+ ""
377
+ ].join("\n");
378
+ }
379
+ async function ensureRateLimitStorageIgnore(projectRoot) {
380
+ const rateLimitRoot = resolve(projectRoot, "storage/framework/rate-limits");
381
+ const ignorePath = resolve(rateLimitRoot, ".gitignore");
382
+ await mkdir(rateLimitRoot, { recursive: true });
383
+ if (!await pathExists(ignorePath)) {
384
+ await writeTextFile(ignorePath, "*\n!.gitignore\n");
385
+ return;
386
+ }
387
+ const currentContents = await readTextFile(ignorePath) ?? "";
388
+ const existingLines = new Set(currentContents.split(/\r?\n/));
389
+ const missingLines = [
390
+ "*",
391
+ "!.gitignore"
392
+ ].filter((line) => !existingLines.has(line));
393
+ if (missingLines.length === 0) {
394
+ return;
395
+ }
396
+ await appendFile(
397
+ ignorePath,
398
+ `${currentContents.length > 0 && !currentContents.endsWith("\n") ? "\n" : ""}${missingLines.join("\n")}
399
+ `,
400
+ "utf8"
401
+ );
402
+ }
403
+ function renderBroadcastConfig(moduleFormat, includeAuthEndpoint, useTypeScriptSyntax) {
404
+ const renderBroadcastScheme = () => {
405
+ return useTypeScriptSyntax ? "env<'http' | 'https'>('BROADCAST_SCHEME', 'http')" : "env('BROADCAST_SCHEME', 'http')";
406
+ };
407
+ if (moduleFormat === "cjs") {
408
+ return [
409
+ "const { defineBroadcastConfig, env } = require('@holo-js/config')",
410
+ "",
411
+ "module.exports = defineBroadcastConfig({",
412
+ " default: env('BROADCAST_CONNECTION', 'holo'),",
413
+ " connections: {",
414
+ " holo: {",
415
+ " driver: 'holo',",
416
+ " appId: env('BROADCAST_APP_ID', 'app-id'),",
417
+ " key: env('BROADCAST_APP_KEY', 'app-key'),",
418
+ " secret: env('BROADCAST_APP_SECRET', 'app-secret'),",
419
+ " options: {",
420
+ " host: env('BROADCAST_HOST', '127.0.0.1'),",
421
+ " port: env('BROADCAST_PORT', 8080),",
422
+ ` scheme: ${renderBroadcastScheme()},`,
423
+ " useTLS: env('BROADCAST_SCHEME', 'http') === 'https',",
424
+ " },",
425
+ /* v8 ignore next 6 -- authEndpoint injection is tested through the install auth flow */
426
+ ...includeAuthEndpoint ? [
427
+ " clientOptions: {",
428
+ " authEndpoint: `${env('APP_URL', 'http://localhost:3000')}/broadcasting/auth`,",
429
+ " },"
430
+ ] : [],
431
+ " },",
432
+ " log: {",
433
+ " driver: 'log',",
434
+ " },",
435
+ " null: {",
436
+ " driver: 'null',",
437
+ " },",
438
+ " },",
439
+ "})",
440
+ ""
441
+ ].join("\n");
442
+ }
443
+ return [
444
+ "import { defineBroadcastConfig, env } from '@holo-js/config'",
445
+ "",
446
+ "export default defineBroadcastConfig({",
447
+ " default: env('BROADCAST_CONNECTION', 'holo'),",
448
+ " connections: {",
449
+ " holo: {",
450
+ " driver: 'holo',",
451
+ " appId: env('BROADCAST_APP_ID', 'app-id'),",
452
+ " key: env('BROADCAST_APP_KEY', 'app-key'),",
453
+ " secret: env('BROADCAST_APP_SECRET', 'app-secret'),",
454
+ " options: {",
455
+ " host: env('BROADCAST_HOST', '127.0.0.1'),",
456
+ " port: env('BROADCAST_PORT', 8080),",
457
+ ` scheme: ${renderBroadcastScheme()},`,
458
+ " useTLS: env('BROADCAST_SCHEME', 'http') === 'https',",
459
+ " },",
460
+ ...includeAuthEndpoint ? [
461
+ " clientOptions: {",
462
+ " authEndpoint: `${env('APP_URL', 'http://localhost:3000')}/broadcasting/auth`,",
463
+ " },"
464
+ ] : [],
465
+ " },",
466
+ " log: {",
467
+ " driver: 'log',",
468
+ " },",
469
+ " null: {",
470
+ " driver: 'null',",
471
+ " },",
472
+ " },",
473
+ "})",
474
+ ""
475
+ ].join("\n");
476
+ }
477
+ function stripBroadcastAuthEndpointBlock(value) {
478
+ return value.replace(
479
+ /(^|\n)\s*clientOptions:\s*\{\n\s*authEndpoint:\s*.*,\n\s*\},/m,
480
+ ""
481
+ );
482
+ }
483
+ function injectBroadcastAuthEndpoint(value) {
484
+ if (value.includes("authEndpoint:")) {
485
+ return value;
486
+ }
487
+ const nextValue = value.replace(
488
+ /(holo:\s*\{[\s\S]*?options:\s*\{[\s\S]*?\n)([ \t]*)\},/m,
489
+ (_match, prefix, indent) => {
490
+ return [
491
+ `${prefix}${indent}},`,
492
+ `${indent}clientOptions: {`,
493
+ `${indent} authEndpoint: \`\${env('APP_URL', 'http://localhost:3000')}/broadcasting/auth\`,`,
494
+ `${indent}},`
495
+ ].join("\n");
496
+ }
497
+ );
498
+ return nextValue === value ? void 0 : nextValue;
499
+ }
500
+ function canSafelyRewriteBroadcastConfig(currentContents, moduleFormat, useTypeScriptSyntax) {
501
+ return stripBroadcastAuthEndpointBlock(currentContents) === stripBroadcastAuthEndpointBlock(
502
+ renderBroadcastConfig(moduleFormat, false, useTypeScriptSyntax)
503
+ );
504
+ }
505
+ function resolveBroadcastConfigTargetPath(projectRoot, manifestPath, moduleFormat) {
506
+ const extension = extname(manifestPath);
507
+ const targetExtension = extension === ".cjs" || extension === ".cts" || extension === ".mjs" || extension === ".mts" ? extension : moduleFormat === "cjs" ? ".cjs" : extension === ".ts" || extension === ".js" ? extension : ".ts";
508
+ return resolve(projectRoot, `config/broadcast${targetExtension}`);
509
+ }
510
+ function renderBroadcastEnvFiles() {
511
+ const env = [
512
+ "BROADCAST_CONNECTION=holo"
513
+ ];
514
+ const example = [
515
+ "BROADCAST_CONNECTION=holo",
516
+ "BROADCAST_APP_ID=",
517
+ "BROADCAST_APP_KEY=",
518
+ "BROADCAST_APP_SECRET="
519
+ ];
520
+ return {
521
+ env,
522
+ example
523
+ };
524
+ }
525
+ function renderNextBroadcastAuthRoute() {
526
+ return [
527
+ "import { renderBroadcastAuthResponse } from '@holo-js/broadcast/auth'",
528
+ "import { holo } from '@/server/holo'",
529
+ "",
530
+ "export async function POST(request: Request) {",
531
+ " const app = await holo.getApp()",
532
+ " const auth = await holo.getAuth()",
533
+ "",
534
+ " return await renderBroadcastAuthResponse(request, {",
535
+ " resolveUser: async () => await auth?.user(),",
536
+ " channelAuth: {",
537
+ " registry: {",
538
+ " projectRoot: app.projectRoot,",
539
+ " channels: app.registry?.channels ?? [],",
540
+ " },",
541
+ " },",
542
+ " })",
543
+ "}",
544
+ ""
545
+ ].join("\n");
546
+ }
547
+ function renderNuxtBroadcastAuthRoute() {
548
+ return [
549
+ "import { defineEventHandler, getHeaders, getRequestURL, readRawBody } from 'h3'",
550
+ "import { renderBroadcastAuthResponse } from '@holo-js/broadcast/auth'",
551
+ "import { holo } from '#imports'",
552
+ "",
553
+ "export default defineEventHandler(async (event) => {",
554
+ " const app = await holo.getApp()",
555
+ " const auth = await holo.getAuth()",
556
+ " const request = new Request(getRequestURL(event), {",
557
+ " method: event.method,",
558
+ " headers: getHeaders(event),",
559
+ " body: await readRawBody(event),",
560
+ " })",
561
+ "",
562
+ " return await renderBroadcastAuthResponse(request, {",
563
+ " resolveUser: async () => await auth?.user(),",
564
+ " channelAuth: {",
565
+ " registry: {",
566
+ " projectRoot: app.projectRoot,",
567
+ " channels: app.registry?.channels ?? [],",
568
+ " },",
569
+ " },",
570
+ " })",
571
+ "})",
572
+ ""
573
+ ].join("\n");
574
+ }
575
+ function renderSvelteBroadcastAuthRoute() {
576
+ return [
577
+ "import { renderBroadcastAuthResponse } from '@holo-js/broadcast/auth'",
578
+ "import { holo } from '$lib/server/holo'",
579
+ "",
580
+ "export async function POST({ request }: { request: Request }) {",
581
+ " const app = await holo.getApp()",
582
+ " const auth = await holo.getAuth()",
583
+ "",
584
+ " return await renderBroadcastAuthResponse(request, {",
585
+ " resolveUser: async () => await auth?.user(),",
586
+ " channelAuth: {",
587
+ " registry: {",
588
+ " projectRoot: app.projectRoot,",
589
+ " channels: app.registry?.channels ?? [],",
590
+ " },",
591
+ " },",
592
+ " })",
593
+ "}",
594
+ ""
595
+ ].join("\n");
596
+ }
597
+ async function syncBroadcastAuthSupportAfterAuthInstall(projectRoot) {
598
+ const { dependencies, devDependencies } = await readPackageJsonDependencyState(projectRoot);
599
+ const framework = detectProjectFrameworkFromPackageJson(dependencies, devDependencies);
600
+ const canCreateBroadcastAuthRoute = framework === "next" || framework === "nuxt" || framework === "sveltekit";
601
+ const authConfigPath = await resolveFirstExistingPath(projectRoot, AUTH_CONFIG_FILE_NAMES);
602
+ const broadcastConfigPath = await resolveFirstExistingPath(projectRoot, BROADCAST_CONFIG_FILE_NAMES);
603
+ if (!authConfigPath || !broadcastConfigPath || !canCreateBroadcastAuthRoute) {
604
+ return {
605
+ updatedBroadcastConfig: false,
606
+ createdBroadcastAuthRoute: false
607
+ };
608
+ }
609
+ const currentBroadcastConfig = await readTextFile(broadcastConfigPath);
610
+ let updatedBroadcastConfig = false;
611
+ let createdBroadcastAuthRoute = false;
612
+ if (!currentBroadcastConfig.includes("authEndpoint:")) {
613
+ const broadcastConfigModuleFormat = resolveConfigModuleFormat(broadcastConfigPath, currentBroadcastConfig);
614
+ const broadcastConfigIsTypeScript = [".ts", ".mts", ".cts"].includes(extname(broadcastConfigPath));
615
+ const rewrittenBroadcastConfig = canSafelyRewriteBroadcastConfig(
616
+ currentBroadcastConfig,
617
+ broadcastConfigModuleFormat,
618
+ broadcastConfigIsTypeScript
619
+ ) ? renderBroadcastConfig(broadcastConfigModuleFormat, true, broadcastConfigIsTypeScript) : injectBroadcastAuthEndpoint(currentBroadcastConfig);
620
+ if (rewrittenBroadcastConfig) {
621
+ await writeTextFile(
622
+ broadcastConfigPath,
623
+ rewrittenBroadcastConfig
624
+ );
625
+ updatedBroadcastConfig = true;
626
+ }
627
+ }
628
+ if (framework === "next") {
629
+ const authRoutePath = resolve(projectRoot, "app/broadcasting/auth/route.ts");
630
+ if (!await pathExists(authRoutePath)) {
631
+ await writeTextFile(authRoutePath, renderNextBroadcastAuthRoute());
632
+ createdBroadcastAuthRoute = true;
633
+ }
634
+ return {
635
+ updatedBroadcastConfig,
636
+ createdBroadcastAuthRoute
637
+ };
638
+ }
639
+ if (framework === "nuxt") {
640
+ const authRoutePath = resolve(projectRoot, "server/routes/broadcasting/auth.post.ts");
641
+ if (!await pathExists(authRoutePath)) {
642
+ await writeTextFile(authRoutePath, renderNuxtBroadcastAuthRoute());
643
+ createdBroadcastAuthRoute = true;
644
+ }
645
+ return {
646
+ updatedBroadcastConfig,
647
+ createdBroadcastAuthRoute
648
+ };
649
+ }
650
+ if (framework === "sveltekit") {
651
+ const authRoutePath = resolve(projectRoot, "src/routes/broadcasting/auth/+server.ts");
652
+ if (!await pathExists(authRoutePath)) {
653
+ await writeTextFile(authRoutePath, renderSvelteBroadcastAuthRoute());
654
+ createdBroadcastAuthRoute = true;
655
+ }
656
+ }
657
+ return {
658
+ updatedBroadcastConfig,
659
+ createdBroadcastAuthRoute
660
+ };
661
+ }
662
+ function renderSessionConfig(defaultDatabaseConnection = "default") {
663
+ return [
664
+ "import { defineSessionConfig, env } from '@holo-js/config'",
665
+ "",
666
+ "export default defineSessionConfig({",
667
+ " driver: env('SESSION_DRIVER', 'file'),",
668
+ " stores: {",
669
+ " database: {",
670
+ " driver: 'database',",
671
+ ` connection: env('SESSION_CONNECTION', '${defaultDatabaseConnection}'),`,
672
+ " table: 'sessions',",
673
+ " },",
674
+ " file: {",
675
+ " driver: 'file',",
676
+ " path: './storage/framework/sessions',",
677
+ " },",
678
+ " },",
679
+ " cookie: {",
680
+ " name: env('SESSION_COOKIE', 'holo_session'),",
681
+ " path: env('SESSION_PATH', '/'),",
682
+ " domain: env('SESSION_DOMAIN'),",
683
+ " secure: env<boolean>('SESSION_SECURE', false),",
684
+ " httpOnly: true,",
685
+ " sameSite: env<'lax' | 'strict' | 'none'>('SESSION_SAME_SITE', 'lax'),",
686
+ " },",
687
+ " idleTimeout: env('SESSION_IDLE_TIMEOUT', 120),",
688
+ " absoluteLifetime: env('SESSION_LIFETIME', 120),",
689
+ " rememberMeLifetime: env('SESSION_REMEMBER_ME_LIFETIME', 43200),",
690
+ "})",
691
+ ""
692
+ ].join("\n");
693
+ }
694
+ function renderAuthConfig(features = {}, moduleFormat = "esm") {
695
+ const envValue = (name, fallback) => {
696
+ if (moduleFormat === "cjs") {
697
+ return typeof fallback === "string" ? `process.env.${name} || ${JSON.stringify(fallback)}` : `process.env.${name}`;
698
+ }
699
+ return typeof fallback === "string" ? `env('${name}', ${JSON.stringify(fallback)})` : `env('${name}')`;
700
+ };
701
+ const socialEnabled = features.social === true || (features.socialProviders?.length ?? 0) > 0;
702
+ const socialProviders = features.socialProviders && features.socialProviders.length > 0 ? features.socialProviders : socialEnabled ? ["google"] : [];
703
+ const lines = [
704
+ moduleFormat === "cjs" ? "module.exports = {" : "import { defineAuthConfig, env } from '@holo-js/config'",
705
+ "",
706
+ ...moduleFormat === "cjs" ? [] : ["export default defineAuthConfig({"],
707
+ " defaults: {",
708
+ " guard: 'web',",
709
+ " passwords: 'users',",
710
+ " },",
711
+ " guards: {",
712
+ " web: {",
713
+ " driver: 'session',",
714
+ " provider: 'users',",
715
+ " },",
716
+ " // admin: {",
717
+ " // driver: 'session',",
718
+ " // provider: 'admins',",
719
+ " // },",
720
+ " },",
721
+ " providers: {",
722
+ " users: {",
723
+ " model: 'User',",
724
+ " identifiers: ['email'],",
725
+ " },",
726
+ " // admins: {",
727
+ " // model: 'Admin',",
728
+ " // identifiers: ['email'],",
729
+ " // },",
730
+ " },",
731
+ " passwords: {",
732
+ " users: {",
733
+ " provider: 'users',",
734
+ " table: 'password_reset_tokens',",
735
+ " expire: 60,",
736
+ " throttle: 60,",
737
+ " },",
738
+ " },",
739
+ " emailVerification: {",
740
+ " required: false,",
741
+ " },",
742
+ " personalAccessTokens: {",
743
+ " defaultAbilities: [],",
744
+ " },",
745
+ ` socialEncryptionKey: ${envValue("AUTH_SOCIAL_ENCRYPTION_KEY")},`
746
+ ];
747
+ if (socialProviders.length > 0) {
748
+ lines.push(" social: {");
749
+ for (const provider of socialProviders) {
750
+ const upper = provider.toUpperCase();
751
+ const defaultScopes = provider === "google" ? ["openid", "email", "profile"] : provider === "github" ? ["read:user", "user:email"] : provider === "discord" ? ["identify", "email"] : provider === "facebook" ? ["email", "public_profile"] : provider === "apple" ? ["name", "email"] : ["openid", "profile", "email"];
752
+ lines.push(
753
+ ` ${provider}: {`,
754
+ ` clientId: ${envValue(`AUTH_${upper}_CLIENT_ID`)},`,
755
+ ` clientSecret: ${envValue(`AUTH_${upper}_CLIENT_SECRET`)},`,
756
+ ` redirectUri: ${envValue(`AUTH_${upper}_REDIRECT_URI`)},`,
757
+ ` scopes: [${defaultScopes.map((scope) => `'${scope}'`).join(", ")}],`,
758
+ " },"
759
+ );
760
+ }
761
+ lines.push(" },");
762
+ }
763
+ if (features.workos) {
764
+ lines.push(
765
+ " workos: {",
766
+ " dashboard: {",
767
+ ` clientId: ${envValue("WORKOS_CLIENT_ID")},`,
768
+ ` apiKey: ${envValue("WORKOS_API_KEY")},`,
769
+ ` cookiePassword: ${envValue("WORKOS_COOKIE_PASSWORD")},`,
770
+ ` redirectUri: ${envValue("WORKOS_REDIRECT_URI")},`,
771
+ ` sessionCookie: ${envValue("WORKOS_SESSION_COOKIE", "wos-session")},`,
772
+ " },",
773
+ " },",
774
+ " // Add a dedicated guard and provider if WorkOS users should resolve through a different model."
775
+ );
776
+ }
777
+ if (features.clerk) {
778
+ lines.push(
779
+ " clerk: {",
780
+ " app: {",
781
+ ` publishableKey: ${envValue("CLERK_PUBLISHABLE_KEY")},`,
782
+ ` secretKey: ${envValue("CLERK_SECRET_KEY")},`,
783
+ ` jwtKey: ${envValue("CLERK_JWT_KEY")},`,
784
+ ` apiUrl: ${envValue("CLERK_API_URL")},`,
785
+ ` frontendApi: ${envValue("CLERK_FRONTEND_API")},`,
786
+ ` sessionCookie: ${envValue("CLERK_SESSION_COOKIE", "__session")},`,
787
+ " },",
788
+ " },",
789
+ " // Add a dedicated guard and provider if Clerk users should resolve through a different model."
790
+ );
791
+ }
792
+ lines.push(moduleFormat === "cjs" ? "}" : "})", "");
793
+ return lines.join("\n");
794
+ }
795
+ function authFeaturesRequireConfigUpdate(features) {
796
+ return features.workos === true || features.clerk === true || features.social === true || (features.socialProviders?.length ?? 0) > 0;
797
+ }
798
+ function detectAuthInstallFeaturesFromConfig(contents) {
799
+ const socialProviders = SUPPORTED_AUTH_SOCIAL_PROVIDERS.filter((provider) => {
800
+ const pattern = new RegExp(`\\b${provider}\\s*:\\s*\\{`);
801
+ return pattern.test(contents);
802
+ });
803
+ return Object.freeze({
804
+ ...socialProviders.length > 0 ? { social: true, socialProviders } : {},
805
+ ...contents.includes(" workos: {") ? { workos: true } : {},
806
+ ...contents.includes(" clerk: {") ? { clerk: true } : {}
807
+ });
808
+ }
809
+ function mergeAuthInstallFeatures(current, requested) {
810
+ const socialProviders = Array.from(/* @__PURE__ */ new Set([
811
+ ...current.socialProviders ?? [],
812
+ ...requested.socialProviders ?? []
813
+ ]));
814
+ return Object.freeze({
815
+ ...current.social === true || requested.social === true || socialProviders.length > 0 ? { social: true } : {},
816
+ ...socialProviders.length > 0 ? { socialProviders } : {},
817
+ ...current.workos === true || requested.workos === true ? { workos: true } : {},
818
+ ...current.clerk === true || requested.clerk === true ? { clerk: true } : {}
819
+ });
820
+ }
821
+ function canSafelyRewriteAuthConfig(currentContents, currentFeatures, moduleFormat) {
822
+ const stripLegacyCurrentUserEndpoint = (value) => value.replace(
823
+ /(^|\n)\s*currentUserEndpoint:\s*\{\n\s*path:\s*.*,\n\s*\},/m,
824
+ ""
825
+ );
826
+ return stripLegacyCurrentUserEndpoint(currentContents) === stripLegacyCurrentUserEndpoint(
827
+ renderAuthConfig(currentFeatures, moduleFormat)
828
+ );
829
+ }
830
+ function resolveConfigModuleFormat(filePath, contents) {
831
+ if (filePath?.endsWith(".cjs") || filePath?.endsWith(".cts") || contents.includes("module.exports =")) {
832
+ return "cjs";
833
+ }
834
+ return "esm";
835
+ }
836
+ function renderAuthEnvFiles(features = {}, defaultDatabaseConnection = "default") {
837
+ const socialEnabled = features.social === true || (features.socialProviders?.length ?? 0) > 0;
838
+ const socialProviders = features.socialProviders && features.socialProviders.length > 0 ? features.socialProviders : socialEnabled ? ["google"] : [];
839
+ const env = [
840
+ "AUTH_SOCIAL_ENCRYPTION_KEY=",
841
+ "SESSION_DRIVER=file",
842
+ `SESSION_CONNECTION=${defaultDatabaseConnection}`,
843
+ "SESSION_COOKIE=holo_session",
844
+ "SESSION_PATH=/",
845
+ "SESSION_DOMAIN=",
846
+ "SESSION_SECURE=false",
847
+ "SESSION_SAME_SITE=lax",
848
+ "SESSION_IDLE_TIMEOUT=120",
849
+ "SESSION_LIFETIME=120",
850
+ "SESSION_REMEMBER_ME_LIFETIME=43200"
851
+ ];
852
+ for (const provider of socialProviders) {
853
+ const upper = provider.toUpperCase();
854
+ env.push(
855
+ `AUTH_${upper}_CLIENT_ID=`,
856
+ `AUTH_${upper}_CLIENT_SECRET=`,
857
+ `AUTH_${upper}_REDIRECT_URI=`
858
+ );
859
+ }
860
+ if (features.workos) {
861
+ env.push(
862
+ "WORKOS_CLIENT_ID=",
863
+ "WORKOS_API_KEY=",
864
+ "WORKOS_COOKIE_PASSWORD=",
865
+ "WORKOS_REDIRECT_URI=",
866
+ "WORKOS_SESSION_COOKIE=wos-session"
867
+ );
868
+ }
869
+ if (features.clerk) {
870
+ env.push(
871
+ "CLERK_PUBLISHABLE_KEY=",
872
+ "CLERK_SECRET_KEY=",
873
+ "CLERK_JWT_KEY=",
874
+ "CLERK_API_URL=",
875
+ "CLERK_FRONTEND_API=",
876
+ "CLERK_SESSION_COOKIE=__session"
877
+ );
878
+ }
879
+ return {
880
+ env,
881
+ example: env.map((line) => `${line.split("=")[0]}=`)
882
+ };
883
+ }
884
+ function renderAuthUserModel(generatedSchemaImportPath = "../db/schema.generated") {
885
+ return [
886
+ `import { tables as holoGeneratedTables } from '${generatedSchemaImportPath}'`,
887
+ "import { defineModel, type TableDefinition } from '@holo-js/db'",
888
+ "",
889
+ "const holoModelTable = (holoGeneratedTables as Partial<Record<string, TableDefinition>>).users",
890
+ "export const holoModelPendingSchema = typeof holoModelTable === 'undefined'",
891
+ "",
892
+ "export default holoModelPendingSchema",
893
+ " ? undefined",
894
+ " : defineModel(holoModelTable, {",
895
+ " fillable: ['name', 'email', 'password', 'avatar', 'email_verified_at'],",
896
+ " hidden: ['password'],",
897
+ " })",
898
+ ""
899
+ ].join("\n");
900
+ }
901
+ function renderAuthorizationPoliciesReadme() {
902
+ return [
903
+ "# Authorization Policies",
904
+ "",
905
+ "Place policy files in this directory.",
906
+ "Export `definePolicy(...)` definitions from `@holo-js/authorization`.",
907
+ ""
908
+ ].join("\n");
909
+ }
910
+ function renderAuthorizationAbilitiesReadme() {
911
+ return [
912
+ "# Authorization Abilities",
913
+ "",
914
+ "Place ability files in this directory.",
915
+ "Export `defineAbility(...)` definitions from `@holo-js/authorization`.",
916
+ ""
917
+ ].join("\n");
918
+ }
919
+ function resolveAuthUserModelSchemaImportPath(userModelPath, generatedSchemaPath) {
920
+ return relativeImportPath(userModelPath, generatedSchemaPath);
921
+ }
922
+ function renderAuthMigration(slug) {
923
+ switch (slug) {
924
+ case "create_users":
925
+ return [
926
+ "import { defineMigration, type MigrationContext } from '@holo-js/db'",
927
+ "",
928
+ "export default defineMigration({",
929
+ " async up({ schema }: MigrationContext) {",
930
+ " await schema.createTable('users', (table) => {",
931
+ " table.id()",
932
+ " table.string('name')",
933
+ " table.string('email').unique()",
934
+ " table.string('password').nullable()",
935
+ " table.string('avatar').nullable()",
936
+ " table.timestamp('email_verified_at').nullable()",
937
+ " table.timestamps()",
938
+ " })",
939
+ " },",
940
+ " async down({ schema }: MigrationContext) {",
941
+ " await schema.dropTable('users')",
942
+ " },",
943
+ "})",
944
+ ""
945
+ ].join("\n");
946
+ case "create_sessions":
947
+ return [
948
+ "import { defineMigration, type MigrationContext } from '@holo-js/db'",
949
+ "",
950
+ "export default defineMigration({",
951
+ " async up({ schema }: MigrationContext) {",
952
+ " await schema.createTable('sessions', (table) => {",
953
+ " table.string('id').primaryKey()",
954
+ " table.string('store').default('database')",
955
+ " table.json('data').default({})",
956
+ " table.timestamp('created_at')",
957
+ " table.timestamp('last_activity_at')",
958
+ " table.timestamp('expires_at')",
959
+ " table.timestamp('invalidated_at').nullable()",
960
+ " table.string('remember_token_hash').nullable()",
961
+ " table.index(['expires_at'])",
962
+ " })",
963
+ " },",
964
+ " async down({ schema }: MigrationContext) {",
965
+ " await schema.dropTable('sessions')",
966
+ " },",
967
+ "})",
968
+ ""
969
+ ].join("\n");
970
+ case "create_auth_identities":
971
+ return [
972
+ "import { defineMigration, type MigrationContext } from '@holo-js/db'",
973
+ "",
974
+ "export default defineMigration({",
975
+ " async up({ schema }: MigrationContext) {",
976
+ " await schema.createTable('auth_identities', (table) => {",
977
+ " table.id()",
978
+ " table.string('user_id')",
979
+ " table.string('guard').default('web')",
980
+ " table.string('auth_provider').default('users')",
981
+ " table.string('provider')",
982
+ " table.string('provider_user_id')",
983
+ " table.string('email').nullable()",
984
+ " table.boolean('email_verified').default(false)",
985
+ " table.json('profile').default({})",
986
+ " table.json('tokens').default({})",
987
+ " table.timestamps()",
988
+ " table.index(['user_id'])",
989
+ " table.unique(['provider', 'provider_user_id'], 'auth_identities_provider_user_unique')",
990
+ " })",
991
+ " },",
992
+ " async down({ schema }: MigrationContext) {",
993
+ " await schema.dropTable('auth_identities')",
994
+ " },",
995
+ "})",
996
+ ""
997
+ ].join("\n");
998
+ case "create_personal_access_tokens":
999
+ return [
1000
+ "import { defineMigration, type MigrationContext } from '@holo-js/db'",
1001
+ "",
1002
+ "export default defineMigration({",
1003
+ " async up({ schema }: MigrationContext) {",
1004
+ " await schema.createTable('personal_access_tokens', (table) => {",
1005
+ " table.uuid('id').primaryKey()",
1006
+ " table.string('provider').default('users')",
1007
+ " table.string('user_id')",
1008
+ " table.string('name')",
1009
+ " table.string('token_hash').unique()",
1010
+ " table.json('abilities').default([])",
1011
+ " table.timestamp('last_used_at').nullable()",
1012
+ " table.timestamp('expires_at').nullable()",
1013
+ " table.timestamps()",
1014
+ " table.index(['provider'])",
1015
+ " table.index(['user_id'])",
1016
+ " })",
1017
+ " },",
1018
+ " async down({ schema }: MigrationContext) {",
1019
+ " await schema.dropTable('personal_access_tokens')",
1020
+ " },",
1021
+ "})",
1022
+ ""
1023
+ ].join("\n");
1024
+ case "create_password_reset_tokens":
1025
+ return [
1026
+ "import { defineMigration, type MigrationContext } from '@holo-js/db'",
1027
+ "",
1028
+ "export default defineMigration({",
1029
+ " async up({ schema }: MigrationContext) {",
1030
+ " await schema.createTable('password_reset_tokens', (table) => {",
1031
+ " table.uuid('id').primaryKey()",
1032
+ " table.string('provider').default('users')",
1033
+ " table.string('email')",
1034
+ " table.string('token_hash')",
1035
+ " table.timestamp('expires_at')",
1036
+ " table.timestamp('used_at').nullable()",
1037
+ " table.timestamps()",
1038
+ " table.index(['provider'])",
1039
+ " table.index(['email'])",
1040
+ " })",
1041
+ " },",
1042
+ " async down({ schema }: MigrationContext) {",
1043
+ " await schema.dropTable('password_reset_tokens')",
1044
+ " },",
1045
+ "})",
1046
+ ""
1047
+ ].join("\n");
1048
+ case "create_email_verification_tokens":
1049
+ return [
1050
+ "import { defineMigration, type MigrationContext } from '@holo-js/db'",
1051
+ "",
1052
+ "export default defineMigration({",
1053
+ " async up({ schema }: MigrationContext) {",
1054
+ " await schema.createTable('email_verification_tokens', (table) => {",
1055
+ " table.uuid('id').primaryKey()",
1056
+ " table.string('provider').default('users')",
1057
+ " table.string('user_id')",
1058
+ " table.string('email')",
1059
+ " table.string('token_hash')",
1060
+ " table.timestamp('expires_at')",
1061
+ " table.timestamp('used_at').nullable()",
1062
+ " table.timestamps()",
1063
+ " table.index(['provider'])",
1064
+ " table.index(['user_id'])",
1065
+ " table.index(['email'])",
1066
+ " })",
1067
+ " },",
1068
+ " async down({ schema }: MigrationContext) {",
1069
+ " await schema.dropTable('email_verification_tokens')",
1070
+ " },",
1071
+ "})",
1072
+ ""
1073
+ ].join("\n");
1074
+ }
1075
+ }
1076
+ function createAuthMigrationFiles(date = /* @__PURE__ */ new Date()) {
1077
+ return AUTH_MIGRATION_SLUGS.map((slug, index) => ({
1078
+ path: createMigrationFileName(slug, new Date(date.getTime() + index * 1e3)),
1079
+ contents: renderAuthMigration(slug)
1080
+ }));
1081
+ }
1082
+ function renderNotificationsMigration() {
1083
+ return [
1084
+ "import { defineMigration, type MigrationContext } from '@holo-js/db'",
1085
+ "",
1086
+ "export default defineMigration({",
1087
+ " async up({ schema }: MigrationContext) {",
1088
+ " await schema.createTable('notifications', (table) => {",
1089
+ " table.string('id').primaryKey()",
1090
+ " table.string('type').nullable()",
1091
+ " table.string('notifiable_type')",
1092
+ " table.string('notifiable_id')",
1093
+ " table.json('data').default({})",
1094
+ " table.timestamp('read_at').nullable()",
1095
+ " table.timestamp('created_at')",
1096
+ " table.timestamp('updated_at')",
1097
+ " table.index(['notifiable_type', 'notifiable_id'])",
1098
+ " table.index(['read_at'])",
1099
+ " })",
1100
+ " },",
1101
+ " async down({ schema }: MigrationContext) {",
1102
+ " await schema.dropTable('notifications')",
1103
+ " },",
1104
+ "})",
1105
+ ""
1106
+ ].join("\n");
1107
+ }
1108
+ function createNotificationsMigrationFiles(date = /* @__PURE__ */ new Date()) {
1109
+ return [{
1110
+ path: createMigrationFileName("create_notifications", date),
1111
+ contents: renderNotificationsMigration()
1112
+ }];
1113
+ }
1114
+ function renderScaffoldAppConfig(projectName) {
1115
+ return [
1116
+ "import type { HoloAppEnv } from '@holo-js/config'",
1117
+ "import { defineAppConfig, env } from '@holo-js/config'",
1118
+ "",
1119
+ "export default defineAppConfig({",
1120
+ ` name: env('APP_NAME', ${JSON.stringify(projectName)}),`,
1121
+ " key: env('APP_KEY'),",
1122
+ " url: env('APP_URL', 'http://localhost:3000'),",
1123
+ " env: env<HoloAppEnv>('APP_ENV', 'development'),",
1124
+ " debug: env<boolean>('APP_DEBUG', true),",
1125
+ " paths: {",
1126
+ " models: 'server/models',",
1127
+ " migrations: 'server/db/migrations',",
1128
+ " seeders: 'server/db/seeders',",
1129
+ " commands: 'server/commands',",
1130
+ " jobs: 'server/jobs',",
1131
+ " events: 'server/events',",
1132
+ " listeners: 'server/listeners',",
1133
+ " generatedSchema: 'server/db/schema.generated.ts',",
1134
+ " },",
1135
+ "})",
1136
+ ""
1137
+ ].join("\n");
1138
+ }
1139
+ function renderScaffoldDatabaseConfig(options) {
1140
+ const packageName = sanitizePackageName(options.projectName) || "holo-app";
1141
+ if (options.databaseDriver === "sqlite") {
1142
+ return [
1143
+ "import { defineDatabaseConfig, env } from '@holo-js/config'",
1144
+ "",
1145
+ "export default defineDatabaseConfig({",
1146
+ " defaultConnection: 'main',",
1147
+ " connections: {",
1148
+ " main: {",
1149
+ " driver: 'sqlite',",
1150
+ " url: env('DB_URL', './storage/database.sqlite'),",
1151
+ " },",
1152
+ " },",
1153
+ "})",
1154
+ ""
1155
+ ].join("\n");
1156
+ }
1157
+ const port = options.databaseDriver === "mysql" ? "3306" : "5432";
1158
+ const username = options.databaseDriver === "mysql" ? "root" : "postgres";
1159
+ const schemaLine = options.databaseDriver === "postgres" ? " schema: env('DB_SCHEMA', 'public')," : void 0;
1160
+ return [
1161
+ "import { defineDatabaseConfig, env } from '@holo-js/config'",
1162
+ "",
1163
+ "export default defineDatabaseConfig({",
1164
+ " defaultConnection: 'main',",
1165
+ " connections: {",
1166
+ " main: {",
1167
+ ` driver: '${options.databaseDriver}',`,
1168
+ " host: env('DB_HOST', '127.0.0.1'),",
1169
+ ` port: env('DB_PORT', '${port}'),`,
1170
+ ` username: env('DB_USERNAME', '${username}'),`,
1171
+ " password: env('DB_PASSWORD'),",
1172
+ ` database: env('DB_DATABASE', '${packageName}'),`,
1173
+ ...schemaLine ? [schemaLine] : [],
1174
+ " },",
1175
+ " },",
1176
+ "})",
1177
+ ""
1178
+ ].join("\n");
1179
+ }
1180
+ function renderScaffoldEnvFiles(options) {
1181
+ const defaultDatabaseConnection = "main";
1182
+ const baseLines = [
1183
+ `APP_NAME=${JSON.stringify(options.projectName)}`,
1184
+ "APP_KEY=",
1185
+ "APP_URL=http://localhost:3000",
1186
+ "APP_ENV=development",
1187
+ "APP_DEBUG=true",
1188
+ `DB_DRIVER=${options.databaseDriver}`
1189
+ ];
1190
+ const driverLines = options.databaseDriver === "sqlite" ? [
1191
+ `DB_URL=${resolveDefaultDatabaseUrl(options.databaseDriver)}`
1192
+ ] : [
1193
+ "DB_HOST=127.0.0.1",
1194
+ `DB_PORT=${options.databaseDriver === "mysql" ? "3306" : "5432"}`,
1195
+ `DB_USERNAME=${options.databaseDriver === "mysql" ? "root" : "postgres"}`,
1196
+ "DB_PASSWORD=",
1197
+ `DB_DATABASE=${sanitizePackageName(options.projectName) || "holo_app"}`,
1198
+ ...options.databaseDriver === "postgres" ? ["DB_SCHEMA=public"] : []
1199
+ ];
1200
+ const storageLines = normalizeScaffoldOptionalPackages(options.optionalPackages).includes("storage") ? [
1201
+ `STORAGE_DEFAULT_DISK=${options.storageDefaultDisk}`,
1202
+ "STORAGE_ROUTE_PREFIX=/storage"
1203
+ ] : [];
1204
+ const authLines = normalizeScaffoldOptionalPackages(options.optionalPackages).includes("auth") ? [...renderAuthEnvFiles({}, defaultDatabaseConnection).env] : [];
1205
+ const cacheLines = normalizeScaffoldOptionalPackages(options.optionalPackages).includes("cache") ? [...renderCacheEnvFiles("file").env] : [];
1206
+ const env = [...baseLines, ...driverLines, ...storageLines, ...authLines, ...cacheLines, ""].join("\n");
1207
+ const example = [
1208
+ "# Copy this file to .env and fill in your local values.",
1209
+ "# Supported layered env files: .env.local, .env.development, .env.production, .env.prod, .env.test",
1210
+ ...[...baseLines, ...driverLines, ...storageLines, ...authLines, ...cacheLines].map((line) => `${line.split("=")[0]}=`),
1211
+ ""
1212
+ ].join("\n");
1213
+ return { env, example };
1214
+ }
1215
+ function renderRedisConnectionEnvFiles() {
1216
+ return {
1217
+ env: [
1218
+ "REDIS_URL=",
1219
+ "REDIS_HOST=127.0.0.1",
1220
+ "REDIS_PORT=6379",
1221
+ "REDIS_USERNAME=",
1222
+ "REDIS_PASSWORD=",
1223
+ "REDIS_DB=0"
1224
+ ],
1225
+ example: [
1226
+ "REDIS_URL=",
1227
+ "REDIS_HOST=",
1228
+ "REDIS_PORT=",
1229
+ "REDIS_USERNAME=",
1230
+ "REDIS_PASSWORD=",
1231
+ "REDIS_DB="
1232
+ ]
1233
+ };
1234
+ }
1235
+ function renderQueueEnvFiles(driver) {
1236
+ if (driver !== "redis") {
1237
+ return {
1238
+ env: [],
1239
+ example: []
1240
+ };
1241
+ }
1242
+ return renderRedisConnectionEnvFiles();
1243
+ }
1244
+ function renderCacheEnvFiles(driver) {
1245
+ if (driver === "redis") {
1246
+ const redis = renderRedisConnectionEnvFiles();
1247
+ return {
1248
+ env: [
1249
+ "CACHE_PREFIX=",
1250
+ ...redis.env
1251
+ ],
1252
+ example: [
1253
+ "CACHE_PREFIX=",
1254
+ ...redis.example
1255
+ ]
1256
+ };
1257
+ }
1258
+ return {
1259
+ env: [
1260
+ "CACHE_PREFIX="
1261
+ ],
1262
+ example: [
1263
+ "CACHE_PREFIX="
1264
+ ]
1265
+ };
1266
+ }
1267
+ function parseEnvKey(line) {
1268
+ const trimmed = line.trim();
1269
+ if (!trimmed || trimmed.startsWith("#")) {
1270
+ return void 0;
1271
+ }
1272
+ const normalized = trimmed.startsWith("export ") ? trimmed.slice(7).trim() : trimmed;
1273
+ const separatorIndex = normalized.indexOf("=");
1274
+ if (separatorIndex <= 0) {
1275
+ return void 0;
1276
+ }
1277
+ return normalized.slice(0, separatorIndex).trim();
1278
+ }
1279
+ function upsertEnvContents(existingContents, additions) {
1280
+ if (additions.length === 0) {
1281
+ return {
1282
+ contents: existingContents,
1283
+ changed: false
1284
+ };
1285
+ }
1286
+ const nextLines = existingContents ? existingContents.replace(/\r\n/g, "\n").split("\n") : [];
1287
+ const existingKeys = new Set(nextLines.map(parseEnvKey).filter((value) => typeof value === "string"));
1288
+ const missingLines = additions.filter((line) => !existingKeys.has(line.slice(0, line.indexOf("=")).trim()));
1289
+ if (missingLines.length === 0) {
1290
+ return {
1291
+ contents: existingContents,
1292
+ changed: false
1293
+ };
1294
+ }
1295
+ if (nextLines.length > 0 && nextLines[nextLines.length - 1]?.trim() !== "") {
1296
+ nextLines.push("");
1297
+ }
1298
+ nextLines.push(...missingLines);
1299
+ return {
1300
+ contents: `${nextLines.join("\n").replace(/\n*$/, "")}
1301
+ `,
1302
+ changed: true
1303
+ };
1304
+ }
1305
+ function normalizeDependencyMap(value) {
1306
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1307
+ return {};
1308
+ }
1309
+ return Object.fromEntries(
1310
+ Object.entries(value).filter(([, dependencyVersion]) => typeof dependencyVersion === "string").sort(([left], [right]) => left.localeCompare(right))
1311
+ );
1312
+ }
1313
+ async function readPackageJsonDependencyState(projectRoot) {
1314
+ const packageJsonPath = resolve(projectRoot, "package.json");
1315
+ const existing = await readTextFile(packageJsonPath);
1316
+ if (!existing) {
1317
+ throw new Error(`Missing package.json in ${projectRoot}.`);
1318
+ }
1319
+ let parsed;
1320
+ try {
1321
+ parsed = JSON.parse(existing);
1322
+ } catch {
1323
+ throw new Error(`Invalid package.json in ${projectRoot}.`);
1324
+ }
1325
+ return {
1326
+ packageJsonPath,
1327
+ parsed,
1328
+ dependencies: normalizeDependencyMap(parsed.dependencies),
1329
+ devDependencies: normalizeDependencyMap(parsed.devDependencies)
1330
+ };
1331
+ }
1332
+ async function writePackageJsonDependencyState(packageJsonPath, parsed, dependencies, devDependencies) {
1333
+ parsed.dependencies = Object.fromEntries(
1334
+ Object.entries(dependencies).sort(([left], [right]) => left.localeCompare(right))
1335
+ );
1336
+ if (Object.keys(devDependencies).length > 0) {
1337
+ parsed.devDependencies = Object.fromEntries(
1338
+ Object.entries(devDependencies).sort(([left], [right]) => left.localeCompare(right))
1339
+ );
1340
+ } else {
1341
+ delete parsed.devDependencies;
1342
+ }
1343
+ await writeTextFile(packageJsonPath, `${JSON.stringify(parsed, null, 2)}
1344
+ `);
1345
+ }
1346
+ function hasLoadedConfigFile(loadedFiles, configName) {
1347
+ return loadedFiles.some((filePath) => {
1348
+ const normalizedPath = filePath.replaceAll("\\", "/");
1349
+ return normalizedPath.endsWith(`/config/${configName}.ts`) || normalizedPath.endsWith(`/config/${configName}.mts`) || normalizedPath.endsWith(`/config/${configName}.js`) || normalizedPath.endsWith(`/config/${configName}.mjs`) || normalizedPath.endsWith(`/config/${configName}.cts`) || normalizedPath.endsWith(`/config/${configName}.cjs`);
1350
+ });
1351
+ }
1352
+ function inferDatabaseDriverFromUrl(value) {
1353
+ if (!value) {
1354
+ return void 0;
1355
+ }
1356
+ const normalized = value.trim().toLowerCase();
1357
+ if (normalized.startsWith("postgres://") || normalized.startsWith("postgresql://")) {
1358
+ return "postgres";
1359
+ }
1360
+ if (normalized.startsWith("mysql://") || normalized.startsWith("mysql2://")) {
1361
+ return "mysql";
1362
+ }
1363
+ if (normalized === ":memory:" || normalized.startsWith("file:") || normalized.startsWith("/") || normalized.startsWith("./") || normalized.startsWith("../") || normalized.endsWith(".db") || normalized.endsWith(".sqlite") || normalized.endsWith(".sqlite3")) {
1364
+ return "sqlite";
1365
+ }
1366
+ return void 0;
1367
+ }
1368
+ function inferConnectionDriver(connection) {
1369
+ if (typeof connection === "string") {
1370
+ return inferDatabaseDriverFromUrl(connection);
1371
+ }
1372
+ const explicitDriver = connection.driver;
1373
+ if (explicitDriver === "sqlite" || explicitDriver === "postgres" || explicitDriver === "mysql") {
1374
+ return explicitDriver;
1375
+ }
1376
+ return inferDatabaseDriverFromUrl(connection.url ?? connection.filename);
1377
+ }
1378
+ function registryHasJobs(registry) {
1379
+ return (registry?.jobs.length ?? 0) > 0;
1380
+ }
1381
+ function registryHasEvents(registry) {
1382
+ return (registry?.events.length ?? 0) > 0 || (registry?.listeners.length ?? 0) > 0;
1383
+ }
1384
+ function registryHasBroadcastDefinitions(registry) {
1385
+ return (registry?.broadcast.length ?? 0) > 0 || (registry?.channels.length ?? 0) > 0;
1386
+ }
1387
+ function registryHasAuthorizationDefinitions(registry) {
1388
+ return (registry?.authorizationPolicies.length ?? 0) > 0 || (registry?.authorizationAbilities.length ?? 0) > 0;
1389
+ }
1390
+ function authConfigUsesSocialProviders(loaded) {
1391
+ return Object.keys(loaded.auth.social).length > 0;
1392
+ }
1393
+ function authConfigUsesWorkosProviders(loaded) {
1394
+ return Object.keys(loaded.auth.workos).length > 0;
1395
+ }
1396
+ function authConfigUsesClerkProviders(loaded) {
1397
+ return Object.keys(loaded.auth.clerk).length > 0;
1398
+ }
1399
+ function mailConfigUsesQueue(loaded) {
1400
+ return loaded.mail.queue.queued || Object.values(loaded.mail.mailers).some((mailer) => mailer.queue.queued);
1401
+ }
1402
+ async function projectHasAuthorizationScaffold(projectRoot) {
1403
+ const project = await loadProjectConfig(projectRoot);
1404
+ const policiesRoot = resolve(projectRoot, project.config.paths.authorizationPolicies ?? "server/policies");
1405
+ const abilitiesRoot = resolve(projectRoot, project.config.paths.authorizationAbilities ?? "server/abilities");
1406
+ return await pathExists(policiesRoot) || await pathExists(abilitiesRoot);
1407
+ }
1408
+ async function projectHasEventsScaffold(projectRoot) {
1409
+ const project = await loadProjectConfig(projectRoot);
1410
+ const eventsRoot = resolve(projectRoot, project.config.paths.events);
1411
+ const listenersRoot = resolve(projectRoot, project.config.paths.listeners);
1412
+ return await pathExists(eventsRoot) || await pathExists(listenersRoot);
1413
+ }
1414
+ function renderEnvFileContents(segments) {
1415
+ const normalized = segments.map((segment) => segment.replace(/\n+$/, "")).filter((segment) => segment.length > 0);
1416
+ return normalized.length > 0 ? `${normalized.join("\n")}
1417
+ ` : "";
1418
+ }
1419
+ function normalizeScaffoldEnvSegments(segments) {
1420
+ return segments.split("\n").map((segment) => segment.trim()).filter((segment) => segment.length > 0);
1421
+ }
1422
+ async function syncManagedDriverDependencies(projectRoot, registry) {
1423
+ const loaded = await loadConfigDirectory(projectRoot, {
1424
+ preferCache: false,
1425
+ processEnv: process.env
1426
+ });
1427
+ const discoveredRegistry = registry ?? await loadGeneratedProjectRegistry(projectRoot);
1428
+ const authConfigured = hasLoadedConfigFile(loaded.loadedFiles, "auth");
1429
+ const broadcastConfigured = hasLoadedConfigFile(loaded.loadedFiles, "broadcast");
1430
+ const cacheConfigured = hasLoadedConfigFile(loaded.loadedFiles, "cache");
1431
+ const mailConfigured = hasLoadedConfigFile(loaded.loadedFiles, "mail");
1432
+ const notificationsConfigured = hasLoadedConfigFile(loaded.loadedFiles, "notifications");
1433
+ const queueConfigured = hasLoadedConfigFile(loaded.loadedFiles, "queue");
1434
+ const securityConfigured = hasLoadedConfigFile(loaded.loadedFiles, "security");
1435
+ const sessionConfigured = hasLoadedConfigFile(loaded.loadedFiles, "session");
1436
+ const storageConfigured = hasLoadedConfigFile(loaded.loadedFiles, "storage");
1437
+ const requiredPackages = /* @__PURE__ */ new Set();
1438
+ const hasAuthorizationScaffold = await projectHasAuthorizationScaffold(projectRoot);
1439
+ const hasEventsScaffold = await projectHasEventsScaffold(projectRoot);
1440
+ const {
1441
+ packageJsonPath,
1442
+ parsed,
1443
+ dependencies,
1444
+ devDependencies
1445
+ } = await readPackageJsonDependencyState(projectRoot);
1446
+ const cachePackageInstalled = typeof dependencies["@holo-js/cache"] !== "undefined" || typeof devDependencies["@holo-js/cache"] !== "undefined";
1447
+ for (const connection of Object.values(loaded.database.connections)) {
1448
+ const inferredDriver = inferConnectionDriver(connection);
1449
+ if (inferredDriver) {
1450
+ requiredPackages.add(DB_DRIVER_PACKAGE_NAMES[inferredDriver]);
1451
+ }
1452
+ }
1453
+ if (authConfigured || sessionConfigured) {
1454
+ requiredPackages.add("@holo-js/session");
1455
+ }
1456
+ if (authConfigured || securityConfigured) {
1457
+ requiredPackages.add("@holo-js/security");
1458
+ }
1459
+ if (authConfigured) {
1460
+ requiredPackages.add("@holo-js/auth");
1461
+ if (authConfigUsesSocialProviders(loaded)) {
1462
+ requiredPackages.add("@holo-js/auth-social");
1463
+ for (const [providerName, provider] of Object.entries(loaded.auth.social)) {
1464
+ if (typeof provider.runtime === "string" && provider.runtime.trim()) {
1465
+ continue;
1466
+ }
1467
+ const builtinPackage = AUTH_SOCIAL_PROVIDER_PACKAGE_NAMES[providerName];
1468
+ if (builtinPackage) {
1469
+ requiredPackages.add(builtinPackage);
1470
+ }
1471
+ }
1472
+ }
1473
+ if (authConfigUsesWorkosProviders(loaded)) {
1474
+ requiredPackages.add("@holo-js/auth-workos");
1475
+ }
1476
+ if (authConfigUsesClerkProviders(loaded)) {
1477
+ requiredPackages.add("@holo-js/auth-clerk");
1478
+ }
1479
+ }
1480
+ if (mailConfigured) {
1481
+ requiredPackages.add("@holo-js/mail");
1482
+ }
1483
+ if (cacheConfigured || cachePackageInstalled) {
1484
+ requiredPackages.add("@holo-js/cache");
1485
+ }
1486
+ if (cacheConfigured) {
1487
+ const cacheDrivers = Object.values(loaded.cache.drivers);
1488
+ if (cacheDrivers.some((driver) => driver.driver === "redis")) {
1489
+ requiredPackages.add("@holo-js/cache-redis");
1490
+ }
1491
+ if (cacheDrivers.some((driver) => driver.driver === "database")) {
1492
+ requiredPackages.add("@holo-js/cache-db");
1493
+ }
1494
+ }
1495
+ if (notificationsConfigured) {
1496
+ requiredPackages.add("@holo-js/notifications");
1497
+ }
1498
+ if (broadcastConfigured || registryHasBroadcastDefinitions(discoveredRegistry)) {
1499
+ requiredPackages.add("@holo-js/broadcast");
1500
+ }
1501
+ if (registryHasAuthorizationDefinitions(discoveredRegistry) || hasAuthorizationScaffold) {
1502
+ requiredPackages.add("@holo-js/authorization");
1503
+ }
1504
+ if (registryHasEvents(discoveredRegistry) || hasEventsScaffold) {
1505
+ requiredPackages.add("@holo-js/events");
1506
+ requiredPackages.add("@holo-js/queue");
1507
+ }
1508
+ if (queueConfigured || registryHasJobs(discoveredRegistry) || mailConfigUsesQueue(loaded)) {
1509
+ requiredPackages.add("@holo-js/queue");
1510
+ if (queueConfigured) {
1511
+ const queueConnections = Object.values(loaded.queue.connections);
1512
+ if (queueConnections.some((connection) => connection.driver === "redis")) {
1513
+ requiredPackages.add("@holo-js/queue-redis");
1514
+ }
1515
+ if (queueConnections.some((connection) => connection.driver === "database") || loaded.queue.failed !== false) {
1516
+ requiredPackages.add("@holo-js/queue-db");
1517
+ }
1518
+ }
1519
+ }
1520
+ if (Object.values(loaded.cache?.drivers ?? {}).some((driver) => driver.driver === "redis") || loaded.security?.rateLimit?.driver === "redis" || Object.values(loaded.session?.stores ?? {}).some((store) => store.driver === "redis") || loaded.broadcast?.worker != null && loaded.broadcast.worker.scaling !== false) {
1521
+ requiredPackages.add("ioredis");
1522
+ }
1523
+ if (storageConfigured) {
1524
+ requiredPackages.add("@holo-js/storage");
1525
+ if (Object.values(loaded.storage.disks).some((disk) => disk.driver === "s3")) {
1526
+ requiredPackages.add("@holo-js/storage-s3");
1527
+ }
1528
+ }
1529
+ let changed = false;
1530
+ const nextVersion = `^${HOLO_PACKAGE_VERSION}`;
1531
+ const removableManagedPackages = /* @__PURE__ */ new Set([
1532
+ ...Object.values(DB_DRIVER_PACKAGE_NAMES),
1533
+ "@holo-js/auth",
1534
+ "@holo-js/auth-clerk",
1535
+ "@holo-js/auth-social",
1536
+ "@holo-js/auth-workos",
1537
+ "@holo-js/authorization",
1538
+ "@holo-js/broadcast",
1539
+ "@holo-js/cache",
1540
+ "@holo-js/cache-db",
1541
+ "@holo-js/cache-redis",
1542
+ "@holo-js/events",
1543
+ "@holo-js/mail",
1544
+ "@holo-js/notifications",
1545
+ "@holo-js/queue",
1546
+ "@holo-js/queue-db",
1547
+ "@holo-js/queue-redis",
1548
+ "@holo-js/security",
1549
+ "@holo-js/session",
1550
+ "@holo-js/storage",
1551
+ "@holo-js/storage-s3",
1552
+ ...Object.values(AUTH_SOCIAL_PROVIDER_PACKAGE_NAMES),
1553
+ "ioredis"
1554
+ ]);
1555
+ for (const packageName of requiredPackages) {
1556
+ const requiredVersion = packageName === "ioredis" ? IOREDIS_PACKAGE_VERSION : nextVersion;
1557
+ if (dependencies[packageName] !== requiredVersion || typeof devDependencies[packageName] !== "undefined") {
1558
+ dependencies[packageName] = requiredVersion;
1559
+ delete devDependencies[packageName];
1560
+ changed = true;
1561
+ }
1562
+ }
1563
+ for (const packageName of removableManagedPackages) {
1564
+ if (requiredPackages.has(packageName)) {
1565
+ continue;
1566
+ }
1567
+ if (typeof dependencies[packageName] !== "undefined" || typeof devDependencies[packageName] !== "undefined") {
1568
+ delete dependencies[packageName];
1569
+ delete devDependencies[packageName];
1570
+ changed = true;
1571
+ }
1572
+ }
1573
+ if (!changed) {
1574
+ return false;
1575
+ }
1576
+ await writePackageJsonDependencyState(packageJsonPath, parsed, dependencies, devDependencies);
1577
+ return true;
1578
+ }
1579
+ async function upsertQueuePackageDependency(projectRoot, driver) {
1580
+ const { packageJsonPath, parsed, dependencies, devDependencies } = await readPackageJsonDependencyState(projectRoot);
1581
+ const queueConfigPath = await resolveFirstExistingPath(projectRoot, QUEUE_CONFIG_FILE_NAMES);
1582
+ const loadedQueueConfig = queueConfigPath ? loadConfigDirectory(projectRoot, {
1583
+ preferCache: false,
1584
+ processEnv: process.env
1585
+ }).then((config) => config.queue).catch(() => void 0) : Promise.resolve(void 0);
1586
+ const nextVersion = `^${HOLO_PACKAGE_VERSION}`;
1587
+ const nextEsbuildVersion = ESBUILD_PACKAGE_VERSION;
1588
+ const queueConfig = typeof driver === "undefined" ? await loadedQueueConfig : void 0;
1589
+ const resolvedQueueDriver = driver && driver !== "sync" ? driver : queueConfig?.connections[queueConfig.default]?.driver ?? driver;
1590
+ const requiresQueueDb = resolvedQueueDriver === "database" || (queueConfig?.failed ?? false) !== false || Object.values(queueConfig?.connections ?? {}).some((connection) => connection.driver === "database");
1591
+ const requiresQueueRedis = resolvedQueueDriver === "redis" || Object.values(queueConfig?.connections ?? {}).some((connection) => connection.driver === "redis");
1592
+ const currentVersion = dependencies["@holo-js/queue"];
1593
+ const currentQueueDbVersion = dependencies["@holo-js/queue-db"];
1594
+ const currentQueueRedisVersion = dependencies["@holo-js/queue-redis"];
1595
+ const currentDevVersion = devDependencies["@holo-js/queue"];
1596
+ const currentDevQueueDbVersion = devDependencies["@holo-js/queue-db"];
1597
+ const currentDevQueueRedisVersion = devDependencies["@holo-js/queue-redis"];
1598
+ const currentEsbuildVersion = dependencies.esbuild;
1599
+ const currentDevEsbuildVersion = devDependencies.esbuild;
1600
+ if (currentVersion === nextVersion && (requiresQueueDb ? currentQueueDbVersion === nextVersion : typeof currentQueueDbVersion === "undefined") && (requiresQueueRedis ? currentQueueRedisVersion === nextVersion : typeof currentQueueRedisVersion === "undefined") && typeof currentDevVersion === "undefined" && typeof currentDevQueueDbVersion === "undefined" && typeof currentDevQueueRedisVersion === "undefined" && currentEsbuildVersion === nextEsbuildVersion && typeof currentDevEsbuildVersion === "undefined") {
1601
+ return false;
1602
+ }
1603
+ dependencies["@holo-js/queue"] = nextVersion;
1604
+ if (requiresQueueDb) {
1605
+ dependencies["@holo-js/queue-db"] = nextVersion;
1606
+ } else {
1607
+ delete dependencies["@holo-js/queue-db"];
1608
+ }
1609
+ if (requiresQueueRedis) {
1610
+ dependencies["@holo-js/queue-redis"] = nextVersion;
1611
+ } else {
1612
+ delete dependencies["@holo-js/queue-redis"];
1613
+ }
1614
+ dependencies.esbuild = nextEsbuildVersion;
1615
+ delete devDependencies["@holo-js/queue"];
1616
+ delete devDependencies["@holo-js/queue-db"];
1617
+ delete devDependencies["@holo-js/queue-redis"];
1618
+ delete devDependencies.esbuild;
1619
+ await writePackageJsonDependencyState(packageJsonPath, parsed, dependencies, devDependencies);
1620
+ return true;
1621
+ }
1622
+ async function upsertEventsPackageDependency(projectRoot) {
1623
+ const { packageJsonPath, parsed, dependencies, devDependencies } = await readPackageJsonDependencyState(projectRoot);
1624
+ const nextVersion = `^${HOLO_PACKAGE_VERSION}`;
1625
+ const currentVersion = dependencies["@holo-js/events"];
1626
+ const currentDevVersion = devDependencies["@holo-js/events"];
1627
+ if (currentVersion === nextVersion && typeof currentDevVersion === "undefined") {
1628
+ return false;
1629
+ }
1630
+ dependencies["@holo-js/events"] = nextVersion;
1631
+ delete devDependencies["@holo-js/events"];
1632
+ await writePackageJsonDependencyState(packageJsonPath, parsed, dependencies, devDependencies);
1633
+ return true;
1634
+ }
1635
+ async function upsertNotificationsPackageDependency(projectRoot) {
1636
+ const { packageJsonPath, parsed, dependencies, devDependencies } = await readPackageJsonDependencyState(projectRoot);
1637
+ const nextVersion = `^${HOLO_PACKAGE_VERSION}`;
1638
+ const currentVersion = dependencies["@holo-js/notifications"];
1639
+ const currentDevVersion = devDependencies["@holo-js/notifications"];
1640
+ if (currentVersion === nextVersion && typeof currentDevVersion === "undefined") {
1641
+ return false;
1642
+ }
1643
+ dependencies["@holo-js/notifications"] = nextVersion;
1644
+ delete devDependencies["@holo-js/notifications"];
1645
+ await writePackageJsonDependencyState(packageJsonPath, parsed, dependencies, devDependencies);
1646
+ return true;
1647
+ }
1648
+ async function upsertMailPackageDependency(projectRoot) {
1649
+ const { packageJsonPath, parsed, dependencies, devDependencies } = await readPackageJsonDependencyState(projectRoot);
1650
+ const nextVersion = `^${HOLO_PACKAGE_VERSION}`;
1651
+ const currentVersion = dependencies["@holo-js/mail"];
1652
+ const currentDevVersion = devDependencies["@holo-js/mail"];
1653
+ if (currentVersion === nextVersion && typeof currentDevVersion === "undefined") {
1654
+ return false;
1655
+ }
1656
+ dependencies["@holo-js/mail"] = nextVersion;
1657
+ delete devDependencies["@holo-js/mail"];
1658
+ await writePackageJsonDependencyState(packageJsonPath, parsed, dependencies, devDependencies);
1659
+ return true;
1660
+ }
1661
+ async function upsertSecurityPackageDependency(projectRoot) {
1662
+ const { packageJsonPath, parsed, dependencies, devDependencies } = await readPackageJsonDependencyState(projectRoot);
1663
+ const nextVersion = `^${HOLO_PACKAGE_VERSION}`;
1664
+ const currentVersion = dependencies["@holo-js/security"];
1665
+ const currentDevVersion = devDependencies["@holo-js/security"];
1666
+ if (currentVersion === nextVersion && typeof currentDevVersion === "undefined") {
1667
+ return false;
1668
+ }
1669
+ dependencies["@holo-js/security"] = nextVersion;
1670
+ delete devDependencies["@holo-js/security"];
1671
+ await writePackageJsonDependencyState(packageJsonPath, parsed, dependencies, devDependencies);
1672
+ return true;
1673
+ }
1674
+ async function upsertCachePackageDependencies(projectRoot, driver = "file") {
1675
+ const { packageJsonPath, parsed, dependencies, devDependencies } = await readPackageJsonDependencyState(projectRoot);
1676
+ const cacheConfigPath = await resolveFirstExistingPath(projectRoot, CACHE_CONFIG_FILE_NAMES);
1677
+ const cacheConfig = cacheConfigPath ? await loadConfigDirectory(projectRoot, {
1678
+ preferCache: false,
1679
+ processEnv: process.env
1680
+ }).then((config) => config.cache).catch(() => void 0) : void 0;
1681
+ const nextVersion = `^${HOLO_PACKAGE_VERSION}`;
1682
+ const requiresCacheRedis = driver === "redis" || Object.values(cacheConfig?.drivers ?? {}).some((connection) => connection.driver === "redis");
1683
+ const requiresCacheDb = driver === "database" || Object.values(cacheConfig?.drivers ?? {}).some((connection) => connection.driver === "database");
1684
+ const currentVersion = dependencies["@holo-js/cache"];
1685
+ const currentCacheDbVersion = dependencies["@holo-js/cache-db"];
1686
+ const currentCacheRedisVersion = dependencies["@holo-js/cache-redis"];
1687
+ const currentDevVersion = devDependencies["@holo-js/cache"];
1688
+ const currentDevCacheDbVersion = devDependencies["@holo-js/cache-db"];
1689
+ const currentDevCacheRedisVersion = devDependencies["@holo-js/cache-redis"];
1690
+ if (currentVersion === nextVersion && (requiresCacheDb ? currentCacheDbVersion === nextVersion : typeof currentCacheDbVersion === "undefined") && (requiresCacheRedis ? currentCacheRedisVersion === nextVersion : typeof currentCacheRedisVersion === "undefined") && typeof currentDevVersion === "undefined" && typeof currentDevCacheDbVersion === "undefined" && typeof currentDevCacheRedisVersion === "undefined") {
1691
+ return false;
1692
+ }
1693
+ dependencies["@holo-js/cache"] = nextVersion;
1694
+ if (requiresCacheDb) {
1695
+ dependencies["@holo-js/cache-db"] = nextVersion;
1696
+ } else {
1697
+ delete dependencies["@holo-js/cache-db"];
1698
+ }
1699
+ if (requiresCacheRedis) {
1700
+ dependencies["@holo-js/cache-redis"] = nextVersion;
1701
+ } else {
1702
+ delete dependencies["@holo-js/cache-redis"];
1703
+ }
1704
+ delete devDependencies["@holo-js/cache"];
1705
+ delete devDependencies["@holo-js/cache-db"];
1706
+ delete devDependencies["@holo-js/cache-redis"];
1707
+ await writePackageJsonDependencyState(packageJsonPath, parsed, dependencies, devDependencies);
1708
+ return true;
1709
+ }
1710
+ function detectProjectFrameworkFromPackageJson(dependencies, devDependencies) {
1711
+ if (dependencies.next || devDependencies.next) {
1712
+ return "next";
1713
+ }
1714
+ if (dependencies.nuxt || devDependencies.nuxt) {
1715
+ return "nuxt";
1716
+ }
1717
+ if (dependencies["@sveltejs/kit"] || devDependencies["@sveltejs/kit"]) {
1718
+ return "sveltekit";
1719
+ }
1720
+ return void 0;
1721
+ }
1722
+ async function upsertBroadcastPackageDependencies(projectRoot) {
1723
+ const { packageJsonPath, parsed, dependencies, devDependencies } = await readPackageJsonDependencyState(projectRoot);
1724
+ const nextVersion = `^${HOLO_PACKAGE_VERSION}`;
1725
+ const framework = detectProjectFrameworkFromPackageJson(dependencies, devDependencies);
1726
+ let changed = false;
1727
+ const requestedPackages = /* @__PURE__ */ new Set([
1728
+ "@holo-js/broadcast",
1729
+ "@holo-js/flux"
1730
+ ]);
1731
+ if (framework === "next") {
1732
+ requestedPackages.add("@holo-js/flux-react");
1733
+ requestedPackages.add("@holo-js/adapter-next");
1734
+ } else if (framework === "nuxt") {
1735
+ requestedPackages.add("@holo-js/flux-vue");
1736
+ requestedPackages.add("@holo-js/adapter-nuxt");
1737
+ } else if (framework === "sveltekit") {
1738
+ requestedPackages.add("@holo-js/flux-svelte");
1739
+ requestedPackages.add("@holo-js/adapter-sveltekit");
1740
+ }
1741
+ for (const packageName of requestedPackages) {
1742
+ if (dependencies[packageName] !== nextVersion || typeof devDependencies[packageName] !== "undefined") {
1743
+ dependencies[packageName] = nextVersion;
1744
+ delete devDependencies[packageName];
1745
+ changed = true;
1746
+ }
1747
+ }
1748
+ if (!changed) {
1749
+ return {
1750
+ updated: false,
1751
+ framework
1752
+ };
1753
+ }
1754
+ await writePackageJsonDependencyState(packageJsonPath, parsed, dependencies, devDependencies);
1755
+ return {
1756
+ updated: true,
1757
+ framework
1758
+ };
1759
+ }
1760
+ async function upsertAuthPackageDependencies(projectRoot, features = {}) {
1761
+ const { packageJsonPath, parsed, dependencies, devDependencies } = await readPackageJsonDependencyState(projectRoot);
1762
+ const nextVersion = `^${HOLO_PACKAGE_VERSION}`;
1763
+ const socialEnabled = features.social === true || (features.socialProviders?.length ?? 0) > 0;
1764
+ const requestedPackages = {
1765
+ "@holo-js/auth": true,
1766
+ "@holo-js/session": true,
1767
+ "@holo-js/auth-social": socialEnabled,
1768
+ "@holo-js/auth-workos": features.workos === true,
1769
+ "@holo-js/auth-clerk": features.clerk === true
1770
+ };
1771
+ const requestedSocialProviders = new Set(features.socialProviders ?? (socialEnabled ? ["google"] : []));
1772
+ let changed = false;
1773
+ for (const [packageName, enabled] of Object.entries(requestedPackages)) {
1774
+ const currentDependency = dependencies[packageName];
1775
+ const currentDevDependency = devDependencies[packageName];
1776
+ if (enabled) {
1777
+ if (currentDependency !== nextVersion || typeof currentDevDependency !== "undefined") {
1778
+ dependencies[packageName] = nextVersion;
1779
+ delete devDependencies[packageName];
1780
+ changed = true;
1781
+ }
1782
+ continue;
1783
+ }
1784
+ if (typeof currentDevDependency !== "undefined") {
1785
+ delete devDependencies[packageName];
1786
+ changed = true;
1787
+ }
1788
+ }
1789
+ for (const [providerName, packageName] of Object.entries(AUTH_SOCIAL_PROVIDER_PACKAGE_NAMES)) {
1790
+ const enabled = requestedSocialProviders.has(providerName);
1791
+ const currentDependency = dependencies[packageName];
1792
+ const currentDevDependency = devDependencies[packageName];
1793
+ if (enabled) {
1794
+ if (currentDependency !== nextVersion || typeof currentDevDependency !== "undefined") {
1795
+ dependencies[packageName] = nextVersion;
1796
+ delete devDependencies[packageName];
1797
+ changed = true;
1798
+ }
1799
+ continue;
1800
+ }
1801
+ if (typeof currentDevDependency !== "undefined") {
1802
+ delete devDependencies[packageName];
1803
+ changed = true;
1804
+ }
1805
+ }
1806
+ if (!changed) {
1807
+ return false;
1808
+ }
1809
+ await writePackageJsonDependencyState(packageJsonPath, parsed, dependencies, devDependencies);
1810
+ return true;
1811
+ }
1812
+ async function upsertAuthorizationPackageDependency(projectRoot) {
1813
+ const { packageJsonPath, parsed, dependencies, devDependencies } = await readPackageJsonDependencyState(projectRoot);
1814
+ const nextVersion = `^${HOLO_PACKAGE_VERSION}`;
1815
+ const currentVersion = dependencies["@holo-js/authorization"];
1816
+ const currentDevVersion = devDependencies["@holo-js/authorization"];
1817
+ if (currentVersion === nextVersion && typeof currentDevVersion === "undefined") {
1818
+ return false;
1819
+ }
1820
+ dependencies["@holo-js/authorization"] = nextVersion;
1821
+ delete devDependencies["@holo-js/authorization"];
1822
+ await writePackageJsonDependencyState(packageJsonPath, parsed, dependencies, devDependencies);
1823
+ return true;
1824
+ }
1825
+ async function resolveExistingModelPath(modelsRoot, modelName) {
1826
+ const supportedExtensions = [".ts", ".mts", ".js", ".mjs", ".cts", ".cjs"];
1827
+ for (const extension of supportedExtensions) {
1828
+ const candidate = resolve(modelsRoot, `${modelName}${extension}`);
1829
+ if (await pathExists(candidate)) {
1830
+ return candidate;
1831
+ }
1832
+ }
1833
+ return void 0;
1834
+ }
1835
+ async function resolveExistingAuthMigrationFiles(migrationsRoot) {
1836
+ const entries = await readdir(migrationsRoot).catch(() => []);
1837
+ const resolved = /* @__PURE__ */ new Map();
1838
+ for (const entry of entries) {
1839
+ for (const slug of AUTH_MIGRATION_SLUGS) {
1840
+ if (entry.endsWith(`_${slug}.ts`) || entry.endsWith(`_${slug}.mts`) || entry.endsWith(`_${slug}.js`) || entry.endsWith(`_${slug}.mjs`) || entry.endsWith(`_${slug}.cts`) || entry.endsWith(`_${slug}.cjs`)) {
1841
+ resolved.set(slug, resolve(migrationsRoot, entry));
1842
+ }
1843
+ }
1844
+ }
1845
+ return resolved;
1846
+ }
1847
+ async function resolveExistingNotificationsMigrationFiles(migrationsRoot) {
1848
+ const entries = await readdir(migrationsRoot).catch(() => []);
1849
+ return entries.filter((entry) => entry.endsWith("_create_notifications.ts") || entry.endsWith("_create_notifications.mts") || entry.endsWith("_create_notifications.js") || entry.endsWith("_create_notifications.mjs") || entry.endsWith("_create_notifications.cts") || entry.endsWith("_create_notifications.cjs")).map((entry) => resolve(migrationsRoot, entry));
1850
+ }
1851
+ async function installAuthIntoProject(projectRoot, features = {}) {
1852
+ const project = await loadProjectConfig(projectRoot);
1853
+ const modelsRoot = resolve(projectRoot, project.config.paths.models);
1854
+ const migrationsRoot = resolve(projectRoot, project.config.paths.migrations);
1855
+ const defaultDatabaseConnection = project.config.database?.defaultConnection ?? "default";
1856
+ const authConfigPath = await resolveFirstExistingPath(projectRoot, AUTH_CONFIG_FILE_NAMES);
1857
+ const sessionConfigPath = await resolveFirstExistingPath(projectRoot, SESSION_CONFIG_FILE_NAMES);
1858
+ const userModelPath = await resolveExistingModelPath(modelsRoot, "User");
1859
+ const existingMigrationFiles = await resolveExistingAuthMigrationFiles(migrationsRoot);
1860
+ const hasAllAuthMigrations = AUTH_MIGRATION_SLUGS.every((slug) => existingMigrationFiles.has(slug));
1861
+ const existingAuthArtifacts = [
1862
+ authConfigPath,
1863
+ userModelPath,
1864
+ ...AUTH_MIGRATION_SLUGS.map((slug) => existingMigrationFiles.get(slug))
1865
+ ].filter((value) => typeof value === "string");
1866
+ if (authConfigPath && userModelPath && hasAllAuthMigrations) {
1867
+ const envPath2 = resolve(projectRoot, ".env");
1868
+ const envExamplePath2 = resolve(projectRoot, ".env.example");
1869
+ const currentAuthConfig = await readTextFile(authConfigPath) ?? "";
1870
+ const currentAuthFeatures = detectAuthInstallFeaturesFromConfig(currentAuthConfig);
1871
+ const nextAuthFeatures = mergeAuthInstallFeatures(currentAuthFeatures, features);
1872
+ const authConfigModuleFormat = resolveConfigModuleFormat(authConfigPath, currentAuthConfig);
1873
+ const nextAuthConfig = renderAuthConfig(nextAuthFeatures, authConfigModuleFormat);
1874
+ const authEnvFiles2 = renderAuthEnvFiles(nextAuthFeatures, defaultDatabaseConnection);
1875
+ const nextEnv2 = upsertEnvContents(await readTextFile(envPath2), authEnvFiles2.env);
1876
+ const nextEnvExample2 = upsertEnvContents(await readTextFile(envExamplePath2), authEnvFiles2.example);
1877
+ const authConfigChanged = authFeaturesRequireConfigUpdate(features) && currentAuthConfig !== nextAuthConfig;
1878
+ if (authConfigChanged) {
1879
+ if (!canSafelyRewriteAuthConfig(currentAuthConfig, currentAuthFeatures, authConfigModuleFormat)) {
1880
+ throw new Error(
1881
+ `Auth support is already installed in ${projectRoot}, but ${authConfigPath} contains manual changes. Refusing to overwrite the existing auth config automatically.`
1882
+ );
1883
+ }
1884
+ await writeTextFile(authConfigPath, nextAuthConfig);
1885
+ }
1886
+ if (nextEnv2.changed && typeof nextEnv2.contents === "string") {
1887
+ await writeTextFile(envPath2, nextEnv2.contents);
1888
+ }
1889
+ if (nextEnvExample2.changed && typeof nextEnvExample2.contents === "string") {
1890
+ await writeTextFile(envExamplePath2, nextEnvExample2.contents);
1891
+ }
1892
+ await syncBroadcastAuthSupportAfterAuthInstall(projectRoot);
1893
+ return {
1894
+ updatedPackageJson: await upsertAuthPackageDependencies(projectRoot, nextAuthFeatures),
1895
+ createdAuthConfig: authConfigChanged,
1896
+ createdSessionConfig: false,
1897
+ createdUserModel: false,
1898
+ createdMigrationFiles: [],
1899
+ updatedEnv: nextEnv2.changed,
1900
+ updatedEnvExample: nextEnvExample2.changed
1901
+ };
1902
+ }
1903
+ const collisions = sessionConfigPath && existingAuthArtifacts.length === 0 ? [] : [
1904
+ ...existingAuthArtifacts,
1905
+ ...sessionConfigPath && existingAuthArtifacts.length > 0 ? [sessionConfigPath] : []
1906
+ ];
1907
+ if (collisions.length > 0) {
1908
+ throw new Error(
1909
+ `Auth support is partially installed. Refusing to overwrite existing files in ${projectRoot}: ${collisions.join(", ")}`
1910
+ );
1911
+ }
1912
+ const authConfigTargetPath = resolve(projectRoot, "config/auth.ts");
1913
+ const sessionConfigTargetPath = resolve(projectRoot, "config/session.ts");
1914
+ const userModelTargetPath = resolve(modelsRoot, "User.ts");
1915
+ const generatedSchemaPath = resolveGeneratedSchemaPath(projectRoot, project.config);
1916
+ const migrationFiles = createAuthMigrationFiles();
1917
+ const authEnvFiles = renderAuthEnvFiles(features, defaultDatabaseConnection);
1918
+ await mkdir(resolve(projectRoot, "config"), { recursive: true });
1919
+ await mkdir(modelsRoot, { recursive: true });
1920
+ await mkdir(migrationsRoot, { recursive: true });
1921
+ await ensureRedisConfigFile(projectRoot);
1922
+ await writeTextFile(authConfigTargetPath, renderAuthConfig(features));
1923
+ if (!sessionConfigPath) {
1924
+ await writeTextFile(sessionConfigTargetPath, renderSessionConfig(defaultDatabaseConnection));
1925
+ }
1926
+ await writeTextFile(
1927
+ userModelTargetPath,
1928
+ renderAuthUserModel(resolveAuthUserModelSchemaImportPath(
1929
+ userModelTargetPath,
1930
+ generatedSchemaPath
1931
+ ))
1932
+ );
1933
+ const createdMigrationFiles = [];
1934
+ for (const migrationFile of migrationFiles) {
1935
+ const migrationPath = resolve(migrationsRoot, migrationFile.path);
1936
+ await writeTextFile(migrationPath, migrationFile.contents);
1937
+ createdMigrationFiles.push(migrationPath);
1938
+ }
1939
+ const envPath = resolve(projectRoot, ".env");
1940
+ const envExamplePath = resolve(projectRoot, ".env.example");
1941
+ const nextEnv = upsertEnvContents(await readTextFile(envPath), authEnvFiles.env);
1942
+ const nextEnvExample = upsertEnvContents(await readTextFile(envExamplePath), authEnvFiles.example);
1943
+ if (nextEnv.changed && typeof nextEnv.contents === "string") {
1944
+ await writeTextFile(envPath, nextEnv.contents);
1945
+ }
1946
+ if (nextEnvExample.changed && typeof nextEnvExample.contents === "string") {
1947
+ await writeTextFile(envExamplePath, nextEnvExample.contents);
1948
+ }
1949
+ await syncBroadcastAuthSupportAfterAuthInstall(projectRoot);
1950
+ return {
1951
+ updatedPackageJson: await upsertAuthPackageDependencies(projectRoot, features),
1952
+ createdAuthConfig: true,
1953
+ createdSessionConfig: !sessionConfigPath,
1954
+ createdUserModel: true,
1955
+ createdMigrationFiles,
1956
+ updatedEnv: nextEnv.changed,
1957
+ updatedEnvExample: nextEnvExample.changed
1958
+ };
1959
+ }
1960
+ async function installAuthorizationIntoProject(projectRoot) {
1961
+ await loadProjectConfig(projectRoot, { required: true });
1962
+ const policiesRoot = resolve(projectRoot, "server/policies");
1963
+ const abilitiesRoot = resolve(projectRoot, "server/abilities");
1964
+ const policiesDirectoryExists = await pathExists(policiesRoot);
1965
+ const abilitiesDirectoryExists = await pathExists(abilitiesRoot);
1966
+ const policiesReadmePath = resolve(policiesRoot, "README.md");
1967
+ const abilitiesReadmePath = resolve(abilitiesRoot, "README.md");
1968
+ const policiesReadmeExists = await pathExists(policiesReadmePath);
1969
+ const abilitiesReadmeExists = await pathExists(abilitiesReadmePath);
1970
+ await mkdir(policiesRoot, { recursive: true });
1971
+ await mkdir(abilitiesRoot, { recursive: true });
1972
+ if (!policiesReadmeExists) {
1973
+ await writeTextFile(policiesReadmePath, renderAuthorizationPoliciesReadme());
1974
+ }
1975
+ if (!abilitiesReadmeExists) {
1976
+ await writeTextFile(abilitiesReadmePath, renderAuthorizationAbilitiesReadme());
1977
+ }
1978
+ return {
1979
+ updatedPackageJson: await upsertAuthorizationPackageDependency(projectRoot),
1980
+ createdPoliciesDirectory: !policiesDirectoryExists,
1981
+ createdAbilitiesDirectory: !abilitiesDirectoryExists,
1982
+ createdPoliciesReadme: !policiesReadmeExists,
1983
+ createdAbilitiesReadme: !abilitiesReadmeExists
1984
+ };
1985
+ }
1986
+ async function installQueueIntoProject(projectRoot, options = {}) {
1987
+ const driver = options.driver ?? "sync";
1988
+ if (!isSupportedQueueInstallerDriver(driver)) {
1989
+ throw new Error(`Unsupported queue driver: ${driver}.`);
1990
+ }
1991
+ const project = await loadProjectConfig(projectRoot);
1992
+ const defaultDatabaseConnection = project.config.database?.defaultConnection ?? "default";
1993
+ const queueConfigPath = await resolveFirstExistingPath(projectRoot, QUEUE_CONFIG_FILE_NAMES) ?? resolve(projectRoot, "config/queue.ts");
1994
+ const queueConfigExists = await pathExists(queueConfigPath);
1995
+ const jobsRoot = resolve(projectRoot, project.config.paths.jobs);
1996
+ const jobsDirectoryExists = await pathExists(jobsRoot);
1997
+ const queueEnvFiles = renderQueueEnvFiles(driver);
1998
+ if (!queueConfigExists) {
1999
+ await writeTextFile(queueConfigPath, renderQueueConfig({
2000
+ driver,
2001
+ defaultDatabaseConnection
2002
+ }));
2003
+ }
2004
+ if (driver === "redis") {
2005
+ await ensureRedisConfigFile(projectRoot);
2006
+ }
2007
+ await mkdir(jobsRoot, { recursive: true });
2008
+ const updatedPackageJson = await upsertQueuePackageDependency(
2009
+ projectRoot,
2010
+ !queueConfigExists || driver !== "sync" ? driver : void 0
2011
+ );
2012
+ const envPath = resolve(projectRoot, ".env");
2013
+ const envExamplePath = resolve(projectRoot, ".env.example");
2014
+ const nextEnv = upsertEnvContents(await readTextFile(envPath), queueEnvFiles.env);
2015
+ const nextEnvExample = upsertEnvContents(await readTextFile(envExamplePath), queueEnvFiles.example);
2016
+ if (nextEnv.changed && typeof nextEnv.contents === "string") {
2017
+ await writeTextFile(envPath, nextEnv.contents);
2018
+ }
2019
+ if (nextEnvExample.changed && typeof nextEnvExample.contents === "string") {
2020
+ await writeTextFile(envExamplePath, nextEnvExample.contents);
2021
+ }
2022
+ return {
2023
+ createdQueueConfig: !queueConfigExists,
2024
+ updatedPackageJson,
2025
+ updatedEnv: nextEnv.changed,
2026
+ updatedEnvExample: nextEnvExample.changed,
2027
+ createdJobsDirectory: !jobsDirectoryExists
2028
+ };
2029
+ }
2030
+ async function installEventsIntoProject(projectRoot) {
2031
+ const project = await loadProjectConfig(projectRoot);
2032
+ const eventsRoot = resolve(projectRoot, project.config.paths.events);
2033
+ const listenersRoot = resolve(projectRoot, project.config.paths.listeners);
2034
+ const eventsDirectoryExists = await pathExists(eventsRoot);
2035
+ const listenersDirectoryExists = await pathExists(listenersRoot);
2036
+ await mkdir(eventsRoot, { recursive: true });
2037
+ await mkdir(listenersRoot, { recursive: true });
2038
+ return {
2039
+ updatedPackageJson: await upsertEventsPackageDependency(projectRoot),
2040
+ createdEventsDirectory: !eventsDirectoryExists,
2041
+ createdListenersDirectory: !listenersDirectoryExists
2042
+ };
2043
+ }
2044
+ async function installNotificationsIntoProject(projectRoot) {
2045
+ const project = await loadProjectConfig(projectRoot);
2046
+ const migrationsRoot = resolve(projectRoot, project.config.paths.migrations);
2047
+ const notificationsConfigPath = await resolveFirstExistingPath(projectRoot, NOTIFICATIONS_CONFIG_FILE_NAMES);
2048
+ const existingMigrationFiles = await resolveExistingNotificationsMigrationFiles(migrationsRoot);
2049
+ await mkdir(resolve(projectRoot, "config"), { recursive: true });
2050
+ await mkdir(migrationsRoot, { recursive: true });
2051
+ if (!notificationsConfigPath) {
2052
+ await writeTextFile(resolve(projectRoot, "config/notifications.ts"), renderNotificationsConfig());
2053
+ }
2054
+ const createdMigrationFiles = [];
2055
+ if (existingMigrationFiles.length === 0) {
2056
+ for (const migrationFile of createNotificationsMigrationFiles()) {
2057
+ const migrationPath = resolve(migrationsRoot, migrationFile.path);
2058
+ await writeTextFile(migrationPath, migrationFile.contents);
2059
+ createdMigrationFiles.push(migrationPath);
2060
+ }
2061
+ }
2062
+ return {
2063
+ updatedPackageJson: await upsertNotificationsPackageDependency(projectRoot),
2064
+ createdNotificationsConfig: !notificationsConfigPath,
2065
+ createdMigrationFiles
2066
+ };
2067
+ }
2068
+ async function installMailIntoProject(projectRoot) {
2069
+ await loadProjectConfig(projectRoot, { required: true });
2070
+ const mailConfigPath = await resolveFirstExistingPath(projectRoot, MAIL_CONFIG_FILE_NAMES);
2071
+ const mailRoot = resolve(projectRoot, "server/mail");
2072
+ const mailDirectoryExists = await pathExists(mailRoot);
2073
+ await mkdir(resolve(projectRoot, "config"), { recursive: true });
2074
+ await mkdir(mailRoot, { recursive: true });
2075
+ if (!mailConfigPath) {
2076
+ await writeTextFile(resolve(projectRoot, "config/mail.ts"), renderMailConfig());
2077
+ }
2078
+ return {
2079
+ updatedPackageJson: await upsertMailPackageDependency(projectRoot),
2080
+ createdMailConfig: !mailConfigPath,
2081
+ createdMailDirectory: !mailDirectoryExists
2082
+ };
2083
+ }
2084
+ async function installSecurityIntoProject(projectRoot) {
2085
+ await loadProjectConfig(projectRoot, { required: true });
2086
+ const securityConfigPath = await resolveFirstExistingPath(projectRoot, SECURITY_CONFIG_FILE_NAMES);
2087
+ await mkdir(resolve(projectRoot, "config"), { recursive: true });
2088
+ await ensureRateLimitStorageIgnore(projectRoot);
2089
+ await ensureRedisConfigFile(projectRoot);
2090
+ if (!securityConfigPath) {
2091
+ await writeTextFile(resolve(projectRoot, "config/security.ts"), renderSecurityConfig());
2092
+ }
2093
+ return {
2094
+ updatedPackageJson: await upsertSecurityPackageDependency(projectRoot),
2095
+ createdSecurityConfig: !securityConfigPath
2096
+ };
2097
+ }
2098
+ async function installCacheIntoProject(projectRoot, options = {}) {
2099
+ const project = await loadProjectConfig(projectRoot, { required: true });
2100
+ const driver = options.driver ?? "file";
2101
+ if (!isSupportedCacheInstallerDriver(driver)) {
2102
+ throw new Error(`Unsupported cache driver: ${driver}.`);
2103
+ }
2104
+ const cacheConfigPath = await resolveFirstExistingPath(projectRoot, CACHE_CONFIG_FILE_NAMES);
2105
+ const loadedConfig = await loadConfigDirectory(projectRoot, {
2106
+ preferCache: false,
2107
+ processEnv: process.env
2108
+ });
2109
+ const defaultDatabaseConnection = project.config.database?.defaultConnection ?? "default";
2110
+ const defaultRedisConnection = loadedConfig.redis.default;
2111
+ const loadedCacheConfig = cacheConfigPath ? loadedConfig.cache : void 0;
2112
+ if (loadedCacheConfig && !Object.values(loadedCacheConfig.drivers).some((entry) => entry.driver === driver)) {
2113
+ throw new Error(
2114
+ `config/cache.ts already exists and does not configure the "${driver}" cache driver. Update your cache config first, then rerun "holo install cache".`
2115
+ );
2116
+ }
2117
+ await mkdir(resolve(projectRoot, "config"), { recursive: true });
2118
+ if (!cacheConfigPath) {
2119
+ await writeTextFile(
2120
+ resolve(projectRoot, "config/cache.ts"),
2121
+ renderCacheConfig(driver, defaultDatabaseConnection, defaultRedisConnection)
2122
+ );
2123
+ }
2124
+ let createdRedisConfig = false;
2125
+ if (driver === "redis") {
2126
+ createdRedisConfig = await ensureRedisConfigFile(projectRoot);
2127
+ }
2128
+ const cacheEnvFiles = renderCacheEnvFiles(driver);
2129
+ const envPath = resolve(projectRoot, ".env");
2130
+ const envExamplePath = resolve(projectRoot, ".env.example");
2131
+ const nextEnv = upsertEnvContents(await readTextFile(envPath), cacheEnvFiles.env);
2132
+ const nextEnvExample = upsertEnvContents(await readTextFile(envExamplePath), cacheEnvFiles.example);
2133
+ if (nextEnv.changed && typeof nextEnv.contents === "string") {
2134
+ await writeTextFile(envPath, nextEnv.contents);
2135
+ }
2136
+ if (nextEnvExample.changed && typeof nextEnvExample.contents === "string") {
2137
+ await writeTextFile(envExamplePath, nextEnvExample.contents);
2138
+ }
2139
+ return {
2140
+ updatedPackageJson: await upsertCachePackageDependencies(projectRoot, driver),
2141
+ createdCacheConfig: !cacheConfigPath,
2142
+ createdRedisConfig,
2143
+ updatedEnv: nextEnv.changed,
2144
+ updatedEnvExample: nextEnvExample.changed,
2145
+ databaseDriver: driver === "database"
2146
+ };
2147
+ }
2148
+ async function installBroadcastIntoProject(projectRoot) {
2149
+ const project = await loadProjectConfig(projectRoot, { required: true });
2150
+ const manifestPath = project.manifestPath;
2151
+ const manifestContents = await readTextFile(manifestPath);
2152
+ const manifestFormat = resolveConfigModuleFormat(manifestPath, manifestContents);
2153
+ const broadcastConfigTargetPath = resolveBroadcastConfigTargetPath(projectRoot, manifestPath, manifestFormat);
2154
+ const broadcastConfigIsTypeScript = [".ts", ".mts", ".cts"].includes(extname(broadcastConfigTargetPath));
2155
+ const broadcastConfigPath = await resolveFirstExistingPath(projectRoot, BROADCAST_CONFIG_FILE_NAMES);
2156
+ const authConfigPath = await resolveFirstExistingPath(projectRoot, AUTH_CONFIG_FILE_NAMES);
2157
+ const { dependencies, devDependencies } = await readPackageJsonDependencyState(projectRoot);
2158
+ const framework = detectProjectFrameworkFromPackageJson(dependencies, devDependencies);
2159
+ const canCreateBroadcastAuthRoute = framework === "next" || framework === "nuxt" || framework === "sveltekit";
2160
+ const broadcastRoot = resolve(projectRoot, "server/broadcast");
2161
+ const channelsRoot = resolve(projectRoot, "server/channels");
2162
+ const broadcastDirectoryExists = await pathExists(broadcastRoot);
2163
+ const channelsDirectoryExists = await pathExists(channelsRoot);
2164
+ await mkdir(resolve(projectRoot, "config"), { recursive: true });
2165
+ await mkdir(broadcastRoot, { recursive: true });
2166
+ await mkdir(channelsRoot, { recursive: true });
2167
+ await ensureRedisConfigFile(projectRoot);
2168
+ if (!broadcastConfigPath) {
2169
+ await writeTextFile(
2170
+ broadcastConfigTargetPath,
2171
+ renderBroadcastConfig(
2172
+ manifestFormat,
2173
+ Boolean(authConfigPath) && canCreateBroadcastAuthRoute,
2174
+ broadcastConfigIsTypeScript
2175
+ )
2176
+ );
2177
+ }
2178
+ const broadcastEnvFiles = renderBroadcastEnvFiles();
2179
+ const envPath = resolve(projectRoot, ".env");
2180
+ const envExamplePath = resolve(projectRoot, ".env.example");
2181
+ const nextEnv = upsertEnvContents(await readTextFile(envPath), broadcastEnvFiles.env);
2182
+ const nextEnvExample = upsertEnvContents(await readTextFile(envExamplePath), broadcastEnvFiles.example);
2183
+ const dependencyResult = await upsertBroadcastPackageDependencies(projectRoot);
2184
+ let createdFrameworkSetup = false;
2185
+ if (framework === "next") {
2186
+ const holoHelperPath = resolve(projectRoot, "server/holo.ts");
2187
+ if (!await pathExists(holoHelperPath)) {
2188
+ await writeTextFile(holoHelperPath, renderNextHoloHelper());
2189
+ createdFrameworkSetup = true;
2190
+ }
2191
+ } else if (framework === "nuxt") {
2192
+ } else if (framework === "sveltekit") {
2193
+ const holoHelperPath = resolve(projectRoot, "src/lib/server/holo.ts");
2194
+ if (!await pathExists(holoHelperPath)) {
2195
+ await writeTextFile(holoHelperPath, renderSvelteHoloHelper());
2196
+ createdFrameworkSetup = true;
2197
+ }
2198
+ }
2199
+ const broadcastAuthSupport = await syncBroadcastAuthSupportAfterAuthInstall(projectRoot);
2200
+ if (nextEnv.changed && typeof nextEnv.contents === "string") {
2201
+ await writeTextFile(envPath, nextEnv.contents);
2202
+ }
2203
+ if (nextEnvExample.changed && typeof nextEnvExample.contents === "string") {
2204
+ await writeTextFile(envExamplePath, nextEnvExample.contents);
2205
+ }
2206
+ return {
2207
+ updatedPackageJson: dependencyResult.updated,
2208
+ createdBroadcastConfig: !broadcastConfigPath,
2209
+ createdBroadcastDirectory: !broadcastDirectoryExists,
2210
+ createdChannelsDirectory: !channelsDirectoryExists,
2211
+ createdBroadcastAuthRoute: broadcastAuthSupport.createdBroadcastAuthRoute,
2212
+ createdFrameworkSetup,
2213
+ updatedEnv: nextEnv.changed,
2214
+ updatedEnvExample: nextEnvExample.changed
2215
+ };
2216
+ }
2217
+ function renderScaffoldGitignore() {
2218
+ return [
2219
+ "node_modules",
2220
+ ".env",
2221
+ ".env.local",
2222
+ ".env.development",
2223
+ ".env.production",
2224
+ ".env.prod",
2225
+ ".env.test",
2226
+ ".holo-js/generated",
2227
+ ".holo-js/runtime",
2228
+ ".nuxt",
2229
+ ".output",
2230
+ ".next",
2231
+ ".svelte-kit",
2232
+ "coverage",
2233
+ "dist",
2234
+ ""
2235
+ ].join("\n");
2236
+ }
2237
+ function renderScaffoldTsconfig(options) {
2238
+ if (options.framework === "nuxt") {
2239
+ return `${JSON.stringify({
2240
+ extends: "./.nuxt/tsconfig.json",
2241
+ compilerOptions: {
2242
+ strict: true,
2243
+ noEmit: true,
2244
+ skipLibCheck: true
2245
+ }
2246
+ }, null, 2)}
2247
+ `;
2248
+ }
2249
+ if (options.framework === "sveltekit") {
2250
+ return `${JSON.stringify({
2251
+ extends: "./.svelte-kit/tsconfig.json",
2252
+ compilerOptions: {
2253
+ strict: true,
2254
+ noEmit: true,
2255
+ skipLibCheck: true
2256
+ },
2257
+ include: [
2258
+ "src/**/*.ts",
2259
+ "src/**/*.svelte",
2260
+ "server/**/*.ts",
2261
+ "config/**/*.ts",
2262
+ ".holo-js/generated/**/*.ts",
2263
+ ".holo-js/generated/**/*.d.ts",
2264
+ "vite.config.ts"
2265
+ ]
2266
+ }, null, 2)}
2267
+ `;
2268
+ }
2269
+ const include = ["next-env.d.ts", "app/**/*.ts", "app/**/*.tsx", "server/**/*.ts", "config/**/*.ts", ".holo-js/generated/**/*.ts", ".holo-js/generated/**/*.d.ts"];
2270
+ return `${JSON.stringify({
2271
+ compilerOptions: {
2272
+ target: "ES2022",
2273
+ module: "ESNext",
2274
+ moduleResolution: "Bundler",
2275
+ strict: true,
2276
+ noEmit: true,
2277
+ skipLibCheck: true,
2278
+ baseUrl: ".",
2279
+ jsx: "preserve",
2280
+ paths: {
2281
+ "~/*": ["./*"],
2282
+ "@/*": ["./*"]
2283
+ }
2284
+ },
2285
+ include
2286
+ }, null, 2)}
2287
+ `;
2288
+ }
2289
+ function renderNuxtAppVue(projectName) {
2290
+ return [
2291
+ "<template>",
2292
+ ' <main class="shell">',
2293
+ ` <h1>${projectName}</h1>`,
2294
+ " <p>Nuxt renders the UI. Holo owns the backend runtime and canonical server directories.</p>",
2295
+ " </main>",
2296
+ "</template>",
2297
+ "",
2298
+ "<style scoped>",
2299
+ ".shell {",
2300
+ " min-height: 100vh;",
2301
+ " display: grid;",
2302
+ " place-content: center;",
2303
+ " gap: 1rem;",
2304
+ " padding: 3rem;",
2305
+ " font-family: sans-serif;",
2306
+ "}",
2307
+ "h1 {",
2308
+ " margin: 0;",
2309
+ " font-size: clamp(2.5rem, 6vw, 4rem);",
2310
+ "}",
2311
+ "p {",
2312
+ " margin: 0;",
2313
+ " max-width: 40rem;",
2314
+ " line-height: 1.6;",
2315
+ "}",
2316
+ "</style>",
2317
+ ""
2318
+ ].join("\n");
2319
+ }
2320
+ function renderNuxtConfig() {
2321
+ return [
2322
+ "export default defineNuxtConfig({",
2323
+ " modules: ['@holo-js/adapter-nuxt'],",
2324
+ " typescript: {",
2325
+ " strict: true,",
2326
+ " },",
2327
+ "})",
2328
+ ""
2329
+ ].join("\n");
2330
+ }
2331
+ function renderNuxtHealthRoute() {
2332
+ return [
2333
+ "export default defineEventHandler(async () => {",
2334
+ " const app = await holo.getApp()",
2335
+ "",
2336
+ " return {",
2337
+ " ok: true,",
2338
+ " app: app.config.app.name,",
2339
+ " env: app.config.app.env,",
2340
+ " models: app.registry?.models.length ?? 0,",
2341
+ " commands: app.registry?.commands.length ?? 0,",
2342
+ " }",
2343
+ "})",
2344
+ ""
2345
+ ].join("\n");
2346
+ }
2347
+ function renderNextConfig(storageEnabled) {
2348
+ if (!storageEnabled) {
2349
+ return [
2350
+ "/** @type {import('next').NextConfig} */",
2351
+ "const nextConfig = {}",
2352
+ "",
2353
+ "export default nextConfig",
2354
+ ""
2355
+ ].join("\n");
2356
+ }
2357
+ return [
2358
+ "const storageRoutePrefix = (() => {",
2359
+ " const raw = process.env.STORAGE_ROUTE_PREFIX?.trim() ?? '/storage'",
2360
+ " if (!raw || raw === '/') {",
2361
+ " return '/storage'",
2362
+ " }",
2363
+ "",
2364
+ " return `/${raw.replace(/^\\/+|\\/+$/g, '')}`",
2365
+ "})()",
2366
+ "",
2367
+ "/** @type {import('next').NextConfig} */",
2368
+ "const nextConfig = {",
2369
+ " async rewrites() {",
2370
+ " if (storageRoutePrefix === '/storage') {",
2371
+ " return []",
2372
+ " }",
2373
+ "",
2374
+ " return [",
2375
+ " {",
2376
+ " source: `${storageRoutePrefix}/:path*`,",
2377
+ " destination: '/storage/:path*',",
2378
+ " },",
2379
+ " ]",
2380
+ " },",
2381
+ "}",
2382
+ "",
2383
+ "export default nextConfig",
2384
+ ""
2385
+ ].join("\n");
2386
+ }
2387
+ function renderNextLayout(projectName) {
2388
+ return [
2389
+ "import type { ReactNode } from 'react'",
2390
+ "",
2391
+ "export const metadata = {",
2392
+ ` title: ${JSON.stringify(projectName)},`,
2393
+ " description: 'Holo on Next.js',",
2394
+ "}",
2395
+ "",
2396
+ "export default function RootLayout({ children }: { children: ReactNode }) {",
2397
+ " return (",
2398
+ ' <html lang="en">',
2399
+ " <body>{children}</body>",
2400
+ " </html>",
2401
+ " )",
2402
+ "}",
2403
+ ""
2404
+ ].join("\n");
2405
+ }
2406
+ function renderNextPage(projectName) {
2407
+ return [
2408
+ "export default function HomePage() {",
2409
+ " return (",
2410
+ " <main style={{ padding: '3rem', fontFamily: 'sans-serif' }}>",
2411
+ ` <h1>${projectName}</h1>`,
2412
+ " <p>Next.js handles rendering. Holo powers the backend runtime and discovered server resources.</p>",
2413
+ " </main>",
2414
+ " )",
2415
+ "}",
2416
+ ""
2417
+ ].join("\n");
2418
+ }
2419
+ function renderNextEnvDts() {
2420
+ return [
2421
+ '/// <reference types="next" />',
2422
+ '/// <reference types="next/image-types/global" />',
2423
+ "",
2424
+ "// Generated by Holo. Do not edit.",
2425
+ ""
2426
+ ].join("\n");
2427
+ }
2428
+ function renderNextHoloHelper() {
2429
+ return [
2430
+ "import { createNextHoloHelpers } from '@holo-js/adapter-next'",
2431
+ "",
2432
+ "export const holo = createNextHoloHelpers()",
2433
+ ""
2434
+ ].join("\n");
2435
+ }
2436
+ function renderPublicStorageHelper() {
2437
+ return [
2438
+ "import { readFile, realpath } from 'node:fs/promises'",
2439
+ "import { extname, resolve, sep } from 'node:path'",
2440
+ "import { normalizeModuleOptions, type RuntimeDiskConfig, type HoloStorageRuntimeConfig } from '@holo-js/storage'",
2441
+ "import type { NormalizedHoloStorageConfig } from '@holo-js/config'",
2442
+ "",
2443
+ "const NAMED_PUBLIC_DISK_ROUTE_SEGMENT = '__holo'",
2444
+ "",
2445
+ "type PublicLocalDisk = RuntimeDiskConfig & {",
2446
+ " driver: 'local' | 'public'",
2447
+ " visibility: 'public'",
2448
+ " root: string",
2449
+ "}",
2450
+ "",
2451
+ "type ResolvedPublicStorageRequest = {",
2452
+ " disk: PublicLocalDisk",
2453
+ " absolutePath: string",
2454
+ "}",
2455
+ "",
2456
+ "function normalizeRequestPath(value: string): string[] {",
2457
+ " return value",
2458
+ " .split('/')",
2459
+ " .map((segment) => {",
2460
+ " const trimmed = segment.trim()",
2461
+ " if (!trimmed) {",
2462
+ " return trimmed",
2463
+ " }",
2464
+ "",
2465
+ " try {",
2466
+ " return decodeURIComponent(trimmed)",
2467
+ " } catch {",
2468
+ " return trimmed",
2469
+ " }",
2470
+ " })",
2471
+ " .filter(Boolean)",
2472
+ "}",
2473
+ "",
2474
+ "function isPublicLocalDisk(disk: RuntimeDiskConfig | undefined): disk is PublicLocalDisk {",
2475
+ " return Boolean(disk && disk.visibility === 'public' && disk.driver !== 's3' && typeof disk.root === 'string')",
2476
+ "}",
2477
+ "",
2478
+ "function resolveAbsolutePath(projectRoot: string, disk: PublicLocalDisk, fileSegments: string[]): string | null {",
2479
+ " const root = resolve(projectRoot, disk.root)",
2480
+ " const absolutePath = resolve(root, ...fileSegments)",
2481
+ " if (absolutePath !== root && !absolutePath.startsWith(`${root}${sep}`)) {",
2482
+ " return null",
2483
+ " }",
2484
+ "",
2485
+ " return absolutePath",
2486
+ "}",
2487
+ "",
2488
+ "function resolveContentType(absolutePath: string): string {",
2489
+ " switch (extname(absolutePath).toLowerCase()) {",
2490
+ " case '.avif': return 'image/avif'",
2491
+ " case '.css': return 'text/css; charset=utf-8'",
2492
+ " case '.gif': return 'image/gif'",
2493
+ " case '.html': return 'text/html; charset=utf-8'",
2494
+ " case '.jpeg':",
2495
+ " case '.jpg': return 'image/jpeg'",
2496
+ " case '.js':",
2497
+ " case '.mjs': return 'text/javascript; charset=utf-8'",
2498
+ " case '.json': return 'application/json; charset=utf-8'",
2499
+ " case '.mp3': return 'audio/mpeg'",
2500
+ " case '.pdf': return 'application/pdf'",
2501
+ " case '.png': return 'image/png'",
2502
+ " case '.svg': return 'image/svg+xml'",
2503
+ " case '.txt': return 'text/plain; charset=utf-8'",
2504
+ " case '.webp': return 'image/webp'",
2505
+ " case '.woff': return 'font/woff'",
2506
+ " case '.woff2': return 'font/woff2'",
2507
+ " default: return 'application/octet-stream'",
2508
+ " }",
2509
+ "}",
2510
+ "",
2511
+ "function createMissingFileResponse(): Response {",
2512
+ " return new Response('Storage file not found.', { status: 404 })",
2513
+ "}",
2514
+ "",
2515
+ "function resolveRouteSegments(routePath: string): string[] | null {",
2516
+ " const segments = normalizeRequestPath(routePath)",
2517
+ " if (segments.length === 0 || segments.includes('..')) {",
2518
+ " return null",
2519
+ " }",
2520
+ "",
2521
+ " return segments",
2522
+ "}",
2523
+ "",
2524
+ "function resolveDefaultPublicStorageRequest(projectRoot: string, config: HoloStorageRuntimeConfig, segments: string[]): ResolvedPublicStorageRequest | null {",
2525
+ " const disk = isPublicLocalDisk(config.disks.public) ? config.disks.public : undefined",
2526
+ " if (!disk) {",
2527
+ " return null",
2528
+ " }",
2529
+ "",
2530
+ " const absolutePath = resolveAbsolutePath(projectRoot, disk, segments)",
2531
+ " return absolutePath ? { disk, absolutePath } : null",
2532
+ "}",
2533
+ "",
2534
+ "function usesReservedNamedDiskNamespace(segments: string[]): boolean {",
2535
+ " return segments[0] === NAMED_PUBLIC_DISK_ROUTE_SEGMENT",
2536
+ "}",
2537
+ "",
2538
+ "function resolveNamedPublicStorageRequest(projectRoot: string, config: HoloStorageRuntimeConfig, segments: string[]): ResolvedPublicStorageRequest | null {",
2539
+ " const namedPublicDisks = Object.values(config.disks).filter((disk): disk is PublicLocalDisk => isPublicLocalDisk(disk) && disk.name !== 'public')",
2540
+ " const usesReservedNamespace = segments[0] === NAMED_PUBLIC_DISK_ROUTE_SEGMENT",
2541
+ " const diskName = usesReservedNamespace ? segments[1] : segments[0]",
2542
+ " const disk = diskName ? namedPublicDisks.find(candidate => candidate.name === diskName) : undefined",
2543
+ " if (!disk) {",
2544
+ " return null",
2545
+ " }",
2546
+ "",
2547
+ " const fileSegments = usesReservedNamespace ? segments.slice(2) : segments.slice(1)",
2548
+ " if (fileSegments.length === 0) {",
2549
+ " return null",
2550
+ " }",
2551
+ "",
2552
+ " const absolutePath = resolveAbsolutePath(projectRoot, disk, fileSegments)",
2553
+ " return absolutePath ? { disk, absolutePath } : null",
2554
+ "}",
2555
+ "",
2556
+ "function resolvePublicStorageRequest(projectRoot: string, config: HoloStorageRuntimeConfig, routePath: string): ResolvedPublicStorageRequest | null {",
2557
+ " const segments = resolveRouteSegments(routePath)",
2558
+ " if (!segments) {",
2559
+ " return null",
2560
+ " }",
2561
+ "",
2562
+ " if (usesReservedNamedDiskNamespace(segments)) {",
2563
+ " return resolveNamedPublicStorageRequest(projectRoot, config, segments) ?? resolveDefaultPublicStorageRequest(projectRoot, config, segments)",
2564
+ " }",
2565
+ "",
2566
+ " return resolveDefaultPublicStorageRequest(projectRoot, config, segments) ?? resolveNamedPublicStorageRequest(projectRoot, config, segments)",
2567
+ "}",
2568
+ "",
2569
+ "function resolveFallbackPublicStorageRequest(projectRoot: string, config: HoloStorageRuntimeConfig, segments: string[], attemptedDiskName: string): ResolvedPublicStorageRequest | null {",
2570
+ " if (usesReservedNamedDiskNamespace(segments)) {",
2571
+ " return null",
2572
+ " }",
2573
+ "",
2574
+ " const candidates = [",
2575
+ " resolveDefaultPublicStorageRequest(projectRoot, config, segments),",
2576
+ " resolveNamedPublicStorageRequest(projectRoot, config, segments),",
2577
+ " ]",
2578
+ "",
2579
+ " return candidates.find(candidate => candidate && candidate.disk.name !== attemptedDiskName) ?? null",
2580
+ "}",
2581
+ "",
2582
+ "export async function createPublicStorageResponse(projectRoot: string, storageConfig: NormalizedHoloStorageConfig, request: Request): Promise<Response> {",
2583
+ " const normalized = normalizeModuleOptions({",
2584
+ " defaultDisk: storageConfig.defaultDisk,",
2585
+ " routePrefix: storageConfig.routePrefix,",
2586
+ " disks: storageConfig.disks,",
2587
+ " })",
2588
+ " const pathname = new URL(request.url).pathname",
2589
+ " const routePath = pathname.startsWith(normalized.routePrefix) ? pathname.slice(normalized.routePrefix.length) : pathname",
2590
+ " const segments = resolveRouteSegments(routePath)",
2591
+ "",
2592
+ " if (!segments) {",
2593
+ " return createMissingFileResponse()",
2594
+ " }",
2595
+ "",
2596
+ " const resolvedRequest = resolvePublicStorageRequest(projectRoot, normalized, routePath)",
2597
+ " if (!resolvedRequest) {",
2598
+ " return createMissingFileResponse()",
2599
+ " }",
2600
+ "",
2601
+ " const tryRead = async (entry: ResolvedPublicStorageRequest): Promise<Response | null> => {",
2602
+ " try {",
2603
+ " const resolvedRoot = await realpath(resolve(projectRoot, entry.disk.root))",
2604
+ " const resolvedPath = await realpath(entry.absolutePath)",
2605
+ " if (resolvedPath !== resolvedRoot && !resolvedPath.startsWith(`${resolvedRoot}${sep}`)) {",
2606
+ " return null",
2607
+ " }",
2608
+ "",
2609
+ " const contents = await readFile(entry.absolutePath)",
2610
+ " return new Response(contents, {",
2611
+ " status: 200,",
2612
+ " headers: { 'content-type': resolveContentType(entry.absolutePath) },",
2613
+ " })",
2614
+ " } catch {",
2615
+ " return null",
2616
+ " }",
2617
+ " }",
2618
+ "",
2619
+ " const primary = await tryRead(resolvedRequest)",
2620
+ " if (primary) {",
2621
+ " return primary",
2622
+ " }",
2623
+ "",
2624
+ " const fallback = resolveFallbackPublicStorageRequest(projectRoot, normalized, segments, resolvedRequest.disk.name)",
2625
+ " return fallback ? ((await tryRead(fallback)) ?? createMissingFileResponse()) : createMissingFileResponse()",
2626
+ "}",
2627
+ ""
2628
+ ].join("\n");
2629
+ }
2630
+ function renderNextHealthRoute() {
2631
+ return [
2632
+ "import { holo } from '@/server/holo'",
2633
+ "",
2634
+ "export async function GET() {",
2635
+ " const app = await holo.getApp()",
2636
+ "",
2637
+ " return Response.json({",
2638
+ " ok: true,",
2639
+ " app: app.config.app.name,",
2640
+ " env: app.config.app.env,",
2641
+ " models: app.registry?.models.length ?? 0,",
2642
+ " commands: app.registry?.commands.length ?? 0,",
2643
+ " })",
2644
+ "}",
2645
+ ""
2646
+ ].join("\n");
2647
+ }
2648
+ function renderNextStorageRoute() {
2649
+ return [
2650
+ "import { holo } from '@/server/holo'",
2651
+ "import { createPublicStorageResponse } from '@/server/lib/public-storage'",
2652
+ "",
2653
+ "export async function GET(request: Request) {",
2654
+ " const app = await holo.getApp()",
2655
+ " return createPublicStorageResponse(app.projectRoot, app.config.storage, request)",
2656
+ "}",
2657
+ ""
2658
+ ].join("\n");
2659
+ }
2660
+ function renderSvelteConfig() {
2661
+ return [
2662
+ "import adapter from '@sveltejs/adapter-node'",
2663
+ "import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'",
2664
+ "",
2665
+ "/** @type {import('@sveltejs/kit').Config} */",
2666
+ "const config = {",
2667
+ " preprocess: vitePreprocess(),",
2668
+ " kit: {",
2669
+ " adapter: adapter(),",
2670
+ " },",
2671
+ "}",
2672
+ "",
2673
+ "export default config",
2674
+ ""
2675
+ ].join("\n");
2676
+ }
2677
+ function renderSvelteHooksServer() {
2678
+ return [
2679
+ "import type { Handle } from '@sveltejs/kit'",
2680
+ "import { env } from '$env/dynamic/private'",
2681
+ "",
2682
+ "function normalizeStorageRoutePrefix(value: string | undefined): string {",
2683
+ " const raw = value?.trim() ?? '/storage'",
2684
+ " if (!raw || raw === '/') {",
2685
+ " return '/storage'",
2686
+ " }",
2687
+ "",
2688
+ " return `/${raw.replace(/^\\/+|\\/+$/g, '')}`",
2689
+ "}",
2690
+ "",
2691
+ "export const handle: Handle = async ({ event, resolve }) => {",
2692
+ " const storageRoutePrefix = normalizeStorageRoutePrefix(env.STORAGE_ROUTE_PREFIX)",
2693
+ "",
2694
+ " if (storageRoutePrefix !== '/storage') {",
2695
+ " const pathname = event.url.pathname",
2696
+ " if (pathname === storageRoutePrefix || pathname.startsWith(`${storageRoutePrefix}/`)) {",
2697
+ " event.url.pathname = `/storage${pathname.slice(storageRoutePrefix.length)}` || '/storage'",
2698
+ " }",
2699
+ " }",
2700
+ "",
2701
+ " return resolve(event)",
2702
+ "}",
2703
+ ""
2704
+ ].join("\n");
2705
+ }
2706
+ function renderSvelteViteConfig(storageEnabled) {
2707
+ const externals = [
2708
+ " '@holo-js/adapter-sveltekit',",
2709
+ " '@holo-js/config',",
2710
+ " '@holo-js/core',",
2711
+ " '@holo-js/db',",
2712
+ ...storageEnabled ? [
2713
+ " '@holo-js/storage',",
2714
+ " '@holo-js/storage/runtime',"
2715
+ ] : [],
2716
+ " 'better-sqlite3',"
2717
+ ];
2718
+ return [
2719
+ "import { sveltekit } from '@sveltejs/kit/vite'",
2720
+ "import { defineConfig } from 'vite'",
2721
+ "",
2722
+ "export default defineConfig({",
2723
+ " plugins: [sveltekit()],",
2724
+ " ssr: {",
2725
+ " external: [",
2726
+ ...externals,
2727
+ " ],",
2728
+ " },",
2729
+ "})",
2730
+ ""
2731
+ ].join("\n");
2732
+ }
2733
+ function renderSvelteAppHtml() {
2734
+ return [
2735
+ "<!doctype html>",
2736
+ '<html lang="en">',
2737
+ " <head>",
2738
+ ' <meta charset="utf-8" />',
2739
+ ' <meta name="viewport" content="width=device-width, initial-scale=1" />',
2740
+ " %sveltekit.head%",
2741
+ " </head>",
2742
+ ' <body data-sveltekit-preload-data="hover">',
2743
+ ' <div style="display: contents">%sveltekit.body%</div>',
2744
+ " </body>",
2745
+ "</html>",
2746
+ ""
2747
+ ].join("\n");
2748
+ }
2749
+ function renderSveltePage(projectName) {
2750
+ return [
2751
+ `<svelte:head><title>${projectName}</title></svelte:head>`,
2752
+ "",
2753
+ '<script lang="ts">',
2754
+ ` const projectName = ${JSON.stringify(projectName)}`,
2755
+ "</script>",
2756
+ "",
2757
+ '<main class="shell">',
2758
+ " <h1>{projectName}</h1>",
2759
+ " <p>SvelteKit owns rendering. Holo owns config, discovery, and backend runtime services.</p>",
2760
+ "</main>",
2761
+ "",
2762
+ "<style>",
2763
+ " .shell {",
2764
+ " min-height: 100vh;",
2765
+ " display: grid;",
2766
+ " place-content: center;",
2767
+ " gap: 1rem;",
2768
+ " padding: 3rem;",
2769
+ " font-family: sans-serif;",
2770
+ " }",
2771
+ " h1 {",
2772
+ " margin: 0;",
2773
+ " font-size: clamp(2.5rem, 6vw, 4rem);",
2774
+ " }",
2775
+ " p {",
2776
+ " margin: 0;",
2777
+ " max-width: 40rem;",
2778
+ " line-height: 1.6;",
2779
+ " }",
2780
+ "</style>",
2781
+ ""
2782
+ ].join("\n");
2783
+ }
2784
+ function renderSvelteHoloHelper() {
2785
+ return [
2786
+ "import { createSvelteKitHoloHelpers } from '@holo-js/adapter-sveltekit'",
2787
+ "",
2788
+ "export const holo = createSvelteKitHoloHelpers()",
2789
+ ""
2790
+ ].join("\n");
2791
+ }
2792
+ function renderSvelteHealthRoute() {
2793
+ return [
2794
+ "import { json } from '@sveltejs/kit'",
2795
+ "import { holo } from '$lib/server/holo'",
2796
+ "",
2797
+ "export async function GET() {",
2798
+ " const app = await holo.getApp()",
2799
+ "",
2800
+ " return json({",
2801
+ " ok: true,",
2802
+ " app: app.config.app.name,",
2803
+ " env: app.config.app.env,",
2804
+ " models: app.registry?.models.length ?? 0,",
2805
+ " commands: app.registry?.commands.length ?? 0,",
2806
+ " })",
2807
+ "}",
2808
+ ""
2809
+ ].join("\n");
2810
+ }
2811
+ function renderSvelteStorageRoute() {
2812
+ return [
2813
+ "import { holo } from '$lib/server/holo'",
2814
+ "import { createPublicStorageResponse } from '../../../../server/lib/public-storage'",
2815
+ "",
2816
+ "export async function GET({ request }: { request: Request }) {",
2817
+ " const app = await holo.getApp()",
2818
+ " return createPublicStorageResponse(app.projectRoot, app.config.storage, request)",
2819
+ "}",
2820
+ ""
2821
+ ].join("\n");
2822
+ }
2823
+ function renderFrameworkFiles(options) {
2824
+ const optionalPackages = normalizeScaffoldOptionalPackages(options.optionalPackages);
2825
+ const storageEnabled = optionalPackages.includes("storage");
2826
+ if (options.framework === "nuxt") {
2827
+ return [
2828
+ { path: "app.vue", contents: renderNuxtAppVue(options.projectName) },
2829
+ { path: "nuxt.config.ts", contents: renderNuxtConfig() },
2830
+ { path: "server/api/holo/health.get.ts", contents: renderNuxtHealthRoute() }
2831
+ ];
2832
+ }
2833
+ if (options.framework === "next") {
2834
+ return [
2835
+ { path: "next.config.mjs", contents: renderNextConfig(storageEnabled) },
2836
+ { path: "next-env.d.ts", contents: renderNextEnvDts() },
2837
+ { path: "app/layout.tsx", contents: renderNextLayout(options.projectName) },
2838
+ { path: "app/page.tsx", contents: renderNextPage(options.projectName) },
2839
+ { path: "app/api/holo/health/route.ts", contents: renderNextHealthRoute() },
2840
+ ...storageEnabled ? [
2841
+ { path: "app/storage/[[...path]]/route.ts", contents: renderNextStorageRoute() },
2842
+ { path: "server/lib/public-storage.ts", contents: renderPublicStorageHelper() }
2843
+ ] : [],
2844
+ { path: "server/holo.ts", contents: renderNextHoloHelper() }
2845
+ ];
2846
+ }
2847
+ return [
2848
+ { path: "svelte.config.js", contents: renderSvelteConfig() },
2849
+ { path: "vite.config.ts", contents: renderSvelteViteConfig(storageEnabled) },
2850
+ ...storageEnabled ? [{ path: "src/hooks.server.ts", contents: renderSvelteHooksServer() }] : [],
2851
+ { path: "src/app.html", contents: renderSvelteAppHtml() },
2852
+ { path: "src/routes/+page.svelte", contents: renderSveltePage(options.projectName) },
2853
+ { path: "src/routes/api/holo/+server.ts", contents: renderSvelteHealthRoute() },
2854
+ ...storageEnabled ? [{ path: "src/routes/storage/[...path]/+server.ts", contents: renderSvelteStorageRoute() }] : [],
2855
+ { path: "src/lib/server/holo.ts", contents: renderSvelteHoloHelper() },
2856
+ ...storageEnabled ? [{ path: "server/lib/public-storage.ts", contents: renderPublicStorageHelper() }] : []
2857
+ ];
2858
+ }
2859
+ function renderFrameworkRunner(options) {
2860
+ const commandName = options.framework === "nuxt" ? "nuxi" : options.framework === "next" ? "next" : "vite";
2861
+ return [
2862
+ "import { existsSync, readFileSync } from 'node:fs'",
2863
+ "import { dirname, resolve } from 'node:path'",
2864
+ "import { fileURLToPath } from 'node:url'",
2865
+ "import { spawn } from 'node:child_process'",
2866
+ "",
2867
+ "const mode = process.argv[2]",
2868
+ "const manifestPath = fileURLToPath(new URL('./project.json', import.meta.url))",
2869
+ "const projectRoot = resolve(dirname(manifestPath), '../..')",
2870
+ "const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'))",
2871
+ "const framework = String(manifest.framework ?? '')",
2872
+ `const commandName = ${JSON.stringify(commandName)}`,
2873
+ "const commandArgs = mode === 'dev'",
2874
+ " ? ['dev']",
2875
+ " : mode === 'build'",
2876
+ " ? framework === 'sveltekit' ? ['build', '--logLevel', 'error'] : ['build']",
2877
+ " : undefined",
2878
+ "",
2879
+ "if (!commandArgs) {",
2880
+ " console.error(`[holo] Unknown framework runner mode: ${String(mode)}`)",
2881
+ " process.exit(1)",
2882
+ "}",
2883
+ "",
2884
+ "const binaryPath = resolve(",
2885
+ " projectRoot,",
2886
+ " 'node_modules',",
2887
+ " '.bin',",
2888
+ " process.platform === 'win32' ? `${commandName}.cmd` : commandName,",
2889
+ ")",
2890
+ "",
2891
+ "const suppressedOutput = framework === 'sveltekit'",
2892
+ " ? new Set([",
2893
+ ` '"try_get_request_store" is imported from external module "@sveltejs/kit/internal/server" but never used in ".svelte-kit/adapter-node/index.js".',`,
2894
+ " ])",
2895
+ " : new Set()",
2896
+ "",
2897
+ "function pipeOutput(stream, target) {",
2898
+ " if (!stream) {",
2899
+ " return",
2900
+ " }",
2901
+ "",
2902
+ " let buffered = ''",
2903
+ " stream.on('data', (chunk) => {",
2904
+ " buffered += chunk.toString()",
2905
+ " const lines = buffered.split(/\\r?\\n/)",
2906
+ " buffered = lines.pop() ?? ''",
2907
+ " for (const line of lines) {",
2908
+ " if (!suppressedOutput.has(line)) {",
2909
+ " target.write(`${line}\\n`)",
2910
+ " }",
2911
+ " }",
2912
+ " })",
2913
+ "",
2914
+ " stream.on('end', () => {",
2915
+ " if (buffered.length > 0 && !suppressedOutput.has(buffered)) {",
2916
+ " target.write(buffered)",
2917
+ " }",
2918
+ " })",
2919
+ "}",
2920
+ "",
2921
+ "if (!existsSync(binaryPath)) {",
2922
+ ' console.error(`[holo] Missing framework binary "${commandName}" for "${framework}". Run your package manager install first.`)',
2923
+ " process.exit(1)",
2924
+ "}",
2925
+ "",
2926
+ "const child = spawn(binaryPath, commandArgs, {",
2927
+ " cwd: projectRoot,",
2928
+ " env: process.env,",
2929
+ " stdio: ['inherit', 'pipe', 'pipe'],",
2930
+ "})",
2931
+ "",
2932
+ "pipeOutput(child.stdout, process.stdout)",
2933
+ "pipeOutput(child.stderr, process.stderr)",
2934
+ "",
2935
+ "child.on('error', (error) => {",
2936
+ " console.error(error instanceof Error ? error.message : String(error))",
2937
+ " process.exit(1)",
2938
+ "})",
2939
+ "",
2940
+ "child.on('close', (code) => {",
2941
+ " process.exit(code ?? 1)",
2942
+ "})",
2943
+ ""
2944
+ ].join("\n");
2945
+ }
2946
+ function resolvePackageManagerVersion(value) {
2947
+ return SCAFFOLD_PACKAGE_MANAGER_VERSIONS[value];
2948
+ }
2949
+ function resolveDefaultDatabaseUrl(driver) {
2950
+ if (driver === "sqlite") {
2951
+ return "./storage/database.sqlite";
2952
+ }
2953
+ return void 0;
2954
+ }
2955
+ function renderScaffoldPackageJson(options) {
2956
+ const packageName = sanitizePackageName(options.projectName) || "holo-app";
2957
+ const optionalPackages = normalizeScaffoldOptionalPackages(options.optionalPackages);
2958
+ const dependencies = {
2959
+ "@holo-js/cli": `^${HOLO_PACKAGE_VERSION}`,
2960
+ "@holo-js/config": `^${HOLO_PACKAGE_VERSION}`,
2961
+ "@holo-js/core": `^${HOLO_PACKAGE_VERSION}`,
2962
+ "@holo-js/db": `^${HOLO_PACKAGE_VERSION}`,
2963
+ [DB_DRIVER_PACKAGE_NAMES[options.databaseDriver]]: `^${HOLO_PACKAGE_VERSION}`,
2964
+ esbuild: ESBUILD_PACKAGE_VERSION
2965
+ };
2966
+ const devDependencies = {
2967
+ typescript: "^5.7.2",
2968
+ "@types/node": "^22.10.2"
2969
+ };
2970
+ if (options.framework === "nuxt") {
2971
+ dependencies.nuxt = SCAFFOLD_FRAMEWORK_VERSIONS.nuxt;
2972
+ dependencies["@holo-js/adapter-nuxt"] = SCAFFOLD_FRAMEWORK_ADAPTER_VERSIONS.nuxt;
2973
+ }
2974
+ if (options.framework === "next") {
2975
+ dependencies.next = SCAFFOLD_FRAMEWORK_VERSIONS.next;
2976
+ dependencies.react = "^19.0.0";
2977
+ dependencies["react-dom"] = "^19.0.0";
2978
+ dependencies["@holo-js/adapter-next"] = SCAFFOLD_FRAMEWORK_ADAPTER_VERSIONS.next;
2979
+ devDependencies["@types/react"] = "^19.0.0";
2980
+ devDependencies["@types/react-dom"] = "^19.0.0";
2981
+ }
2982
+ if (options.framework === "sveltekit") {
2983
+ dependencies["@holo-js/adapter-sveltekit"] = SCAFFOLD_FRAMEWORK_ADAPTER_VERSIONS.sveltekit;
2984
+ dependencies["@sveltejs/adapter-node"] = "^5.0.0";
2985
+ dependencies["@sveltejs/kit"] = SCAFFOLD_FRAMEWORK_VERSIONS.sveltekit;
2986
+ dependencies["@sveltejs/vite-plugin-svelte"] = "^4.0.0";
2987
+ dependencies.svelte = "^5.0.0";
2988
+ dependencies.vite = "^5.0.0";
2989
+ }
2990
+ if (optionalPackages.includes("storage")) {
2991
+ dependencies["@holo-js/storage"] = SCAFFOLD_FRAMEWORK_RUNTIME_VERSIONS[options.framework]["@holo-js/storage"];
2992
+ }
2993
+ if (optionalPackages.includes("events")) {
2994
+ dependencies["@holo-js/events"] = `^${HOLO_PACKAGE_VERSION}`;
2995
+ }
2996
+ if (optionalPackages.includes("queue")) {
2997
+ dependencies["@holo-js/queue"] = `^${HOLO_PACKAGE_VERSION}`;
2998
+ }
2999
+ if (optionalPackages.includes("validation")) {
3000
+ dependencies["@holo-js/validation"] = `^${HOLO_PACKAGE_VERSION}`;
3001
+ }
3002
+ if (optionalPackages.includes("forms")) {
3003
+ dependencies["@holo-js/forms"] = `^${HOLO_PACKAGE_VERSION}`;
3004
+ }
3005
+ if (optionalPackages.includes("auth")) {
3006
+ dependencies["@holo-js/auth"] = `^${HOLO_PACKAGE_VERSION}`;
3007
+ dependencies["@holo-js/session"] = `^${HOLO_PACKAGE_VERSION}`;
3008
+ }
3009
+ if (optionalPackages.includes("authorization")) {
3010
+ dependencies["@holo-js/authorization"] = `^${HOLO_PACKAGE_VERSION}`;
3011
+ }
3012
+ if (optionalPackages.includes("notifications")) {
3013
+ dependencies["@holo-js/notifications"] = `^${HOLO_PACKAGE_VERSION}`;
3014
+ }
3015
+ if (optionalPackages.includes("mail")) {
3016
+ dependencies["@holo-js/mail"] = `^${HOLO_PACKAGE_VERSION}`;
3017
+ }
3018
+ if (optionalPackages.includes("broadcast")) {
3019
+ dependencies["@holo-js/broadcast"] = `^${HOLO_PACKAGE_VERSION}`;
3020
+ dependencies["@holo-js/flux"] = `^${HOLO_PACKAGE_VERSION}`;
3021
+ if (options.framework === "next") {
3022
+ dependencies["@holo-js/flux-react"] = `^${HOLO_PACKAGE_VERSION}`;
3023
+ } else if (options.framework === "nuxt") {
3024
+ dependencies["@holo-js/flux-vue"] = `^${HOLO_PACKAGE_VERSION}`;
3025
+ } else if (options.framework === "sveltekit") {
3026
+ dependencies["@holo-js/flux-svelte"] = `^${HOLO_PACKAGE_VERSION}`;
3027
+ }
3028
+ }
3029
+ if (optionalPackages.includes("security")) {
3030
+ dependencies["@holo-js/security"] = `^${HOLO_PACKAGE_VERSION}`;
3031
+ }
3032
+ if (optionalPackages.includes("cache")) {
3033
+ dependencies["@holo-js/cache"] = `^${HOLO_PACKAGE_VERSION}`;
3034
+ }
3035
+ return `${JSON.stringify({
3036
+ name: packageName,
3037
+ private: true,
3038
+ type: "module",
3039
+ packageManager: resolvePackageManagerVersion(options.packageManager),
3040
+ scripts: {
3041
+ ...options.framework === "nuxt" ? { postinstall: "nuxt prepare" } : {},
3042
+ prepare: "holo prepare",
3043
+ dev: "holo dev",
3044
+ build: "holo build",
3045
+ ["config:cache"]: "holo config:cache",
3046
+ ["config:clear"]: "holo config:clear",
3047
+ ["holo:dev"]: "node ./.holo-js/framework/run.mjs dev",
3048
+ ["holo:build"]: "node ./.holo-js/framework/run.mjs build"
3049
+ },
3050
+ dependencies,
3051
+ devDependencies
3052
+ }, null, 2)}
3053
+ `;
3054
+ }
3055
+ async function scaffoldProject(projectRoot, options) {
3056
+ const existingEntries = await readdir(projectRoot).catch(() => []);
3057
+ if (existingEntries.length > 0) {
3058
+ throw new Error(`Refusing to scaffold into a non-empty directory: ${projectRoot}`);
3059
+ }
3060
+ const { env, example } = renderScaffoldEnvFiles(options);
3061
+ const config = normalizeHoloProjectConfig();
3062
+ const generatedSchemaPath = resolveGeneratedSchemaPath(projectRoot, config);
3063
+ const optionalPackages = normalizeScaffoldOptionalPackages(options.optionalPackages);
3064
+ const storageEnabled = optionalPackages.includes("storage");
3065
+ const queueEnabled = optionalPackages.includes("queue");
3066
+ const eventsEnabled = optionalPackages.includes("events");
3067
+ const authEnabled = optionalPackages.includes("auth");
3068
+ const authorizationEnabled = optionalPackages.includes("authorization");
3069
+ const notificationsEnabled = optionalPackages.includes("notifications");
3070
+ const mailEnabled = optionalPackages.includes("mail");
3071
+ const broadcastEnabled = optionalPackages.includes("broadcast");
3072
+ const securityEnabled = optionalPackages.includes("security");
3073
+ const cacheEnabled = optionalPackages.includes("cache");
3074
+ const broadcastEnvFiles = broadcastEnabled ? renderBroadcastEnvFiles() : void 0;
3075
+ const baseEnv = normalizeScaffoldEnvSegments(env);
3076
+ const baseExample = normalizeScaffoldEnvSegments(example);
3077
+ const scaffoldEnvSegments = broadcastEnvFiles ? [...baseEnv, ...broadcastEnvFiles.env] : baseEnv;
3078
+ const scaffoldEnvExampleSegments = broadcastEnvFiles ? [...baseExample, ...broadcastEnvFiles.example] : baseExample;
3079
+ const scaffoldEnv = renderEnvFileContents(scaffoldEnvSegments);
3080
+ const scaffoldEnvExample = renderEnvFileContents(scaffoldEnvExampleSegments);
3081
+ await mkdir(projectRoot, { recursive: true });
3082
+ await mkdir(resolve(projectRoot, "config"), { recursive: true });
3083
+ await mkdir(resolve(projectRoot, ".holo-js", "framework"), { recursive: true });
3084
+ await mkdir(resolve(projectRoot, config.paths.models), { recursive: true });
3085
+ await mkdir(resolve(projectRoot, config.paths.commands), { recursive: true });
3086
+ if (queueEnabled) {
3087
+ await mkdir(resolve(projectRoot, config.paths.jobs), { recursive: true });
3088
+ }
3089
+ if (eventsEnabled) {
3090
+ await mkdir(resolve(projectRoot, config.paths.events), { recursive: true });
3091
+ await mkdir(resolve(projectRoot, config.paths.listeners), { recursive: true });
3092
+ }
3093
+ if (authorizationEnabled) {
3094
+ await mkdir(resolve(projectRoot, "server/policies"), { recursive: true });
3095
+ await mkdir(resolve(projectRoot, "server/abilities"), { recursive: true });
3096
+ }
3097
+ if (mailEnabled) {
3098
+ await mkdir(resolve(projectRoot, "server/mail"), { recursive: true });
3099
+ }
3100
+ if (broadcastEnabled) {
3101
+ await mkdir(resolve(projectRoot, "server/broadcast"), { recursive: true });
3102
+ await mkdir(resolve(projectRoot, "server/channels"), { recursive: true });
3103
+ }
3104
+ await mkdir(resolve(projectRoot, "server/db/factories"), { recursive: true });
3105
+ await mkdir(resolve(projectRoot, "server/db/migrations"), { recursive: true });
3106
+ await mkdir(resolve(projectRoot, "server/db/seeders"), { recursive: true });
3107
+ await mkdir(resolve(projectRoot, "server/db/schema"), { recursive: true });
3108
+ await mkdir(resolve(projectRoot, config.paths.observers), { recursive: true });
3109
+ await mkdir(resolve(projectRoot, "storage"), { recursive: true });
3110
+ if (storageEnabled) {
3111
+ await mkdir(resolve(projectRoot, "storage/app/public"), { recursive: true });
3112
+ }
3113
+ await writeFile(resolve(projectRoot, "package.json"), renderScaffoldPackageJson(options), "utf8");
3114
+ await writeFile(resolve(projectRoot, ".gitignore"), renderScaffoldGitignore(), "utf8");
3115
+ await writeFile(resolve(projectRoot, ".env"), scaffoldEnv, "utf8");
3116
+ await writeFile(resolve(projectRoot, ".env.example"), scaffoldEnvExample, "utf8");
3117
+ await writeFile(resolve(projectRoot, "config/app.ts"), renderScaffoldAppConfig(options.projectName), "utf8");
3118
+ await writeFile(resolve(projectRoot, "config/database.ts"), renderScaffoldDatabaseConfig(options), "utf8");
3119
+ await writeFile(resolve(projectRoot, "config/redis.ts"), renderRedisConfig(), "utf8");
3120
+ if (queueEnabled) {
3121
+ await writeFile(resolve(projectRoot, "config/queue.ts"), renderQueueConfig({
3122
+ driver: "sync",
3123
+ defaultDatabaseConnection: "main"
3124
+ }), "utf8");
3125
+ }
3126
+ if (notificationsEnabled) {
3127
+ await writeFile(resolve(projectRoot, "config/notifications.ts"), renderNotificationsConfig(), "utf8");
3128
+ for (const migrationFile of createNotificationsMigrationFiles()) {
3129
+ await writeFile(resolve(projectRoot, config.paths.migrations, migrationFile.path), migrationFile.contents, "utf8");
3130
+ }
3131
+ }
3132
+ if (mailEnabled) {
3133
+ await writeFile(resolve(projectRoot, "config/mail.ts"), renderMailConfig(), "utf8");
3134
+ }
3135
+ if (broadcastEnabled) {
3136
+ await writeFile(resolve(projectRoot, "config/broadcast.ts"), renderBroadcastConfig("esm", false, true), "utf8");
3137
+ }
3138
+ if (securityEnabled) {
3139
+ await writeFile(resolve(projectRoot, "config/security.ts"), renderSecurityConfig(), "utf8");
3140
+ await ensureRateLimitStorageIgnore(projectRoot);
3141
+ }
3142
+ if (cacheEnabled) {
3143
+ await writeFile(resolve(projectRoot, "config/cache.ts"), renderCacheConfig("file", "main"), "utf8");
3144
+ }
3145
+ if (authEnabled) {
3146
+ await writeFile(resolve(projectRoot, "config/auth.ts"), renderAuthConfig(), "utf8");
3147
+ await writeFile(resolve(projectRoot, "config/session.ts"), renderSessionConfig("main"), "utf8");
3148
+ const userModelPath = resolve(projectRoot, config.paths.models, "User.ts");
3149
+ await writeFile(
3150
+ userModelPath,
3151
+ renderAuthUserModel(resolveAuthUserModelSchemaImportPath(
3152
+ userModelPath,
3153
+ generatedSchemaPath
3154
+ )),
3155
+ "utf8"
3156
+ );
3157
+ for (const migrationFile of createAuthMigrationFiles()) {
3158
+ await writeFile(resolve(projectRoot, config.paths.migrations, migrationFile.path), migrationFile.contents, "utf8");
3159
+ }
3160
+ }
3161
+ if (broadcastEnabled && authEnabled) {
3162
+ await syncBroadcastAuthSupportAfterAuthInstall(projectRoot);
3163
+ }
3164
+ if (authorizationEnabled) {
3165
+ await writeFile(resolve(projectRoot, "server/policies/README.md"), renderAuthorizationPoliciesReadme(), "utf8");
3166
+ await writeFile(resolve(projectRoot, "server/abilities/README.md"), renderAuthorizationAbilitiesReadme(), "utf8");
3167
+ }
3168
+ if (storageEnabled) {
3169
+ await writeFile(resolve(projectRoot, "config/storage.ts"), renderStorageConfig(), "utf8");
3170
+ }
3171
+ await writeFile(resolve(projectRoot, ".holo-js/framework/run.mjs"), renderFrameworkRunner(options), "utf8");
3172
+ await writeFile(resolve(projectRoot, ".holo-js/framework/project.json"), `${JSON.stringify(options, null, 2)}
3173
+ `, "utf8");
3174
+ await writeFile(resolve(projectRoot, "tsconfig.json"), renderScaffoldTsconfig(options), "utf8");
3175
+ await writeFile(generatedSchemaPath, renderGeneratedSchemaPlaceholder(), "utf8");
3176
+ for (const file of renderFrameworkFiles(options)) {
3177
+ await writeTextFile(resolve(projectRoot, file.path), file.contents);
3178
+ }
3179
+ if (options.databaseDriver === "sqlite") {
3180
+ await writeFile(resolve(projectRoot, "storage/database.sqlite"), "", "utf8");
3181
+ }
3182
+ }
3183
+
3184
+ export {
3185
+ renderStorageConfig,
3186
+ renderMediaConfig,
3187
+ renderQueueConfig,
3188
+ renderCacheConfig,
3189
+ renderRedisConfig,
3190
+ renderNotificationsConfig,
3191
+ renderMailConfig,
3192
+ renderSecurityConfig,
3193
+ injectBroadcastAuthEndpoint,
3194
+ resolveBroadcastConfigTargetPath,
3195
+ renderSessionConfig,
3196
+ renderAuthConfig,
3197
+ authFeaturesRequireConfigUpdate,
3198
+ detectAuthInstallFeaturesFromConfig,
3199
+ renderAuthEnvFiles,
3200
+ renderAuthUserModel,
3201
+ renderAuthMigration,
3202
+ renderNotificationsMigration,
3203
+ renderScaffoldAppConfig,
3204
+ renderScaffoldDatabaseConfig,
3205
+ renderScaffoldEnvFiles,
3206
+ renderQueueEnvFiles,
3207
+ renderCacheEnvFiles,
3208
+ hasLoadedConfigFile,
3209
+ inferDatabaseDriverFromUrl,
3210
+ inferConnectionDriver,
3211
+ renderEnvFileContents,
3212
+ normalizeScaffoldEnvSegments,
3213
+ syncManagedDriverDependencies,
3214
+ upsertEventsPackageDependency,
3215
+ upsertNotificationsPackageDependency,
3216
+ upsertMailPackageDependency,
3217
+ upsertSecurityPackageDependency,
3218
+ upsertCachePackageDependencies,
3219
+ upsertAuthPackageDependencies,
3220
+ installAuthIntoProject,
3221
+ installAuthorizationIntoProject,
3222
+ installQueueIntoProject,
3223
+ installEventsIntoProject,
3224
+ installNotificationsIntoProject,
3225
+ installMailIntoProject,
3226
+ installSecurityIntoProject,
3227
+ installCacheIntoProject,
3228
+ installBroadcastIntoProject,
3229
+ renderScaffoldGitignore,
3230
+ renderScaffoldTsconfig,
3231
+ renderFrameworkFiles,
3232
+ renderFrameworkRunner,
3233
+ resolvePackageManagerVersion,
3234
+ resolveDefaultDatabaseUrl,
3235
+ renderScaffoldPackageJson,
3236
+ scaffoldProject
3237
+ };