package-installer-cli 1.2.0 → 1.3.1

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.
@@ -9,15 +9,16 @@ import chalk from 'chalk';
9
9
  import ora from 'ora';
10
10
  import { installPackages } from './dependencyInstaller.js';
11
11
  import { detectLanguageFromFiles } from './languageConfig.js';
12
- import { cacheProjectData, getCachedProject } from './cacheManager.js';
12
+ import { cacheProjectData } from './cacheManager.js';
13
13
  import { getCliRootPath, getFeaturesJsonPath } from './pathResolver.js';
14
+ import { getAvailableFeatures } from '../commands/add.js';
14
15
  // Get the directory of this file for proper path resolution
15
16
  const __filename = fileURLToPath(import.meta.url);
16
17
  const __dirname = dirname(__filename);
17
18
  // Load supported features from cached or direct file access
18
19
  let SUPPORTED_FEATURES = {};
19
20
  /**
20
- * Load features from cache or file system
21
+ * Load features from cache or file system with new jsonPath structure
21
22
  */
22
23
  async function loadFeatures() {
23
24
  try {
@@ -25,7 +26,59 @@ async function loadFeatures() {
25
26
  const featuresPath = getFeaturesJsonPath();
26
27
  if (await fs.pathExists(featuresPath)) {
27
28
  const featuresData = await fs.readJson(featuresPath);
28
- SUPPORTED_FEATURES = featuresData.features || featuresData;
29
+ const featuresConfig = featuresData.features || featuresData;
30
+ // Get available features using the centralized function
31
+ const availableFeatures = await getAvailableFeatures();
32
+ // Process each feature and load its individual JSON file
33
+ for (const [featureName, config] of Object.entries(featuresConfig)) {
34
+ const featureConfig = config;
35
+ if (featureConfig.jsonPath) {
36
+ try {
37
+ // Load the individual feature JSON file
38
+ const individualFeaturePath = path.resolve(path.dirname(featuresPath), featureConfig.jsonPath);
39
+ if (await fs.pathExists(individualFeaturePath)) {
40
+ const individualFeatureData = await fs.readJson(individualFeaturePath);
41
+ // Merge the base config with the individual feature data
42
+ // The individual JSON files directly contain the provider structure
43
+ SUPPORTED_FEATURES[featureName] = {
44
+ supportedFrameworks: featureConfig.supportedFrameworks || [],
45
+ supportedLanguages: featureConfig.supportedLanguages || [],
46
+ files: individualFeatureData, // Direct provider structure
47
+ description: featureConfig.description
48
+ };
49
+ }
50
+ else {
51
+ console.warn(chalk.yellow(`⚠️ Individual feature file not found: ${individualFeaturePath}`));
52
+ // Fallback to base config
53
+ SUPPORTED_FEATURES[featureName] = {
54
+ supportedFrameworks: featureConfig.supportedFrameworks || [],
55
+ supportedLanguages: featureConfig.supportedLanguages || [],
56
+ files: {},
57
+ description: featureConfig.description
58
+ };
59
+ }
60
+ }
61
+ catch (error) {
62
+ console.warn(chalk.yellow(`⚠️ Could not load individual feature file for ${featureName}`));
63
+ // Fallback to base config
64
+ SUPPORTED_FEATURES[featureName] = {
65
+ supportedFrameworks: featureConfig.supportedFrameworks || [],
66
+ supportedLanguages: featureConfig.supportedLanguages || [],
67
+ files: {},
68
+ description: featureConfig.description
69
+ };
70
+ }
71
+ }
72
+ else {
73
+ // Legacy format - direct files in config
74
+ SUPPORTED_FEATURES[featureName] = {
75
+ supportedFrameworks: featureConfig.supportedFrameworks || [],
76
+ supportedLanguages: featureConfig.supportedLanguages || [],
77
+ files: featureConfig.files || {},
78
+ description: featureConfig.description
79
+ };
80
+ }
81
+ }
29
82
  }
30
83
  else {
31
84
  console.warn(chalk.yellow(`⚠️ Features file not found at: ${featuresPath}`));
@@ -35,10 +88,17 @@ async function loadFeatures() {
35
88
  console.warn(chalk.yellow('⚠️ Could not load features.json, using fallback configuration'));
36
89
  }
37
90
  }
38
- // Initialize features on module load
39
- await loadFeatures();
91
+ // Lazy loading flag
92
+ let featuresLoaded = false;
93
+ // Lazy load features when needed
94
+ async function ensureFeaturesLoaded() {
95
+ if (!featuresLoaded) {
96
+ await loadFeatures();
97
+ featuresLoaded = true;
98
+ }
99
+ }
40
100
  // Export for use in other modules
41
- export { SUPPORTED_FEATURES };
101
+ export { SUPPORTED_FEATURES, ensureFeaturesLoaded };
42
102
  // Re-export path utilities for backward compatibility
43
103
  export { getCliRootPath } from './pathResolver.js';
44
104
  /**
@@ -49,7 +109,6 @@ export { getCliRootPath } from './pathResolver.js';
49
109
  */
50
110
  async function detectNextjsSrcStructure(projectPath) {
51
111
  try {
52
- // Check if src folder exists and contains typical Next.js folders
53
112
  const srcPath = path.join(projectPath, 'src');
54
113
  if (!await fs.pathExists(srcPath)) {
55
114
  return false;
@@ -64,19 +123,26 @@ async function detectNextjsSrcStructure(projectPath) {
64
123
  if (await fs.pathExists(srcPagesPath)) {
65
124
  return true;
66
125
  }
67
- // Check for components directory in src (common pattern)
68
- const srcComponentsPath = path.join(srcPath, 'components');
69
- if (await fs.pathExists(srcComponentsPath)) {
70
- return true;
126
+ // Check for components, lib, utils directories in src (common Next.js patterns)
127
+ const commonDirs = ['components', 'lib', 'utils', 'styles', 'hooks'];
128
+ for (const dir of commonDirs) {
129
+ const dirPath = path.join(srcPath, dir);
130
+ if (await fs.pathExists(dirPath)) {
131
+ return true;
132
+ }
71
133
  }
72
- return false;
134
+ // Check for any TypeScript/JavaScript files in src root
135
+ const srcFiles = await fs.readdir(srcPath);
136
+ const codeFiles = srcFiles.filter(file => file.endsWith('.ts') || file.endsWith('.tsx') ||
137
+ file.endsWith('.js') || file.endsWith('.jsx'));
138
+ return codeFiles.length > 0;
73
139
  }
74
140
  catch (error) {
75
141
  return false;
76
142
  }
77
143
  }
78
144
  /**
79
- * Adjust file path for Next.js src folder structure (Next.js only)
145
+ * Adjust file path for Next.js src folder structure (Next.js specific)
80
146
  * Dynamically places files in src/ folder based on their path structure
81
147
  */
82
148
  function adjustNextjsSrcFilePath(filePath, hasSrcFolder, projectPath) {
@@ -84,47 +150,119 @@ function adjustNextjsSrcFilePath(filePath, hasSrcFolder, projectPath) {
84
150
  if (!hasSrcFolder) {
85
151
  return path.join(projectPath, filePath);
86
152
  }
153
+ // Files that should ALWAYS be in root regardless of src folder for Next.js
154
+ const rootOnlyFiles = [
155
+ '.env', '.env.local', '.env.example', '.env.development', '.env.production',
156
+ 'package.json', 'next.config.js', 'next.config.mjs', 'next.config.ts',
157
+ 'tailwind.config.js', 'tailwind.config.ts', 'postcss.config.js', 'postcss.config.ts',
158
+ 'middleware.ts', 'middleware.js', 'tsconfig.json', 'jsconfig.json'
159
+ ];
160
+ const fileName = path.basename(filePath);
161
+ // Always put public/ files in root public/
162
+ if (filePath.startsWith('public/')) {
163
+ return path.join(projectPath, filePath);
164
+ }
165
+ // Always put root-only files in project root
166
+ if (rootOnlyFiles.includes(fileName)) {
167
+ return path.join(projectPath, filePath);
168
+ }
169
+ // If filePath already starts with src/, keep as is
170
+ if (filePath.startsWith('src/')) {
171
+ return path.join(projectPath, filePath);
172
+ }
173
+ // For app/pages/components/lib/utils/hooks/styles/types, put in src/
174
+ const srcDirs = ['app', 'pages', 'components', 'lib', 'utils', 'styles', 'hooks', 'types'];
175
+ if (srcDirs.some(dir => filePath.startsWith(dir + '/'))) {
176
+ return path.join(projectPath, 'src', filePath);
177
+ }
178
+ // For .ts/.tsx/.js/.jsx files not in config, put in src/
179
+ if (fileName.match(/\.(ts|tsx|js|jsx)$/) && !fileName.includes('config')) {
180
+ return path.join(projectPath, 'src', filePath);
181
+ }
182
+ // Default: put in src/
183
+ return path.join(projectPath, 'src', filePath);
184
+ }
185
+ /**
186
+ * Adjust file path for framework-specific folder structures
187
+ * Dynamically places files based on detected project structure
188
+ */
189
+ function adjustFrameworkFilePath(filePath, framework, hasSrcFolder, projectPath) {
87
190
  // Files that should ALWAYS be in root regardless of src folder
88
191
  const rootOnlyFiles = [
89
192
  '.env',
90
193
  '.env.local',
91
194
  '.env.example',
195
+ '.env.development',
196
+ '.env.production',
92
197
  'package.json',
93
198
  'next.config.js',
94
199
  'next.config.mjs',
200
+ 'next.config.ts',
95
201
  'tailwind.config.js',
96
202
  'tailwind.config.ts',
97
203
  'postcss.config.js',
204
+ 'postcss.config.ts',
98
205
  'middleware.ts',
99
- 'middleware.js'
206
+ 'middleware.js',
207
+ 'vite.config.js',
208
+ 'vite.config.ts',
209
+ 'nuxt.config.js',
210
+ 'nuxt.config.ts',
211
+ 'vue.config.js',
212
+ 'angular.json',
213
+ 'nest-cli.json',
214
+ 'tsconfig.json',
215
+ 'jsconfig.json',
216
+ '.gitignore',
217
+ 'README.md',
218
+ 'docker-compose.yml',
219
+ 'Dockerfile'
100
220
  ];
101
221
  const fileName = path.basename(filePath);
222
+ const fileDir = path.dirname(filePath);
102
223
  // Check if this file should always be in root
103
224
  if (rootOnlyFiles.includes(fileName) || filePath.startsWith('public/')) {
104
225
  return path.join(projectPath, filePath);
105
226
  }
106
- // For all other files, place them in src/ folder if src structure is used
107
- return path.join(projectPath, 'src', filePath);
227
+ // Framework-specific logic for src folder structure
228
+ switch (framework) {
229
+ case 'nextjs':
230
+ return adjustNextjsSrcFilePath(filePath, hasSrcFolder, projectPath);
231
+ case 'reactjs':
232
+ case 'vuejs':
233
+ case 'angularjs': {
234
+ if (hasSrcFolder && !filePath.startsWith('src/')) {
235
+ const appDirs = ['components', 'pages', 'lib', 'utils', 'hooks', 'services', 'types'];
236
+ const isAppFile = appDirs.some(dir => filePath.startsWith(dir + '/')) ||
237
+ fileName.match(/\.(jsx?|tsx?|vue)$/) && !fileName.includes('config');
238
+ if (isAppFile) {
239
+ return path.join(projectPath, 'src', filePath);
240
+ }
241
+ }
242
+ return path.join(projectPath, filePath);
243
+ }
244
+ case 'nestjs': {
245
+ if (!filePath.startsWith('src/') && !rootOnlyFiles.includes(fileName)) {
246
+ return path.join(projectPath, 'src', filePath);
247
+ }
248
+ return path.join(projectPath, filePath);
249
+ }
250
+ default: {
251
+ if (hasSrcFolder && !filePath.startsWith('src/') && !rootOnlyFiles.includes(fileName)) {
252
+ const backendFiles = ['controllers', 'routes', 'services', 'utils', 'middleware', 'models'];
253
+ const shouldGoInSrc = backendFiles.some(dir => filePath.startsWith(dir + '/')) ||
254
+ (fileName.match(/\.(js|ts)$/) && fileDir !== '.' && !fileName.includes('config'));
255
+ if (shouldGoInSrc) {
256
+ return path.join(projectPath, 'src', filePath);
257
+ }
258
+ }
259
+ return path.join(projectPath, filePath);
260
+ }
261
+ }
108
262
  }
109
263
  export async function detectProjectStack(projectPath) {
110
264
  try {
111
- // Check cache first
112
- const cachedProject = await getCachedProject(projectPath);
113
- if (cachedProject) {
114
- const packageManager = await detectPackageManager(projectPath);
115
- let hasSrcFolder = await fs.pathExists(path.join(projectPath, 'src'));
116
- // For Next.js projects, do a more thorough src folder detection
117
- if (cachedProject.framework === 'nextjs') {
118
- hasSrcFolder = await detectNextjsSrcStructure(projectPath);
119
- }
120
- return {
121
- framework: cachedProject.framework,
122
- language: cachedProject.language,
123
- projectLanguage: cachedProject.language,
124
- packageManager,
125
- hasSrcFolder
126
- };
127
- }
265
+ // Skip cache lookup for simplicity - always detect fresh
128
266
  // Detect language first
129
267
  const files = await fs.readdir(projectPath);
130
268
  const detectedLanguages = detectLanguageFromFiles(files);
@@ -178,7 +316,7 @@ export async function detectProjectStack(projectPath) {
178
316
  hasSrcFolder = await fs.pathExists(path.join(projectPath, 'src'));
179
317
  }
180
318
  // Cache the detected information
181
- await cacheProjectData(projectPath, packageJson.name || path.basename(projectPath), typeof projectLanguage === 'string' ? projectLanguage : 'unknown', framework, Object.keys(dependencies), 0);
319
+ await cacheProjectData(projectPath, packageJson.name || path.basename(projectPath), typeof projectLanguage === 'string' ? projectLanguage : 'unknown');
182
320
  }
183
321
  return {
184
322
  framework,
@@ -216,26 +354,37 @@ export async function addFeature(featureName, provider, projectPath = process.cw
216
354
  const spinner = ora(chalk.hex('#9c88ff')(`Adding ${featureName} feature...`)).start();
217
355
  try {
218
356
  // Ensure features are loaded
219
- await loadFeatures();
357
+ await ensureFeaturesLoaded();
358
+ // Validate project path exists
359
+ if (!await fs.pathExists(projectPath)) {
360
+ throw new Error(`Project path does not exist: ${projectPath}`);
361
+ }
220
362
  // Get project information
221
363
  const projectInfo = await detectProjectStack(projectPath);
222
364
  if (!projectInfo.framework) {
223
- throw new Error('Could not detect project framework');
365
+ spinner.warn(chalk.yellow('Could not detect project framework automatically'));
366
+ console.log(chalk.hex('#95afc0')('📋 Supported frameworks: nextjs, expressjs, nestjs, reactjs, vuejs, angularjs, remixjs'));
367
+ throw new Error('Could not detect project framework. Please ensure you\'re in a valid project directory.');
224
368
  }
225
369
  // Get feature configuration
226
370
  const featureConfig = SUPPORTED_FEATURES[featureName];
227
371
  if (!featureConfig) {
228
- throw new Error(`Feature '${featureName}' not found in features.json`);
372
+ const availableFeatures = Object.keys(SUPPORTED_FEATURES);
373
+ throw new Error(`Feature '${featureName}' not found. Available features: ${availableFeatures.join(', ')}`);
229
374
  }
230
375
  // Check if feature supports this framework
231
376
  if (!featureConfig.supportedFrameworks.includes(projectInfo.framework)) {
232
- throw new Error(`Feature '${featureName}' is not supported for ${projectInfo.framework} projects`);
377
+ throw new Error(`Feature '${featureName}' is not supported for ${projectInfo.framework} projects. Supported frameworks: ${featureConfig.supportedFrameworks.join(', ')}`);
233
378
  }
234
- // For features with providers (like auth), prompt for provider selection
379
+ spinner.text = chalk.hex('#9c88ff')(`Detected ${projectInfo.framework} project (${projectInfo.projectLanguage})`);
380
+ // Check if this feature has a simple structure (framework-based) or complex (provider-based)
235
381
  let selectedProvider = provider;
236
- if (!selectedProvider && featureConfig.files) {
237
- const availableProviders = Object.keys(featureConfig.files);
382
+ const availableProviders = Object.keys(featureConfig.files);
383
+ const hasSimpleStructure = availableProviders.includes(projectInfo.framework);
384
+ if (!hasSimpleStructure && !selectedProvider && featureConfig.files) {
385
+ // Complex structure with providers
238
386
  if (availableProviders.length > 1) {
387
+ spinner.stop();
239
388
  const inquirer = await import('inquirer');
240
389
  const { provider: chosenProvider } = await inquirer.default.prompt([
241
390
  {
@@ -246,11 +395,16 @@ export async function addFeature(featureName, provider, projectPath = process.cw
246
395
  }
247
396
  ]);
248
397
  selectedProvider = chosenProvider;
398
+ spinner.start(chalk.hex('#9c88ff')(`Adding ${featureName} (${selectedProvider}) feature...`));
249
399
  }
250
400
  else {
251
401
  selectedProvider = availableProviders[0];
252
402
  }
253
403
  }
404
+ else if (hasSimpleStructure) {
405
+ // Simple structure - use framework as the "provider"
406
+ selectedProvider = projectInfo.framework;
407
+ }
254
408
  // Get files for the specific provider, framework, and language
255
409
  const files = getFeatureFiles(featureConfig, selectedProvider, projectInfo.framework, projectInfo.projectLanguage);
256
410
  if (Object.keys(files).length === 0) {
@@ -266,6 +420,12 @@ export async function addFeature(featureName, provider, projectPath = process.cw
266
420
  console.log(chalk.gray(`📊 Feature ${featureName} used for ${projectInfo.framework || 'unknown'} project`));
267
421
  // Show setup instructions
268
422
  showSetupInstructions(featureName, selectedProvider);
423
+ // Show additional helpful messages
424
+ console.log(`\n${chalk.hex('#f39c12')('📋 Next Steps:')}`);
425
+ console.log(chalk.hex('#95afc0')('• Review the created/updated files to ensure they match your project needs'));
426
+ console.log(chalk.hex('#95afc0')('• Update environment variables in .env files with your actual values'));
427
+ console.log(chalk.hex('#95afc0')('• Test the feature integration by running your project'));
428
+ console.log(chalk.hex('#95afc0')('• Check the documentation for any additional configuration steps'));
269
429
  }
270
430
  catch (error) {
271
431
  spinner.fail(chalk.red(`❌ Failed to add ${featureName} feature: ${error.message}`));
@@ -274,8 +434,35 @@ export async function addFeature(featureName, provider, projectPath = process.cw
274
434
  }
275
435
  /**
276
436
  * Get feature files for a specific provider, framework, and language
437
+ * Handles both structures:
438
+ * 1. provider -> framework -> language -> files (auth, ai, etc.)
439
+ * 2. framework -> files (docker, gitignore, etc.)
277
440
  */
278
441
  function getFeatureFiles(featureConfig, provider, framework, language) {
442
+ // Check if this is a simple framework-based structure (no providers)
443
+ if (featureConfig.files[framework] && !featureConfig.files[provider]) {
444
+ // Simple structure: framework -> files
445
+ const frameworkConfig = featureConfig.files[framework];
446
+ if (frameworkConfig && typeof frameworkConfig === 'object') {
447
+ // Check if it has action properties (direct files) or language subdirectories
448
+ const firstKey = Object.keys(frameworkConfig)[0];
449
+ if (firstKey && frameworkConfig[firstKey]?.action) {
450
+ // Direct files with actions
451
+ return frameworkConfig;
452
+ }
453
+ else if (frameworkConfig[language]) {
454
+ // Has language subdirectories
455
+ return frameworkConfig[language];
456
+ }
457
+ else if (frameworkConfig['typescript'] && language === 'javascript') {
458
+ // Fallback to typescript
459
+ console.log(chalk.yellow(`⚠️ JavaScript templates not available, using TypeScript templates`));
460
+ return frameworkConfig['typescript'];
461
+ }
462
+ }
463
+ return frameworkConfig || {};
464
+ }
465
+ // Complex structure: provider -> framework -> language -> files
279
466
  const providerConfig = featureConfig.files[provider];
280
467
  if (!providerConfig)
281
468
  return {};
@@ -294,44 +481,77 @@ function getFeatureFiles(featureConfig, provider, framework, language) {
294
481
  }
295
482
  return languageConfig;
296
483
  }
484
+ /**
485
+ * Resolve template file path with fallback strategies
486
+ * Handles dynamic resolution for all framework files
487
+ */
488
+ async function resolveTemplateFilePath(featureName, provider, framework, language, filePath) {
489
+ const cliRoot = getCliRootPath();
490
+ // Primary path strategies in order of preference
491
+ const pathStrategies = [
492
+ // 1. Full path with all parameters
493
+ path.join(cliRoot, 'features', featureName, provider, framework, language, filePath),
494
+ // 2. Without language subfolder (framework-only)
495
+ path.join(cliRoot, 'features', featureName, provider, framework, filePath),
496
+ // 3. Generic provider path (no framework/language)
497
+ path.join(cliRoot, 'features', featureName, provider, filePath),
498
+ // 4. Feature root path (no provider/framework/language)
499
+ path.join(cliRoot, 'features', featureName, filePath),
500
+ // 5. Try with typescript if javascript doesn't exist
501
+ ...(language === 'javascript' ? [
502
+ path.join(cliRoot, 'features', featureName, provider, framework, 'typescript', filePath)
503
+ ] : []),
504
+ // 6. Try with javascript if typescript doesn't exist
505
+ ...(language === 'typescript' ? [
506
+ path.join(cliRoot, 'features', featureName, provider, framework, 'javascript', filePath)
507
+ ] : [])
508
+ ];
509
+ // Try each strategy until we find an existing file
510
+ for (const templatePath of pathStrategies) {
511
+ try {
512
+ if (await fs.pathExists(templatePath)) {
513
+ return templatePath;
514
+ }
515
+ }
516
+ catch (error) {
517
+ // Continue to next strategy
518
+ continue;
519
+ }
520
+ }
521
+ return null;
522
+ }
297
523
  /**
298
524
  * Process a single feature file based on its action
299
525
  */
300
526
  async function processFeatureFile(filePath, fileConfig, featureName, provider, projectInfo, projectPath) {
301
527
  const { action } = fileConfig;
302
- // Try to get template content from file system
303
- let sourceContent = null;
304
- // Get the CLI root path for accessing feature templates
305
- const cliRoot = getCliRootPath();
306
- const featureTemplatePath = path.join(cliRoot, 'features', featureName, provider, projectInfo.framework, projectInfo.projectLanguage);
307
- const sourceFilePath = path.join(featureTemplatePath, filePath);
308
- // Load from file system
309
- if (await fs.pathExists(sourceFilePath)) {
310
- sourceContent = await fs.readFile(sourceFilePath, 'utf-8');
311
- }
312
- // Handle file path adjustment based on project structure
313
- let targetFilePath = path.join(projectPath, filePath);
314
- // For Next.js projects with src folder structure, adjust file paths accordingly
315
- if (projectInfo.framework === 'nextjs' && projectInfo.hasSrcFolder) {
316
- targetFilePath = adjustNextjsSrcFilePath(filePath, projectInfo.hasSrcFolder, projectPath);
528
+ // Resolve template file path with dynamic fallback strategies
529
+ const sourceFilePath = await resolveTemplateFilePath(featureName, provider, projectInfo.framework, projectInfo.projectLanguage, filePath);
530
+ if (!sourceFilePath) {
531
+ console.warn(chalk.yellow(`⚠️ Template file not found for: ${filePath}`));
532
+ console.log(chalk.gray(` Searched in feature: ${featureName}, provider: ${provider}, framework: ${projectInfo.framework}, language: ${projectInfo.projectLanguage}`));
533
+ console.log(chalk.gray(` This might be due to running a globally installed CLI. Consider using 'npx' or installing locally.`));
534
+ return;
317
535
  }
536
+ // Handle file path adjustment based on project structure - framework agnostic
537
+ let targetFilePath = adjustFrameworkFilePath(filePath, projectInfo.framework || 'unknown', projectInfo.hasSrcFolder || false, projectPath);
318
538
  // Ensure all parent directories exist before processing
319
539
  await fs.ensureDir(path.dirname(targetFilePath));
320
540
  switch (action) {
321
541
  case 'install':
322
- await handlePackageInstallation(sourceFilePath, projectPath, projectInfo.packageManager || 'npm');
542
+ await handlePackageInstallation(sourceFilePath, projectPath, projectInfo.packageManager || 'npm', projectInfo.language);
323
543
  break;
324
544
  case 'create':
325
- await handleFileCreation(sourceFilePath, targetFilePath, sourceContent);
545
+ await handleFileCreation(sourceFilePath, targetFilePath);
326
546
  break;
327
547
  case 'overwrite':
328
- await handleFileOverwrite(sourceFilePath, targetFilePath, sourceContent);
548
+ await handleFileOverwrite(sourceFilePath, targetFilePath);
329
549
  break;
330
550
  case 'append':
331
- await handleFileAppend(sourceFilePath, targetFilePath, sourceContent);
551
+ await handleFileAppend(sourceFilePath, targetFilePath);
332
552
  break;
333
553
  case 'prepend':
334
- await handleFilePrepend(sourceFilePath, targetFilePath, sourceContent);
554
+ await handleFilePrepend(sourceFilePath, targetFilePath);
335
555
  break;
336
556
  default:
337
557
  console.warn(chalk.yellow(`⚠️ Unknown action '${action}' for file: ${filePath}`));
@@ -340,78 +560,116 @@ async function processFeatureFile(filePath, fileConfig, featureName, provider, p
340
560
  /**
341
561
  * Handle package.json installation
342
562
  */
343
- async function handlePackageInstallation(sourceFilePath, projectPath, packageManager) {
563
+ async function handlePackageInstallation(sourceFilePath, projectPath, packageManager, language) {
344
564
  try {
345
565
  if (await fs.pathExists(sourceFilePath)) {
346
566
  const packageData = await fs.readJson(sourceFilePath);
347
567
  const dependencies = packageData.dependencies || {};
348
568
  const devDependencies = packageData.devDependencies || {};
349
- const allDeps = { ...dependencies, ...devDependencies };
350
- const depNames = Object.keys(allDeps);
351
- if (depNames.length > 0) {
352
- console.log(chalk.blue(`📦 Installing packages: ${depNames.join(', ')}`));
353
- await installPackages(projectPath, 'javascript', depNames);
569
+ const allDeps = Object.keys(dependencies);
570
+ const allDevDeps = Object.keys(devDependencies);
571
+ if (allDeps.length > 0 || allDevDeps.length > 0) {
572
+ console.log(chalk.blue(`📦 Installing packages with ${packageManager}:`));
573
+ // Install regular dependencies
574
+ if (allDeps.length > 0) {
575
+ console.log(chalk.cyan(` Dependencies: ${allDeps.join(', ')}`));
576
+ try {
577
+ await installPackages(projectPath, language || 'javascript', allDeps, {
578
+ isDev: false,
579
+ timeout: 180000 // 3 minutes timeout
580
+ });
581
+ }
582
+ catch (error) {
583
+ console.warn(chalk.yellow(`⚠️ Failed to auto-install dependencies: ${error.message}`));
584
+ console.log(chalk.yellow(`💡 Please install these dependencies manually:`));
585
+ console.log(chalk.hex('#95afc0')(` ${getInstallCommand(packageManager, allDeps, false)}`));
586
+ }
587
+ }
588
+ // Install dev dependencies
589
+ if (allDevDeps.length > 0) {
590
+ console.log(chalk.cyan(` Dev Dependencies: ${allDevDeps.join(', ')}`));
591
+ try {
592
+ await installPackages(projectPath, language || 'javascript', allDevDeps, {
593
+ isDev: true,
594
+ timeout: 180000 // 3 minutes timeout
595
+ });
596
+ }
597
+ catch (error) {
598
+ console.warn(chalk.yellow(` ⚠️ Failed to auto-install dev dependencies: ${error.message}`));
599
+ console.log(chalk.yellow(` 💡 Please install these dev dependencies manually:`));
600
+ console.log(chalk.hex('#95afc0')(` ${getInstallCommand(packageManager, allDevDeps, true)}`));
601
+ }
602
+ }
603
+ }
604
+ else {
605
+ console.log(chalk.yellow(`⚠️ No packages found to install in: ${path.relative(process.cwd(), sourceFilePath)}`));
354
606
  }
355
607
  }
608
+ else {
609
+ console.warn(chalk.yellow(`⚠️ Package.json template file not found: ${path.relative(process.cwd(), sourceFilePath)}`));
610
+ console.log(chalk.gray(` This might be due to running a globally installed CLI. Consider using 'npx' or installing locally.`));
611
+ }
356
612
  }
357
613
  catch (error) {
358
614
  console.warn(chalk.yellow(`⚠️ Could not install packages: ${error.message}`));
615
+ console.log(chalk.yellow(`💡 Please install dependencies manually by checking the feature's package.json file.`));
359
616
  }
360
617
  }
361
618
  /**
362
619
  * Handle file creation (only if it doesn't exist)
363
620
  */
364
- async function handleFileCreation(sourceFilePath, targetFilePath, cachedContent) {
621
+ async function handleFileCreation(sourceFilePath, targetFilePath) {
365
622
  if (await fs.pathExists(targetFilePath)) {
366
623
  console.log(chalk.yellow(`⚠️ File already exists, skipping: ${path.relative(process.cwd(), targetFilePath)}`));
367
624
  return;
368
625
  }
369
- if (cachedContent) {
370
- await fs.outputFile(targetFilePath, cachedContent);
626
+ try {
627
+ if (await fs.pathExists(sourceFilePath)) {
628
+ await copyTemplateFile(sourceFilePath, targetFilePath);
629
+ console.log(chalk.green(`✅ Created: ${path.relative(process.cwd(), targetFilePath)}`));
630
+ }
631
+ else {
632
+ console.log(chalk.yellow(`⚠️ Template file not found, skipping: ${path.relative(process.cwd(), sourceFilePath)}`));
633
+ console.log(chalk.gray(` This might be due to running a globally installed CLI. Consider using 'npx' or installing locally.`));
634
+ }
371
635
  }
372
- else {
373
- await copyTemplateFile(sourceFilePath, targetFilePath);
636
+ catch (error) {
637
+ console.error(chalk.red(`❌ Failed to create file ${path.relative(process.cwd(), targetFilePath)}: ${error.message}`));
638
+ throw error;
374
639
  }
375
- console.log(chalk.green(`✅ Created: ${path.relative(process.cwd(), targetFilePath)}`));
376
640
  }
377
641
  /**
378
642
  * Handle file overwrite (replace existing content or create if doesn't exist)
379
643
  */
380
- async function handleFileOverwrite(sourceFilePath, targetFilePath, cachedContent) {
644
+ async function handleFileOverwrite(sourceFilePath, targetFilePath) {
381
645
  // Ensure target directory exists
382
646
  await fs.ensureDir(path.dirname(targetFilePath));
383
647
  const fileExists = await fs.pathExists(targetFilePath);
384
648
  try {
385
- if (cachedContent) {
386
- await fs.outputFile(targetFilePath, cachedContent);
387
- }
388
- else {
389
- // Check if source template exists
390
- if (await fs.pathExists(sourceFilePath)) {
391
- await copyTemplateFile(sourceFilePath, targetFilePath);
649
+ // Check if source template exists
650
+ if (await fs.pathExists(sourceFilePath)) {
651
+ await copyTemplateFile(sourceFilePath, targetFilePath);
652
+ if (fileExists) {
653
+ console.log(chalk.green(`✅ Updated: ${path.relative(process.cwd(), targetFilePath)}`));
392
654
  }
393
655
  else {
394
- console.log(chalk.yellow(`⚠️ Template file not found, skipping: ${path.relative(process.cwd(), sourceFilePath)}`));
395
- console.log(chalk.gray(` This might be due to running a globally installed CLI. Consider using 'npx' or installing locally.`));
396
- return;
656
+ console.log(chalk.green(`✅ Created: ${path.relative(process.cwd(), targetFilePath)}`));
397
657
  }
398
658
  }
399
- if (fileExists) {
400
- console.log(chalk.green(`✅ Updated: ${path.relative(process.cwd(), targetFilePath)}`));
401
- }
402
659
  else {
403
- console.log(chalk.green(`✅ Created: ${path.relative(process.cwd(), targetFilePath)}`));
660
+ console.log(chalk.yellow(`⚠️ Template file not found, skipping overwrite: ${path.relative(process.cwd(), sourceFilePath)}`));
661
+ console.log(chalk.gray(` This might be due to running a globally installed CLI. Consider using 'npx' or installing locally.`));
404
662
  }
405
663
  }
406
664
  catch (error) {
407
- console.error(chalk.red(`❌ Failed to overwrite/create ${path.relative(process.cwd(), targetFilePath)}: ${error}`));
665
+ console.error(chalk.red(`❌ Failed to overwrite/create ${path.relative(process.cwd(), targetFilePath)}: ${error.message}`));
408
666
  throw error;
409
667
  }
410
668
  }
411
669
  /**
412
670
  * Handle file append (add content to end of file, create if doesn't exist)
413
671
  */
414
- async function handleFileAppend(sourceFilePath, targetFilePath, cachedContent) {
672
+ async function handleFileAppend(sourceFilePath, targetFilePath) {
415
673
  // Ensure target directory exists
416
674
  await fs.ensureDir(path.dirname(targetFilePath));
417
675
  const fileExists = await fs.pathExists(targetFilePath);
@@ -421,38 +679,40 @@ async function handleFileAppend(sourceFilePath, targetFilePath, cachedContent) {
421
679
  existingContent = await fs.readFile(targetFilePath, 'utf8');
422
680
  }
423
681
  let contentToAppend = '';
424
- if (cachedContent) {
425
- contentToAppend = cachedContent;
682
+ // Check if source template exists
683
+ if (await fs.pathExists(sourceFilePath)) {
684
+ contentToAppend = await fs.readFile(sourceFilePath, 'utf8');
426
685
  }
427
686
  else {
428
- // Check if source template exists
429
- if (await fs.pathExists(sourceFilePath)) {
430
- contentToAppend = await fs.readFile(sourceFilePath, 'utf8');
687
+ console.log(chalk.yellow(`⚠️ Template file not found, skipping append: ${path.relative(process.cwd(), sourceFilePath)}`));
688
+ console.log(chalk.gray(` This might be due to running a globally installed CLI. Consider using 'npx' or installing locally.`));
689
+ return;
690
+ }
691
+ // Only append if the content isn't already present (avoid duplicates)
692
+ if (!existingContent.includes(contentToAppend.trim())) {
693
+ const separator = existingContent.endsWith('\n') || !existingContent ? '' : '\n';
694
+ const newContent = existingContent + separator + contentToAppend;
695
+ await fs.outputFile(targetFilePath, newContent);
696
+ if (fileExists) {
697
+ console.log(chalk.green(`✅ Appended to: ${path.relative(process.cwd(), targetFilePath)}`));
431
698
  }
432
699
  else {
433
- console.log(chalk.yellow(`⚠️ Template file not found, skipping append: ${path.relative(process.cwd(), sourceFilePath)}`));
434
- console.log(chalk.gray(` This might be due to running a globally installed CLI. Consider using 'npx' or installing locally.`));
435
- return;
700
+ console.log(chalk.green(`✅ Created with content: ${path.relative(process.cwd(), targetFilePath)}`));
436
701
  }
437
702
  }
438
- const newContent = existingContent + contentToAppend;
439
- await fs.outputFile(targetFilePath, newContent);
440
- if (fileExists) {
441
- console.log(chalk.green(`✅ Appended to: ${path.relative(process.cwd(), targetFilePath)}`));
442
- }
443
703
  else {
444
- console.log(chalk.green(`✅ Created with content: ${path.relative(process.cwd(), targetFilePath)}`));
704
+ console.log(chalk.yellow(`⚠️ Content already exists in file, skipping append: ${path.relative(process.cwd(), targetFilePath)}`));
445
705
  }
446
706
  }
447
707
  catch (error) {
448
- console.error(chalk.red(`❌ Failed to append/create ${path.relative(process.cwd(), targetFilePath)}: ${error}`));
708
+ console.error(chalk.red(`❌ Failed to append/create ${path.relative(process.cwd(), targetFilePath)}: ${error.message}`));
449
709
  throw error;
450
710
  }
451
711
  }
452
712
  /**
453
713
  * Handle file prepend (add content to beginning of file, create if doesn't exist)
454
714
  */
455
- async function handleFilePrepend(sourceFilePath, targetFilePath, cachedContent) {
715
+ async function handleFilePrepend(sourceFilePath, targetFilePath) {
456
716
  // Ensure target directory exists
457
717
  await fs.ensureDir(path.dirname(targetFilePath));
458
718
  const fileExists = await fs.pathExists(targetFilePath);
@@ -462,32 +722,33 @@ async function handleFilePrepend(sourceFilePath, targetFilePath, cachedContent)
462
722
  existingContent = await fs.readFile(targetFilePath, 'utf-8');
463
723
  }
464
724
  let templateContent;
465
- if (cachedContent) {
466
- templateContent = cachedContent;
725
+ // Check if source template exists
726
+ if (await fs.pathExists(sourceFilePath)) {
727
+ templateContent = await fs.readFile(sourceFilePath, 'utf-8');
467
728
  }
468
729
  else {
469
- // Check if source template exists
470
- if (await fs.pathExists(sourceFilePath)) {
471
- templateContent = await fs.readFile(sourceFilePath, 'utf-8');
730
+ console.log(chalk.yellow(`⚠️ Template file not found, skipping prepend: ${path.relative(process.cwd(), sourceFilePath)}`));
731
+ console.log(chalk.gray(` This might be due to running a globally installed CLI. Consider using 'npx' or installing locally.`));
732
+ return;
733
+ }
734
+ // Only prepend if the content isn't already present (avoid duplicates)
735
+ if (!existingContent.includes(templateContent.trim())) {
736
+ const separator = templateContent.endsWith('\n') ? '' : '\n';
737
+ const newContent = templateContent + separator + existingContent;
738
+ await fs.outputFile(targetFilePath, newContent);
739
+ if (fileExists) {
740
+ console.log(chalk.green(`✅ Prepended to: ${path.relative(process.cwd(), targetFilePath)}`));
472
741
  }
473
742
  else {
474
- console.log(chalk.yellow(`⚠️ Template file not found, skipping prepend: ${path.relative(process.cwd(), sourceFilePath)}`));
475
- console.log(chalk.gray(` This might be due to running a globally installed CLI. Consider using 'npx' or installing locally.`));
476
- return;
743
+ console.log(chalk.green(`✅ Created with content: ${path.relative(process.cwd(), targetFilePath)}`));
477
744
  }
478
745
  }
479
- const separator = templateContent.endsWith('\n') ? '' : '\n';
480
- const newContent = templateContent + separator + existingContent;
481
- await fs.outputFile(targetFilePath, newContent);
482
- if (fileExists) {
483
- console.log(chalk.green(`✅ Prepended to: ${path.relative(process.cwd(), targetFilePath)}`));
484
- }
485
746
  else {
486
- console.log(chalk.green(`✅ Created with content: ${path.relative(process.cwd(), targetFilePath)}`));
747
+ console.log(chalk.yellow(`⚠️ Content already exists in file, skipping prepend: ${path.relative(process.cwd(), targetFilePath)}`));
487
748
  }
488
749
  }
489
750
  catch (error) {
490
- console.error(chalk.red(`❌ Failed to prepend/create ${path.relative(process.cwd(), targetFilePath)}: ${error}`));
751
+ console.error(chalk.red(`❌ Failed to prepend/create ${path.relative(process.cwd(), targetFilePath)}: ${error.message}`));
491
752
  throw error;
492
753
  }
493
754
  }
@@ -563,3 +824,24 @@ function showSetupInstructions(featureName, provider) {
563
824
  console.log(chalk.hex('#95afc0')(`Check the documentation for ${featureName} configuration`));
564
825
  }
565
826
  }
827
+ /**
828
+ * Generate the correct install command for different package managers
829
+ */
830
+ function getInstallCommand(packageManager, packages, isDev) {
831
+ switch (packageManager) {
832
+ case 'npm':
833
+ return `npm install ${isDev ? '--save-dev' : ''} ${packages.join(' ')}`;
834
+ case 'yarn':
835
+ return `yarn add ${isDev ? '--dev' : ''} ${packages.join(' ')}`;
836
+ case 'pnpm':
837
+ return `pnpm add ${isDev ? '--save-dev' : ''} ${packages.join(' ')}`;
838
+ case 'bun':
839
+ return `bun add ${isDev ? '--dev' : ''} ${packages.join(' ')}`;
840
+ case 'gem':
841
+ return `gem install ${packages.join(' ')}`;
842
+ case 'pip':
843
+ return `pip install ${packages.join(' ')}`;
844
+ default:
845
+ return `npm install ${isDev ? '--save-dev' : ''} ${packages.join(' ')}`;
846
+ }
847
+ }