create-lumen 2.0.0-alpha.2 → 2.0.0-beta.2

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 (174) hide show
  1. package/README.md +24 -6
  2. package/bin/cli.js +2 -1
  3. package/package.json +2 -2
  4. package/schema/lumen.config.v2.json +29 -0
  5. package/src/agent-context.js +71 -47
  6. package/src/cleanup.js +19 -1
  7. package/src/cli-args.js +131 -5
  8. package/src/configure.js +8 -4
  9. package/src/dependencies.js +46 -13
  10. package/src/engine/capabilities.js +71 -1
  11. package/src/engine/composition.js +334 -17
  12. package/src/engine/providers/base-provider.js +4 -4
  13. package/src/env.js +24 -6
  14. package/src/main.js +38 -15
  15. package/src/manifest/emit.js +14 -1
  16. package/src/manifest/schema.js +30 -0
  17. package/src/prompts.js +227 -23
  18. package/templates/bases/next/app-router/js/.provenance.json +19 -0
  19. package/templates/bases/next/app-router/js/README.md +36 -0
  20. package/templates/bases/next/app-router/js/jsconfig.json +7 -0
  21. package/templates/bases/next/app-router/js/next.config.mjs +6 -0
  22. package/templates/bases/next/app-router/js/package.json +15 -0
  23. package/templates/bases/next/app-router/js/public/file.svg +1 -0
  24. package/templates/bases/next/app-router/js/public/globe.svg +1 -0
  25. package/templates/bases/next/app-router/js/public/next.svg +1 -0
  26. package/templates/bases/next/app-router/js/public/vercel.svg +1 -0
  27. package/templates/bases/next/app-router/js/public/window.svg +1 -0
  28. package/templates/bases/next/app-router/js/src/app/api/hello/route.js +5 -0
  29. package/templates/bases/next/app-router/js/src/app/blog/[slug]/page.js +33 -0
  30. package/templates/bases/next/app-router/js/src/app/favicon.ico +0 -0
  31. package/templates/bases/next/app-router/js/src/app/globals.css +49 -0
  32. package/templates/bases/next/app-router/js/src/app/layout.js +25 -0
  33. package/templates/bases/next/app-router/js/src/app/loading.js +15 -0
  34. package/templates/bases/next/app-router/js/src/app/not-found.js +29 -0
  35. package/templates/bases/next/app-router/js/src/app/page.js +69 -0
  36. package/templates/bases/next/app-router/js/src/app/page.module.css +150 -0
  37. package/templates/bases/next/app-router/ts/.provenance.json +19 -0
  38. package/templates/bases/next/app-router/ts/README.md +36 -0
  39. package/templates/bases/next/app-router/ts/next.config.ts +7 -0
  40. package/templates/bases/next/app-router/ts/package.json +21 -0
  41. package/templates/bases/next/app-router/ts/public/file.svg +1 -0
  42. package/templates/bases/next/app-router/ts/public/globe.svg +1 -0
  43. package/templates/bases/next/app-router/ts/public/next.svg +1 -0
  44. package/templates/bases/next/app-router/ts/public/vercel.svg +1 -0
  45. package/templates/bases/next/app-router/ts/public/window.svg +1 -0
  46. package/templates/bases/next/app-router/ts/src/app/api/hello/route.ts +5 -0
  47. package/templates/bases/next/app-router/ts/src/app/blog/[slug]/page.tsx +38 -0
  48. package/templates/bases/next/app-router/ts/src/app/favicon.ico +0 -0
  49. package/templates/bases/next/app-router/ts/src/app/globals.css +49 -0
  50. package/templates/bases/next/app-router/ts/src/app/layout.tsx +30 -0
  51. package/templates/bases/next/app-router/ts/src/app/loading.tsx +15 -0
  52. package/templates/bases/next/app-router/ts/src/app/not-found.tsx +29 -0
  53. package/templates/bases/next/app-router/ts/src/app/page.module.css +150 -0
  54. package/templates/bases/next/app-router/ts/src/app/page.tsx +69 -0
  55. package/templates/bases/next/app-router/ts/tsconfig.json +34 -0
  56. package/templates/bases/next/pages-router/js/.provenance.json +19 -0
  57. package/templates/bases/next/pages-router/js/README.md +40 -0
  58. package/templates/bases/next/pages-router/js/jsconfig.json +7 -0
  59. package/templates/bases/next/pages-router/js/next.config.mjs +7 -0
  60. package/templates/bases/next/pages-router/js/package.json +15 -0
  61. package/templates/bases/next/pages-router/js/public/favicon.ico +0 -0
  62. package/templates/bases/next/pages-router/js/public/file.svg +1 -0
  63. package/templates/bases/next/pages-router/js/public/globe.svg +1 -0
  64. package/templates/bases/next/pages-router/js/public/next.svg +1 -0
  65. package/templates/bases/next/pages-router/js/public/vercel.svg +1 -0
  66. package/templates/bases/next/pages-router/js/public/window.svg +1 -0
  67. package/templates/bases/next/pages-router/js/src/pages/404.js +35 -0
  68. package/templates/bases/next/pages-router/js/src/pages/500.js +35 -0
  69. package/templates/bases/next/pages-router/js/src/pages/_app.js +20 -0
  70. package/templates/bases/next/pages-router/js/src/pages/_document.js +15 -0
  71. package/templates/bases/next/pages-router/js/src/pages/api/hello.js +5 -0
  72. package/templates/bases/next/pages-router/js/src/pages/blog/[slug].js +41 -0
  73. package/templates/bases/next/pages-router/js/src/pages/index.js +77 -0
  74. package/templates/bases/next/pages-router/js/src/styles/Home.module.css +149 -0
  75. package/templates/bases/next/pages-router/js/src/styles/globals.css +42 -0
  76. package/templates/bases/next/pages-router/ts/.provenance.json +19 -0
  77. package/templates/bases/next/pages-router/ts/README.md +40 -0
  78. package/templates/bases/next/pages-router/ts/next.config.ts +8 -0
  79. package/templates/bases/next/pages-router/ts/package.json +21 -0
  80. package/templates/bases/next/pages-router/ts/public/favicon.ico +0 -0
  81. package/templates/bases/next/pages-router/ts/public/file.svg +1 -0
  82. package/templates/bases/next/pages-router/ts/public/globe.svg +1 -0
  83. package/templates/bases/next/pages-router/ts/public/next.svg +1 -0
  84. package/templates/bases/next/pages-router/ts/public/vercel.svg +1 -0
  85. package/templates/bases/next/pages-router/ts/public/window.svg +1 -0
  86. package/templates/bases/next/pages-router/ts/src/pages/404.tsx +35 -0
  87. package/templates/bases/next/pages-router/ts/src/pages/500.tsx +35 -0
  88. package/templates/bases/next/pages-router/ts/src/pages/_app.tsx +21 -0
  89. package/templates/bases/next/pages-router/ts/src/pages/_document.tsx +15 -0
  90. package/templates/bases/next/pages-router/ts/src/pages/api/hello.ts +13 -0
  91. package/templates/bases/next/pages-router/ts/src/pages/blog/[slug].tsx +46 -0
  92. package/templates/bases/next/pages-router/ts/src/pages/index.tsx +77 -0
  93. package/templates/bases/next/pages-router/ts/src/styles/Home.module.css +149 -0
  94. package/templates/bases/next/pages-router/ts/src/styles/globals.css +42 -0
  95. package/templates/bases/next/pages-router/ts/tsconfig.json +29 -0
  96. package/templates/conditional/database/prisma/db.js +14 -0
  97. package/templates/conditional/database/prisma/db.ts +16 -0
  98. package/templates/conditional/database/prisma/schema.prisma +9 -0
  99. package/templates/conditional/database/prisma/seed.js +16 -0
  100. package/templates/conditional/database/prisma/seed.ts +16 -0
  101. package/templates/conditional/docker/.dockerignore +10 -0
  102. package/templates/conditional/docker/Dockerfile +57 -0
  103. package/templates/conditional/docker/docker-compose.yml +35 -0
  104. package/templates/conditional/linting/biome/biome.json +31 -0
  105. package/templates/conditional/next/config/cloudflare/next.config.mjs +6 -0
  106. package/templates/conditional/next/config/cloudflare/next.config.ts +7 -0
  107. package/templates/conditional/next/config/node/next.config.mjs +6 -0
  108. package/templates/conditional/next/config/node/next.config.ts +7 -0
  109. package/templates/conditional/next/config/static/next.config.mjs +6 -0
  110. package/templates/conditional/next/config/static/next.config.ts +7 -0
  111. package/templates/conditional/next/config/turbopack/next.config.mjs +7 -0
  112. package/templates/conditional/next/config/turbopack/next.config.ts +8 -0
  113. package/templates/conditional/next/config/vercel/next.config.mjs +6 -0
  114. package/templates/conditional/next/config/vercel/next.config.ts +7 -0
  115. package/templates/conditional/next/config/webpack/next.config.mjs +9 -0
  116. package/templates/conditional/next/config/webpack/next.config.ts +10 -0
  117. package/templates/conditional/next/font/BUILD_SIZE_NOTE.md +17 -0
  118. package/templates/conditional/next/font/app-router/src/app/layout.js +25 -0
  119. package/templates/conditional/next/font/app-router/src/app/layout.tsx +30 -0
  120. package/templates/conditional/next/font/pages-router/src/pages/_app.js +20 -0
  121. package/templates/conditional/next/font/pages-router/src/pages/_app.tsx +21 -0
  122. package/templates/conditional/next/font/pages-router/src/pages/_document.js +18 -0
  123. package/templates/conditional/next/font/pages-router/src/pages/_document.tsx +18 -0
  124. package/templates/conditional/next/font/src/app/layout.js +25 -0
  125. package/templates/conditional/next/font/src/app/layout.tsx +30 -0
  126. package/templates/conditional/next/font/src/lib/fonts.js +11 -0
  127. package/templates/conditional/next/font/src/lib/fonts.ts +11 -0
  128. package/templates/conditional/next/font/src/pages/_app.js +20 -0
  129. package/templates/conditional/next/font/src/pages/_app.tsx +21 -0
  130. package/templates/conditional/next/font/src/pages/_document.js +18 -0
  131. package/templates/conditional/next/font/src/pages/_document.tsx +18 -0
  132. package/templates/conditional/next/image/BUILD_SIZE_NOTE.md +15 -0
  133. package/templates/conditional/next/image/app-router/src/components/ImageExample.jsx +34 -0
  134. package/templates/conditional/next/image/app-router/src/components/ImageExample.tsx +44 -0
  135. package/templates/conditional/next/image/pages-router/src/components/ImageExample.jsx +34 -0
  136. package/templates/conditional/next/image/pages-router/src/components/ImageExample.tsx +44 -0
  137. package/templates/conditional/next/image/src/components/ImageExample.jsx +44 -0
  138. package/templates/conditional/next/image/src/components/ImageExample.tsx +54 -0
  139. package/templates/conditional/next/image/src/components/image/index.js +1 -0
  140. package/templates/conditional/next/image/src/components/image/index.ts +2 -0
  141. package/templates/conditional/next/route-groups/js/src/app/(admin)/layout.jsx +23 -0
  142. package/templates/conditional/next/route-groups/js/src/app/(auth)/layout.jsx +17 -0
  143. package/templates/conditional/next/route-groups/js/src/app/(private)/layout.jsx +27 -0
  144. package/templates/conditional/next/route-groups/js/src/app/(public)/layout.jsx +28 -0
  145. package/templates/conditional/next/route-groups/js/src/app/(public)/page.jsx +38 -0
  146. package/templates/conditional/next/route-groups/js/src/middleware.js +48 -0
  147. package/templates/conditional/next/route-groups/ts/src/app/(admin)/layout.tsx +28 -0
  148. package/templates/conditional/next/route-groups/ts/src/app/(auth)/layout.tsx +22 -0
  149. package/templates/conditional/next/route-groups/ts/src/app/(private)/layout.tsx +32 -0
  150. package/templates/conditional/next/route-groups/ts/src/app/(public)/layout.tsx +33 -0
  151. package/templates/conditional/next/route-groups/ts/src/app/(public)/page.tsx +38 -0
  152. package/templates/conditional/next/route-groups/ts/src/middleware.ts +49 -0
  153. package/templates/conditional/next/server-components/README.md +13 -0
  154. package/templates/conditional/next/server-components/eslint.config.mjs +47 -0
  155. package/templates/conditional/next/server-components/rules/no-server-imports-in-client.js +89 -0
  156. package/templates/conditional/next/server-components/rules/no-server-imports-in-client.mjs +88 -0
  157. package/templates/conditional/next/server-components/src/components/boundaries/ClientCounter.jsx +48 -0
  158. package/templates/conditional/next/server-components/src/components/boundaries/ClientCounter.tsx +53 -0
  159. package/templates/conditional/next/server-components/src/components/boundaries/ServerCard.jsx +41 -0
  160. package/templates/conditional/next/server-components/src/components/boundaries/ServerCard.tsx +46 -0
  161. package/templates/conditional/next/server-components/src/components/boundaries/index.js +2 -0
  162. package/templates/conditional/next/server-components/src/components/boundaries/index.ts +4 -0
  163. package/templates/conditional/testing/jest/next/app-router/src/__tests__/page.test.jsx +10 -0
  164. package/templates/conditional/testing/jest/next/app-router/src/__tests__/page.test.tsx +10 -0
  165. package/templates/conditional/testing/jest/next/jest.config.mjs +20 -0
  166. package/templates/conditional/testing/jest/next/jest.config.ts +20 -0
  167. package/templates/conditional/testing/jest/next/pages-router/src/__tests__/index.test.jsx +10 -0
  168. package/templates/conditional/testing/jest/next/pages-router/src/__tests__/index.test.tsx +10 -0
  169. package/templates/conditional/testing/vitest/next/app-router/src/__tests__/page.test.jsx +11 -0
  170. package/templates/conditional/testing/vitest/next/app-router/src/__tests__/page.test.tsx +11 -0
  171. package/templates/conditional/testing/vitest/next/pages-router/src/__tests__/index.test.jsx +11 -0
  172. package/templates/conditional/testing/vitest/next/pages-router/src/__tests__/index.test.tsx +11 -0
  173. package/templates/conditional/testing/vitest/next/vitest.config.js +16 -0
  174. package/templates/conditional/testing/vitest/next/vitest.config.ts +16 -0
@@ -41,9 +41,13 @@ export const TOOLING_ORDER = [
41
41
  "router",
42
42
  "icons",
43
43
  "api",
44
+ "database",
44
45
  "testing",
45
46
  "linter",
46
47
  "feature-script",
48
+ "next-config",
49
+ "route-groups",
50
+ "docker",
47
51
  "formatter",
48
52
  ];
49
53
 
@@ -149,6 +153,15 @@ export function resolveFragments(responses = {}, templatesDir) {
149
153
  try { await fsp.rm(path.join(projectPath, "src", "App.jsx"), { force: true }); } catch {}
150
154
  try { await fsp.rm(path.join(projectPath, "src", "index.css"), { force: true }); } catch {}
151
155
  try { await fsp.rm(path.join(projectPath, "src", "App.css"), { force: true }); } catch {}
156
+ if (fw.name === "next") {
157
+ // Next cleanup: remove Vite defaults without touching app/
158
+ try { await fsp.rm(path.join(projectPath, "src", "main.tsx"), { force: true }); } catch {}
159
+ try { await fsp.rm(path.join(projectPath, "src", "main.jsx"), { force: true }); } catch {}
160
+ try { await fsp.rm(path.join(projectPath, "index.html"), { force: true }); } catch {}
161
+ try { await fsp.rm(path.join(projectPath, "vite.config.ts"), { force: true }); } catch {}
162
+ try { await fsp.rm(path.join(projectPath, "vite.config.js"), { force: true }); } catch {}
163
+ try { await fsp.rm(path.join(projectPath, "public", "vite.svg"), { force: true }); } catch {}
164
+ }
152
165
  },
153
166
  });
154
167
 
@@ -164,6 +177,19 @@ export function resolveFragments(responses = {}, templatesDir) {
164
177
  sourceDir: archSrcDir,
165
178
  targetDir: "src",
166
179
  action: async ({ projectPath }) => {
180
+ if (fw.name === "next") {
181
+ if (fw.variant === "pages-router") {
182
+ try { await fsp.rm(path.join(projectPath, "src", "app"), { recursive: true, force: true }); } catch {}
183
+ } else {
184
+ // Next App Router: preserve file-system routes; purge React/Vite-specific application layer
185
+ try { await fsp.rm(path.join(projectPath, "src", "app", "App.tsx"), { force: true }); } catch {}
186
+ try { await fsp.rm(path.join(projectPath, "src", "app", "App.jsx"), { force: true }); } catch {}
187
+ try { await fsp.rm(path.join(projectPath, "src", "app", "hooks"), { recursive: true, force: true }); } catch {}
188
+ try { await fsp.rm(path.join(projectPath, "src", "app", "contexts"), { recursive: true, force: true }); } catch {}
189
+ try { await fsp.rm(path.join(projectPath, "src", "app", "providers"), { recursive: true, force: true }); } catch {}
190
+ }
191
+ return;
192
+ }
167
193
  // Copy architecture main entry
168
194
  try {
169
195
  await fsp.access(archMainSrc);
@@ -194,6 +220,16 @@ export function resolveFragments(responses = {}, templatesDir) {
194
220
  id: `styling:${cssFramework}:components`,
195
221
  sourceDir: componentStylesDir,
196
222
  targetDir: "src",
223
+ action: async ({ projectPath }) => {
224
+ if (fw.name === "next") {
225
+ if (fw.variant === "pages-router") {
226
+ try { await fsp.rm(path.join(projectPath, "src", "app"), { recursive: true, force: true }); } catch {}
227
+ } else {
228
+ try { await fsp.rm(path.join(projectPath, "src", "app", "App.tsx"), { force: true }); } catch {}
229
+ try { await fsp.rm(path.join(projectPath, "src", "app", "App.jsx"), { force: true }); } catch {}
230
+ }
231
+ }
232
+ },
197
233
  });
198
234
  }
199
235
 
@@ -210,6 +246,7 @@ export function resolveFragments(responses = {}, templatesDir) {
210
246
  sourceDir: path.join(templatesDir, "conditional", "state", "redux", architecture, "src"),
211
247
  targetDir: "src",
212
248
  action: async ({ projectPath }) => {
249
+ if (fw.name === "next") return;
213
250
  await updateMainWithProvider(projectPath, "redux", architecture, ext);
214
251
  },
215
252
  });
@@ -306,6 +343,106 @@ export function resolveFragments(responses = {}, templatesDir) {
306
343
  id: `tooling:api:${responses.apiClient}`,
307
344
  sourceDir: apiDir,
308
345
  targetDir: "src",
346
+ action: async ({ projectPath }) => {
347
+ if (fw.name === "next") {
348
+ const configCandidates = [
349
+ path.join(projectPath, "src", "config", `axios.config.${extConfig}`),
350
+ path.join(projectPath, "src", "shared", "lib", "axios", `api.config.${extConfig}`),
351
+ path.join(projectPath, "src", "config", `api.config.${extConfig}`),
352
+ path.join(projectPath, "src", "shared", "api", `api.config.${extConfig}`),
353
+ ];
354
+ for (const cfgPath of configCandidates) {
355
+ try {
356
+ let content = await fsp.readFile(cfgPath, "utf8");
357
+ if (content.includes("import.meta.env.VITE_API_URL")) {
358
+ content = content.replace(
359
+ /import\.meta\.env\.VITE_API_URL/g,
360
+ "process.env.NEXT_PUBLIC_API_URL"
361
+ );
362
+ await fsp.writeFile(cfgPath, content, "utf8");
363
+ }
364
+ } catch {}
365
+ }
366
+ }
367
+ },
368
+ });
369
+ }
370
+
371
+ // 5.45 Database (Prisma)
372
+ if (responses.database === "prisma") {
373
+ fragments.push({
374
+ scope: FRAGMENT_SCOPES.TOOLING,
375
+ category: "database",
376
+ id: "tooling:database:prisma",
377
+ scripts: {
378
+ "db:generate": "prisma generate",
379
+ "db:push": "prisma db push",
380
+ "db:seed": "prisma db seed",
381
+ "db:studio": "prisma studio",
382
+ },
383
+ action: async ({ projectPath, templatesDir: tDir }) => {
384
+ const prismaTplDir = path.join(tDir, "conditional", "database", "prisma");
385
+ const prismaDir = path.join(projectPath, "prisma");
386
+ await fsp.mkdir(prismaDir, { recursive: true });
387
+
388
+ // Copy schema.prisma
389
+ const schemaSrc = path.join(prismaTplDir, "schema.prisma");
390
+ const schemaDest = path.join(prismaDir, "schema.prisma");
391
+ try {
392
+ await fsp.access(schemaSrc);
393
+ await fsp.copyFile(schemaSrc, schemaDest);
394
+ } catch {}
395
+
396
+ // Copy seed file: prisma/seed.ts or prisma/seed.js
397
+ const seedFileName = language === "ts" ? "seed.ts" : "seed.js";
398
+ const seedSrc = path.join(prismaTplDir, seedFileName);
399
+ const seedDest = path.join(prismaDir, seedFileName);
400
+ try {
401
+ await fsp.access(seedSrc);
402
+ await fsp.copyFile(seedSrc, seedDest);
403
+ } catch {}
404
+
405
+ // Copy client singleton: src/lib/db.ts or src/lib/db.js
406
+ const libDir = path.join(projectPath, "src", "lib");
407
+ await fsp.mkdir(libDir, { recursive: true });
408
+ const dbFileName = language === "ts" ? "db.ts" : "db.js";
409
+ const dbSrc = path.join(prismaTplDir, dbFileName);
410
+ const dbDest = path.join(libDir, dbFileName);
411
+ try {
412
+ await fsp.access(dbSrc);
413
+ await fsp.copyFile(dbSrc, dbDest);
414
+ } catch {}
415
+
416
+ // Configure prisma.seed in package.json
417
+ const pkgPath = path.join(projectPath, "package.json");
418
+ try {
419
+ const content = await fsp.readFile(pkgPath, "utf8");
420
+ const pkg = JSON.parse(content);
421
+ pkg.prisma = {
422
+ seed: language === "ts" ? "tsx prisma/seed.ts" : "node prisma/seed.js",
423
+ };
424
+ await fsp.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
425
+ } catch {}
426
+
427
+ // Append DATABASE_URL and DIRECT_URL to .env.example
428
+ const envExamplePath = path.join(projectPath, ".env.example");
429
+ const envLines = [
430
+ "",
431
+ "# Database (Prisma + PostgreSQL / Supabase)",
432
+ 'DATABASE_URL="postgresql://postgres:[YOUR-PASSWORD]@db.[YOUR-PROJECT-REF].supabase.co:5432/postgres"',
433
+ 'DIRECT_URL="postgresql://postgres:[YOUR-PASSWORD]@db.[YOUR-PROJECT-REF].supabase.co:5432/postgres"',
434
+ ].join("\n");
435
+ try {
436
+ let envContent = "";
437
+ try {
438
+ envContent = await fsp.readFile(envExamplePath, "utf8");
439
+ } catch {}
440
+ if (!envContent.includes("DATABASE_URL")) {
441
+ envContent = envContent.trimEnd() + "\n" + envLines.trimStart() + "\n";
442
+ await writeFileRecursive(envExamplePath, envContent);
443
+ }
444
+ } catch {}
445
+ },
309
446
  });
310
447
  }
311
448
 
@@ -325,11 +462,26 @@ export function resolveFragments(responses = {}, templatesDir) {
325
462
  scripts: testScripts,
326
463
  action: async ({ projectPath }) => {
327
464
  // Config file to project root
328
- const configSrc = path.join(testingDir, `${testingFramework}.config.${extConfig}`);
329
- try {
330
- await fsp.access(configSrc);
331
- await fsp.copyFile(configSrc, path.join(projectPath, `${testingFramework}.config.${extConfig}`));
332
- } catch {}
465
+ if (testingFramework === "jest" && fw.name === "next") {
466
+ const nextConfigExt = language === "ts" ? "ts" : "mjs";
467
+ const configSrc = path.join(testingDir, "next", `jest.config.${nextConfigExt}`);
468
+ try {
469
+ await fsp.access(configSrc);
470
+ await fsp.copyFile(configSrc, path.join(projectPath, `jest.config.${nextConfigExt}`));
471
+ } catch {}
472
+ } else if (testingFramework === "vitest" && fw.name === "next") {
473
+ const configSrc = path.join(testingDir, "next", `vitest.config.${extConfig}`);
474
+ try {
475
+ await fsp.access(configSrc);
476
+ await fsp.copyFile(configSrc, path.join(projectPath, `vitest.config.${extConfig}`));
477
+ } catch {}
478
+ } else {
479
+ const configSrc = path.join(testingDir, `${testingFramework}.config.${extConfig}`);
480
+ try {
481
+ await fsp.access(configSrc);
482
+ await fsp.copyFile(configSrc, path.join(projectPath, `${testingFramework}.config.${extConfig}`));
483
+ } catch {}
484
+ }
333
485
 
334
486
  // Setup file to src/test/setup.*
335
487
  const setupSrc = path.join(testingDir, `src/test/setup.${extConfig}`);
@@ -345,20 +497,39 @@ export function resolveFragments(responses = {}, templatesDir) {
345
497
  await fsp.access(jestSetupSrc);
346
498
  await fsp.copyFile(jestSetupSrc, path.join(projectPath, `jest.setup.${extConfig}`));
347
499
  } catch {}
348
- const babelSrc = path.join(testingDir, "babel.config.cjs");
500
+ if (fw.name !== "next") {
501
+ const babelSrc = path.join(testingDir, "babel.config.cjs");
502
+ try {
503
+ await fsp.access(babelSrc);
504
+ await fsp.copyFile(babelSrc, path.join(projectPath, "babel.config.cjs"));
505
+ } catch {}
506
+ }
507
+ }
508
+
509
+ if (fw.name === "next") {
510
+ const testFileName = fw.variant === "pages-router" ? "index" : "page";
511
+ const testSrc = path.join(
512
+ testingDir,
513
+ "next",
514
+ fw.variant,
515
+ "src",
516
+ "__tests__",
517
+ `${testFileName}.test.${ext}`
518
+ );
349
519
  try {
350
- await fsp.access(babelSrc);
351
- await fsp.copyFile(babelSrc, path.join(projectPath, "babel.config.cjs"));
520
+ await fsp.access(testSrc);
521
+ const testDest = path.join(projectPath, "src", "__tests__", `${testFileName}.test.${ext}`);
522
+ await writeFileRecursive(testDest, await fsp.readFile(testSrc, "utf8"));
523
+ } catch {}
524
+ } else {
525
+ // Architecture-scoped test file
526
+ const testSrc = path.join(testingDir, architecture, `src/__tests__/App.test.${ext}`);
527
+ try {
528
+ await fsp.access(testSrc);
529
+ const testDest = path.join(projectPath, "src", "__tests__", `App.test.${ext}`);
530
+ await writeFileRecursive(testDest, await fsp.readFile(testSrc, "utf8"));
352
531
  } catch {}
353
532
  }
354
-
355
- // Architecture-scoped test file
356
- const testSrc = path.join(testingDir, architecture, `src/__tests__/App.test.${ext}`);
357
- try {
358
- await fsp.access(testSrc);
359
- const testDest = path.join(projectPath, "src", "__tests__", `App.test.${ext}`);
360
- await writeFileRecursive(testDest, await fsp.readFile(testSrc, "utf8"));
361
- } catch {}
362
533
  },
363
534
  });
364
535
  }
@@ -406,6 +577,23 @@ export function resolveFragments(responses = {}, templatesDir) {
406
577
  } catch {}
407
578
  },
408
579
  });
580
+ } else if (responses.linter === "biome") {
581
+ fragments.push({
582
+ scope: FRAGMENT_SCOPES.TOOLING,
583
+ category: "linter",
584
+ id: "tooling:linter:biome",
585
+ scripts: {
586
+ lint: "biome check .",
587
+ "lint:fix": "biome check --write .",
588
+ },
589
+ action: async ({ projectPath }) => {
590
+ const configSrc = path.join(templatesDir, "conditional", "linting", "biome", "biome.json");
591
+ try {
592
+ await fsp.access(configSrc);
593
+ await fsp.copyFile(configSrc, path.join(projectPath, "biome.json"));
594
+ } catch {}
595
+ },
596
+ });
409
597
  }
410
598
 
411
599
  // 5.7 Feature Script (Feature-based only)
@@ -437,7 +625,136 @@ export function resolveFragments(responses = {}, templatesDir) {
437
625
  });
438
626
  }
439
627
 
440
- // 5.8 Formatter
628
+ // 5.75 Next.js Configuration & Adapter Overlays
629
+ if (fw.name === "next") {
630
+ const nextScripts = {};
631
+ if (responses.bundler === "turbopack") {
632
+ nextScripts.dev = "next dev --turbopack";
633
+ } else if (responses.bundler === "webpack") {
634
+ nextScripts.dev = "next dev --webpack";
635
+ }
636
+
637
+ fragments.push({
638
+ scope: FRAGMENT_SCOPES.TOOLING,
639
+ category: "next-config",
640
+ id: "tooling:next:config",
641
+ scripts: nextScripts,
642
+ action: async ({ projectPath }) => {
643
+ const configExt = language === "ts" ? "ts" : "mjs";
644
+
645
+ // Apply adapter overlay if specified and not "none"
646
+ if (responses.adapter && responses.adapter !== "none") {
647
+ const adapterConfigSrc = path.join(
648
+ templatesDir,
649
+ "conditional",
650
+ "next",
651
+ "config",
652
+ responses.adapter,
653
+ `next.config.${configExt}`
654
+ );
655
+ try {
656
+ await fsp.access(adapterConfigSrc);
657
+ await fsp.copyFile(adapterConfigSrc, path.join(projectPath, `next.config.${configExt}`));
658
+ } catch {}
659
+ } else if (responses.bundler === "turbopack" || responses.bundler === "webpack") {
660
+ // If no adapter specified, but bundler config overlay exists, apply bundler overlay
661
+ const bundlerConfigSrc = path.join(
662
+ templatesDir,
663
+ "conditional",
664
+ "next",
665
+ "config",
666
+ responses.bundler,
667
+ `next.config.${configExt}`
668
+ );
669
+ try {
670
+ await fsp.access(bundlerConfigSrc);
671
+ await fsp.copyFile(bundlerConfigSrc, path.join(projectPath, `next.config.${configExt}`));
672
+ } catch {}
673
+ }
674
+
675
+ // Apply reactCompiler flag to next.config if enabled
676
+ if (responses.reactCompiler === true) {
677
+ const targetConfigPath = path.join(projectPath, `next.config.${configExt}`);
678
+ try {
679
+ let content = await fsp.readFile(targetConfigPath, "utf8");
680
+ if (!content.includes("reactCompiler")) {
681
+ content = content.replace(
682
+ /const nextConfig(?::\s*NextConfig)?\s*=\s*\{/,
683
+ (match) => `${match}\n reactCompiler: true,`
684
+ );
685
+ await fsp.writeFile(targetConfigPath, content, "utf8");
686
+ }
687
+ } catch {}
688
+ }
689
+
690
+ // Apply standalone output to next.config if docker is enabled
691
+ if (responses.docker === true) {
692
+ const targetConfigPath = path.join(projectPath, `next.config.${configExt}`);
693
+ try {
694
+ let content = await fsp.readFile(targetConfigPath, "utf8");
695
+ if (!content.includes('"standalone"') && !content.includes("'standalone'")) {
696
+ content = content.replace(
697
+ /const nextConfig(?::\s*NextConfig)?\s*=\s*\{/,
698
+ (match) => `${match}\n output: "standalone",`
699
+ );
700
+ await fsp.writeFile(targetConfigPath, content, "utf8");
701
+ }
702
+ } catch {}
703
+ }
704
+ },
705
+ });
706
+ }
707
+
708
+ // 5.8 Route Groups Preset & Auth Proxy Template (#64)
709
+ if (responses.routeGroups === true && fw.name === "next" && fw.variant === "app-router") {
710
+ fragments.push({
711
+ scope: FRAGMENT_SCOPES.TOOLING,
712
+ category: "route-groups",
713
+ id: "tooling:next:route-groups",
714
+ sourceDir: path.join(templatesDir, "conditional", "next", "route-groups", language),
715
+ targetDir: "",
716
+ action: async ({ projectPath }) => {
717
+ // Remove colliding root page so it does not collide with (public)/page
718
+ try {
719
+ await fsp.rm(path.join(projectPath, "src", "app", `page.${ext}`), { force: true });
720
+ } catch {}
721
+ const oppositeExt = language === "ts" ? "jsx" : "tsx";
722
+ try {
723
+ await fsp.rm(path.join(projectPath, "src", "app", `page.${oppositeExt}`), { force: true });
724
+ } catch {}
725
+ // Also remove page.js / page.tsx in case base provider was named page.js
726
+ try {
727
+ await fsp.rm(path.join(projectPath, "src", "app", "page.js"), { force: true });
728
+ } catch {}
729
+ try {
730
+ await fsp.rm(path.join(projectPath, "src", "app", "page.tsx"), { force: true });
731
+ } catch {}
732
+ },
733
+ });
734
+ }
735
+
736
+ // 5.9 Docker (Multi-stage Dockerfile and docker-compose for Next.js standalone)
737
+ if (responses.docker === true && hasCapability(fw, CAPABILITIES.DOCKER)) {
738
+ fragments.push({
739
+ scope: FRAGMENT_SCOPES.TOOLING,
740
+ category: "docker",
741
+ id: "tooling:docker",
742
+ action: async ({ projectPath }) => {
743
+ const dockerSrcDir = path.join(templatesDir, "conditional", "docker");
744
+ const filesToCopy = ["Dockerfile", ".dockerignore", "docker-compose.yml"];
745
+ for (const file of filesToCopy) {
746
+ const srcFile = path.join(dockerSrcDir, file);
747
+ const destFile = path.join(projectPath, file);
748
+ try {
749
+ await fsp.access(srcFile);
750
+ await fsp.copyFile(srcFile, destFile);
751
+ } catch {}
752
+ }
753
+ },
754
+ });
755
+ }
756
+
757
+ // 5.10 Formatter
441
758
  if (responses.formatter && responses.formatter !== "none") {
442
759
  const isPrettier = responses.formatter === "prettier";
443
760
  const configFile = isPrettier ? ".prettierrc" : ".oxfmtrc.json";
@@ -125,7 +125,7 @@ export class NextAppRouterProvider extends BaseProvider {
125
125
  getPackageSet({ options = {} } = {}) {
126
126
  const isTs = options.language === "ts";
127
127
  const deps = {
128
- next: "^15.0.0",
128
+ next: "^16.0.0",
129
129
  react: "^19.0.0",
130
130
  "react-dom": "^19.0.0",
131
131
  };
@@ -134,7 +134,7 @@ export class NextAppRouterProvider extends BaseProvider {
134
134
  devDeps.typescript = "^5.7.0";
135
135
  devDeps["@types/react"] = "^19.0.0";
136
136
  devDeps["@types/react-dom"] = "^19.0.0";
137
- devDeps["@types/node"] = "^20.0.0";
137
+ devDeps["@types/node"] = "^22.0.0";
138
138
  }
139
139
  return {
140
140
  dependencies: deps,
@@ -159,7 +159,7 @@ export class NextPagesRouterProvider extends BaseProvider {
159
159
  getPackageSet({ options = {} } = {}) {
160
160
  const isTs = options.language === "ts";
161
161
  const deps = {
162
- next: "^15.0.0",
162
+ next: "^16.0.0",
163
163
  react: "^19.0.0",
164
164
  "react-dom": "^19.0.0",
165
165
  };
@@ -168,7 +168,7 @@ export class NextPagesRouterProvider extends BaseProvider {
168
168
  devDeps.typescript = "^5.7.0";
169
169
  devDeps["@types/react"] = "^19.0.0";
170
170
  devDeps["@types/react-dom"] = "^19.0.0";
171
- devDeps["@types/node"] = "^20.0.0";
171
+ devDeps["@types/node"] = "^22.0.0";
172
172
  }
173
173
  return {
174
174
  dependencies: deps,
package/src/env.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import path from "path";
2
2
  import { promises as fsp } from "fs";
3
+ import { normalizeFramework } from "./engine/capabilities.js";
3
4
 
4
5
  const ENV_GITIGNORE = `
5
6
  # Environment variables
@@ -8,12 +9,29 @@ const ENV_GITIGNORE = `
8
9
  !.env.example
9
10
  `;
10
11
 
11
- export async function copyEnvExample(projectPath, templatesDir) {
12
- const src = path.join(templatesDir, ".env.example");
13
- try {
14
- await fsp.access(src);
15
- await fsp.copyFile(src, path.join(projectPath, ".env.example"));
16
- } catch {}
12
+ const NEXT_ENV_EXAMPLE = `# Copy this file to \`.env.local\` (or \`.env\`) and fill in your values.
13
+ # See https://nextjs.org/docs/app/building-your-application/configuring/environment-variables for details.
14
+
15
+ NEXT_PUBLIC_API_URL=https://api.example.com
16
+ `;
17
+
18
+ export async function copyEnvExample(projectPath, templatesDir, responses = {}) {
19
+ const fw = normalizeFramework(
20
+ responses.framework || {
21
+ name: responses.frameworkName,
22
+ variant: responses.frameworkVariant,
23
+ }
24
+ );
25
+
26
+ if (fw.name === "next") {
27
+ await fsp.writeFile(path.join(projectPath, ".env.example"), NEXT_ENV_EXAMPLE, "utf8");
28
+ } else {
29
+ const src = path.join(templatesDir, ".env.example");
30
+ try {
31
+ await fsp.access(src);
32
+ await fsp.copyFile(src, path.join(projectPath, ".env.example"));
33
+ } catch {}
34
+ }
17
35
 
18
36
  // Ensure the project gitignore excludes env files but keeps .env.example.
19
37
  const gitignore = path.join(projectPath, ".gitignore");
package/src/main.js CHANGED
@@ -12,7 +12,7 @@ import { injectArchitecture, injectConditionals, injectFormatter } from "@/injec
12
12
  import { getPkgManager } from "@/utils/pkg-manager.js";
13
13
  import { handleDevServer } from "@/utils/dev-server.js";
14
14
  import { getUserInputs, onCancel } from "@/prompts.js";
15
- import { runBaseInstall, runGitInit, runViteCreate } from "@/scaffold.js";
15
+ import { runBaseInstall, runGitInit, runViteCreate, scaffoldBase } from "@/scaffold.js";
16
16
  import { installAllDeps } from "@/dependencies.js";
17
17
  import { generateReadme } from "@/readme.js";
18
18
  import { runProjectFormat } from "@/format.js";
@@ -20,6 +20,8 @@ import { emitManifest } from "@/manifest/emit.js";
20
20
  import { emitAgentContext } from "@/agent-context.js";
21
21
 
22
22
  import { parseCliArgs, loadManifestSource, manifestToResponses, PRESETS } from "@/cli-args.js";
23
+ import { getProvider } from "@/engine/providers/base-provider.js";
24
+ import { normalizeFramework } from "@/engine/capabilities.js";
23
25
 
24
26
  const __filename = fileURLToPath(import.meta.url);
25
27
  const __dirname = path.dirname(__filename);
@@ -48,6 +50,7 @@ export async function main(options = {}) {
48
50
  projectName: nameArg,
49
51
  manifest: manifestSource,
50
52
  template: templateName,
53
+ framework: frameworkArg,
51
54
  arch: archArg,
52
55
  } = options;
53
56
  const pkg = getPkgManager();
@@ -94,20 +97,40 @@ export async function main(options = {}) {
94
97
  ...preset,
95
98
  };
96
99
  } else {
97
- responses = await getUserInputs(projectName, { quickSetup, initialArch: archArg });
100
+ responses = await getUserInputs(projectName, { quickSetup, initialArch: archArg, framework: frameworkArg });
98
101
  }
99
102
 
100
103
  const projectPath = path.resolve(CURRENT_DIR, projectName);
101
104
 
102
- // 4. Scaffold via vite create
105
+ // 4. Scaffold the base project (provider-owned bases; Vite shells out, Next copies)
106
+ const fw = normalizeFramework(
107
+ responses.framework || { name: responses.frameworkName, variant: responses.frameworkVariant }
108
+ );
103
109
  const createSpin = spinner();
104
- createSpin.start("Creating your Vite + React project...");
105
- try {
106
- await runViteCreate(projectPath, projectName, pkg, responses.language);
107
- createSpin.stop(chalk.green("Project created successfully!"));
108
- } catch (err) {
109
- createSpin.stop(chalk.red("Failed to create project."));
110
- throw err;
110
+ if (fw.name === "next") {
111
+ createSpin.start("Creating your Next.js project...");
112
+ try {
113
+ const provider = getProvider(fw.name, fw.variant);
114
+ const based = await scaffoldBase(projectPath, provider, responses.language, TEMPLATES_DIR);
115
+ if (!based) {
116
+ throw new Error(
117
+ `Next.js base template not found for variant "${fw.variant}" (${responses.language}).`
118
+ );
119
+ }
120
+ createSpin.stop(chalk.green("Project created successfully!"));
121
+ } catch (err) {
122
+ createSpin.stop(chalk.red("Failed to create project."));
123
+ throw err;
124
+ }
125
+ } else {
126
+ createSpin.start("Creating your Vite + React project...");
127
+ try {
128
+ await runViteCreate(projectPath, projectName, pkg, responses.language);
129
+ createSpin.stop(chalk.green("Project created successfully!"));
130
+ } catch (err) {
131
+ createSpin.stop(chalk.red("Failed to create project."));
132
+ throw err;
133
+ }
111
134
  }
112
135
 
113
136
  // 5. Install base dependencies
@@ -234,7 +257,7 @@ export async function main(options = {}) {
234
257
  await cleanupBoilerplate(projectPath);
235
258
 
236
259
  // 12.5 Copy .env.example into the project
237
- await copyEnvExample(projectPath, TEMPLATES_DIR);
260
+ await copyEnvExample(projectPath, TEMPLATES_DIR, responses);
238
261
 
239
262
  // 12.6 Emit lumen.config.json (manifest v2) — byte-deterministic, Zod-validated
240
263
  const manifestSpin = spinner();
@@ -247,15 +270,15 @@ export async function main(options = {}) {
247
270
  throw err;
248
271
  }
249
272
 
250
- // 12.7 Emit .lumen agent context dir (only when agentDocs is enabled)
273
+ // 12.7 Emit AGENTS.md at root (only when agentDocs is enabled)
251
274
  if (responses.agentDocs === true || responses._manifestSource?.agentDocs === true) {
252
275
  const agentSpin = spinner();
253
- agentSpin.start("Writing .lumen agent context...");
276
+ agentSpin.start("Writing AGENTS.md...");
254
277
  try {
255
278
  await emitAgentContext(projectPath, responses._manifestSource ?? responses);
256
- agentSpin.stop(chalk.green(".lumen agent context written."));
279
+ agentSpin.stop(chalk.green("AGENTS.md written."));
257
280
  } catch (err) {
258
- agentSpin.stop(chalk.red("Failed to write .lumen agent context."));
281
+ agentSpin.stop(chalk.red("Failed to write AGENTS.md."));
259
282
  throw err;
260
283
  }
261
284
  }
@@ -64,7 +64,7 @@ export function buildManifest(responses, opts = {}) {
64
64
  };
65
65
  // Include optional axes only if present in responses (future overlays)
66
66
  if (responses.bundler) framework.bundler = responses.bundler;
67
- if (responses.adapter) framework.adapter = responses.adapter;
67
+ if (responses.adapter && responses.adapter !== "none") framework.adapter = responses.adapter;
68
68
 
69
69
  const styling = {
70
70
  engine: responses.cssFramework || "tailwind",
@@ -117,8 +117,18 @@ export function buildManifest(responses, opts = {}) {
117
117
  };
118
118
 
119
119
  // Optional cross-cutting flags — only include if explicitly set (keeps minimal manifest byte-stable)
120
+ if (responses.database && responses.database !== "none") {
121
+ manifest.database = {
122
+ orm: responses.database,
123
+ provider: "postgresql",
124
+ };
125
+ }
126
+ if (typeof responses.docker === "boolean") manifest.docker = responses.docker;
127
+ if (typeof responses.routeGroups === "boolean") manifest.routeGroups = responses.routeGroups;
120
128
  if (typeof responses.reactCompiler === "boolean") manifest.reactCompiler = responses.reactCompiler;
129
+ if (typeof responses.routeGroups === "boolean") manifest.routeGroups = responses.routeGroups;
121
130
  if (typeof responses.agentDocs === "boolean") manifest.agentDocs = responses.agentDocs;
131
+ if (typeof responses.docker === "boolean") manifest.docker = responses.docker;
122
132
 
123
133
  // Enforce stable top-level key order for determinism (like formatter pass)
124
134
  const ordered = {};
@@ -133,6 +143,9 @@ export function buildManifest(responses, opts = {}) {
133
143
  "docs",
134
144
  "paths",
135
145
  "tooling",
146
+ "database",
147
+ "docker",
148
+ "routeGroups",
136
149
  "reactCompiler",
137
150
  "agentDocs",
138
151
  ];