stackkit 0.2.1 → 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,10 +89,26 @@ async function getAddConfig(module, options, projectInfo) {
78
89
  if (!moduleMetadata) {
79
90
  throw new Error(`Auth provider "${provider}" not found`);
80
91
  }
92
+ if (projectInfo) {
93
+ if (moduleMetadata.supportedFrameworks &&
94
+ !moduleMetadata.supportedFrameworks.includes(projectInfo.framework)) {
95
+ throw new Error(`${moduleMetadata.displayName} is not supported on ${projectInfo.framework}`);
96
+ }
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`);
106
+ }
107
+ }
81
108
  return {
82
109
  module: "auth",
83
110
  provider,
84
- displayName: moduleMetadata.displayName,
111
+ displayName: moduleMetadata.displayName || provider,
85
112
  metadata: moduleMetadata,
86
113
  };
87
114
  }
@@ -89,22 +116,30 @@ async function getAddConfig(module, options, projectInfo) {
89
116
  // Unknown module type
90
117
  throw new Error(`Unknown module type "${module}". Use "database" or "auth", or specify a provider directly.`);
91
118
  }
92
- async function getInteractiveConfig(modulesDir, projectInfo) {
119
+ async function getInteractiveConfig(modulesDir, projectInfo, options) {
120
+ const projectRoot = process.cwd();
121
+ const discovered = await (0, module_discovery_1.discoverModules)(modulesDir);
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");
125
+ const categories = [
126
+ { name: "Database", value: "database" },
127
+ ];
128
+ // Offer Auth category when there's at least one compatible auth option
129
+ if (compatibleAuths.length > 0) {
130
+ categories.push({ name: "Auth", value: "auth" });
131
+ }
93
132
  const answers = await inquirer_1.default.prompt([
94
133
  {
95
134
  type: "list",
96
135
  name: "category",
97
136
  message: "What would you like to add?",
98
- choices: [
99
- { name: "Database", value: "database" },
100
- { name: "Auth", value: "auth" },
101
- ],
137
+ choices: categories,
102
138
  },
103
139
  ]);
104
140
  const category = answers.category;
105
- const discovered = await (0, module_discovery_1.discoverModules)(modulesDir);
106
141
  if (category === "database") {
107
- 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);
108
143
  const dbAnswers = await inquirer_1.default.prompt([
109
144
  {
110
145
  type: "list",
@@ -135,7 +170,57 @@ async function getInteractiveConfig(modulesDir, projectInfo) {
135
170
  };
136
171
  }
137
172
  else if (category === "auth") {
138
- const authChoices = (discovered.auth || []).map((a) => ({ name: a.displayName, value: a.name }));
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
+ }
222
+ const dbString = projectInfo?.hasPrisma ? "prisma" : "none";
223
+ const authChoices = (0, module_discovery_1.getCompatibleAuthOptions)(discovered.auth || [], projectInfo?.framework || defaultFramework, dbString);
139
224
  const authAnswers = await inquirer_1.default.prompt([
140
225
  {
141
226
  type: "list",
@@ -145,19 +230,30 @@ async function getInteractiveConfig(modulesDir, projectInfo) {
145
230
  },
146
231
  ]);
147
232
  const selectedAuth = authAnswers.auth;
233
+ if (selectedAuth === "none") {
234
+ logger_1.logger.info("Cancelled");
235
+ process.exit(0);
236
+ }
148
237
  const metadata = await loadModuleMetadata(modulesDir, selectedAuth, selectedAuth);
149
238
  if (!metadata) {
150
239
  throw new Error(`Auth provider "${selectedAuth}" not found`);
151
240
  }
152
- if (projectInfo && metadata.supportedFrameworks && !metadata.supportedFrameworks.includes(projectInfo.framework)) {
241
+ if (projectInfo &&
242
+ metadata.supportedFrameworks &&
243
+ !metadata.supportedFrameworks.includes(projectInfo.framework)) {
153
244
  throw new Error(`Auth provider "${selectedAuth}" does not support ${projectInfo.framework}`);
154
245
  }
155
- return {
246
+ const result = {
156
247
  module: "auth",
157
248
  provider: selectedAuth,
158
249
  displayName: metadata.displayName || selectedAuth,
159
250
  metadata,
160
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;
161
257
  }
162
258
  throw new Error("Invalid selection");
163
259
  }
@@ -233,6 +329,32 @@ async function addModuleToProject(projectRoot, projectInfo, config, options) {
233
329
  }
234
330
  }
235
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
+ }
236
358
  if (config.module === "database" && config.provider) {
237
359
  const parsed = (0, shared_1.parseDatabaseOption)(config.provider);
238
360
  selectedModules.database = parsed.database;
@@ -244,19 +366,6 @@ async function addModuleToProject(projectRoot, projectInfo, config, options) {
244
366
  selectedModules.auth = config.provider;
245
367
  }
246
368
  const postInstall = await gen.applyToProject(selectedModules, [], projectRoot);
247
- if (postInstall && postInstall.length > 0 && !options?.dryRun) {
248
- const postInstallSpinner = logger_1.logger.startSpinner("Running post-install commands...");
249
- try {
250
- for (const command of postInstall) {
251
- (0, child_process_1.execSync)(command, { cwd: projectRoot, stdio: "pipe" });
252
- }
253
- postInstallSpinner.succeed("Post-install commands completed");
254
- }
255
- catch (error) {
256
- postInstallSpinner.fail("Failed to run post-install commands");
257
- throw error;
258
- }
259
- }
260
369
  if (!options?.dryRun && options?.install !== false) {
261
370
  const installSpinner = logger_1.logger.startSpinner("Installing dependencies...");
262
371
  try {
@@ -268,18 +377,40 @@ async function addModuleToProject(projectRoot, projectInfo, config, options) {
268
377
  throw err;
269
378
  }
270
379
  }
380
+ if (postInstall && postInstall.length > 0 && !options?.dryRun) {
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.");
384
+ }
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
+ }
397
+ }
398
+ }
271
399
  return;
272
400
  }
273
401
  const mergedDeps = {};
274
402
  const mergedDevDeps = {};
275
- if (moduleMetadata.frameworkConfigs?.shared?.dependencies) {
276
- 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);
277
410
  }
278
- if (moduleMetadata.frameworkConfigs?.shared?.devDependencies) {
279
- Object.assign(mergedDevDeps, moduleMetadata.frameworkConfigs.shared.devDependencies);
411
+ catch {
412
+ // ignore malformed frameworkConfigs
280
413
  }
281
- // Adapter-specific dependencies are applied via generator metadata; frameworkConfigs still merge above.
282
- // Do not mutate the loaded module metadata here; use mergedDeps/mergedDevDeps for installation.
283
414
  const variables = {};
284
415
  if (selectedProvider) {
285
416
  variables.provider = selectedProvider;
@@ -315,16 +446,34 @@ async function addModuleToProject(projectRoot, projectInfo, config, options) {
315
446
  await applyFrameworkPatches(projectRoot, moduleMetadata.frameworkPatches, projectInfo.framework);
316
447
  }
317
448
  if (moduleMetadata.postInstall && moduleMetadata.postInstall.length > 0 && !options?.dryRun) {
318
- const postInstallSpinner = logger_1.logger.startSpinner("Running post-install commands...");
319
- try {
320
- for (const command of moduleMetadata.postInstall) {
321
- (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
+ }
322
460
  }
323
- postInstallSpinner.succeed("Post-install commands completed");
324
461
  }
325
- catch (error) {
326
- postInstallSpinner.fail("Failed to run post-install commands");
327
- 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
+ }
328
477
  }
329
478
  }
330
479
  if (Object.keys(mergedDeps).length > 0 && options?.install !== false) {
@@ -345,16 +494,34 @@ async function addModuleToProject(projectRoot, projectInfo, config, options) {
345
494
  logger_1.logger.info(`Would add dev dependencies: ${devDeps.join(", ")}`);
346
495
  }
347
496
  }
348
- if (moduleMetadata.envVars && moduleMetadata.envVars.length > 0) {
349
- const processedEnvVars = moduleMetadata.envVars.map((envVar) => ({
350
- ...envVar,
351
- value: envVar.value?.replace(/\{\{(\w+)\}\}/g, (match, key) => variables[key] || match),
352
- }));
353
- if (!options?.dryRun) {
354
- 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;
355
501
  }
356
- else {
357
- 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
+ }
358
525
  }
359
526
  }
360
527
  }
@@ -446,8 +613,11 @@ async function findModulePath(modulesDir, moduleName, provider) {
446
613
  const metadataPath = path_1.default.join(modulePath, "module.json");
447
614
  if (await fs_extra_1.default.pathExists(metadataPath)) {
448
615
  const metadata = await fs_extra_1.default.readJSON(metadataPath);
449
- if (provider && moduleDir === provider) {
450
- return modulePath;
616
+ if (provider) {
617
+ const baseProvider = String(provider).split("-")[0];
618
+ if (moduleDir === provider || moduleDir === baseProvider) {
619
+ return modulePath;
620
+ }
451
621
  }
452
622
  if (!provider && metadata.name === moduleName) {
453
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-")) {