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.
- package/package.json +1 -1
- package/schema/lumen.config.v2.json +29 -0
- package/src/agent-context.js +71 -47
- package/src/cleanup.js +19 -1
- package/src/cli-args.js +26 -0
- package/src/dependencies.js +42 -15
- package/src/engine/capabilities.js +49 -0
- package/src/engine/composition.js +242 -17
- package/src/env.js +24 -6
- package/src/main.js +5 -5
- package/src/manifest/emit.js +13 -0
- package/src/manifest/schema.js +30 -0
- package/src/prompts.js +77 -4
- package/templates/conditional/database/prisma/db.js +14 -0
- package/templates/conditional/database/prisma/db.ts +16 -0
- package/templates/conditional/database/prisma/schema.prisma +9 -0
- package/templates/conditional/database/prisma/seed.js +16 -0
- package/templates/conditional/database/prisma/seed.ts +16 -0
- package/templates/conditional/docker/.dockerignore +10 -0
- package/templates/conditional/docker/Dockerfile +57 -0
- package/templates/conditional/docker/docker-compose.yml +35 -0
- package/templates/conditional/next/route-groups/js/src/app/(admin)/layout.jsx +23 -0
- package/templates/conditional/next/route-groups/js/src/app/(auth)/layout.jsx +17 -0
- package/templates/conditional/next/route-groups/js/src/app/(private)/layout.jsx +27 -0
- package/templates/conditional/next/route-groups/js/src/app/(public)/layout.jsx +28 -0
- package/templates/conditional/next/route-groups/js/src/app/(public)/page.jsx +38 -0
- package/templates/conditional/next/route-groups/js/src/middleware.js +48 -0
- package/templates/conditional/next/route-groups/ts/src/app/(admin)/layout.tsx +28 -0
- package/templates/conditional/next/route-groups/ts/src/app/(auth)/layout.tsx +22 -0
- package/templates/conditional/next/route-groups/ts/src/app/(private)/layout.tsx +32 -0
- package/templates/conditional/next/route-groups/ts/src/app/(public)/layout.tsx +33 -0
- package/templates/conditional/next/route-groups/ts/src/app/(public)/page.tsx +38 -0
- package/templates/conditional/next/route-groups/ts/src/middleware.ts +49 -0
- package/templates/conditional/testing/jest/next/app-router/src/__tests__/page.test.jsx +10 -0
- package/templates/conditional/testing/jest/next/app-router/src/__tests__/page.test.tsx +10 -0
- package/templates/conditional/testing/jest/next/jest.config.mjs +20 -0
- package/templates/conditional/testing/jest/next/jest.config.ts +20 -0
- package/templates/conditional/testing/jest/next/pages-router/src/__tests__/index.test.jsx +10 -0
- package/templates/conditional/testing/jest/next/pages-router/src/__tests__/index.test.tsx +10 -0
- package/templates/conditional/testing/vitest/next/app-router/src/__tests__/page.test.jsx +11 -0
- package/templates/conditional/testing/vitest/next/app-router/src/__tests__/page.test.tsx +11 -0
- package/templates/conditional/testing/vitest/next/pages-router/src/__tests__/index.test.jsx +11 -0
- package/templates/conditional/testing/vitest/next/pages-router/src/__tests__/index.test.tsx +11 -0
- package/templates/conditional/testing/vitest/next/vitest.config.js +16 -0
- 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
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
-
|
|
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(
|
|
361
|
-
|
|
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.
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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 .
|
|
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 .
|
|
276
|
+
agentSpin.start("Writing AGENTS.md...");
|
|
277
277
|
try {
|
|
278
278
|
await emitAgentContext(projectPath, responses._manifestSource ?? responses);
|
|
279
|
-
agentSpin.stop(chalk.green(".
|
|
279
|
+
agentSpin.stop(chalk.green("AGENTS.md written."));
|
|
280
280
|
} catch (err) {
|
|
281
|
-
agentSpin.stop(chalk.red("Failed to write .
|
|
281
|
+
agentSpin.stop(chalk.red("Failed to write AGENTS.md."));
|
|
282
282
|
throw err;
|
|
283
283
|
}
|
|
284
284
|
}
|
package/src/manifest/emit.js
CHANGED
|
@@ -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
|
];
|
package/src/manifest/schema.js
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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 .
|
|
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 .
|
|
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;
|