@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.
@@ -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
+ };
@@ -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('which', [command], { shell: true });
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('docker', args, { shell: true });
90
+ const dockerPull = spawn(`docker ${args.join(' ')}`, { shell: true });
91
91
  let stderrOutput = '';
92
92
 
93
93
  dockerPull.stdout.on('data', () => { });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cerema/cadriciel",
3
- "version": "1.6.6",
3
+ "version": "1.7.2",
4
4
  "main": "index.js",
5
5
  "license": "MIT",
6
6
  "npm": ">=8.0.0",