@cerema/cadriciel 1.6.6 → 1.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli/global/create.js +257 -0
- package/cli/global/install.js +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
module.exports = (args) => {
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const fse = require('fs-extra');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const chalk = require('chalk-v2');
|
|
6
|
+
const inquirer = require('inquirer');
|
|
7
|
+
const { spawnSync } = require('child_process');
|
|
8
|
+
const ora = require('ora');
|
|
9
|
+
const os = require('os');
|
|
10
|
+
|
|
11
|
+
const CACHE_DIR = path.join(os.homedir(), '.cadriciel', 'starters-cache');
|
|
12
|
+
const PACKAGE_NAME = '@cadriciel/starters';
|
|
13
|
+
|
|
14
|
+
// Helper: Fetch/Update the starters package
|
|
15
|
+
const fetchStarters = async () => {
|
|
16
|
+
const spinner = ora('Fetching latest starters...').start();
|
|
17
|
+
try {
|
|
18
|
+
fse.ensureDirSync(CACHE_DIR);
|
|
19
|
+
// We use npm install to handle downloading and unpacking
|
|
20
|
+
// We install to a distinct prefix to avoid polluting global or current project
|
|
21
|
+
// --no-save ensures it doesn't try to write to a package.json
|
|
22
|
+
const result = spawnSync(`npm install ${PACKAGE_NAME}@latest --force --no-save --prefix "${CACHE_DIR}"`, {
|
|
23
|
+
stdio: 'pipe',
|
|
24
|
+
encoding: 'utf-8',
|
|
25
|
+
shell: true
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
if (result.status !== 0) {
|
|
29
|
+
throw new Error(`npm install failed: ${result.stderr}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
spinner.succeed('Starters updated.');
|
|
33
|
+
// The package will be in CACHE_DIR/node_modules/@cadriciel/starters
|
|
34
|
+
return path.join(CACHE_DIR, 'node_modules', PACKAGE_NAME);
|
|
35
|
+
} catch (err) {
|
|
36
|
+
spinner.fail('Failed to fetch starters.');
|
|
37
|
+
console.error(chalk.red(err.message));
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Helper: Scan for starters
|
|
43
|
+
const scanStarters = (startersPath) => {
|
|
44
|
+
if (!fs.existsSync(startersPath)) return [];
|
|
45
|
+
|
|
46
|
+
// Check files is list of possible starter folders
|
|
47
|
+
// We look for any directory that has a starter.config.json
|
|
48
|
+
const entries = fs.readdirSync(startersPath, { withFileTypes: true });
|
|
49
|
+
const starters = [];
|
|
50
|
+
|
|
51
|
+
for (const entry of entries) {
|
|
52
|
+
if (entry.isDirectory()) {
|
|
53
|
+
const configPath = path.join(startersPath, entry.name, 'starter.config.json');
|
|
54
|
+
if (fs.existsSync(configPath)) {
|
|
55
|
+
try {
|
|
56
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
57
|
+
starters.push({
|
|
58
|
+
id: entry.name,
|
|
59
|
+
name: config.name || entry.name,
|
|
60
|
+
description: config.description || 'No description',
|
|
61
|
+
path: path.join(startersPath, entry.name),
|
|
62
|
+
config: config
|
|
63
|
+
});
|
|
64
|
+
} catch (e) {
|
|
65
|
+
// Ignore malformed configs
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return starters;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// Helper: Generate Project
|
|
74
|
+
const generateProject = async (starter, options, targetName) => {
|
|
75
|
+
const targetPath = path.resolve(process.cwd(), targetName);
|
|
76
|
+
|
|
77
|
+
if (fs.existsSync(targetPath)) {
|
|
78
|
+
const { replace } = await inquirer.prompt([
|
|
79
|
+
{
|
|
80
|
+
type: 'confirm',
|
|
81
|
+
name: 'replace',
|
|
82
|
+
message: `Directory ${targetName} already exists. Overwrite?`,
|
|
83
|
+
default: false
|
|
84
|
+
}
|
|
85
|
+
]);
|
|
86
|
+
if (!replace) return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const spinner = ora(`Generating project ${targetName}...`).start();
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
fse.ensureDirSync(targetPath);
|
|
93
|
+
|
|
94
|
+
// 1. Process features
|
|
95
|
+
// Loop through configured options in the starter config
|
|
96
|
+
const starterOptions = starter.config.options || {};
|
|
97
|
+
|
|
98
|
+
// Always process 'core' (implied) or features selected by user
|
|
99
|
+
// We need to map options.selectedFeatures keys to starterOptions keys
|
|
100
|
+
const selectedKeys = options.selectedFeatures || [];
|
|
101
|
+
|
|
102
|
+
// Collect all sources to copy
|
|
103
|
+
const copyTasks = [];
|
|
104
|
+
|
|
105
|
+
// Add Core/Default implicit sections if they exist in config structure
|
|
106
|
+
// The config structure: options: { "featureA": { type: "feature", ... }, "core": { type: "core", ... } }
|
|
107
|
+
Object.keys(starterOptions).forEach(key => {
|
|
108
|
+
const opt = starterOptions[key];
|
|
109
|
+
|
|
110
|
+
// Include if it is 'core' OR if it is in selectedKeys
|
|
111
|
+
// OR if it is default true and not opt-out (simplification: just rely on selection for optional ones)
|
|
112
|
+
// For CLI 'checklist', we usually only pass explicitly selected items back,
|
|
113
|
+
// so we must ensure 'required' or 'core' items are handled.
|
|
114
|
+
|
|
115
|
+
let shouldInclude = false;
|
|
116
|
+
if (opt.type === 'core') shouldInclude = true;
|
|
117
|
+
else if (selectedKeys.includes(key)) shouldInclude = true;
|
|
118
|
+
|
|
119
|
+
if (shouldInclude && opt.sources) {
|
|
120
|
+
opt.sources.forEach(source => {
|
|
121
|
+
copyTasks.push(source);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Execute Copy
|
|
127
|
+
copyTasks.forEach(task => {
|
|
128
|
+
const srcPath = path.join(starter.path, task.src);
|
|
129
|
+
const destPath = path.join(targetPath, task.dest);
|
|
130
|
+
|
|
131
|
+
if (fs.existsSync(srcPath)) {
|
|
132
|
+
// Check if file or directory
|
|
133
|
+
const stat = fs.statSync(srcPath);
|
|
134
|
+
if (stat.isDirectory()) {
|
|
135
|
+
fse.copySync(srcPath, destPath);
|
|
136
|
+
} else {
|
|
137
|
+
fse.ensureDirSync(path.dirname(destPath));
|
|
138
|
+
fse.copySync(srcPath, destPath);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// 2. Run Hooks (post_create)
|
|
144
|
+
if (starter.config.hooks && starter.config.hooks.post_create) {
|
|
145
|
+
// Copy the hook script if needed or just run it?
|
|
146
|
+
// Usually hooks run inside the project.
|
|
147
|
+
// For now, we just log. Smart implementation would spawn the script.
|
|
148
|
+
spinner.info(`Post-create hook defined: ${starter.config.hooks.post_create}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
spinner.succeed(`Project ${targetName} created successfully!`);
|
|
152
|
+
console.log(chalk.green(`\nNext steps:\n cd ${targetName}`));
|
|
153
|
+
console.log(chalk.green(` # Install dependencies (frontend & backend)`));
|
|
154
|
+
console.log(chalk.green(` cd frontend && bun install`));
|
|
155
|
+
console.log(chalk.green(` cd backend && bun install`));
|
|
156
|
+
console.log(chalk.green(` # Start the project`));
|
|
157
|
+
console.log(chalk.green(` bun run dev`));
|
|
158
|
+
|
|
159
|
+
} catch (err) {
|
|
160
|
+
spinner.fail('Project generation failed.');
|
|
161
|
+
console.error(chalk.red(err));
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
info: {
|
|
167
|
+
title: 'create',
|
|
168
|
+
label: 'beta',
|
|
169
|
+
description: 'Create a new project from Cadriciel templates',
|
|
170
|
+
},
|
|
171
|
+
start: async (cmdArgs) => {
|
|
172
|
+
console.log(chalk.bold.cyan('Cadriciel Project Generator'));
|
|
173
|
+
|
|
174
|
+
// Check for --test flag
|
|
175
|
+
const isTestMode = cmdArgs.includes('--test');
|
|
176
|
+
// Remove --test from args so it doesn't interfere with project name
|
|
177
|
+
const filteredArgs = cmdArgs.filter(arg => arg !== '--test');
|
|
178
|
+
|
|
179
|
+
// 1. Fetch Starters
|
|
180
|
+
let startersPath;
|
|
181
|
+
if (isTestMode) {
|
|
182
|
+
console.log(chalk.yellow('Running in TEST mode: Using current directory for starters.'));
|
|
183
|
+
startersPath = process.cwd();
|
|
184
|
+
} else {
|
|
185
|
+
startersPath = await fetchStarters();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (!startersPath) return;
|
|
189
|
+
|
|
190
|
+
const starters = scanStarters(startersPath);
|
|
191
|
+
if (starters.length === 0) {
|
|
192
|
+
console.log(chalk.yellow('No starters found in the registry.'));
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// 2. Ask for Project Name (if not provided)
|
|
197
|
+
let args = [...filteredArgs];
|
|
198
|
+
if (args.length > 0 && args[0] === 'create') {
|
|
199
|
+
args.shift();
|
|
200
|
+
}
|
|
201
|
+
let targetName = args[0];
|
|
202
|
+
if (!targetName) {
|
|
203
|
+
const answers = await inquirer.prompt([
|
|
204
|
+
{
|
|
205
|
+
type: 'input',
|
|
206
|
+
name: 'name',
|
|
207
|
+
message: 'Project name:',
|
|
208
|
+
validate: input => !!input || 'Name is required'
|
|
209
|
+
}
|
|
210
|
+
]);
|
|
211
|
+
targetName = answers.name;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// 3. Select Starter
|
|
215
|
+
const { selectedStarterId } = await inquirer.prompt([
|
|
216
|
+
{
|
|
217
|
+
type: 'list',
|
|
218
|
+
name: 'selectedStarterId',
|
|
219
|
+
message: 'Select a template:',
|
|
220
|
+
choices: starters.map(s => ({
|
|
221
|
+
name: `${chalk.bold(s.name)} - ${s.description}`,
|
|
222
|
+
value: s.id
|
|
223
|
+
}))
|
|
224
|
+
}
|
|
225
|
+
]);
|
|
226
|
+
|
|
227
|
+
const starter = starters.find(s => s.id === selectedStarterId);
|
|
228
|
+
|
|
229
|
+
// 4. Select Features
|
|
230
|
+
// Determine selectable features from config
|
|
231
|
+
const optionsConfig = starter.config.options || {};
|
|
232
|
+
const selectableFeatures = Object.keys(optionsConfig)
|
|
233
|
+
.filter(key => optionsConfig[key].type !== 'core') // Exclude core
|
|
234
|
+
.map(key => ({
|
|
235
|
+
name: optionsConfig[key].description || key,
|
|
236
|
+
value: key,
|
|
237
|
+
checked: optionsConfig[key].default === true
|
|
238
|
+
}));
|
|
239
|
+
|
|
240
|
+
let selectedFeatures = [];
|
|
241
|
+
if (selectableFeatures.length > 0) {
|
|
242
|
+
const featureAnswers = await inquirer.prompt([
|
|
243
|
+
{
|
|
244
|
+
type: 'checkbox',
|
|
245
|
+
name: 'features',
|
|
246
|
+
message: 'Select features:',
|
|
247
|
+
choices: selectableFeatures
|
|
248
|
+
}
|
|
249
|
+
]);
|
|
250
|
+
selectedFeatures = featureAnswers.features;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// 5. Generate
|
|
254
|
+
await generateProject(starter, { selectedFeatures }, targetName);
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
};
|
package/cli/global/install.js
CHANGED
|
@@ -7,7 +7,7 @@ module.exports = (args) => {
|
|
|
7
7
|
const { spawn } = require('child_process');
|
|
8
8
|
|
|
9
9
|
const checkIfCommandExists = (command, callback) => {
|
|
10
|
-
const proc = spawn(
|
|
10
|
+
const proc = spawn(`which ${command}`, { shell: true });
|
|
11
11
|
|
|
12
12
|
let found = false;
|
|
13
13
|
proc.stdout.on('data', (data) => {
|
|
@@ -87,7 +87,7 @@ module.exports = (args) => {
|
|
|
87
87
|
}
|
|
88
88
|
args.push(imageEntry.image);
|
|
89
89
|
|
|
90
|
-
const dockerPull = spawn(
|
|
90
|
+
const dockerPull = spawn(`docker ${args.join(' ')}`, { shell: true });
|
|
91
91
|
let stderrOutput = '';
|
|
92
92
|
|
|
93
93
|
dockerPull.stdout.on('data', () => { });
|