stackkit 0.2.2 → 0.2.6

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 (34) hide show
  1. package/bin/stackkit.js +10 -3
  2. package/dist/cli/add.js +188 -61
  3. package/dist/cli/create.js +75 -35
  4. package/dist/cli/doctor.js +36 -161
  5. package/dist/cli/list.js +10 -40
  6. package/dist/index.js +6 -6
  7. package/dist/lib/conversion/js-conversion.js +27 -11
  8. package/dist/lib/discovery/installed-detection.d.ts +7 -0
  9. package/dist/lib/discovery/installed-detection.js +134 -0
  10. package/dist/lib/discovery/module-discovery.d.ts +1 -1
  11. package/dist/lib/discovery/module-discovery.js +26 -17
  12. package/dist/lib/discovery/shared.js +9 -7
  13. package/dist/lib/generation/code-generator.d.ts +2 -0
  14. package/dist/lib/generation/code-generator.js +98 -4
  15. package/dist/lib/generation/generator-utils.d.ts +1 -1
  16. package/dist/lib/generation/generator-utils.js +5 -3
  17. package/dist/lib/project/detect.js +59 -29
  18. package/dist/types/index.d.ts +21 -17
  19. package/modules/auth/better-auth/files/lib/auth.ts +2 -2
  20. package/package.json +13 -10
  21. package/templates/express/gitignore +23 -0
  22. package/templates/nextjs/gitignore +42 -0
  23. package/templates/nextjs/next-env.d.ts +6 -0
  24. package/templates/react/dist/assets/index-D4AHT4dU.js +193 -0
  25. package/templates/react/dist/assets/index-rpwj5ZOX.css +1 -0
  26. package/templates/react/dist/index.html +14 -0
  27. package/templates/react/dist/vite.svg +1 -0
  28. package/templates/react/gitignore +44 -0
  29. package/templates/react/src/utils/utils.ts +1 -1
  30. /package/templates/express/{.env.example → env.example} +0 -0
  31. /package/templates/nextjs/{.env.example → env.example} +0 -0
  32. /package/templates/react/{.env.example → env.example} +0 -0
  33. /package/templates/react/{.prettierignore → prettierignore} +0 -0
  34. /package/templates/react/{.prettierrc → prettierrc} +0 -0
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,17 @@ 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 = [...config.preAdded.map((p) => p.displayName), config.displayName].map((s) => chalk_1.default.bold(s));
35
+ logger_1.logger.success(`Added ${addedNames.join(" and ")}`);
36
+ }
37
+ else {
38
+ logger_1.logger.success(`Added ${chalk_1.default.bold(config.displayName)}`);
39
+ }
32
40
  logger_1.logger.newLine();
33
41
  }
34
42
  catch (error) {
@@ -42,7 +50,7 @@ async function addCommand(module, options) {
42
50
  async function getAddConfig(module, options, projectInfo) {
43
51
  const modulesDir = path_1.default.join((0, package_root_1.getPackageRoot)(), "modules");
44
52
  if (!module) {
45
- return await getInteractiveConfig(modulesDir, projectInfo);
53
+ return await getInteractiveConfig(modulesDir, projectInfo, options);
46
54
  }
47
55
  if (module === "database" || module === "auth") {
48
56
  if (!options?.provider) {
@@ -67,8 +75,8 @@ async function getAddConfig(module, options, projectInfo) {
67
75
  module: "database",
68
76
  provider: adapterProvider,
69
77
  displayName: parsed.database === "prisma" && parsed.provider
70
- ? `${moduleMetadata.displayName} (${parsed.provider})`
71
- : moduleMetadata.displayName,
78
+ ? `${moduleMetadata.displayName || baseProvider} (${parsed.provider})`
79
+ : moduleMetadata.displayName || baseProvider,
72
80
  metadata: moduleMetadata,
73
81
  };
74
82
  }
@@ -78,24 +86,26 @@ async function getAddConfig(module, options, projectInfo) {
78
86
  if (!moduleMetadata) {
79
87
  throw new Error(`Auth provider "${provider}" not found`);
80
88
  }
81
- // Validate compatibility with existing project
82
89
  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");
90
+ if (moduleMetadata.supportedFrameworks &&
91
+ !moduleMetadata.supportedFrameworks.includes(projectInfo.framework)) {
92
+ throw new Error(`${moduleMetadata.displayName} is not supported on ${projectInfo.framework}`);
87
93
  }
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");
94
+ const dbName = projectInfo.hasPrisma
95
+ ? "prisma"
96
+ : projectInfo.hasDatabase
97
+ ? "other"
98
+ : "none";
99
+ if (moduleMetadata.compatibility &&
100
+ moduleMetadata.compatibility.databases &&
101
+ !moduleMetadata.compatibility.databases.includes(dbName)) {
102
+ throw new Error(`${moduleMetadata.displayName} is not compatible with the project's database configuration`);
93
103
  }
94
104
  }
95
105
  return {
96
106
  module: "auth",
97
107
  provider,
98
- displayName: moduleMetadata.displayName,
108
+ displayName: moduleMetadata.displayName || provider,
99
109
  metadata: moduleMetadata,
100
110
  };
101
111
  }
@@ -103,15 +113,17 @@ async function getAddConfig(module, options, projectInfo) {
103
113
  // Unknown module type
104
114
  throw new Error(`Unknown module type "${module}". Use "database" or "auth", or specify a provider directly.`);
105
115
  }
106
- async function getInteractiveConfig(modulesDir, projectInfo) {
107
- // Discover modules once and use compatibility info to decide which categories to show
116
+ async function getInteractiveConfig(modulesDir, projectInfo, options) {
117
+ const projectRoot = process.cwd();
108
118
  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");
119
+ // Prefer discovered framework, then projectInfo.framework; leave empty if unknown
120
+ const defaultFramework = (discovered.frameworks && discovered.frameworks[0]?.name) || projectInfo?.framework || "";
121
+ const compatibleAuths = (0, module_discovery_1.getCompatibleAuthOptions)(discovered.auth || [], projectInfo?.framework || defaultFramework, projectInfo?.hasPrisma ? "prisma" : "none", discovered.frameworks);
110
122
  const categories = [
111
123
  { name: "Database", value: "database" },
112
124
  ];
113
- // Only show Auth category if there is at least one compatible auth option
114
- if (compatibleAuths.length > 1) {
125
+ // Offer Auth category when there's at least one compatible auth option
126
+ if (compatibleAuths.length > 0) {
115
127
  categories.push({ name: "Auth", value: "auth" });
116
128
  }
117
129
  const answers = await inquirer_1.default.prompt([
@@ -124,7 +136,7 @@ async function getInteractiveConfig(modulesDir, projectInfo) {
124
136
  ]);
125
137
  const category = answers.category;
126
138
  if (category === "database") {
127
- const dbChoices = (0, module_discovery_1.getDatabaseChoices)(discovered.databases || [], projectInfo?.framework || "nextjs");
139
+ const dbChoices = (0, module_discovery_1.getDatabaseChoices)(discovered.databases || [], projectInfo?.framework || defaultFramework);
128
140
  const dbAnswers = await inquirer_1.default.prompt([
129
141
  {
130
142
  type: "list",
@@ -155,9 +167,51 @@ async function getInteractiveConfig(modulesDir, projectInfo) {
155
167
  };
156
168
  }
157
169
  else if (category === "auth") {
158
- // Filter auth choices based on project compatibility (framework + database)
170
+ let preAddedForReturn;
171
+ if (!projectInfo?.hasDatabase) {
172
+ logger_1.logger.warn("No database detected in the project. Authentication requires a database.");
173
+ const dbChoices = (0, module_discovery_1.getDatabaseChoices)(discovered.databases || [], projectInfo?.framework || defaultFramework);
174
+ const dbAnswer = await inquirer_1.default.prompt([
175
+ {
176
+ type: "list",
177
+ name: "database",
178
+ message: "Select a database to add before authentication:",
179
+ choices: dbChoices,
180
+ },
181
+ ]);
182
+ const selectedDb = dbAnswer.database;
183
+ if (!selectedDb || selectedDb === "none") {
184
+ logger_1.logger.info("Cancelled — authentication requires a database");
185
+ process.exit(0);
186
+ }
187
+ let dbConfig;
188
+ if (selectedDb.startsWith("prisma-")) {
189
+ const provider = selectedDb.split("-")[1];
190
+ dbConfig = {
191
+ module: "database",
192
+ provider: `prisma-${provider}`,
193
+ displayName: `Prisma (${provider})`,
194
+ metadata: (await loadModuleMetadata(modulesDir, "prisma", "prisma")),
195
+ };
196
+ }
197
+ else {
198
+ const meta = (await loadModuleMetadata(modulesDir, selectedDb, selectedDb));
199
+ if (!meta)
200
+ throw new Error(`Database provider "${selectedDb}" not found`);
201
+ dbConfig = {
202
+ module: "database",
203
+ provider: selectedDb,
204
+ displayName: meta.displayName || selectedDb,
205
+ metadata: meta,
206
+ };
207
+ }
208
+ await addModuleToProject(projectRoot, projectInfo || (await (0, detect_1.detectProjectInfo)(projectRoot)), dbConfig, options);
209
+ projectInfo = await (0, detect_1.detectProjectInfo)(projectRoot);
210
+ dbConfig.preAdded = dbConfig.preAdded || [];
211
+ preAddedForReturn = dbConfig;
212
+ }
159
213
  const dbString = projectInfo?.hasPrisma ? "prisma" : "none";
160
- const authChoices = (0, module_discovery_1.getCompatibleAuthOptions)(discovered.auth || [], projectInfo?.framework || "nextjs", dbString);
214
+ const authChoices = (0, module_discovery_1.getCompatibleAuthOptions)(discovered.auth || [], projectInfo?.framework || defaultFramework, dbString);
161
215
  const authAnswers = await inquirer_1.default.prompt([
162
216
  {
163
217
  type: "list",
@@ -180,16 +234,19 @@ async function getInteractiveConfig(modulesDir, projectInfo) {
180
234
  !metadata.supportedFrameworks.includes(projectInfo.framework)) {
181
235
  throw new Error(`Auth provider "${selectedAuth}" does not support ${projectInfo.framework}`);
182
236
  }
183
- return {
237
+ const result = {
184
238
  module: "auth",
185
239
  provider: selectedAuth,
186
240
  displayName: metadata.displayName || selectedAuth,
187
241
  metadata,
188
242
  };
243
+ if (typeof preAddedForReturn !== "undefined" && preAddedForReturn) {
244
+ result.preAdded = [preAddedForReturn];
245
+ }
246
+ return result;
189
247
  }
190
248
  throw new Error("Invalid selection");
191
249
  }
192
- /* removed unused getProviderConfig — discovery-based flows handle providers */
193
250
  async function addModuleToProject(projectRoot, projectInfo, config, options) {
194
251
  const moduleMetadata = config.metadata;
195
252
  const selectedProvider = config.provider;
@@ -261,6 +318,29 @@ async function addModuleToProject(projectRoot, projectInfo, config, options) {
261
318
  }
262
319
  }
263
320
  const selectedModules = { framework: projectInfo.framework };
321
+ try {
322
+ const pkg = await fs_extra_1.default.readJson(path_1.default.join(projectRoot, "package.json"));
323
+ const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
324
+ if (projectInfo.hasPrisma) {
325
+ selectedModules.database = "prisma";
326
+ const prismaSchema = path_1.default.join(projectRoot, "prisma", "schema.prisma");
327
+ if (await fs_extra_1.default.pathExists(prismaSchema)) {
328
+ const content = await fs_extra_1.default.readFile(prismaSchema, "utf-8");
329
+ const dsMatch = content.match(/datasource\s+\w+\s*\{([\s\S]*?)\}/i);
330
+ if (dsMatch && dsMatch[1]) {
331
+ const provMatch = dsMatch[1].match(/provider\s*=\s*["']([^"']+)["']/i);
332
+ if (provMatch && provMatch[1])
333
+ selectedModules.prismaProvider = provMatch[1];
334
+ }
335
+ }
336
+ }
337
+ else if (deps["mongoose"]) {
338
+ selectedModules.database = "mongoose";
339
+ }
340
+ }
341
+ catch {
342
+ // ignore detection errors
343
+ }
264
344
  if (config.module === "database" && config.provider) {
265
345
  const parsed = (0, shared_1.parseDatabaseOption)(config.provider);
266
346
  selectedModules.database = parsed.database;
@@ -272,7 +352,6 @@ async function addModuleToProject(projectRoot, projectInfo, config, options) {
272
352
  selectedModules.auth = config.provider;
273
353
  }
274
354
  const postInstall = await gen.applyToProject(selectedModules, [], projectRoot);
275
- // Install dependencies first, then run post-install commands (e.g. prisma generate)
276
355
  if (!options?.dryRun && options?.install !== false) {
277
356
  const installSpinner = logger_1.logger.startSpinner("Installing dependencies...");
278
357
  try {
@@ -285,30 +364,39 @@ async function addModuleToProject(projectRoot, projectInfo, config, options) {
285
364
  }
286
365
  }
287
366
  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");
367
+ const createdFiles = gen.getCreatedFiles ? gen.getCreatedFiles() : [];
368
+ if (createdFiles.length === 0) {
369
+ logger_1.logger.warn("Skipping post-install commands — no files were created by generators to act upon.");
294
370
  }
295
- catch (error) {
296
- postInstallSpinner.fail("Failed to run post-install commands");
297
- throw error;
371
+ else {
372
+ const postInstallSpinner = logger_1.logger.startSpinner("Running post-install commands...");
373
+ try {
374
+ for (const command of postInstall) {
375
+ (0, child_process_1.execSync)(command, { cwd: projectRoot, stdio: "pipe" });
376
+ }
377
+ postInstallSpinner.succeed("Post-install commands completed");
378
+ }
379
+ catch (error) {
380
+ postInstallSpinner.fail("Failed to run post-install commands");
381
+ throw error;
382
+ }
298
383
  }
299
384
  }
300
385
  return;
301
386
  }
302
387
  const mergedDeps = {};
303
388
  const mergedDevDeps = {};
304
- if (moduleMetadata.frameworkConfigs?.shared?.dependencies) {
305
- Object.assign(mergedDeps, moduleMetadata.frameworkConfigs.shared.dependencies);
389
+ try {
390
+ const shared = moduleMetadata.frameworkConfigs
391
+ ?.shared;
392
+ if (shared && shared.dependencies)
393
+ Object.assign(mergedDeps, shared.dependencies);
394
+ if (shared && shared.devDependencies)
395
+ Object.assign(mergedDevDeps, shared.devDependencies);
306
396
  }
307
- if (moduleMetadata.frameworkConfigs?.shared?.devDependencies) {
308
- Object.assign(mergedDevDeps, moduleMetadata.frameworkConfigs.shared.devDependencies);
397
+ catch {
398
+ // ignore malformed frameworkConfigs
309
399
  }
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
400
  const variables = {};
313
401
  if (selectedProvider) {
314
402
  variables.provider = selectedProvider;
@@ -344,16 +432,34 @@ async function addModuleToProject(projectRoot, projectInfo, config, options) {
344
432
  await applyFrameworkPatches(projectRoot, moduleMetadata.frameworkPatches, projectInfo.framework);
345
433
  }
346
434
  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" });
435
+ const createdFiles = [];
436
+ if (Array.isArray(moduleMetadata.patches)) {
437
+ for (const p of moduleMetadata.patches) {
438
+ if (p.type === "create-file") {
439
+ const destPath = p.destination;
440
+ if (typeof destPath === "string") {
441
+ const dest = path_1.default.join(projectRoot, destPath);
442
+ if (await fs_extra_1.default.pathExists(dest))
443
+ createdFiles.push(destPath);
444
+ }
445
+ }
351
446
  }
352
- postInstallSpinner.succeed("Post-install commands completed");
353
447
  }
354
- catch (error) {
355
- postInstallSpinner.fail("Failed to run post-install commands");
356
- throw error;
448
+ if (createdFiles.length === 0) {
449
+ logger_1.logger.warn("Skipping module post-install commands — no files were created by module patches to act upon.");
450
+ }
451
+ else {
452
+ const postInstallSpinner = logger_1.logger.startSpinner("Running post-install commands...");
453
+ try {
454
+ for (const command of moduleMetadata.postInstall) {
455
+ (0, child_process_1.execSync)(command, { cwd: projectRoot, stdio: "pipe" });
456
+ }
457
+ postInstallSpinner.succeed("Post-install commands completed");
458
+ }
459
+ catch (error) {
460
+ postInstallSpinner.fail("Failed to run post-install commands");
461
+ throw error;
462
+ }
357
463
  }
358
464
  }
359
465
  if (Object.keys(mergedDeps).length > 0 && options?.install !== false) {
@@ -374,16 +480,34 @@ async function addModuleToProject(projectRoot, projectInfo, config, options) {
374
480
  logger_1.logger.info(`Would add dev dependencies: ${devDeps.join(", ")}`);
375
481
  }
376
482
  }
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 });
483
+ if (moduleMetadata.envVars) {
484
+ let envArray = [];
485
+ if (Array.isArray(moduleMetadata.envVars)) {
486
+ envArray = moduleMetadata.envVars;
384
487
  }
385
- else {
386
- logger_1.logger.log(` ${chalk_1.default.dim("~")} .env.example`);
488
+ else if (typeof moduleMetadata.envVars === "object") {
489
+ envArray = Object.entries(moduleMetadata.envVars).map(([k, v]) => ({
490
+ key: k,
491
+ value: String(v),
492
+ }));
493
+ }
494
+ if (envArray.length > 0) {
495
+ const processedEnvVars = envArray.map((envVar) => ({
496
+ key: envVar.key || "",
497
+ value: envVar.value?.replace(/\{\{(\w+)\}\}/g, (match, key) => variables[key] || match),
498
+ required: false,
499
+ }));
500
+ if (!options?.dryRun) {
501
+ const envVarsToAdd = processedEnvVars.map((e) => ({
502
+ key: e.key,
503
+ value: e.value,
504
+ required: !!e.required,
505
+ }));
506
+ await (0, env_editor_1.addEnvVariables)(projectRoot, envVarsToAdd, { force: options?.force });
507
+ }
508
+ else {
509
+ logger_1.logger.log(` ${chalk_1.default.dim("~")} .env.example`);
510
+ }
387
511
  }
388
512
  }
389
513
  }
@@ -475,8 +599,11 @@ async function findModulePath(modulesDir, moduleName, provider) {
475
599
  const metadataPath = path_1.default.join(modulePath, "module.json");
476
600
  if (await fs_extra_1.default.pathExists(metadataPath)) {
477
601
  const metadata = await fs_extra_1.default.readJSON(metadataPath);
478
- if (provider && moduleDir === provider) {
479
- return modulePath;
602
+ if (provider) {
603
+ const baseProvider = String(provider).split("-")[0];
604
+ if (moduleDir === provider || moduleDir === baseProvider) {
605
+ return modulePath;
606
+ }
480
607
  }
481
608
  if (!provider && metadata.name === moduleName) {
482
609
  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,47 +161,66 @@ 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
- // If a prisma-* choice is selected above, `prismaProvider` will be derived from it,
164
- // otherwise prompt for provider when `prisma` is selected directly.
165
208
  {
166
209
  type: "list",
167
210
  name: "prismaProvider",
168
211
  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
- ],
212
+ when: (answers) => answers.database === "prisma" && prismaProviders.length > 0,
213
+ choices: prismaProviders.map((p) => ({
214
+ name: p.charAt(0).toUpperCase() + p.slice(1),
215
+ value: p,
216
+ })),
176
217
  },
177
218
  {
178
219
  type: "list",
179
220
  name: "auth",
180
221
  message: "Select authentication:",
181
222
  when: (answers) => answers.database !== "none" || answers.framework === "react",
182
- choices: (answers) => (0, module_discovery_1.getCompatibleAuthOptions)(discoveredModules.auth, answers.framework, answers.database || "none"),
223
+ choices: (answers) => (0, module_discovery_1.getCompatibleAuthOptions)(discoveredModules.auth, answers.framework, answers.database || "none", discoveredModules.frameworks),
183
224
  },
184
225
  {
185
226
  type: "list",
@@ -204,7 +245,6 @@ async function getProjectConfig(projectName, options) {
204
245
  default: "pnpm",
205
246
  },
206
247
  ]));
207
- // Normalize database answer (interactive flow): handle values like `prisma-postgresql`
208
248
  let databaseAnswer = answers.framework === "react" ? "none" : answers.database;
209
249
  let prismaProviderAnswer = answers.prismaProvider;
210
250
  if (typeof databaseAnswer === "string" && databaseAnswer.startsWith("prisma-")) {