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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +23 -22
  2. package/bin/cli.js +1 -6
  3. package/package.json +2 -2
  4. package/schema/fixtures/v2/valid/next-app-router-feature-based.json +1 -1
  5. package/schema/fixtures/v2/valid/next-app-router-none.json +1 -1
  6. package/schema/fixtures/v2/valid/next-pages-router-feature-based.json +1 -1
  7. package/schema/fixtures/v2/valid/next-pages-router-none.json +1 -1
  8. package/schema/lumen.config.v2.json +2 -3
  9. package/src/cleanup.js +21 -5
  10. package/src/cli-args.js +53 -2
  11. package/src/configure.js +36 -8
  12. package/src/css.js +17 -7
  13. package/src/dependencies.js +9 -2
  14. package/src/engine/capabilities.js +2 -2
  15. package/src/engine/composition.js +48 -10
  16. package/src/env.js +149 -12
  17. package/src/format.js +14 -6
  18. package/src/injector.js +66 -14
  19. package/src/main.js +57 -12
  20. package/src/manifest/emit.js +23 -7
  21. package/src/manifest/paths.js +1 -25
  22. package/src/manifest/schema.js +3 -3
  23. package/src/prompts.js +25 -8
  24. package/src/readme.js +0 -16
  25. package/src/utils/dev-server.js +40 -8
  26. package/templates/.env.example +1 -1
  27. package/templates/architectures/feature-based/scripts/create-feature.mjs +2 -2
  28. package/templates/conditional/linting/biome/biome.json +17 -11
  29. package/templates/conditional/linting/eslint/next/eslint.config.mjs +2 -11
  30. package/templates/conditional/testing/vitest/next/vitest.config.js +1 -1
  31. package/templates/conditional/testing/vitest/next/vitest.config.ts +1 -1
  32. package/templates/css/tailwind/postcss.config.mjs +3 -1
  33. package/schema/fixtures/v2/valid/next-app-router-hybrid.json +0 -35
  34. package/schema/fixtures/v2/valid/next-pages-router-hybrid.json +0 -35
  35. package/templates/architectures/hybrid/main.jsx +0 -10
  36. package/templates/architectures/hybrid/main.tsx +0 -10
  37. package/templates/architectures/hybrid/src/App.jsx +0 -20
  38. package/templates/architectures/hybrid/src/App.tsx +0 -20
  39. package/templates/architectures/hybrid/src/features/.gitkeep +0 -1
  40. package/templates/architectures/hybrid/src/shared/components/ui/Button.jsx +0 -31
  41. package/templates/architectures/hybrid/src/shared/components/ui/Button.tsx +0 -36
  42. package/templates/architectures/hybrid/src/shared/components/ui/index.js +0 -1
  43. package/templates/architectures/hybrid/src/shared/components/ui/index.ts +0 -1
  44. package/templates/architectures/hybrid/src/shared/hooks/useLocalStorage.jsx +0 -28
  45. package/templates/architectures/hybrid/src/shared/hooks/useLocalStorage.ts +0 -28
  46. package/templates/architectures/hybrid/src/shared/layouts/.gitkeep +0 -0
  47. package/templates/architectures/hybrid/src/shared/stores/.gitkeep +0 -0
  48. package/templates/architectures/hybrid/src/shared/styles/.gitkeep +0 -0
  49. package/templates/architectures/hybrid/src/shared/types/index.js +0 -20
  50. package/templates/architectures/hybrid/src/shared/types/index.ts +0 -16
  51. package/templates/architectures/hybrid/src/shared/utils/cn.jsx +0 -3
  52. package/templates/architectures/hybrid/src/shared/utils/cn.ts +0 -5
  53. package/templates/conditional/testing/jest/hybrid/src/__tests__/App.test.jsx +0 -11
  54. package/templates/conditional/testing/jest/hybrid/src/__tests__/App.test.tsx +0 -11
  55. package/templates/conditional/testing/vitest/hybrid/src/__tests__/App.test.jsx +0 -10
  56. package/templates/conditional/testing/vitest/hybrid/src/__tests__/App.test.tsx +0 -10
package/src/env.js CHANGED
@@ -9,13 +9,38 @@ const ENV_GITIGNORE = `
9
9
  !.env.example
10
10
  `;
11
11
 
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.
12
+ export async function resolveFramework(projectPath, responses = {}) {
13
+ if (responses.framework || responses.frameworkName) {
14
+ return normalizeFramework(
15
+ responses.framework || {
16
+ name: responses.frameworkName,
17
+ variant: responses.frameworkVariant,
18
+ }
19
+ );
20
+ }
14
21
 
15
- NEXT_PUBLIC_API_URL=https://api.example.com
16
- `;
22
+ // Try reading lumen.config.json if already written
23
+ try {
24
+ const manifestPath = path.join(projectPath, "lumen.config.json");
25
+ const content = await fsp.readFile(manifestPath, "utf8");
26
+ const parsed = JSON.parse(content);
27
+ if (parsed.framework) return normalizeFramework(parsed.framework);
28
+ } catch {}
17
29
 
18
- export async function copyEnvExample(projectPath, templatesDir, responses = {}) {
30
+ // Try checking package.json
31
+ try {
32
+ const pkgPath = path.join(projectPath, "package.json");
33
+ const content = await fsp.readFile(pkgPath, "utf8");
34
+ const pkg = JSON.parse(content);
35
+ if (pkg.dependencies?.next || pkg.devDependencies?.next) {
36
+ return { name: "next", variant: "app-router" };
37
+ }
38
+ } catch {}
39
+
40
+ return { name: "react", variant: "vite" };
41
+ }
42
+
43
+ export function buildEnvExample(responses = {}, existingContent = "") {
19
44
  const fw = normalizeFramework(
20
45
  responses.framework || {
21
46
  name: responses.frameworkName,
@@ -23,16 +48,128 @@ export async function copyEnvExample(projectPath, templatesDir, responses = {})
23
48
  }
24
49
  );
25
50
 
26
- if (fw.name === "next") {
27
- await fsp.writeFile(path.join(projectPath, ".env.example"), NEXT_ENV_EXAMPLE, "utf8");
51
+ const isNext = fw.name === "next";
52
+ const lines = [];
53
+
54
+ if (isNext) {
55
+ const routerDocs =
56
+ fw.variant === "pages-router"
57
+ ? "https://nextjs.org/docs/pages/building-your-application/configuring/environment-variables"
58
+ : "https://nextjs.org/docs/app/building-your-application/configuring/environment-variables";
59
+
60
+ lines.push("# Copy this file to `.env.local` (or `.env`) and fill in your values.");
61
+ lines.push(`# See ${routerDocs} for details.`);
62
+ lines.push("");
63
+
64
+ // Public API client URL
65
+ lines.push("# Public API URL (accessible in browser and server via process.env.NEXT_PUBLIC_API_URL)");
66
+ lines.push("NEXT_PUBLIC_API_URL=https://api.example.com");
67
+
68
+ // Database (Prisma)
69
+ if (responses.database === "prisma" || existingContent.includes("DATABASE_URL")) {
70
+ const isSupabase =
71
+ responses.databaseProvider === "supabase" ||
72
+ responses.dbProvider === "supabase" ||
73
+ (!responses.databaseProvider && existingContent.includes("pooler.supabase.com"));
74
+
75
+ lines.push("");
76
+ if (isSupabase) {
77
+ lines.push("# Database (Prisma + Supabase PostgreSQL)");
78
+ lines.push("# Transaction connection pooler (port 6543) with PgBouncer - used by Next.js Server Components / Actions:");
79
+ lines.push('DATABASE_URL="postgresql://postgres.[YOUR-PROJECT-REF]:[YOUR-PASSWORD]@aws-0-[REGION].pooler.supabase.com:6543/postgres?pgbouncer=true"');
80
+ lines.push("");
81
+ lines.push("# Direct connection (port 5432) - used by Prisma CLI for migrations:");
82
+ lines.push('DIRECT_URL="postgresql://postgres.[YOUR-PROJECT-REF]:[YOUR-PASSWORD]@aws-0-[REGION].pooler.supabase.com:5432/postgres"');
83
+ lines.push("");
84
+ lines.push("# Local development fallback (local PostgreSQL / Docker compose / Supabase local CLI):");
85
+ lines.push('# DATABASE_URL="postgresql://postgres:postgres@localhost:5432/app?schema=public"');
86
+ lines.push('# DIRECT_URL="postgresql://postgres:postgres@localhost:5432/app?schema=public"');
87
+ lines.push("");
88
+ lines.push("# Supabase API credentials (optional, for Supabase Auth / Storage)");
89
+ lines.push('# NEXT_PUBLIC_SUPABASE_URL="https://[YOUR-PROJECT-REF].supabase.co"');
90
+ lines.push('# NEXT_PUBLIC_SUPABASE_ANON_KEY="eyJhbGciOi..."');
91
+ } else {
92
+ lines.push("# Database (Prisma + PostgreSQL)");
93
+ lines.push("# Local development with PostgreSQL or Docker compose (docker compose up -d db):");
94
+ lines.push('DATABASE_URL="postgresql://postgres:postgres@localhost:5432/app?schema=public"');
95
+ lines.push('DIRECT_URL="postgresql://postgres:postgres@localhost:5432/app?schema=public"');
96
+ lines.push("");
97
+ lines.push("# Supabase / cloud PostgreSQL with connection pooling (Transaction mode / port 6543):");
98
+ lines.push('# DATABASE_URL="postgresql://postgres:[YOUR-PASSWORD]@db.[YOUR-PROJECT-REF].supabase.co:6543/postgres?pgbouncer=true"');
99
+ lines.push('# DIRECT_URL="postgresql://postgres:[YOUR-PASSWORD]@db.[YOUR-PROJECT-REF].supabase.co:5432/postgres"');
100
+ }
101
+ }
102
+
103
+ // Docker
104
+ if (responses.docker === true || existingContent.includes("PORT=")) {
105
+ lines.push("");
106
+ lines.push("# Server / Container Port (optional, defaults to 3000)");
107
+ lines.push("# PORT=3000");
108
+ }
28
109
  } 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 {}
110
+ // React / Vite
111
+ lines.push("# Copy this file to `.env.local` (or `.env`) and fill in your values.");
112
+ lines.push("# See https://vite.dev/guide/env-and-mode for how env vars are exposed.");
113
+ lines.push("");
114
+ lines.push("# Application API URL (accessible in Vite via import.meta.env.VITE_API_URL)");
115
+ lines.push("VITE_API_URL=https://api.example.com");
116
+ }
117
+
118
+ lines.push("");
119
+ let result = lines.join("\n");
120
+
121
+ // If there was existing custom content, preserve custom non-framework variables
122
+ if (existingContent && existingContent.trim()) {
123
+ const existingLines = existingContent.split("\n");
124
+ const preservedLines = [];
125
+ for (const line of existingLines) {
126
+ const trimmed = line.trim();
127
+ if (!trimmed || trimmed.startsWith("#")) continue;
128
+
129
+ // Purge Vite references from Next.js scaffolds
130
+ if (
131
+ isNext &&
132
+ (trimmed.startsWith("VITE_") ||
133
+ trimmed.includes("vitejs.dev") ||
134
+ trimmed.includes("vite.dev"))
135
+ ) {
136
+ continue;
137
+ }
138
+ // Purge Next.js references from React scaffolds
139
+ if (!isNext && trimmed.startsWith("NEXT_PUBLIC_")) {
140
+ continue;
141
+ }
142
+
143
+ // Check variable name
144
+ const eqIdx = trimmed.indexOf("=");
145
+ if (eqIdx !== -1) {
146
+ const varName = trimmed.slice(0, eqIdx).trim();
147
+ if (!result.includes(varName + "=")) {
148
+ preservedLines.push(line);
149
+ }
150
+ }
151
+ }
152
+ if (preservedLines.length > 0) {
153
+ result = result.trimEnd() + "\n\n# Custom environment variables\n" + preservedLines.join("\n") + "\n";
154
+ }
34
155
  }
35
156
 
157
+ return result;
158
+ }
159
+
160
+ export async function copyEnvExample(projectPath, templatesDir, responses = {}) {
161
+ const fw = await resolveFramework(projectPath, responses);
162
+ const resolvedResponses = { ...responses, framework: fw };
163
+
164
+ const envPath = path.join(projectPath, ".env.example");
165
+ let existingContent = "";
166
+ try {
167
+ existingContent = await fsp.readFile(envPath, "utf8");
168
+ } catch {}
169
+
170
+ const envContent = buildEnvExample(resolvedResponses, existingContent);
171
+ await fsp.writeFile(envPath, envContent, "utf8");
172
+
36
173
  // Ensure the project gitignore excludes env files but keeps .env.example.
37
174
  const gitignore = path.join(projectPath, ".gitignore");
38
175
  try {
package/src/format.js CHANGED
@@ -17,12 +17,20 @@ async function exists(p) {
17
17
  // is already on PATH (e.g. verification harnesses resolving from a vendored
18
18
  // node_modules symlink).
19
19
  export async function runProjectFormat(projectPath, responses) {
20
- if (!responses.formatter || responses.formatter === "none") return;
20
+ if (responses.formatter && responses.formatter !== "none") {
21
+ const formatter = responses.formatter;
22
+ const bin = path.join(projectPath, "node_modules", ".bin", formatter);
23
+ const command = (await exists(bin)) ? bin : formatter;
24
+ const args = formatter === "prettier" ? ["--write", "."] : ["."];
21
25
 
22
- const formatter = responses.formatter;
23
- const bin = path.join(projectPath, "node_modules", ".bin", formatter);
24
- const command = (await exists(bin)) ? bin : formatter;
25
- const args = formatter === "prettier" ? ["--write", "."] : ["."];
26
+ await execa(command, args, { cwd: projectPath, stdio: "pipe" });
27
+ return;
28
+ }
26
29
 
27
- await execa(command, args, { cwd: projectPath, stdio: "pipe" });
30
+ // When Biome is the linter and no separate formatter is chosen, use Biome's native check --write
31
+ if (responses.linter === "biome") {
32
+ const bin = path.join(projectPath, "node_modules", ".bin", "biome");
33
+ const command = (await exists(bin)) ? bin : "biome";
34
+ await execa(command, ["check", "--write", "."], { cwd: projectPath, stdio: "pipe" });
35
+ }
28
36
  }
package/src/injector.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { promises as fsp } from "fs";
2
2
  import path from "path";
3
+ import { normalizeFramework } from "./engine/capabilities.js";
3
4
  import {
4
5
  FRAGMENT_SCOPES,
5
6
  resolveFragments,
@@ -31,9 +32,25 @@ export async function injectArchitecture(
31
32
  templatesDir,
32
33
  architecture,
33
34
  language,
34
- cssFramework
35
+ cssFramework,
36
+ frameworkOptions = {}
35
37
  ) {
36
- const responses = { architecture, language, cssFramework };
38
+ const fwObj =
39
+ typeof frameworkOptions === "object" && frameworkOptions !== null
40
+ ? frameworkOptions
41
+ : typeof frameworkOptions === "string"
42
+ ? { name: frameworkOptions }
43
+ : {};
44
+ const framework = normalizeFramework(fwObj);
45
+ const responses = {
46
+ architecture,
47
+ language,
48
+ cssFramework,
49
+ framework,
50
+ frameworkName: framework.name,
51
+ frameworkVariant: framework.variant,
52
+ ...fwObj,
53
+ };
37
54
  const allFragments = resolveFragments(responses, templatesDir);
38
55
  const archFragments = allFragments.filter(
39
56
  (f) =>
@@ -150,35 +167,59 @@ export function getComponentsJson({
150
167
  architecture = "feature-based",
151
168
  language = "ts",
152
169
  }) {
153
- const isNext = framework === "next";
170
+ const fwName =
171
+ typeof framework === "object" && framework !== null
172
+ ? framework.name
173
+ : String(framework);
174
+ const fwVariant =
175
+ typeof framework === "object" && framework !== null
176
+ ? framework.variant
177
+ : "app-router";
178
+ const isNext = fwName === "next";
154
179
  const isTs = language === "ts";
180
+ const isFeatureBased = architecture === "feature-based";
155
181
 
156
182
  if (isNext) {
183
+ const cssPath =
184
+ fwVariant === "pages-router"
185
+ ? "src/styles/globals.css"
186
+ : "src/app/globals.css";
187
+
157
188
  // Next App Router contract stub
158
189
  return {
159
190
  $schema: "https://ui.shadcn.com/schema.json",
160
191
  style: "new-york",
161
- rsc: true,
192
+ rsc: fwVariant !== "pages-router",
162
193
  tsx: isTs,
163
194
  tailwind: {
164
195
  config: "",
165
- css: "app/globals.css",
196
+ css: cssPath,
166
197
  baseColor: "neutral",
167
198
  cssVariables: true,
168
199
  prefix: "",
169
200
  },
170
- aliases: {
171
- components: "@/components",
172
- utils: "@/lib/utils",
173
- ui: "@/components/ui",
174
- lib: "@/lib",
175
- hooks: "@/hooks",
176
- },
201
+ aliases: isFeatureBased
202
+ ? {
203
+ components: "@/shared/components",
204
+ utils: "@/lib/utils",
205
+ ui: "@/shared/components/ui",
206
+ lib: "@/lib",
207
+ hooks: "@/shared/hooks",
208
+ }
209
+ : {
210
+ components: "@/components",
211
+ utils: "@/lib/utils",
212
+ ui: "@/components/ui",
213
+ lib: "@/lib",
214
+ hooks: "@/hooks",
215
+ },
177
216
  iconLibrary: "lucide",
217
+ registries: {
218
+ "@lumen": "https://raw.githubusercontent.com/strophix-software/lumen-registry/main/public/r",
219
+ },
178
220
  };
179
221
  }
180
222
 
181
- const isFeatureBased = architecture === "feature-based";
182
223
  return {
183
224
  $schema: "https://ui.shadcn.com/schema.json",
184
225
  style: "new-york",
@@ -209,6 +250,9 @@ export function getComponentsJson({
209
250
  hooks: "@/hooks",
210
251
  },
211
252
  iconLibrary: "lucide",
253
+ registries: {
254
+ "@lumen": "https://raw.githubusercontent.com/strophix-software/lumen-registry/main/public/r",
255
+ },
212
256
  };
213
257
  }
214
258
 
@@ -232,6 +276,7 @@ export async function injectUiKit(
232
276
  const codeExt = isTs ? "tsx" : "jsx";
233
277
  const utilExt = isTs ? "ts" : "js";
234
278
  const framework = responses.framework || responses.frameworkName || "vite";
279
+ const isNext = typeof framework === "object" ? framework.name === "next" : framework === "next";
235
280
 
236
281
  // 1. Write components.json
237
282
  const componentsJson = getComponentsJson({
@@ -313,7 +358,14 @@ export async function injectUiKit(
313
358
 
314
359
  try {
315
360
  await fsp.access(buttonTemplate);
316
- await fsp.copyFile(buttonTemplate, buttonDest);
361
+ let buttonContent = await fsp.readFile(buttonTemplate, "utf8");
362
+ if (isNext) {
363
+ buttonContent = buttonContent.replace(
364
+ /\/\/ eslint-disable-next-line react-refresh\/only-export-components\r?\n/,
365
+ ""
366
+ );
367
+ }
368
+ await fsp.writeFile(buttonDest, buttonContent, "utf8");
317
369
  } catch {}
318
370
 
319
371
  try {
package/src/main.js CHANGED
@@ -26,9 +26,9 @@ import { normalizeFramework } from "@/engine/capabilities.js";
26
26
  const __filename = fileURLToPath(import.meta.url);
27
27
  const __dirname = path.dirname(__filename);
28
28
  const TEMPLATES_DIR = path.join(__dirname, "../templates");
29
- const CURRENT_DIR = process.cwd();
30
29
 
31
- export function resolveProjectName({ quickSetup = false, projectName: nameArg, cwd = CURRENT_DIR, rawArgs } = {}) {
30
+ export function resolveProjectName({ quickSetup = false, projectName: nameArg, cwd, rawArgs } = {}) {
31
+ const currentDir = cwd || process.cwd();
32
32
  const cli = parseCliArgs(rawArgs || process.argv.slice(2));
33
33
  const cliArg = nameArg || cli.projectName || null;
34
34
 
@@ -37,7 +37,7 @@ export function resolveProjectName({ quickSetup = false, projectName: nameArg, c
37
37
  }
38
38
 
39
39
  if (quickSetup || cli.quickSetup || cli.manifest || cli.template) {
40
- const defaultName = path.basename(cwd).trim() || "my-app";
40
+ const defaultName = path.basename(currentDir).trim() || "my-app";
41
41
  return defaultName === "." ? "my-app" : defaultName;
42
42
  }
43
43
 
@@ -52,16 +52,31 @@ export async function main(options = {}) {
52
52
  template: templateName,
53
53
  framework: frameworkArg,
54
54
  arch: archArg,
55
+ language: languageArg,
56
+ cssFramework: cssArg,
57
+ uiKit: uiKitArg,
58
+ database: databaseArg,
59
+ databaseProvider: dbProviderArg,
60
+ docker: dockerArg,
61
+ testing: testingArg,
62
+ linter: linterArg,
63
+ formatter: formatterArg,
64
+ routeGroups: routeGroupsArg,
65
+ bundler: bundlerArg,
66
+ adapter: adapterArg,
67
+ reactCompiler: reactCompilerArg,
68
+ agentDocs: agentDocsArg,
55
69
  } = options;
56
70
  const pkg = getPkgManager();
57
71
 
58
72
  const isNonInteractive = Boolean(quickSetup || manifestSource || templateName);
73
+ const currentDir = options.cwd || process.cwd();
59
74
 
60
75
  // 1. Project name
61
76
  let projectName = resolveProjectName({
62
77
  quickSetup: isNonInteractive,
63
78
  projectName: nameArg,
64
- cwd: CURRENT_DIR,
79
+ cwd: currentDir,
65
80
  });
66
81
 
67
82
  if (projectName) {
@@ -83,7 +98,7 @@ export async function main(options = {}) {
83
98
  // 3. Collect user preferences (interactive or non-interactive)
84
99
  let responses;
85
100
  if (manifestSource) {
86
- const manifestObj = loadManifestSource(manifestSource, CURRENT_DIR);
101
+ const manifestObj = loadManifestSource(manifestSource, currentDir);
87
102
  responses = manifestToResponses(manifestObj, projectName);
88
103
  } else if (templateName) {
89
104
  const preset = PRESETS[templateName];
@@ -100,7 +115,23 @@ export async function main(options = {}) {
100
115
  responses = await getUserInputs(projectName, { quickSetup, initialArch: archArg, framework: frameworkArg });
101
116
  }
102
117
 
103
- const projectPath = path.resolve(CURRENT_DIR, projectName);
118
+ // Overlay explicit CLI flags if provided
119
+ if (languageArg) responses.language = languageArg;
120
+ if (cssArg) responses.cssFramework = cssArg;
121
+ if (uiKitArg) responses.uiKit = uiKitArg;
122
+ if (linterArg) responses.linter = linterArg;
123
+ if (formatterArg) responses.formatter = formatterArg;
124
+ if (databaseArg) responses.database = databaseArg;
125
+ if (dbProviderArg) responses.databaseProvider = dbProviderArg;
126
+ if (testingArg) responses.testing = testingArg;
127
+ if (dockerArg) responses.docker = true;
128
+ if (routeGroupsArg) responses.routeGroups = true;
129
+ if (bundlerArg) responses.bundler = bundlerArg;
130
+ if (adapterArg) responses.adapter = adapterArg;
131
+ if (reactCompilerArg !== null && reactCompilerArg !== undefined) responses.reactCompiler = reactCompilerArg;
132
+ if (agentDocsArg !== null && agentDocsArg !== undefined) responses.agentDocs = agentDocsArg;
133
+
134
+ const projectPath = path.resolve(currentDir, projectName);
104
135
 
105
136
  // 4. Scaffold the base project (provider-owned bases; Vite shells out, Next copies)
106
137
  const fw = normalizeFramework(
@@ -168,7 +199,8 @@ export async function main(options = {}) {
168
199
  TEMPLATES_DIR,
169
200
  responses.architecture,
170
201
  responses.language,
171
- responses.cssFramework
202
+ responses.cssFramework,
203
+ responses.framework || { name: responses.frameworkName, variant: responses.frameworkVariant }
172
204
  );
173
205
  archSpin.stop(chalk.green("Architecture scaffolding complete."));
174
206
  } catch (err) {
@@ -233,6 +265,7 @@ export async function main(options = {}) {
233
265
  architecture: responses.architecture,
234
266
  ext: responses.language === "ts" ? "tsx" : "jsx",
235
267
  pkg,
268
+ framework: responses.framework || { name: responses.frameworkName, variant: responses.frameworkVariant },
236
269
  });
237
270
  if (responses.cssFramework !== "none") {
238
271
  cssSpin.stop(chalk.green(`${cssLabel} setup complete.`));
@@ -246,7 +279,12 @@ export async function main(options = {}) {
246
279
  const configSpin = spinner();
247
280
  configSpin.start("Configuring project...");
248
281
  try {
249
- await configureProject(projectPath, responses.language, responses.cssFramework);
282
+ await configureProject(
283
+ projectPath,
284
+ responses.language,
285
+ responses.cssFramework,
286
+ responses.framework || { name: responses.frameworkName, variant: responses.frameworkVariant }
287
+ );
250
288
  configSpin.stop(chalk.green("Project configured."));
251
289
  } catch (err) {
252
290
  configSpin.stop(chalk.red("Failed to configure project."));
@@ -254,7 +292,7 @@ export async function main(options = {}) {
254
292
  }
255
293
 
256
294
  // 12. Cleanup boilerplate
257
- await cleanupBoilerplate(projectPath);
295
+ await cleanupBoilerplate(projectPath, responses);
258
296
 
259
297
  // 12.5 Copy .env.example into the project
260
298
  await copyEnvExample(projectPath, TEMPLATES_DIR, responses);
@@ -297,9 +335,16 @@ export async function main(options = {}) {
297
335
  }
298
336
 
299
337
  // 13.5 Format the freshly generated project so it is canonical on init
300
- if (responses.formatter && responses.formatter !== "none") {
338
+ const shouldFormat =
339
+ (responses.formatter && responses.formatter !== "none") ||
340
+ responses.linter === "biome";
341
+ if (shouldFormat) {
342
+ const fmtName =
343
+ responses.formatter && responses.formatter !== "none"
344
+ ? responses.formatter
345
+ : "biome";
301
346
  const fmtPassSpin = spinner();
302
- fmtPassSpin.start(`Formatting project with ${responses.formatter}...`);
347
+ fmtPassSpin.start(`Formatting project with ${fmtName}...`);
303
348
  try {
304
349
  await runProjectFormat(projectPath, responses);
305
350
  fmtPassSpin.stop(chalk.green("Project formatted."));
@@ -317,5 +362,5 @@ export async function main(options = {}) {
317
362
  );
318
363
 
319
364
  // 14. Offer dev server
320
- await handleDevServer(pkg, projectName);
365
+ await handleDevServer(pkg, projectPath, { isNonInteractive, projectName });
321
366
  }
@@ -15,25 +15,34 @@ const FORMAT_COMMANDS = {
15
15
  };
16
16
 
17
17
  /**
18
- * Build default harness commands from tooling choices.
18
+ * Build default harness commands from tooling choices and test framework.
19
19
  * Skips lint/format when tooling is "none"; skips types when language is not "ts".
20
- * Test + build are always included so commands.length >= 1.
21
- * @param {{ linter?: string, formatter?: string, language?: string }} tooling
20
+ * Skips test when testing is "none" (or not configured).
21
+ * Build is always included so commands.length >= 1.
22
+ * @param {{ linter?: string, formatter?: string, language?: string, testing?: string }} tooling
23
+ * @param {string} [testing]
22
24
  * @returns {Array<{ name: string, command: string, required: boolean }>}
23
25
  */
24
- export function defaultHarnessCommands(tooling = {}) {
26
+ export function defaultHarnessCommands(tooling = {}, testing = null) {
25
27
  const { linter = "none", formatter = "none", language = "ts" } = tooling;
28
+ const testFramework = testing ?? tooling.testing ?? "none";
26
29
  const commands = [];
27
30
  if (linter && linter !== "none" && LINT_COMMANDS[linter]) {
28
31
  commands.push({ name: "lint", command: LINT_COMMANDS[linter], required: true });
29
32
  }
30
33
  if (formatter && formatter !== "none" && FORMAT_COMMANDS[formatter]) {
31
34
  commands.push({ name: "format", command: FORMAT_COMMANDS[formatter], required: true });
35
+ } else if (linter === "biome" && (!formatter || formatter === "none" || formatter === "biome")) {
36
+ commands.push({ name: "format", command: "biome check --write .", required: true });
32
37
  }
33
38
  if (language === "ts") {
34
39
  commands.push({ name: "types", command: "tsc -b", required: true });
35
40
  }
36
- commands.push({ name: "test", command: "vitest run", required: true });
41
+ if (testFramework === "vitest") {
42
+ commands.push({ name: "test", command: "vitest run", required: true });
43
+ } else if (testFramework === "jest") {
44
+ commands.push({ name: "test", command: "npm test", required: true });
45
+ }
37
46
  commands.push({ name: "build", command: "npm run build", required: true });
38
47
  return commands.length > 0 ? commands : [...HARNESS_DEFAULT_COMMANDS];
39
48
  }
@@ -99,8 +108,15 @@ export function buildManifest(responses, opts = {}) {
99
108
  formatter: responses.formatter || "none",
100
109
  };
101
110
 
111
+ const resolvedTesting =
112
+ responses.testing !== undefined
113
+ ? responses.testing
114
+ : framework.name === "next"
115
+ ? "none"
116
+ : "vitest";
117
+
102
118
  const harness = responses.harness ?? {
103
- commands: defaultHarnessCommands(tooling),
119
+ commands: defaultHarnessCommands(tooling, resolvedTesting),
104
120
  };
105
121
 
106
122
  const manifest = {
@@ -120,7 +136,7 @@ export function buildManifest(responses, opts = {}) {
120
136
  if (responses.database && responses.database !== "none") {
121
137
  manifest.database = {
122
138
  orm: responses.database,
123
- provider: "postgresql",
139
+ provider: responses.databaseProvider || "postgresql",
124
140
  };
125
141
  }
126
142
  if (typeof responses.docker === "boolean") manifest.docker = responses.docker;
@@ -16,7 +16,7 @@ export function resolvePaths({ architecture, framework = {} } = {}) {
16
16
  const variant = framework.variant || (fwName === "next" ? "app-router" : "vite");
17
17
 
18
18
  // Helper to pick Next.js pages root based on variant
19
- const nextPagesForVariant = (v) => (v === "pages-router" ? "pages" : "app");
19
+ const nextPagesForVariant = (v) => (v === "pages-router" ? "src/pages" : "src/app");
20
20
  const isNext = fwName === "next";
21
21
 
22
22
  // -- feature-based (React + Vite canonical, also Next feature-based)
@@ -69,30 +69,6 @@ export function resolvePaths({ architecture, framework = {} } = {}) {
69
69
  };
70
70
  }
71
71
 
72
- // -- hybrid (Next.js: feature + shared). For React, fallback to feature-based
73
- if (arch === "hybrid") {
74
- if (isNext) {
75
- const nextRoot = nextPagesForVariant(variant);
76
- return {
77
- features: "src/features",
78
- components: "src/shared/components",
79
- services: "src/shared/services",
80
- hooks: "src/shared/hooks",
81
- pages: nextRoot,
82
- ui: "src/shared/components/ui",
83
- };
84
- }
85
- // React hybrid = hybrid not valid, but map to feature-based for determinism
86
- return {
87
- features: "src/features",
88
- components: "src/shared/components",
89
- services: "src/shared/services",
90
- hooks: "src/shared/hooks",
91
- pages: "src/app/router",
92
- ui: "src/shared/components/ui",
93
- };
94
- }
95
-
96
72
  // -- none (minimal flat src/ — no imposed convention)
97
73
  if (arch === "none") {
98
74
  if (isNext) {
@@ -13,7 +13,7 @@ export const FRAMEWORK_BUNDLERS = ["turbopack", "webpack"];
13
13
  export const FRAMEWORK_ADAPTERS = ["node", "vercel", "cloudflare", "static"];
14
14
 
15
15
  export const STYLING_ENGINES = ["tailwind", "none"];
16
- export const ARCHITECTURE_PRESETS = ["feature-based", "type-based", "hybrid", "none"];
16
+ export const ARCHITECTURE_PRESETS = ["feature-based", "type-based", "none"];
17
17
  // Legacy alias: docs used architecture.type, canonical is architecture.preset.
18
18
  // Keep both in sync; parseManifest normalizes to expose both keys.
19
19
  export const ARCHITECTURE_TYPES = ARCHITECTURE_PRESETS;
@@ -24,7 +24,7 @@ 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
26
  export const DATABASE_ORMS = ["prisma", "none"];
27
- export const DATABASE_PROVIDERS = ["postgresql"];
27
+ export const DATABASE_PROVIDERS = ["postgresql", "supabase"];
28
28
 
29
29
  // ---------------------------------------------------------------------------
30
30
  // Sub-schemas
@@ -212,7 +212,7 @@ export const manifestSchemaV2 = z
212
212
  const allowedByFramework =
213
213
  framework.name === "react"
214
214
  ? ["feature-based", "type-based", "none"]
215
- : ["feature-based", "hybrid", "none"];
215
+ : ["feature-based", "none"];
216
216
  if (!allowedByFramework.includes(archValue)) {
217
217
  ctx.addIssue({
218
218
  code: "custom",