package-installer-cli 1.2.0 → 1.3.0

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