create-lumen 2.0.0-beta.1 → 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 (45) hide show
  1. package/package.json +1 -1
  2. package/schema/lumen.config.v2.json +29 -0
  3. package/src/agent-context.js +71 -47
  4. package/src/cleanup.js +19 -1
  5. package/src/cli-args.js +26 -0
  6. package/src/dependencies.js +42 -15
  7. package/src/engine/capabilities.js +49 -0
  8. package/src/engine/composition.js +242 -17
  9. package/src/env.js +24 -6
  10. package/src/main.js +5 -5
  11. package/src/manifest/emit.js +13 -0
  12. package/src/manifest/schema.js +30 -0
  13. package/src/prompts.js +77 -4
  14. package/templates/conditional/database/prisma/db.js +14 -0
  15. package/templates/conditional/database/prisma/db.ts +16 -0
  16. package/templates/conditional/database/prisma/schema.prisma +9 -0
  17. package/templates/conditional/database/prisma/seed.js +16 -0
  18. package/templates/conditional/database/prisma/seed.ts +16 -0
  19. package/templates/conditional/docker/.dockerignore +10 -0
  20. package/templates/conditional/docker/Dockerfile +57 -0
  21. package/templates/conditional/docker/docker-compose.yml +35 -0
  22. package/templates/conditional/next/route-groups/js/src/app/(admin)/layout.jsx +23 -0
  23. package/templates/conditional/next/route-groups/js/src/app/(auth)/layout.jsx +17 -0
  24. package/templates/conditional/next/route-groups/js/src/app/(private)/layout.jsx +27 -0
  25. package/templates/conditional/next/route-groups/js/src/app/(public)/layout.jsx +28 -0
  26. package/templates/conditional/next/route-groups/js/src/app/(public)/page.jsx +38 -0
  27. package/templates/conditional/next/route-groups/js/src/middleware.js +48 -0
  28. package/templates/conditional/next/route-groups/ts/src/app/(admin)/layout.tsx +28 -0
  29. package/templates/conditional/next/route-groups/ts/src/app/(auth)/layout.tsx +22 -0
  30. package/templates/conditional/next/route-groups/ts/src/app/(private)/layout.tsx +32 -0
  31. package/templates/conditional/next/route-groups/ts/src/app/(public)/layout.tsx +33 -0
  32. package/templates/conditional/next/route-groups/ts/src/app/(public)/page.tsx +38 -0
  33. package/templates/conditional/next/route-groups/ts/src/middleware.ts +49 -0
  34. package/templates/conditional/testing/jest/next/app-router/src/__tests__/page.test.jsx +10 -0
  35. package/templates/conditional/testing/jest/next/app-router/src/__tests__/page.test.tsx +10 -0
  36. package/templates/conditional/testing/jest/next/jest.config.mjs +20 -0
  37. package/templates/conditional/testing/jest/next/jest.config.ts +20 -0
  38. package/templates/conditional/testing/jest/next/pages-router/src/__tests__/index.test.jsx +10 -0
  39. package/templates/conditional/testing/jest/next/pages-router/src/__tests__/index.test.tsx +10 -0
  40. package/templates/conditional/testing/vitest/next/app-router/src/__tests__/page.test.jsx +11 -0
  41. package/templates/conditional/testing/vitest/next/app-router/src/__tests__/page.test.tsx +11 -0
  42. package/templates/conditional/testing/vitest/next/pages-router/src/__tests__/index.test.jsx +11 -0
  43. package/templates/conditional/testing/vitest/next/pages-router/src/__tests__/index.test.tsx +11 -0
  44. package/templates/conditional/testing/vitest/next/vitest.config.js +16 -0
  45. package/templates/conditional/testing/vitest/next/vitest.config.ts +16 -0
@@ -41,10 +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",
47
48
  "next-config",
49
+ "route-groups",
50
+ "docker",
48
51
  "formatter",
49
52
  ];
50
53
 
@@ -174,6 +177,19 @@ export function resolveFragments(responses = {}, templatesDir) {
174
177
  sourceDir: archSrcDir,
175
178
  targetDir: "src",
176
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
+ }
177
193
  // Copy architecture main entry
178
194
  try {
179
195
  await fsp.access(archMainSrc);
@@ -204,6 +220,16 @@ export function resolveFragments(responses = {}, templatesDir) {
204
220
  id: `styling:${cssFramework}:components`,
205
221
  sourceDir: componentStylesDir,
206
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
+ },
207
233
  });
208
234
  }
209
235
 
@@ -220,6 +246,7 @@ export function resolveFragments(responses = {}, templatesDir) {
220
246
  sourceDir: path.join(templatesDir, "conditional", "state", "redux", architecture, "src"),
221
247
  targetDir: "src",
222
248
  action: async ({ projectPath }) => {
249
+ if (fw.name === "next") return;
223
250
  await updateMainWithProvider(projectPath, "redux", architecture, ext);
224
251
  },
225
252
  });
@@ -316,6 +343,106 @@ export function resolveFragments(responses = {}, templatesDir) {
316
343
  id: `tooling:api:${responses.apiClient}`,
317
344
  sourceDir: apiDir,
318
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
+ },
319
446
  });
320
447
  }
321
448
 
@@ -335,11 +462,26 @@ export function resolveFragments(responses = {}, templatesDir) {
335
462
  scripts: testScripts,
336
463
  action: async ({ projectPath }) => {
337
464
  // Config file to project root
338
- const configSrc = path.join(testingDir, `${testingFramework}.config.${extConfig}`);
339
- try {
340
- await fsp.access(configSrc);
341
- await fsp.copyFile(configSrc, path.join(projectPath, `${testingFramework}.config.${extConfig}`));
342
- } 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
+ }
343
485
 
344
486
  // Setup file to src/test/setup.*
345
487
  const setupSrc = path.join(testingDir, `src/test/setup.${extConfig}`);
@@ -355,20 +497,39 @@ export function resolveFragments(responses = {}, templatesDir) {
355
497
  await fsp.access(jestSetupSrc);
356
498
  await fsp.copyFile(jestSetupSrc, path.join(projectPath, `jest.setup.${extConfig}`));
357
499
  } catch {}
358
- 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
+ );
519
+ try {
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}`);
359
527
  try {
360
- await fsp.access(babelSrc);
361
- await fsp.copyFile(babelSrc, path.join(projectPath, "babel.config.cjs"));
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"));
362
531
  } catch {}
363
532
  }
364
-
365
- // Architecture-scoped test file
366
- const testSrc = path.join(testingDir, architecture, `src/__tests__/App.test.${ext}`);
367
- try {
368
- await fsp.access(testSrc);
369
- const testDest = path.join(projectPath, "src", "__tests__", `App.test.${ext}`);
370
- await writeFileRecursive(testDest, await fsp.readFile(testSrc, "utf8"));
371
- } catch {}
372
533
  },
373
534
  });
374
535
  }
@@ -525,11 +686,75 @@ export function resolveFragments(responses = {}, templatesDir) {
525
686
  }
526
687
  } catch {}
527
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
+ }
528
753
  },
529
754
  });
530
755
  }
531
756
 
532
- // 5.8 Formatter
757
+ // 5.10 Formatter
533
758
  if (responses.formatter && responses.formatter !== "none") {
534
759
  const isPrettier = responses.formatter === "prettier";
535
760
  const configFile = isPrettier ? ".prettierrc" : ".oxfmtrc.json";
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
@@ -257,7 +257,7 @@ export async function main(options = {}) {
257
257
  await cleanupBoilerplate(projectPath);
258
258
 
259
259
  // 12.5 Copy .env.example into the project
260
- await copyEnvExample(projectPath, TEMPLATES_DIR);
260
+ await copyEnvExample(projectPath, TEMPLATES_DIR, responses);
261
261
 
262
262
  // 12.6 Emit lumen.config.json (manifest v2) — byte-deterministic, Zod-validated
263
263
  const manifestSpin = spinner();
@@ -270,15 +270,15 @@ export async function main(options = {}) {
270
270
  throw err;
271
271
  }
272
272
 
273
- // 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)
274
274
  if (responses.agentDocs === true || responses._manifestSource?.agentDocs === true) {
275
275
  const agentSpin = spinner();
276
- agentSpin.start("Writing .lumen agent context...");
276
+ agentSpin.start("Writing AGENTS.md...");
277
277
  try {
278
278
  await emitAgentContext(projectPath, responses._manifestSource ?? responses);
279
- agentSpin.stop(chalk.green(".lumen agent context written."));
279
+ agentSpin.stop(chalk.green("AGENTS.md written."));
280
280
  } catch (err) {
281
- agentSpin.stop(chalk.red("Failed to write .lumen agent context."));
281
+ agentSpin.stop(chalk.red("Failed to write AGENTS.md."));
282
282
  throw err;
283
283
  }
284
284
  }
@@ -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
  ];
@@ -23,6 +23,8 @@ export const DOCS_LANGUAGES = ["en", "es"];
23
23
  export const TOOLING_LANGUAGES = ["ts", "js"];
24
24
  export const TOOLING_LINTERS = ["eslint", "oxlint", "biome", "none"];
25
25
  export const TOOLING_FORMATTERS = ["prettier", "oxfmt", "none"];
26
+ export const DATABASE_ORMS = ["prisma", "none"];
27
+ export const DATABASE_PROVIDERS = ["postgresql"];
26
28
 
27
29
  // ---------------------------------------------------------------------------
28
30
  // Sub-schemas
@@ -121,6 +123,13 @@ const toolingSchema = z
121
123
  })
122
124
  .strict();
123
125
 
126
+ const databaseSchema = z
127
+ .object({
128
+ orm: z.enum(DATABASE_ORMS),
129
+ provider: z.enum(DATABASE_PROVIDERS).optional().default("postgresql"),
130
+ })
131
+ .strict();
132
+
124
133
  // ---------------------------------------------------------------------------
125
134
  // Top-level manifest v2 - strict, no extra keys except $schema
126
135
  // ---------------------------------------------------------------------------
@@ -136,6 +145,9 @@ export const manifestSchemaV2 = z
136
145
  docs: docsSchema,
137
146
  paths: pathsSchema,
138
147
  tooling: toolingSchema,
148
+ database: databaseSchema.optional(),
149
+ docker: z.boolean().optional(),
150
+ routeGroups: z.boolean().optional(),
139
151
  reactCompiler: z.boolean().optional(),
140
152
  agentDocs: z.boolean().optional(),
141
153
  })
@@ -177,6 +189,24 @@ export const manifestSchemaV2 = z
177
189
  });
178
190
  }
179
191
 
192
+ // database only for Next.js (#62)
193
+ if (val.database !== undefined && val.database.orm !== "none" && framework.name !== "next") {
194
+ ctx.addIssue({
195
+ code: "custom",
196
+ path: ["database", "orm"],
197
+ message: `database is only valid when framework.name is "next"`,
198
+ });
199
+ }
200
+
201
+ // routeGroups only for Next.js App Router (#64)
202
+ if (val.routeGroups === true && (framework.name !== "next" || framework.variant !== "app-router")) {
203
+ ctx.addIssue({
204
+ code: "custom",
205
+ path: ["routeGroups"],
206
+ message: `routeGroups is only valid when framework is "next:app-router"`,
207
+ });
208
+ }
209
+
180
210
  // architecture scoping per framework (#34) — canonical `preset`, legacy `type` fallback
181
211
  const archValue = architecture.preset ?? architecture.type;
182
212
  const allowedByFramework =
package/src/prompts.js CHANGED
@@ -1,4 +1,4 @@
1
- import { isCancel, note, select } from "@clack/prompts";
1
+ import { isCancel, note, select } from "@clack/prompts";
2
2
  import chalk from "chalk";
3
3
  import { configExists, loadConfig, saveConfig } from "@/config-cache.js";
4
4
  import {
@@ -71,9 +71,18 @@ export function formatConfig(responses) {
71
71
  if (responses.adapter && responses.adapter !== "none") {
72
72
  lines.push(`• ${chalk.bold("Adapter:")} ${chalk.cyan(responses.adapter)}`);
73
73
  }
74
+ if (responses.database && responses.database !== "none") {
75
+ lines.push(`• ${chalk.bold("Database:")} ${chalk.cyan(responses.database === "prisma" ? "Prisma ORM" : responses.database)}`);
76
+ }
74
77
  if (responses.reactCompiler !== undefined) {
75
78
  lines.push(`• ${chalk.bold("React Compiler:")} ${responses.reactCompiler ? chalk.green("Yes") : chalk.red("No")}`);
76
79
  }
80
+ if (responses.routeGroups !== undefined) {
81
+ lines.push(`• ${chalk.bold("Route Groups:")} ${responses.routeGroups ? chalk.green("Yes") : chalk.red("No")}`);
82
+ }
83
+ if (responses.docker !== undefined) {
84
+ lines.push(`• ${chalk.bold("Docker:")} ${responses.docker ? chalk.green("Yes") : chalk.red("No")}`);
85
+ }
77
86
  lines.push(
78
87
  `• ${chalk.bold("State:")} ${chalk.blue(state)}`,
79
88
  `• ${chalk.bold("Icons:")} ${chalk.blue(responses.iconLibrary === "none" ? "None" : responses.iconLibrary === "lucide" ? "Lucide" : "Huge")}`,
@@ -410,7 +419,15 @@ export async function getUserInputs(
410
419
  { label: "Jest", value: "jest" },
411
420
  { label: "None", value: "none" },
412
421
  ],
413
- defaultInitial: "vitest",
422
+ defaultInitial: (ans) => {
423
+ const fw = normalizeFramework(
424
+ ans.framework || {
425
+ name: ans.frameworkName,
426
+ variant: ans.frameworkVariant,
427
+ }
428
+ );
429
+ return fw.name === "next" ? "none" : "vitest";
430
+ },
414
431
  },
415
432
  {
416
433
  id: "router",
@@ -456,6 +473,24 @@ export async function getUserInputs(
456
473
  ]),
457
474
  defaultInitial: "none",
458
475
  },
476
+ {
477
+ id: "database",
478
+ skip: () => !isPromptVisible(framework, "database"),
479
+ onSkip: (ans) => {
480
+ ans.database = "none";
481
+ },
482
+ message: "Which database ORM do you want to use?",
483
+ getOptions: () =>
484
+ filterCompatibleChoices(framework, "database", [
485
+ {
486
+ label: "Prisma ORM",
487
+ value: "prisma",
488
+ hint: "PostgreSQL schema, client singleton, seed script",
489
+ },
490
+ { label: "None", value: "none" },
491
+ ]),
492
+ defaultInitial: "none",
493
+ },
459
494
  {
460
495
  id: "reactCompiler",
461
496
  skip: () => !isPromptVisible(framework, "reactCompiler"),
@@ -469,6 +504,32 @@ export async function getUserInputs(
469
504
  ],
470
505
  defaultInitial: false,
471
506
  },
507
+ {
508
+ id: "routeGroups",
509
+ skip: () => !isPromptVisible(framework, "routeGroups"),
510
+ onSkip: (ans) => {
511
+ ans.routeGroups = false;
512
+ },
513
+ message: "Include route groups preset (public, auth, private, admin)?",
514
+ getOptions: () => [
515
+ { label: "No (default)", value: false },
516
+ { label: "Yes", value: true },
517
+ ],
518
+ defaultInitial: false,
519
+ },
520
+ {
521
+ id: "docker",
522
+ skip: () => !isPromptVisible(framework, "docker"),
523
+ onSkip: (ans) => {
524
+ ans.docker = false;
525
+ },
526
+ message: "Include Docker setup?",
527
+ getOptions: () => [
528
+ { label: "No (default)", value: false },
529
+ { label: "Yes", value: true, hint: "Multi-stage Dockerfile and docker-compose" },
530
+ ],
531
+ defaultInitial: false,
532
+ },
472
533
  {
473
534
  id: "stateManagement",
474
535
  message: "Which state management library do you want to use?",
@@ -560,10 +621,10 @@ export async function getUserInputs(
560
621
  },
561
622
  {
562
623
  id: "agentDocs",
563
- message: "Generate .lumen/ agent context for AI assistants?",
624
+ message: "Generate AGENTS.md for AI assistants?",
564
625
  getOptions: () => [
565
626
  { label: "No (default)", value: false },
566
- { label: "Yes", value: true, hint: "Emits .lumen/ with project, architecture and conventions" },
627
+ { label: "Yes", value: true, hint: "Emits AGENTS.md referencing lumen.config.json (SSOT)" },
567
628
  ],
568
629
  defaultInitial: false,
569
630
  },
@@ -620,8 +681,11 @@ export async function getUserInputs(
620
681
  quickSetup: _quickSetup,
621
682
  bundler,
622
683
  adapter,
684
+ database,
623
685
  reactCompiler,
686
+ routeGroups,
624
687
  agentDocs,
688
+ docker,
625
689
  ...picked
626
690
  } = responses;
627
691
  // Final safety net: BACK-navigation can reselect the framework after other
@@ -638,12 +702,21 @@ export async function getUserInputs(
638
702
  if (adapter !== undefined && adapter !== "none") {
639
703
  finalResponses.adapter = adapter;
640
704
  }
705
+ if (database !== undefined && database !== "none") {
706
+ finalResponses.database = database;
707
+ }
641
708
  if (reactCompiler !== undefined) {
642
709
  finalResponses.reactCompiler = reactCompiler;
643
710
  }
711
+ if (routeGroups !== undefined) {
712
+ finalResponses.routeGroups = routeGroups;
713
+ }
644
714
  if (agentDocs !== undefined) {
645
715
  finalResponses.agentDocs = agentDocs;
646
716
  }
717
+ if (docker !== undefined) {
718
+ finalResponses.docker = docker;
719
+ }
647
720
 
648
721
  // No config cache write until final confirm!
649
722
  await saveConfigFn(finalResponses);
@@ -0,0 +1,14 @@
1
+ import { PrismaClient } from "@prisma/client";
2
+
3
+ const globalForPrisma = globalThis;
4
+
5
+ export const db =
6
+ globalForPrisma.prisma ||
7
+ new PrismaClient({
8
+ log:
9
+ process.env.NODE_ENV === "development"
10
+ ? ["query", "error", "warn"]
11
+ : ["error"],
12
+ });
13
+
14
+ if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = db;
@@ -0,0 +1,16 @@
1
+ import { PrismaClient } from "@prisma/client";
2
+
3
+ const globalForPrisma = globalThis as unknown as {
4
+ prisma: PrismaClient | undefined;
5
+ };
6
+
7
+ export const db =
8
+ globalForPrisma.prisma ??
9
+ new PrismaClient({
10
+ log:
11
+ process.env.NODE_ENV === "development"
12
+ ? ["query", "error", "warn"]
13
+ : ["error"],
14
+ });
15
+
16
+ if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = db;
@@ -0,0 +1,9 @@
1
+ datasource db {
2
+ provider = "postgresql"
3
+ url = env("DATABASE_URL")
4
+ directUrl = env("DIRECT_URL")
5
+ }
6
+
7
+ generator client {
8
+ provider = "prisma-client-js"
9
+ }