package-installer-cli 1.1.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +1 -1
- data/dist/commands/add.js +109 -75
- data/dist/commands/analyze.js +43 -35
- data/dist/commands/cache.js +142 -6
- data/dist/commands/check.js +119 -72
- data/dist/commands/clean.js +230 -92
- data/dist/commands/clone.js +66 -44
- data/dist/commands/create.js +74 -53
- data/dist/commands/deploy.js +30 -22
- data/dist/commands/doctor.js +27 -28
- data/dist/commands/env.js +44 -31
- data/dist/commands/update.js +599 -113
- data/dist/commands/upgrade-cli.js +32 -24
- data/dist/index.js +58 -15
- data/dist/utils/banner.js +1 -1
- data/dist/utils/cacheManager.js +57 -124
- data/dist/utils/dependencyInstaller.js +0 -46
- data/dist/utils/featureInstaller.js +489 -153
- data/dist/utils/helpFormatter.js +110 -0
- data/dist/utils/languageConfig.js +167 -298
- data/dist/utils/pathResolver.js +34 -72
- data/dist/utils/templateResolver.js +3 -4
- data/dist/utils/utils.js +20 -5
- data/lib/package_installer_cli.rb +1 -1
- data/templates/django/django-full-stack-template/django-project-template/deployment/docker/build.sh +0 -0
- data/templates/django/django-full-stack-template/django-project-template/deployment/docker/manage.py +0 -0
- data/templates/django/django-full-stack-template/django-project-template/src/compile-locale.sh +0 -0
- data/templates/django/django-full-stack-template/django-project-template/src/compile-sass.sh +0 -0
- data/templates/django/django-full-stack-template/django-project-template/src/manage.py +0 -0
- data/templates/django/django-inertia-svelte-template/django-inertia-svelte-template-starter/manage.py +0 -0
- data/templates/flask/flask-cookiecutter-advance-template/cookiecutter-docker.sh +0 -0
- data/templates/flask/flask-cookiecutter-advance-template/{{cookiecutter.app_name}}/autoapp.py +0 -0
- data/templates/flask/flask-project-template/apply.sh +0 -0
- data/templates/react-native/typescript/template/android/gradlew +0 -0
- data/templates/ruby/rails_7_esbuild_hotwire_tailwindcss_starter/bin/bundle +0 -0
- data/templates/ruby/rails_7_esbuild_hotwire_tailwindcss_starter/bin/dev +0 -0
- data/templates/ruby/rails_7_esbuild_hotwire_tailwindcss_starter/bin/docker-entrypoint +0 -0
- data/templates/ruby/rails_7_esbuild_hotwire_tailwindcss_starter/bin/rails +0 -0
- data/templates/ruby/rails_7_esbuild_hotwire_tailwindcss_starter/bin/rake +0 -0
- data/templates/ruby/rails_7_esbuild_hotwire_tailwindcss_starter/bin/setup +0 -0
- data/templates/ruby/ruby-on-rails-apis-template/bin/bundle +0 -0
- data/templates/ruby/ruby-on-rails-apis-template/bin/rails +0 -0
- data/templates/ruby/ruby-on-rails-apis-template/bin/rake +0 -0
- data/templates/ruby/ruby-on-rails-apis-template/bin/setup +0 -0
- data/templates/ruby/ruby-on-rails-apis-template/bin/spring +0 -0
- data/templates/ruby/ruby-on-rails-apis-template/bin/update +0 -0
- data/templates/ruby/ruby-on-rails-boilerplate-template/bin/bundle +0 -0
- data/templates/ruby/ruby-on-rails-boilerplate-template/bin/dev +0 -0
- data/templates/ruby/ruby-on-rails-boilerplate-template/bin/importmap +0 -0
- data/templates/ruby/ruby-on-rails-boilerplate-template/bin/rails +0 -0
- data/templates/ruby/ruby-on-rails-boilerplate-template/bin/rake +0 -0
- data/templates/ruby/ruby-on-rails-boilerplate-template/bin/setup +0 -0
- metadata +6 -5
|
@@ -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
|
|
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
|
-
|
|
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}`));
|
|
@@ -45,96 +99,164 @@ export { getCliRootPath } from './pathResolver.js';
|
|
|
45
99
|
* Detect the current project's framework and language with improved logic
|
|
46
100
|
*/
|
|
47
101
|
/**
|
|
48
|
-
* Detect if a Next.js project uses src folder structure
|
|
102
|
+
* Detect if a Next.js project uses src folder structure (Next.js only)
|
|
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;
|
|
56
109
|
}
|
|
57
|
-
// Check for app directory in src
|
|
110
|
+
// Check for Next.js App Router (app directory in src)
|
|
58
111
|
const srcAppPath = path.join(srcPath, 'app');
|
|
59
112
|
if (await fs.pathExists(srcAppPath)) {
|
|
60
113
|
return true;
|
|
61
114
|
}
|
|
62
|
-
// Check for pages directory in src
|
|
115
|
+
// Check for Next.js Pages Router (pages directory in src)
|
|
63
116
|
const srcPagesPath = path.join(srcPath, 'pages');
|
|
64
117
|
if (await fs.pathExists(srcPagesPath)) {
|
|
65
118
|
return true;
|
|
66
119
|
}
|
|
67
|
-
// Check for components
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
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
|
|
139
|
+
* Adjust file path for Next.js src folder structure (Next.js specific)
|
|
140
|
+
* Dynamically places files in src/ folder based on their path structure
|
|
80
141
|
*/
|
|
81
|
-
function
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
'
|
|
89
|
-
'
|
|
90
|
-
'
|
|
91
|
-
'
|
|
92
|
-
'styles/' // Only component-specific styles, not global ones
|
|
142
|
+
function adjustNextjsSrcFilePath(filePath, hasSrcFolder, projectPath) {
|
|
143
|
+
// If project doesn't use src folder, return original path
|
|
144
|
+
if (!hasSrcFolder) {
|
|
145
|
+
return path.join(projectPath, filePath);
|
|
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'
|
|
93
153
|
];
|
|
94
|
-
|
|
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) {
|
|
184
|
+
// Files that should ALWAYS be in root regardless of src folder
|
|
95
185
|
const rootOnlyFiles = [
|
|
96
|
-
'middleware.ts',
|
|
97
|
-
'middleware.js',
|
|
98
|
-
'next.config.js',
|
|
99
|
-
'next.config.mjs',
|
|
100
186
|
'.env',
|
|
101
187
|
'.env.local',
|
|
102
188
|
'.env.example',
|
|
189
|
+
'.env.development',
|
|
190
|
+
'.env.production',
|
|
103
191
|
'package.json',
|
|
192
|
+
'next.config.js',
|
|
193
|
+
'next.config.mjs',
|
|
194
|
+
'next.config.ts',
|
|
104
195
|
'tailwind.config.js',
|
|
105
196
|
'tailwind.config.ts',
|
|
106
|
-
'postcss.config.js'
|
|
197
|
+
'postcss.config.js',
|
|
198
|
+
'postcss.config.ts',
|
|
199
|
+
'middleware.ts',
|
|
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'
|
|
107
214
|
];
|
|
108
|
-
// Check if this file should always be in root
|
|
109
215
|
const fileName = path.basename(filePath);
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
216
|
+
const fileDir = path.dirname(filePath);
|
|
217
|
+
// Check if this file should always be in root
|
|
218
|
+
if (rootOnlyFiles.includes(fileName) || filePath.startsWith('public/')) {
|
|
219
|
+
return path.join(projectPath, filePath);
|
|
220
|
+
}
|
|
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
|
+
}
|
|
116
255
|
}
|
|
117
|
-
return filePath;
|
|
118
256
|
}
|
|
119
257
|
export async function detectProjectStack(projectPath) {
|
|
120
258
|
try {
|
|
121
|
-
//
|
|
122
|
-
const cachedProject = await getCachedProject(projectPath);
|
|
123
|
-
if (cachedProject) {
|
|
124
|
-
const packageManager = await detectPackageManager(projectPath);
|
|
125
|
-
let hasSrcFolder = await fs.pathExists(path.join(projectPath, 'src'));
|
|
126
|
-
// For Next.js projects, do a more thorough src folder detection
|
|
127
|
-
if (cachedProject.framework === 'nextjs') {
|
|
128
|
-
hasSrcFolder = await detectNextjsSrcStructure(projectPath);
|
|
129
|
-
}
|
|
130
|
-
return {
|
|
131
|
-
framework: cachedProject.framework,
|
|
132
|
-
language: cachedProject.language,
|
|
133
|
-
projectLanguage: cachedProject.language,
|
|
134
|
-
packageManager,
|
|
135
|
-
hasSrcFolder
|
|
136
|
-
};
|
|
137
|
-
}
|
|
259
|
+
// Skip cache lookup for simplicity - always detect fresh
|
|
138
260
|
// Detect language first
|
|
139
261
|
const files = await fs.readdir(projectPath);
|
|
140
262
|
const detectedLanguages = detectLanguageFromFiles(files);
|
|
@@ -183,12 +305,12 @@ export async function detectProjectStack(projectPath) {
|
|
|
183
305
|
else if (dependencies['@remix-run/react']) {
|
|
184
306
|
framework = 'remixjs';
|
|
185
307
|
}
|
|
186
|
-
// For
|
|
308
|
+
// For other frameworks, simple src folder check
|
|
187
309
|
if (framework !== 'nextjs' && !hasSrcFolder) {
|
|
188
310
|
hasSrcFolder = await fs.pathExists(path.join(projectPath, 'src'));
|
|
189
311
|
}
|
|
190
312
|
// Cache the detected information
|
|
191
|
-
await cacheProjectData(projectPath, packageJson.name || path.basename(projectPath), typeof projectLanguage === 'string' ? projectLanguage : 'unknown'
|
|
313
|
+
await cacheProjectData(projectPath, packageJson.name || path.basename(projectPath), typeof projectLanguage === 'string' ? projectLanguage : 'unknown');
|
|
192
314
|
}
|
|
193
315
|
return {
|
|
194
316
|
framework,
|
|
@@ -227,25 +349,36 @@ export async function addFeature(featureName, provider, projectPath = process.cw
|
|
|
227
349
|
try {
|
|
228
350
|
// Ensure features are loaded
|
|
229
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
|
+
}
|
|
230
356
|
// Get project information
|
|
231
357
|
const projectInfo = await detectProjectStack(projectPath);
|
|
232
358
|
if (!projectInfo.framework) {
|
|
233
|
-
|
|
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.');
|
|
234
362
|
}
|
|
235
363
|
// Get feature configuration
|
|
236
364
|
const featureConfig = SUPPORTED_FEATURES[featureName];
|
|
237
365
|
if (!featureConfig) {
|
|
238
|
-
|
|
366
|
+
const availableFeatures = Object.keys(SUPPORTED_FEATURES);
|
|
367
|
+
throw new Error(`Feature '${featureName}' not found. Available features: ${availableFeatures.join(', ')}`);
|
|
239
368
|
}
|
|
240
369
|
// Check if feature supports this framework
|
|
241
370
|
if (!featureConfig.supportedFrameworks.includes(projectInfo.framework)) {
|
|
242
|
-
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(', ')}`);
|
|
243
372
|
}
|
|
244
|
-
|
|
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)
|
|
245
375
|
let selectedProvider = provider;
|
|
246
|
-
|
|
247
|
-
|
|
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
|
|
248
380
|
if (availableProviders.length > 1) {
|
|
381
|
+
spinner.stop();
|
|
249
382
|
const inquirer = await import('inquirer');
|
|
250
383
|
const { provider: chosenProvider } = await inquirer.default.prompt([
|
|
251
384
|
{
|
|
@@ -256,11 +389,16 @@ export async function addFeature(featureName, provider, projectPath = process.cw
|
|
|
256
389
|
}
|
|
257
390
|
]);
|
|
258
391
|
selectedProvider = chosenProvider;
|
|
392
|
+
spinner.start(chalk.hex('#9c88ff')(`Adding ${featureName} (${selectedProvider}) feature...`));
|
|
259
393
|
}
|
|
260
394
|
else {
|
|
261
395
|
selectedProvider = availableProviders[0];
|
|
262
396
|
}
|
|
263
397
|
}
|
|
398
|
+
else if (hasSimpleStructure) {
|
|
399
|
+
// Simple structure - use framework as the "provider"
|
|
400
|
+
selectedProvider = projectInfo.framework;
|
|
401
|
+
}
|
|
264
402
|
// Get files for the specific provider, framework, and language
|
|
265
403
|
const files = getFeatureFiles(featureConfig, selectedProvider, projectInfo.framework, projectInfo.projectLanguage);
|
|
266
404
|
if (Object.keys(files).length === 0) {
|
|
@@ -276,6 +414,12 @@ export async function addFeature(featureName, provider, projectPath = process.cw
|
|
|
276
414
|
console.log(chalk.gray(`📊 Feature ${featureName} used for ${projectInfo.framework || 'unknown'} project`));
|
|
277
415
|
// Show setup instructions
|
|
278
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'));
|
|
279
423
|
}
|
|
280
424
|
catch (error) {
|
|
281
425
|
spinner.fail(chalk.red(`❌ Failed to add ${featureName} feature: ${error.message}`));
|
|
@@ -284,8 +428,35 @@ export async function addFeature(featureName, provider, projectPath = process.cw
|
|
|
284
428
|
}
|
|
285
429
|
/**
|
|
286
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.)
|
|
287
434
|
*/
|
|
288
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
|
|
289
460
|
const providerConfig = featureConfig.files[provider];
|
|
290
461
|
if (!providerConfig)
|
|
291
462
|
return {};
|
|
@@ -304,44 +475,77 @@ function getFeatureFiles(featureConfig, provider, framework, language) {
|
|
|
304
475
|
}
|
|
305
476
|
return languageConfig;
|
|
306
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
|
+
}
|
|
307
517
|
/**
|
|
308
518
|
* Process a single feature file based on its action
|
|
309
519
|
*/
|
|
310
520
|
async function processFeatureFile(filePath, fileConfig, featureName, provider, projectInfo, projectPath) {
|
|
311
521
|
const { action } = fileConfig;
|
|
312
|
-
//
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
if (await fs.pathExists(sourceFilePath)) {
|
|
320
|
-
sourceContent = await fs.readFile(sourceFilePath, 'utf-8');
|
|
321
|
-
}
|
|
322
|
-
// Handle file path adjustment based on project structure
|
|
323
|
-
let targetFilePath = path.join(projectPath, filePath);
|
|
324
|
-
// For Next.js projects, adjust file paths based on src folder structure
|
|
325
|
-
if (projectInfo.framework === 'nextjs') {
|
|
326
|
-
targetFilePath = adjustNextjsFilePath(filePath, projectInfo.hasSrcFolder || false, 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;
|
|
327
529
|
}
|
|
530
|
+
// Handle file path adjustment based on project structure - framework agnostic
|
|
531
|
+
let targetFilePath = adjustFrameworkFilePath(filePath, projectInfo.framework || 'unknown', projectInfo.hasSrcFolder || false, projectPath);
|
|
328
532
|
// Ensure all parent directories exist before processing
|
|
329
533
|
await fs.ensureDir(path.dirname(targetFilePath));
|
|
330
534
|
switch (action) {
|
|
331
535
|
case 'install':
|
|
332
|
-
await handlePackageInstallation(sourceFilePath, projectPath, projectInfo.packageManager || 'npm');
|
|
536
|
+
await handlePackageInstallation(sourceFilePath, projectPath, projectInfo.packageManager || 'npm', projectInfo.language);
|
|
333
537
|
break;
|
|
334
538
|
case 'create':
|
|
335
|
-
await handleFileCreation(sourceFilePath, targetFilePath
|
|
539
|
+
await handleFileCreation(sourceFilePath, targetFilePath);
|
|
336
540
|
break;
|
|
337
541
|
case 'overwrite':
|
|
338
|
-
await handleFileOverwrite(sourceFilePath, targetFilePath
|
|
542
|
+
await handleFileOverwrite(sourceFilePath, targetFilePath);
|
|
339
543
|
break;
|
|
340
544
|
case 'append':
|
|
341
|
-
await handleFileAppend(sourceFilePath, targetFilePath
|
|
545
|
+
await handleFileAppend(sourceFilePath, targetFilePath);
|
|
342
546
|
break;
|
|
343
547
|
case 'prepend':
|
|
344
|
-
await handleFilePrepend(sourceFilePath, targetFilePath
|
|
548
|
+
await handleFilePrepend(sourceFilePath, targetFilePath);
|
|
345
549
|
break;
|
|
346
550
|
default:
|
|
347
551
|
console.warn(chalk.yellow(`⚠️ Unknown action '${action}' for file: ${filePath}`));
|
|
@@ -350,120 +554,231 @@ async function processFeatureFile(filePath, fileConfig, featureName, provider, p
|
|
|
350
554
|
/**
|
|
351
555
|
* Handle package.json installation
|
|
352
556
|
*/
|
|
353
|
-
async function handlePackageInstallation(sourceFilePath, projectPath, packageManager) {
|
|
557
|
+
async function handlePackageInstallation(sourceFilePath, projectPath, packageManager, language) {
|
|
354
558
|
try {
|
|
355
559
|
if (await fs.pathExists(sourceFilePath)) {
|
|
356
560
|
const packageData = await fs.readJson(sourceFilePath);
|
|
357
561
|
const dependencies = packageData.dependencies || {};
|
|
358
562
|
const devDependencies = packageData.devDependencies || {};
|
|
359
|
-
const allDeps =
|
|
360
|
-
const
|
|
361
|
-
if (
|
|
362
|
-
console.log(chalk.blue(`📦 Installing packages
|
|
363
|
-
|
|
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)}`));
|
|
364
600
|
}
|
|
365
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
|
+
}
|
|
366
606
|
}
|
|
367
607
|
catch (error) {
|
|
368
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.`));
|
|
369
610
|
}
|
|
370
611
|
}
|
|
371
612
|
/**
|
|
372
613
|
* Handle file creation (only if it doesn't exist)
|
|
373
614
|
*/
|
|
374
|
-
async function handleFileCreation(sourceFilePath, targetFilePath
|
|
615
|
+
async function handleFileCreation(sourceFilePath, targetFilePath) {
|
|
375
616
|
if (await fs.pathExists(targetFilePath)) {
|
|
376
617
|
console.log(chalk.yellow(`⚠️ File already exists, skipping: ${path.relative(process.cwd(), targetFilePath)}`));
|
|
377
618
|
return;
|
|
378
619
|
}
|
|
379
|
-
|
|
380
|
-
await fs.
|
|
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
|
+
}
|
|
381
629
|
}
|
|
382
|
-
|
|
383
|
-
|
|
630
|
+
catch (error) {
|
|
631
|
+
console.error(chalk.red(`❌ Failed to create file ${path.relative(process.cwd(), targetFilePath)}: ${error.message}`));
|
|
632
|
+
throw error;
|
|
384
633
|
}
|
|
385
|
-
console.log(chalk.green(`✅ Created: ${path.relative(process.cwd(), targetFilePath)}`));
|
|
386
634
|
}
|
|
387
635
|
/**
|
|
388
|
-
* Handle file overwrite (replace existing content)
|
|
636
|
+
* Handle file overwrite (replace existing content or create if doesn't exist)
|
|
389
637
|
*/
|
|
390
|
-
async function handleFileOverwrite(sourceFilePath, targetFilePath
|
|
391
|
-
|
|
392
|
-
|
|
638
|
+
async function handleFileOverwrite(sourceFilePath, targetFilePath) {
|
|
639
|
+
// Ensure target directory exists
|
|
640
|
+
await fs.ensureDir(path.dirname(targetFilePath));
|
|
641
|
+
const fileExists = await fs.pathExists(targetFilePath);
|
|
642
|
+
try {
|
|
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)}`));
|
|
648
|
+
}
|
|
649
|
+
else {
|
|
650
|
+
console.log(chalk.green(`✅ Created: ${path.relative(process.cwd(), targetFilePath)}`));
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
else {
|
|
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.`));
|
|
656
|
+
}
|
|
393
657
|
}
|
|
394
|
-
|
|
395
|
-
|
|
658
|
+
catch (error) {
|
|
659
|
+
console.error(chalk.red(`❌ Failed to overwrite/create ${path.relative(process.cwd(), targetFilePath)}: ${error.message}`));
|
|
660
|
+
throw error;
|
|
396
661
|
}
|
|
397
|
-
console.log(chalk.green(`✅ Updated: ${path.relative(process.cwd(), targetFilePath)}`));
|
|
398
662
|
}
|
|
399
663
|
/**
|
|
400
|
-
* Handle file append (add content to end of file)
|
|
664
|
+
* Handle file append (add content to end of file, create if doesn't exist)
|
|
401
665
|
*/
|
|
402
|
-
async function handleFileAppend(sourceFilePath, targetFilePath
|
|
666
|
+
async function handleFileAppend(sourceFilePath, targetFilePath) {
|
|
667
|
+
// Ensure target directory exists
|
|
668
|
+
await fs.ensureDir(path.dirname(targetFilePath));
|
|
669
|
+
const fileExists = await fs.pathExists(targetFilePath);
|
|
403
670
|
let existingContent = '';
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
671
|
+
try {
|
|
672
|
+
if (fileExists) {
|
|
673
|
+
existingContent = await fs.readFile(targetFilePath, 'utf8');
|
|
674
|
+
}
|
|
675
|
+
let contentToAppend = '';
|
|
676
|
+
// Check if source template exists
|
|
677
|
+
if (await fs.pathExists(sourceFilePath)) {
|
|
678
|
+
contentToAppend = await fs.readFile(sourceFilePath, 'utf8');
|
|
679
|
+
}
|
|
680
|
+
else {
|
|
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)}`));
|
|
692
|
+
}
|
|
693
|
+
else {
|
|
694
|
+
console.log(chalk.green(`✅ Created with content: ${path.relative(process.cwd(), targetFilePath)}`));
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
else {
|
|
698
|
+
console.log(chalk.yellow(`⚠️ Content already exists in file, skipping append: ${path.relative(process.cwd(), targetFilePath)}`));
|
|
699
|
+
}
|
|
410
700
|
}
|
|
411
|
-
|
|
412
|
-
|
|
701
|
+
catch (error) {
|
|
702
|
+
console.error(chalk.red(`❌ Failed to append/create ${path.relative(process.cwd(), targetFilePath)}: ${error.message}`));
|
|
703
|
+
throw error;
|
|
413
704
|
}
|
|
414
|
-
const separator = existingContent && !existingContent.endsWith('\n') ? '\n\n' : '\n';
|
|
415
|
-
const newContent = existingContent + separator + templateContent;
|
|
416
|
-
// Ensure target directory exists
|
|
417
|
-
await fs.ensureDir(path.dirname(targetFilePath));
|
|
418
|
-
await fs.outputFile(targetFilePath, newContent);
|
|
419
|
-
console.log(chalk.green(`✅ Appended to: ${path.relative(process.cwd(), targetFilePath)}`));
|
|
420
705
|
}
|
|
421
706
|
/**
|
|
422
|
-
* Handle file prepend (add content to beginning of file)
|
|
707
|
+
* Handle file prepend (add content to beginning of file, create if doesn't exist)
|
|
423
708
|
*/
|
|
424
|
-
async function handleFilePrepend(sourceFilePath, targetFilePath
|
|
709
|
+
async function handleFilePrepend(sourceFilePath, targetFilePath) {
|
|
710
|
+
// Ensure target directory exists
|
|
711
|
+
await fs.ensureDir(path.dirname(targetFilePath));
|
|
712
|
+
const fileExists = await fs.pathExists(targetFilePath);
|
|
425
713
|
let existingContent = '';
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
714
|
+
try {
|
|
715
|
+
if (fileExists) {
|
|
716
|
+
existingContent = await fs.readFile(targetFilePath, 'utf-8');
|
|
717
|
+
}
|
|
718
|
+
let templateContent;
|
|
719
|
+
// Check if source template exists
|
|
720
|
+
if (await fs.pathExists(sourceFilePath)) {
|
|
721
|
+
templateContent = await fs.readFile(sourceFilePath, 'utf-8');
|
|
722
|
+
}
|
|
723
|
+
else {
|
|
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)}`));
|
|
735
|
+
}
|
|
736
|
+
else {
|
|
737
|
+
console.log(chalk.green(`✅ Created with content: ${path.relative(process.cwd(), targetFilePath)}`));
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
else {
|
|
741
|
+
console.log(chalk.yellow(`⚠️ Content already exists in file, skipping prepend: ${path.relative(process.cwd(), targetFilePath)}`));
|
|
742
|
+
}
|
|
432
743
|
}
|
|
433
|
-
|
|
434
|
-
|
|
744
|
+
catch (error) {
|
|
745
|
+
console.error(chalk.red(`❌ Failed to prepend/create ${path.relative(process.cwd(), targetFilePath)}: ${error.message}`));
|
|
746
|
+
throw error;
|
|
435
747
|
}
|
|
436
|
-
const separator = templateContent.endsWith('\n') ? '' : '\n';
|
|
437
|
-
const newContent = templateContent + separator + existingContent;
|
|
438
|
-
// Ensure target directory exists
|
|
439
|
-
await fs.ensureDir(path.dirname(targetFilePath));
|
|
440
|
-
await fs.outputFile(targetFilePath, newContent);
|
|
441
|
-
console.log(chalk.green(`✅ Prepended to: ${path.relative(process.cwd(), targetFilePath)}`));
|
|
442
748
|
}
|
|
443
749
|
/**
|
|
444
|
-
* Copy template file to target location with
|
|
750
|
+
* Copy template file to target location with framework-agnostic content processing
|
|
445
751
|
*/
|
|
446
752
|
async function copyTemplateFile(sourceFilePath, targetFilePath) {
|
|
447
753
|
if (!await fs.pathExists(sourceFilePath)) {
|
|
754
|
+
const relativePath = path.relative(process.cwd(), sourceFilePath);
|
|
755
|
+
console.error(chalk.red(`❌ Template file not found: ${relativePath}`));
|
|
756
|
+
console.error(chalk.yellow(`💡 This might be due to running a globally installed CLI. Consider using 'npx' or installing locally.`));
|
|
448
757
|
throw new Error(`Template file not found: ${sourceFilePath}`);
|
|
449
758
|
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
759
|
+
try {
|
|
760
|
+
// Ensure target directory exists
|
|
761
|
+
await fs.ensureDir(path.dirname(targetFilePath));
|
|
762
|
+
// For code files, we might need to adjust import paths based on project structure
|
|
763
|
+
if (path.extname(sourceFilePath).match(/\.(js|jsx|ts|tsx)$/)) {
|
|
764
|
+
const templateContent = await fs.readFile(sourceFilePath, 'utf-8');
|
|
765
|
+
// Process content based on project structure (framework-agnostic)
|
|
766
|
+
let processedContent = templateContent;
|
|
767
|
+
// Adjust import paths for src-based project structures
|
|
768
|
+
if (targetFilePath.includes('/src/')) {
|
|
769
|
+
processedContent = processedContent.replace(/from ['"]@\//g, 'from "@/');
|
|
770
|
+
processedContent = processedContent.replace(/from ['"]\.\.\//g, 'from "../');
|
|
771
|
+
}
|
|
772
|
+
await fs.writeFile(targetFilePath, processedContent);
|
|
773
|
+
}
|
|
774
|
+
else {
|
|
775
|
+
// For non-code files, just copy directly
|
|
776
|
+
await fs.copy(sourceFilePath, targetFilePath);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
catch (error) {
|
|
780
|
+
console.error(chalk.red(`❌ Failed to copy template file: ${error}`));
|
|
781
|
+
throw error;
|
|
467
782
|
}
|
|
468
783
|
}
|
|
469
784
|
/**
|
|
@@ -503,3 +818,24 @@ function showSetupInstructions(featureName, provider) {
|
|
|
503
818
|
console.log(chalk.hex('#95afc0')(`Check the documentation for ${featureName} configuration`));
|
|
504
819
|
}
|
|
505
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
|
+
}
|