stackkit 0.2.2 → 0.2.3

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/bin/stackkit.js CHANGED
@@ -1,4 +1,11 @@
1
1
  #!/usr/bin/env node
2
-
3
- // eslint-disable-next-line
4
- require("../dist/index.js");
2
+ /* eslint-disable */
3
+ try {
4
+ require("../dist/index.js");
5
+ } catch (err) {
6
+ console.error(
7
+ "Failed to load compiled CLI (did you run 'npm run build'?)",
8
+ err && err.message ? err.message : err,
9
+ );
10
+ process.exitCode = 1;
11
+ }
package/dist/cli/add.js CHANGED
@@ -26,9 +26,20 @@ async function addCommand(module, options) {
26
26
  const projectInfo = await (0, detect_1.detectProjectInfo)(projectRoot);
27
27
  spinner.succeed(`Detected ${projectInfo.framework} (${projectInfo.router} router, ${projectInfo.language})`);
28
28
  const config = await getAddConfig(module, options, projectInfo);
29
- await addModuleToProject(projectRoot, projectInfo, config, options);
29
+ // Refresh project detection in case getAddConfig performed a pre-add (database)
30
+ const refreshedProjectInfo = await (0, detect_1.detectProjectInfo)(projectRoot);
31
+ await addModuleToProject(projectRoot, refreshedProjectInfo, config, options);
30
32
  logger_1.logger.newLine();
31
- logger_1.logger.success(`Added ${chalk_1.default.bold(config.displayName)}`);
33
+ if (config.preAdded && config.preAdded.length > 0) {
34
+ const addedNames = [
35
+ ...config.preAdded.map((p) => p.displayName),
36
+ config.displayName,
37
+ ].map((s) => chalk_1.default.bold(s));
38
+ logger_1.logger.success(`Added ${addedNames.join(" and ")}`);
39
+ }
40
+ else {
41
+ logger_1.logger.success(`Added ${chalk_1.default.bold(config.displayName)}`);
42
+ }
32
43
  logger_1.logger.newLine();
33
44
  }
34
45
  catch (error) {
@@ -42,7 +53,7 @@ async function addCommand(module, options) {
42
53
  async function getAddConfig(module, options, projectInfo) {
43
54
  const modulesDir = path_1.default.join((0, package_root_1.getPackageRoot)(), "modules");
44
55
  if (!module) {
45
- return await getInteractiveConfig(modulesDir, projectInfo);
56
+ return await getInteractiveConfig(modulesDir, projectInfo, options);
46
57
  }
47
58
  if (module === "database" || module === "auth") {
48
59
  if (!options?.provider) {
@@ -67,8 +78,8 @@ async function getAddConfig(module, options, projectInfo) {
67
78
  module: "database",
68
79
  provider: adapterProvider,
69
80
  displayName: parsed.database === "prisma" && parsed.provider
70
- ? `${moduleMetadata.displayName} (${parsed.provider})`
71
- : moduleMetadata.displayName,
81
+ ? `${moduleMetadata.displayName || baseProvider} (${parsed.provider})`
82
+ : moduleMetadata.displayName || baseProvider,
72
83
  metadata: moduleMetadata,
73
84
  };
74
85
  }
@@ -78,24 +89,26 @@ async function getAddConfig(module, options, projectInfo) {
78
89
  if (!moduleMetadata) {
79
90
  throw new Error(`Auth provider "${provider}" not found`);
80
91
  }
81
- // Validate compatibility with existing project
82
92
  if (projectInfo) {
83
- // Auth.js requires Next.js + Prisma
84
- if (provider === "authjs" &&
85
- (projectInfo.framework !== "nextjs" || !projectInfo.hasPrisma)) {
86
- throw new Error("Auth.js is only supported with Next.js and Prisma database in this project");
93
+ if (moduleMetadata.supportedFrameworks &&
94
+ !moduleMetadata.supportedFrameworks.includes(projectInfo.framework)) {
95
+ throw new Error(`${moduleMetadata.displayName} is not supported on ${projectInfo.framework}`);
87
96
  }
88
- // Better-auth requires a database for server frameworks (non-react)
89
- if (provider === "better-auth" &&
90
- !projectInfo.hasDatabase &&
91
- projectInfo.framework !== "react") {
92
- throw new Error("Better Auth requires a database for server frameworks in this project");
97
+ const dbName = projectInfo.hasPrisma
98
+ ? "prisma"
99
+ : projectInfo.hasDatabase
100
+ ? "other"
101
+ : "none";
102
+ if (moduleMetadata.compatibility &&
103
+ moduleMetadata.compatibility.databases &&
104
+ !moduleMetadata.compatibility.databases.includes(dbName)) {
105
+ throw new Error(`${moduleMetadata.displayName} is not compatible with the project's database configuration`);
93
106
  }
94
107
  }
95
108
  return {
96
109
  module: "auth",
97
110
  provider,
98
- displayName: moduleMetadata.displayName,
111
+ displayName: moduleMetadata.displayName || provider,
99
112
  metadata: moduleMetadata,
100
113
  };
101
114
  }
@@ -103,15 +116,17 @@ async function getAddConfig(module, options, projectInfo) {
103
116
  // Unknown module type
104
117
  throw new Error(`Unknown module type "${module}". Use "database" or "auth", or specify a provider directly.`);
105
118
  }
106
- async function getInteractiveConfig(modulesDir, projectInfo) {
107
- // Discover modules once and use compatibility info to decide which categories to show
119
+ async function getInteractiveConfig(modulesDir, projectInfo, options) {
120
+ const projectRoot = process.cwd();
108
121
  const discovered = await (0, module_discovery_1.discoverModules)(modulesDir);
109
- const compatibleAuths = (0, module_discovery_1.getCompatibleAuthOptions)(discovered.auth || [], projectInfo?.framework || "nextjs", projectInfo?.hasPrisma ? "prisma" : "none");
122
+ // Prefer discovered framework, then projectInfo.framework; leave empty if unknown
123
+ const defaultFramework = (discovered.frameworks && discovered.frameworks[0]?.name) || projectInfo?.framework || "";
124
+ const compatibleAuths = (0, module_discovery_1.getCompatibleAuthOptions)(discovered.auth || [], projectInfo?.framework || defaultFramework, projectInfo?.hasPrisma ? "prisma" : "none");
110
125
  const categories = [
111
126
  { name: "Database", value: "database" },
112
127
  ];
113
- // Only show Auth category if there is at least one compatible auth option
114
- if (compatibleAuths.length > 1) {
128
+ // Offer Auth category when there's at least one compatible auth option
129
+ if (compatibleAuths.length > 0) {
115
130
  categories.push({ name: "Auth", value: "auth" });
116
131
  }
117
132
  const answers = await inquirer_1.default.prompt([
@@ -124,7 +139,7 @@ async function getInteractiveConfig(modulesDir, projectInfo) {
124
139
  ]);
125
140
  const category = answers.category;
126
141
  if (category === "database") {
127
- const dbChoices = (0, module_discovery_1.getDatabaseChoices)(discovered.databases || [], projectInfo?.framework || "nextjs");
142
+ const dbChoices = (0, module_discovery_1.getDatabaseChoices)(discovered.databases || [], projectInfo?.framework || defaultFramework);
128
143
  const dbAnswers = await inquirer_1.default.prompt([
129
144
  {
130
145
  type: "list",
@@ -155,9 +170,57 @@ async function getInteractiveConfig(modulesDir, projectInfo) {
155
170
  };
156
171
  }
157
172
  else if (category === "auth") {
158
- // Filter auth choices based on project compatibility (framework + database)
173
+ let preAddedForReturn;
174
+ // If no database detected, require the user to select/add a database first
175
+ if (!projectInfo?.hasDatabase) {
176
+ logger_1.logger.warn("No database detected in the project. Authentication requires a database.");
177
+ const dbChoices = (0, module_discovery_1.getDatabaseChoices)(discovered.databases || [], projectInfo?.framework || defaultFramework);
178
+ const dbAnswer = await inquirer_1.default.prompt([
179
+ {
180
+ type: "list",
181
+ name: "database",
182
+ message: "Select a database to add before authentication:",
183
+ choices: dbChoices,
184
+ },
185
+ ]);
186
+ const selectedDb = dbAnswer.database;
187
+ if (!selectedDb || selectedDb === "none") {
188
+ logger_1.logger.info("Cancelled — authentication requires a database");
189
+ process.exit(0);
190
+ }
191
+ // Build a database AddConfig and add it immediately, then refresh projectInfo
192
+ let dbConfig;
193
+ if (selectedDb.startsWith("prisma-")) {
194
+ const provider = selectedDb.split("-")[1];
195
+ dbConfig = {
196
+ module: "database",
197
+ provider: `prisma-${provider}`,
198
+ displayName: `Prisma (${provider})`,
199
+ metadata: (await loadModuleMetadata(modulesDir, "prisma", "prisma")),
200
+ };
201
+ }
202
+ else {
203
+ const meta = (await loadModuleMetadata(modulesDir, selectedDb, selectedDb));
204
+ if (!meta)
205
+ throw new Error(`Database provider "${selectedDb}" not found`);
206
+ dbConfig = {
207
+ module: "database",
208
+ provider: selectedDb,
209
+ displayName: meta.displayName || selectedDb,
210
+ metadata: meta,
211
+ };
212
+ }
213
+ // Add the database first (suppress its top-level success message)
214
+ await addModuleToProject(projectRoot, projectInfo || (await (0, detect_1.detectProjectInfo)(projectRoot)), dbConfig, options);
215
+ // Refresh project info after database install and record pre-added
216
+ projectInfo = await (0, detect_1.detectProjectInfo)(projectRoot);
217
+ // attach preAdded so caller can summarize chained additions
218
+ dbConfig.preAdded = dbConfig.preAdded || [];
219
+ // store for returning later
220
+ preAddedForReturn = dbConfig;
221
+ }
159
222
  const dbString = projectInfo?.hasPrisma ? "prisma" : "none";
160
- const authChoices = (0, module_discovery_1.getCompatibleAuthOptions)(discovered.auth || [], projectInfo?.framework || "nextjs", dbString);
223
+ const authChoices = (0, module_discovery_1.getCompatibleAuthOptions)(discovered.auth || [], projectInfo?.framework || defaultFramework, dbString);
161
224
  const authAnswers = await inquirer_1.default.prompt([
162
225
  {
163
226
  type: "list",
@@ -180,12 +243,17 @@ async function getInteractiveConfig(modulesDir, projectInfo) {
180
243
  !metadata.supportedFrameworks.includes(projectInfo.framework)) {
181
244
  throw new Error(`Auth provider "${selectedAuth}" does not support ${projectInfo.framework}`);
182
245
  }
183
- return {
246
+ const result = {
184
247
  module: "auth",
185
248
  provider: selectedAuth,
186
249
  displayName: metadata.displayName || selectedAuth,
187
250
  metadata,
188
251
  };
252
+ // If we added a DB earlier in this flow, include it for grouped messaging
253
+ if (typeof preAddedForReturn !== "undefined" && preAddedForReturn) {
254
+ result.preAdded = [preAddedForReturn];
255
+ }
256
+ return result;
189
257
  }
190
258
  throw new Error("Invalid selection");
191
259
  }
@@ -261,6 +329,32 @@ async function addModuleToProject(projectRoot, projectInfo, config, options) {
261
329
  }
262
330
  }
263
331
  const selectedModules = { framework: projectInfo.framework };
332
+ // Populate database context from detected project state so generator conditions work
333
+ try {
334
+ const pkg = await fs_extra_1.default.readJson(path_1.default.join(projectRoot, "package.json"));
335
+ const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
336
+ if (projectInfo.hasPrisma) {
337
+ selectedModules.database = "prisma";
338
+ // Parse provider specifically from the `datasource` block to avoid
339
+ // capturing the generator block (which uses provider = "prisma-client-js").
340
+ const prismaSchema = path_1.default.join(projectRoot, "prisma", "schema.prisma");
341
+ if (await fs_extra_1.default.pathExists(prismaSchema)) {
342
+ const content = await fs_extra_1.default.readFile(prismaSchema, "utf-8");
343
+ const dsMatch = content.match(/datasource\s+\w+\s*\{([\s\S]*?)\}/i);
344
+ if (dsMatch && dsMatch[1]) {
345
+ const provMatch = dsMatch[1].match(/provider\s*=\s*["']([^"']+)["']/i);
346
+ if (provMatch && provMatch[1])
347
+ selectedModules.prismaProvider = provMatch[1];
348
+ }
349
+ }
350
+ }
351
+ else if (deps["mongoose"]) {
352
+ selectedModules.database = "mongoose";
353
+ }
354
+ }
355
+ catch {
356
+ // ignore detection errors
357
+ }
264
358
  if (config.module === "database" && config.provider) {
265
359
  const parsed = (0, shared_1.parseDatabaseOption)(config.provider);
266
360
  selectedModules.database = parsed.database;
@@ -272,7 +366,6 @@ async function addModuleToProject(projectRoot, projectInfo, config, options) {
272
366
  selectedModules.auth = config.provider;
273
367
  }
274
368
  const postInstall = await gen.applyToProject(selectedModules, [], projectRoot);
275
- // Install dependencies first, then run post-install commands (e.g. prisma generate)
276
369
  if (!options?.dryRun && options?.install !== false) {
277
370
  const installSpinner = logger_1.logger.startSpinner("Installing dependencies...");
278
371
  try {
@@ -285,30 +378,39 @@ async function addModuleToProject(projectRoot, projectInfo, config, options) {
285
378
  }
286
379
  }
287
380
  if (postInstall && postInstall.length > 0 && !options?.dryRun) {
288
- const postInstallSpinner = logger_1.logger.startSpinner("Running post-install commands...");
289
- try {
290
- for (const command of postInstall) {
291
- (0, child_process_1.execSync)(command, { cwd: projectRoot, stdio: "pipe" });
292
- }
293
- postInstallSpinner.succeed("Post-install commands completed");
381
+ const createdFiles = gen.getCreatedFiles ? gen.getCreatedFiles() : [];
382
+ if (createdFiles.length === 0) {
383
+ logger_1.logger.warn("Skipping post-install commands — no files were created by generators to act upon.");
294
384
  }
295
- catch (error) {
296
- postInstallSpinner.fail("Failed to run post-install commands");
297
- throw error;
385
+ else {
386
+ const postInstallSpinner = logger_1.logger.startSpinner("Running post-install commands...");
387
+ try {
388
+ for (const command of postInstall) {
389
+ (0, child_process_1.execSync)(command, { cwd: projectRoot, stdio: "pipe" });
390
+ }
391
+ postInstallSpinner.succeed("Post-install commands completed");
392
+ }
393
+ catch (error) {
394
+ postInstallSpinner.fail("Failed to run post-install commands");
395
+ throw error;
396
+ }
298
397
  }
299
398
  }
300
399
  return;
301
400
  }
302
401
  const mergedDeps = {};
303
402
  const mergedDevDeps = {};
304
- if (moduleMetadata.frameworkConfigs?.shared?.dependencies) {
305
- Object.assign(mergedDeps, moduleMetadata.frameworkConfigs.shared.dependencies);
403
+ try {
404
+ const shared = moduleMetadata.frameworkConfigs
405
+ ?.shared;
406
+ if (shared && shared.dependencies)
407
+ Object.assign(mergedDeps, shared.dependencies);
408
+ if (shared && shared.devDependencies)
409
+ Object.assign(mergedDevDeps, shared.devDependencies);
306
410
  }
307
- if (moduleMetadata.frameworkConfigs?.shared?.devDependencies) {
308
- Object.assign(mergedDevDeps, moduleMetadata.frameworkConfigs.shared.devDependencies);
411
+ catch {
412
+ // ignore malformed frameworkConfigs
309
413
  }
310
- // Adapter-specific dependencies are applied via generator metadata; frameworkConfigs still merge above.
311
- // Do not mutate the loaded module metadata here; use mergedDeps/mergedDevDeps for installation.
312
414
  const variables = {};
313
415
  if (selectedProvider) {
314
416
  variables.provider = selectedProvider;
@@ -344,16 +446,34 @@ async function addModuleToProject(projectRoot, projectInfo, config, options) {
344
446
  await applyFrameworkPatches(projectRoot, moduleMetadata.frameworkPatches, projectInfo.framework);
345
447
  }
346
448
  if (moduleMetadata.postInstall && moduleMetadata.postInstall.length > 0 && !options?.dryRun) {
347
- const postInstallSpinner = logger_1.logger.startSpinner("Running post-install commands...");
348
- try {
349
- for (const command of moduleMetadata.postInstall) {
350
- (0, child_process_1.execSync)(command, { cwd: projectRoot, stdio: "pipe" });
449
+ const createdFiles = [];
450
+ if (Array.isArray(moduleMetadata.patches)) {
451
+ for (const p of moduleMetadata.patches) {
452
+ if (p.type === "create-file") {
453
+ const destPath = p.destination;
454
+ if (typeof destPath === "string") {
455
+ const dest = path_1.default.join(projectRoot, destPath);
456
+ if (await fs_extra_1.default.pathExists(dest))
457
+ createdFiles.push(destPath);
458
+ }
459
+ }
351
460
  }
352
- postInstallSpinner.succeed("Post-install commands completed");
353
461
  }
354
- catch (error) {
355
- postInstallSpinner.fail("Failed to run post-install commands");
356
- throw error;
462
+ if (createdFiles.length === 0) {
463
+ logger_1.logger.warn("Skipping module post-install commands — no files were created by module patches to act upon.");
464
+ }
465
+ else {
466
+ const postInstallSpinner = logger_1.logger.startSpinner("Running post-install commands...");
467
+ try {
468
+ for (const command of moduleMetadata.postInstall) {
469
+ (0, child_process_1.execSync)(command, { cwd: projectRoot, stdio: "pipe" });
470
+ }
471
+ postInstallSpinner.succeed("Post-install commands completed");
472
+ }
473
+ catch (error) {
474
+ postInstallSpinner.fail("Failed to run post-install commands");
475
+ throw error;
476
+ }
357
477
  }
358
478
  }
359
479
  if (Object.keys(mergedDeps).length > 0 && options?.install !== false) {
@@ -374,16 +494,34 @@ async function addModuleToProject(projectRoot, projectInfo, config, options) {
374
494
  logger_1.logger.info(`Would add dev dependencies: ${devDeps.join(", ")}`);
375
495
  }
376
496
  }
377
- if (moduleMetadata.envVars && moduleMetadata.envVars.length > 0) {
378
- const processedEnvVars = moduleMetadata.envVars.map((envVar) => ({
379
- ...envVar,
380
- value: envVar.value?.replace(/\{\{(\w+)\}\}/g, (match, key) => variables[key] || match),
381
- }));
382
- if (!options?.dryRun) {
383
- await (0, env_editor_1.addEnvVariables)(projectRoot, processedEnvVars, { force: options?.force });
497
+ if (moduleMetadata.envVars) {
498
+ let envArray = [];
499
+ if (Array.isArray(moduleMetadata.envVars)) {
500
+ envArray = moduleMetadata.envVars;
384
501
  }
385
- else {
386
- logger_1.logger.log(` ${chalk_1.default.dim("~")} .env.example`);
502
+ else if (typeof moduleMetadata.envVars === "object") {
503
+ envArray = Object.entries(moduleMetadata.envVars).map(([k, v]) => ({
504
+ key: k,
505
+ value: String(v),
506
+ }));
507
+ }
508
+ if (envArray.length > 0) {
509
+ const processedEnvVars = envArray.map((envVar) => ({
510
+ key: envVar.key || "",
511
+ value: envVar.value?.replace(/\{\{(\w+)\}\}/g, (match, key) => variables[key] || match),
512
+ required: false,
513
+ }));
514
+ if (!options?.dryRun) {
515
+ const envVarsToAdd = processedEnvVars.map((e) => ({
516
+ key: e.key,
517
+ value: e.value,
518
+ required: !!e.required,
519
+ }));
520
+ await (0, env_editor_1.addEnvVariables)(projectRoot, envVarsToAdd, { force: options?.force });
521
+ }
522
+ else {
523
+ logger_1.logger.log(` ${chalk_1.default.dim("~")} .env.example`);
524
+ }
387
525
  }
388
526
  }
389
527
  }
@@ -475,8 +613,11 @@ async function findModulePath(modulesDir, moduleName, provider) {
475
613
  const metadataPath = path_1.default.join(modulePath, "module.json");
476
614
  if (await fs_extra_1.default.pathExists(metadataPath)) {
477
615
  const metadata = await fs_extra_1.default.readJSON(metadataPath);
478
- if (provider && moduleDir === provider) {
479
- return modulePath;
616
+ if (provider) {
617
+ const baseProvider = String(provider).split("-")[0];
618
+ if (moduleDir === provider || moduleDir === baseProvider) {
619
+ return modulePath;
620
+ }
480
621
  }
481
622
  if (!provider && metadata.name === moduleName) {
482
623
  return modulePath;
@@ -45,12 +45,23 @@ async function getProjectConfig(projectName, options) {
45
45
  const optionsProvided = flagsProvided || !!(options && (options.yes || options.y));
46
46
  if (optionsProvided) {
47
47
  if (options && (options.yes || options.y) && !flagsProvided) {
48
+ const defaultFramework = discoveredModules.frameworks && discoveredModules.frameworks.length > 0
49
+ ? discoveredModules.frameworks[0].name
50
+ : "";
51
+ const defaultDatabase = discoveredModules.databases && discoveredModules.databases.length > 0
52
+ ? discoveredModules.databases[0].name
53
+ : "none";
54
+ const prismaProviders = (0, shared_1.getPrismaProvidersFromGenerator)((0, package_root_1.getPackageRoot)());
55
+ const defaultPrismaProvider = prismaProviders.length > 0 ? prismaProviders[0] : undefined;
56
+ const defaultAuth = discoveredModules.auth && discoveredModules.auth.length > 0
57
+ ? discoveredModules.auth[0].name
58
+ : "none";
48
59
  return {
49
60
  projectName: projectName || "my-app",
50
- framework: "nextjs",
51
- database: "prisma",
52
- prismaProvider: "postgresql",
53
- auth: "better-auth",
61
+ framework: defaultFramework,
62
+ database: defaultDatabase,
63
+ prismaProvider: defaultPrismaProvider,
64
+ auth: defaultAuth,
54
65
  language: "typescript",
55
66
  packageManager: "pnpm",
56
67
  };
@@ -100,12 +111,22 @@ async function getProjectConfig(projectName, options) {
100
111
  if (authOpt && authOpt !== "none") {
101
112
  auth = authOpt;
102
113
  }
103
- const finalFramework = (framework || "nextjs");
104
- if (auth === "authjs" && (database !== "prisma" || finalFramework !== "nextjs")) {
105
- throw new Error("Auth.js is only supported with Next.js and Prisma database");
106
- }
107
- if (auth === "better-auth" && database === "none" && finalFramework !== "react") {
108
- throw new Error("Better Auth requires a database for server frameworks");
114
+ const finalFramework = (framework || (discoveredModules.frameworks[0]?.name ?? ""));
115
+ // Validate auth compatibility using discovered module metadata when available
116
+ if (auth && auth !== "none" && discoveredModules.auth) {
117
+ const authMeta = discoveredModules.auth.find((a) => a.name === auth);
118
+ if (authMeta) {
119
+ if (authMeta.supportedFrameworks &&
120
+ !authMeta.supportedFrameworks.includes(finalFramework)) {
121
+ throw new Error(`${authMeta.displayName || auth} is not supported on ${finalFramework}`);
122
+ }
123
+ const dbName = database === "prisma" ? "prisma" : database === "none" ? "none" : "other";
124
+ if (authMeta.compatibility &&
125
+ authMeta.compatibility.databases &&
126
+ !authMeta.compatibility.databases.includes(dbName)) {
127
+ throw new Error(`${authMeta.displayName || auth} is not compatible with the selected database configuration`);
128
+ }
129
+ }
109
130
  }
110
131
  return {
111
132
  projectName: projectName || "my-app",
@@ -117,6 +138,7 @@ async function getProjectConfig(projectName, options) {
117
138
  packageManager: (pm || "pnpm"),
118
139
  };
119
140
  }
141
+ const prismaProviders = (0, shared_1.getPrismaProvidersFromGenerator)((0, package_root_1.getPackageRoot)());
120
142
  const answers = (await inquirer_1.default.prompt([
121
143
  {
122
144
  type: "input",
@@ -139,26 +161,49 @@ async function getProjectConfig(projectName, options) {
139
161
  type: "list",
140
162
  name: "framework",
141
163
  message: "Select framework:",
142
- choices: discoveredModules.frameworks && discoveredModules.frameworks.length > 0
143
- ? discoveredModules.frameworks.map((f) => ({ name: f.displayName, value: f.name }))
144
- : [
145
- { name: "Next.js", value: "nextjs" },
146
- { name: "Express.js", value: "express" },
147
- { name: "React (Vite)", value: "react" },
148
- ],
164
+ choices: (() => {
165
+ if (discoveredModules.frameworks && discoveredModules.frameworks.length > 0) {
166
+ return discoveredModules.frameworks.map((f) => ({ name: f.displayName, value: f.name }));
167
+ }
168
+ // Fallback: read templates dir directly
169
+ try {
170
+ const templatesDir = path_1.default.join((0, package_root_1.getPackageRoot)(), "templates");
171
+ if (fs_extra_1.default.existsSync(templatesDir)) {
172
+ const dirs = fs_extra_1.default.readdirSync(templatesDir).filter((d) => d !== "node_modules");
173
+ return dirs.map((d) => ({ name: d.charAt(0).toUpperCase() + d.slice(1), value: d }));
174
+ }
175
+ }
176
+ catch {
177
+ // ignore
178
+ }
179
+ return [];
180
+ })(),
149
181
  },
150
182
  {
151
183
  type: "list",
152
184
  name: "database",
153
185
  message: "Select database/ORM:",
154
186
  when: (answers) => answers.framework !== "react",
155
- choices: (answers) => discoveredModules.databases && discoveredModules.databases.length > 0
156
- ? (0, module_discovery_1.getDatabaseChoices)(discoveredModules.databases, answers.framework)
157
- : [
158
- { name: "Prisma", value: "prisma" },
159
- { name: "Mongoose", value: "mongoose" },
160
- { name: "None", value: "none" },
161
- ],
187
+ choices: (answers) => {
188
+ if (discoveredModules.databases && discoveredModules.databases.length > 0) {
189
+ return (0, module_discovery_1.getDatabaseChoices)(discoveredModules.databases, answers.framework);
190
+ }
191
+ // Fallback: scan modules/database directory
192
+ try {
193
+ const modulesDir = path_1.default.join((0, package_root_1.getPackageRoot)(), "modules", "database");
194
+ if (fs_extra_1.default.existsSync(modulesDir)) {
195
+ const dbs = fs_extra_1.default
196
+ .readdirSync(modulesDir)
197
+ .map((d) => ({ name: d.charAt(0).toUpperCase() + d.slice(1), value: d }));
198
+ dbs.push({ name: "None", value: "none" });
199
+ return dbs;
200
+ }
201
+ }
202
+ catch {
203
+ // ignore
204
+ }
205
+ return [{ name: "None", value: "none" }];
206
+ },
162
207
  },
163
208
  // If a prisma-* choice is selected above, `prismaProvider` will be derived from it,
164
209
  // otherwise prompt for provider when `prisma` is selected directly.
@@ -166,13 +211,11 @@ async function getProjectConfig(projectName, options) {
166
211
  type: "list",
167
212
  name: "prismaProvider",
168
213
  message: "Select database provider for Prisma:",
169
- when: (answers) => answers.database === "prisma",
170
- choices: [
171
- { name: "PostgreSQL", value: "postgresql" },
172
- { name: "MongoDB", value: "mongodb" },
173
- { name: "MySQL", value: "mysql" },
174
- { name: "SQLite", value: "sqlite" },
175
- ],
214
+ when: (answers) => answers.database === "prisma" && prismaProviders.length > 0,
215
+ choices: prismaProviders.map((p) => ({
216
+ name: p.charAt(0).toUpperCase() + p.slice(1),
217
+ value: p,
218
+ })),
176
219
  },
177
220
  {
178
221
  type: "list",
@@ -204,7 +247,6 @@ async function getProjectConfig(projectName, options) {
204
247
  default: "pnpm",
205
248
  },
206
249
  ]));
207
- // Normalize database answer (interactive flow): handle values like `prisma-postgresql`
208
250
  let databaseAnswer = answers.framework === "react" ? "none" : answers.database;
209
251
  let prismaProviderAnswer = answers.prismaProvider;
210
252
  if (typeof databaseAnswer === "string" && databaseAnswer.startsWith("prisma-")) {