@cerema/cadriciel 1.7.4 → 1.8.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.
@@ -11,6 +11,157 @@ module.exports = (args) => {
11
11
  const CACHE_DIR = path.join(os.homedir(), '.cadriciel', 'starters-cache');
12
12
  const PACKAGE_NAME = '@cadriciel/starters';
13
13
 
14
+ // Helper: Parse CLI arguments
15
+ const parseArgs = (cmdArgs) => {
16
+ const options = {
17
+ name: null,
18
+ template: null,
19
+ features: null,
20
+ allFeatures: false,
21
+ defaults: false,
22
+ list: false,
23
+ listFeatures: null,
24
+ yes: false,
25
+ test: false,
26
+ help: false
27
+ };
28
+
29
+ for (let i = 0; i < cmdArgs.length; i++) {
30
+ const arg = cmdArgs[i];
31
+ const nextArg = cmdArgs[i + 1];
32
+
33
+ switch (arg) {
34
+ case '-n':
35
+ case '--name':
36
+ options.name = nextArg;
37
+ i++;
38
+ break;
39
+ case '-t':
40
+ case '--template':
41
+ options.template = nextArg;
42
+ i++;
43
+ break;
44
+ case '-f':
45
+ case '--features':
46
+ options.features = nextArg ? nextArg.split(',').map(f => f.trim()) : [];
47
+ i++;
48
+ break;
49
+ case '-a':
50
+ case '--all-features':
51
+ options.allFeatures = true;
52
+ break;
53
+ case '-d':
54
+ case '--defaults':
55
+ options.defaults = true;
56
+ break;
57
+ case '-l':
58
+ case '--list':
59
+ options.list = true;
60
+ break;
61
+ case '--list-features':
62
+ options.listFeatures = nextArg;
63
+ i++;
64
+ break;
65
+ case '-y':
66
+ case '--yes':
67
+ options.yes = true;
68
+ break;
69
+ case '--test':
70
+ options.test = true;
71
+ break;
72
+ case '-h':
73
+ case '--help':
74
+ options.help = true;
75
+ break;
76
+ default:
77
+ // Positional argument (project name if not already set)
78
+ if (!arg.startsWith('-') && !options.name && arg !== 'create') {
79
+ options.name = arg;
80
+ }
81
+ break;
82
+ }
83
+ }
84
+
85
+ return options;
86
+ };
87
+
88
+ // Helper: Display help
89
+ const showHelp = () => {
90
+ console.log(chalk.bold.cyan('\nUsage: cad create [options] [project-name]\n'));
91
+ console.log(chalk.bold('Options:'));
92
+ console.log(' -n, --name <name> Project name');
93
+ console.log(' -t, --template <id> Template ID (skip template selection)');
94
+ console.log(' -f, --features <list> Comma-separated list of features to enable');
95
+ console.log(' -a, --all-features Enable all available features');
96
+ console.log(' -d, --defaults Use default feature selection');
97
+ console.log(' -l, --list List available templates');
98
+ console.log(' --list-features <id> List features for a specific template');
99
+ console.log(' -y, --yes Auto-confirm prompts (e.g., overwrite)');
100
+ console.log(' --test Use current directory for starters (dev mode)');
101
+ console.log(' -h, --help Show this help message');
102
+ console.log(chalk.bold('\nExamples:'));
103
+ console.log(chalk.gray(' # Interactive mode (default)'));
104
+ console.log(' cad create');
105
+ console.log(' cad create my-project');
106
+ console.log(chalk.gray('\n # Automated mode'));
107
+ console.log(' cad create -n my-project -t basic -d');
108
+ console.log(' cad create -n my-project -t basic -f auth,api');
109
+ console.log(' cad create -n my-project -t basic -a -y');
110
+ console.log(chalk.gray('\n # List templates and features'));
111
+ console.log(' cad create --list');
112
+ console.log(' cad create --list-features basic');
113
+ };
114
+
115
+ // Helper: List templates
116
+ const listTemplates = (starters) => {
117
+ console.log(chalk.bold.cyan('\nAvailable templates:\n'));
118
+ starters.forEach(s => {
119
+ console.log(` ${chalk.bold.green(s.id)}`);
120
+ console.log(` Name: ${s.name}`);
121
+ console.log(` Description: ${s.description}`);
122
+ console.log('');
123
+ });
124
+ };
125
+
126
+ // Helper: List features for a template
127
+ const listFeatures = (starter) => {
128
+ const optionsConfig = starter.config.options || {};
129
+ const coreFeatures = [];
130
+ const optionalFeatures = [];
131
+
132
+ Object.keys(optionsConfig).forEach(key => {
133
+ const opt = optionsConfig[key];
134
+ if (opt.type === 'core') {
135
+ coreFeatures.push({ id: key, ...opt });
136
+ } else {
137
+ optionalFeatures.push({ id: key, ...opt });
138
+ }
139
+ });
140
+
141
+ console.log(chalk.bold.cyan(`\nFeatures for template: ${starter.name}\n`));
142
+
143
+ if (coreFeatures.length > 0) {
144
+ console.log(chalk.bold('Core (always included):'));
145
+ coreFeatures.forEach(f => {
146
+ console.log(` ${chalk.gray('•')} ${f.id}${f.description ? ` - ${f.description}` : ''}`);
147
+ });
148
+ console.log('');
149
+ }
150
+
151
+ if (optionalFeatures.length > 0) {
152
+ console.log(chalk.bold('Optional:'));
153
+ optionalFeatures.forEach(f => {
154
+ const defaultTag = f.default ? chalk.green(' [default]') : '';
155
+ console.log(` ${chalk.gray('•')} ${chalk.bold(f.id)}${f.description ? ` - ${f.description}` : ''}${defaultTag}`);
156
+ });
157
+ console.log('');
158
+ }
159
+
160
+ if (optionalFeatures.length === 0 && coreFeatures.length === 0) {
161
+ console.log(chalk.yellow(' No features defined for this template.'));
162
+ }
163
+ };
164
+
14
165
  // Helper: Fetch/Update the starters package
15
166
  const fetchStarters = async () => {
16
167
  const spinner = ora('Fetching latest starters...').start();
@@ -71,19 +222,23 @@ module.exports = (args) => {
71
222
  };
72
223
 
73
224
  // Helper: Generate Project
74
- const generateProject = async (starter, options, targetName) => {
225
+ const generateProject = async (starter, options, targetName, autoConfirm = false) => {
75
226
  const targetPath = path.resolve(process.cwd(), targetName);
76
227
 
77
228
  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;
229
+ if (autoConfirm) {
230
+ console.log(chalk.yellow(`Directory ${targetName} already exists. Overwriting...`));
231
+ } else {
232
+ const { replace } = await inquirer.prompt([
233
+ {
234
+ type: 'confirm',
235
+ name: 'replace',
236
+ message: `Directory ${targetName} already exists. Overwrite?`,
237
+ default: false
238
+ }
239
+ ]);
240
+ if (!replace) return;
241
+ }
87
242
  }
88
243
 
89
244
  const spinner = ora(`Generating project ${targetName}...`).start();
@@ -133,7 +288,14 @@ module.exports = (args) => {
133
288
  // Check if file or directory
134
289
  const stat = await fs.promises.stat(srcPath);
135
290
  const filterFunc = (src, dest) => {
136
- if (path.basename(src) === 'node_modules') return false;
291
+ const basename = path.basename(src);
292
+ // Exclude node_modules and runtime state files
293
+ if (basename === 'node_modules') return false;
294
+ // Exclude runtime state files in .store (uuid, workspace, runtime json)
295
+ if (basename === '.uuid' ||
296
+ basename === '.workspace.json' ||
297
+ basename === '.node-runtime.json' ||
298
+ basename === '.rls.json') return false;
137
299
  return true;
138
300
  }
139
301
 
@@ -175,16 +337,23 @@ module.exports = (args) => {
175
337
  description: 'Create a new project from Cadriciel templates',
176
338
  },
177
339
  start: async (cmdArgs) => {
340
+ // Parse CLI arguments
341
+ const opts = parseArgs(cmdArgs);
342
+
343
+ // Show help if requested
344
+ if (opts.help) {
345
+ showHelp();
346
+ return;
347
+ }
348
+
178
349
  console.log(chalk.bold.cyan('Cadriciel Project Generator'));
179
350
 
180
- // Check for --test flag
181
- const isTestMode = cmdArgs.includes('--test');
182
- // Remove --test from args so it doesn't interfere with project name
183
- const filteredArgs = cmdArgs.filter(arg => arg !== '--test');
351
+ // Determine if running in automated mode
352
+ const isAutomated = opts.template && opts.name && (opts.features || opts.allFeatures || opts.defaults);
184
353
 
185
354
  // 1. Fetch Starters
186
355
  let startersPath;
187
- if (isTestMode) {
356
+ if (opts.test) {
188
357
  console.log(chalk.yellow('Running in TEST mode: Using current directory for starters.'));
189
358
  startersPath = process.cwd();
190
359
  } else {
@@ -199,12 +368,26 @@ module.exports = (args) => {
199
368
  return;
200
369
  }
201
370
 
202
- // 2. Ask for Project Name (if not provided)
203
- let args = [...filteredArgs];
204
- if (args.length > 0 && args[0] === 'create') {
205
- args.shift();
371
+ // Handle --list: show available templates and exit
372
+ if (opts.list) {
373
+ listTemplates(starters);
374
+ return;
375
+ }
376
+
377
+ // Handle --list-features: show features for a template and exit
378
+ if (opts.listFeatures) {
379
+ const starter = starters.find(s => s.id === opts.listFeatures);
380
+ if (!starter) {
381
+ console.log(chalk.red(`Template '${opts.listFeatures}' not found.`));
382
+ console.log(chalk.gray('Use --list to see available templates.'));
383
+ return;
384
+ }
385
+ listFeatures(starter);
386
+ return;
206
387
  }
207
- let targetName = args[0];
388
+
389
+ // 2. Get Project Name
390
+ let targetName = opts.name;
208
391
  if (!targetName) {
209
392
  const answers = await inquirer.prompt([
210
393
  {
@@ -218,25 +401,38 @@ module.exports = (args) => {
218
401
  }
219
402
 
220
403
  // 3. Select Starter
221
- const { selectedStarterId } = await inquirer.prompt([
222
- {
223
- type: 'list',
224
- name: 'selectedStarterId',
225
- message: 'Select a template:',
226
- choices: starters.map(s => ({
227
- name: `${chalk.bold(s.name)} - ${s.description}${isTestMode ? ` (${s.path})` : ''}`,
228
- value: s.id
229
- }))
404
+ let starter;
405
+ if (opts.template) {
406
+ starter = starters.find(s => s.id === opts.template);
407
+ if (!starter) {
408
+ console.log(chalk.red(`Template '${opts.template}' not found.`));
409
+ console.log(chalk.gray('Use --list to see available templates.'));
410
+ return;
230
411
  }
231
- ]);
232
-
233
- const starter = starters.find(s => s.id === selectedStarterId);
412
+ console.log(chalk.gray(`Using template: ${starter.name}`));
413
+ } else if (opts.defaults || opts.allFeatures || opts.features) {
414
+ // In automated mode without explicit template, use first available
415
+ starter = starters[0];
416
+ console.log(chalk.gray(`Using default template: ${starter.name}`));
417
+ } else {
418
+ const { selectedStarterId } = await inquirer.prompt([
419
+ {
420
+ type: 'list',
421
+ name: 'selectedStarterId',
422
+ message: 'Select a template:',
423
+ choices: starters.map(s => ({
424
+ name: `${chalk.bold(s.name)} - ${s.description}${opts.test ? ` (${s.path})` : ''}`,
425
+ value: s.id
426
+ }))
427
+ }
428
+ ]);
429
+ starter = starters.find(s => s.id === selectedStarterId);
430
+ }
234
431
 
235
432
  // 4. Select Features
236
- // Determine selectable features from config
237
433
  const optionsConfig = starter.config.options || {};
238
434
  const selectableFeatures = Object.keys(optionsConfig)
239
- .filter(key => optionsConfig[key].type !== 'core') // Exclude core
435
+ .filter(key => optionsConfig[key].type !== 'core')
240
436
  .map(key => ({
241
437
  name: optionsConfig[key].description || key,
242
438
  value: key,
@@ -244,7 +440,26 @@ module.exports = (args) => {
244
440
  }));
245
441
 
246
442
  let selectedFeatures = [];
247
- if (selectableFeatures.length > 0) {
443
+
444
+ if (opts.allFeatures) {
445
+ // Enable all features
446
+ selectedFeatures = selectableFeatures.map(f => f.value);
447
+ console.log(chalk.gray(`Enabling all features: ${selectedFeatures.join(', ') || 'none'}`));
448
+ } else if (opts.features) {
449
+ // Use specified features
450
+ const validFeatures = selectableFeatures.map(f => f.value);
451
+ const invalidFeatures = opts.features.filter(f => !validFeatures.includes(f));
452
+ if (invalidFeatures.length > 0) {
453
+ console.log(chalk.yellow(`Warning: Unknown features ignored: ${invalidFeatures.join(', ')}`));
454
+ }
455
+ selectedFeatures = opts.features.filter(f => validFeatures.includes(f));
456
+ console.log(chalk.gray(`Selected features: ${selectedFeatures.join(', ') || 'none'}`));
457
+ } else if (opts.defaults) {
458
+ // Use default features
459
+ selectedFeatures = selectableFeatures.filter(f => f.checked).map(f => f.value);
460
+ console.log(chalk.gray(`Using default features: ${selectedFeatures.join(', ') || 'none'}`));
461
+ } else if (selectableFeatures.length > 0) {
462
+ // Interactive mode
248
463
  const featureAnswers = await inquirer.prompt([
249
464
  {
250
465
  type: 'checkbox',
@@ -257,9 +472,9 @@ module.exports = (args) => {
257
472
  }
258
473
 
259
474
  // 5. Generate
260
- await generateProject(starter, { selectedFeatures }, targetName);
475
+ await generateProject(starter, { selectedFeatures }, targetName, opts.yes);
261
476
 
262
- if (isTestMode) process.exit(0);
477
+ if (opts.test) process.exit(0);
263
478
  },
264
479
  };
265
480
  };
@@ -1,226 +1,94 @@
1
1
  module.exports = (args) => {
2
- const { exec } = require('child_process');
3
- const Listr = require('listr');
4
- const chalk = require('chalk-v2');
5
- const os = require('os');
6
- const fs = require('fs');
7
- const path = require('path');
8
- const platform = os.platform();
9
- const checkInstallation = (command, postProcess = (output) => output) => {
10
- return new Promise((resolve, reject) => {
11
- exec(command, (error, stdout) => {
12
- if (error) {
13
- reject(new Error('Not installed'));
14
- } else {
15
- resolve(postProcess(stdout.trim()));
16
- }
17
- });
18
- });
19
- };
20
- const checkOneOfInstallations = (candidates = [], label = 'commande') => {
21
- const tryCandidate = (index) => {
22
- if (index >= candidates.length) {
23
- return Promise.reject(new Error(`${label} non détecté`));
24
- }
25
- const candidate = candidates[index];
26
- const postProcess =
27
- candidate.postProcess ||
28
- ((output) => `${candidate.label || candidate.command}: ${output}`);
29
- return checkInstallation(candidate.command, postProcess).catch(() =>
30
- tryCandidate(index + 1)
31
- );
32
- };
33
- return tryCandidate(0);
34
- };
35
- const PACKAGE_MANAGERS = [
2
+ const { exec } = require('child_process');
3
+ const Listr = require('listr');
4
+ const chalk = require('chalk-v2');
5
+ const os = require('os');
6
+ const platform = os.platform();
7
+ const checkInstallation = (command, postProcess = (output) => output) => {
8
+ return new Promise((resolve, reject) => {
9
+ exec(command, (error, stdout) => {
10
+ if (error) {
11
+ reject(new Error('Not installed'));
12
+ } else {
13
+ resolve(postProcess(stdout.trim()));
14
+ }
15
+ });
16
+ });
17
+ };
18
+ return {
19
+ info: {
20
+ title: 'doctor',
21
+ description: `Affichage des pré-requis du cadriciel.`,
22
+ },
23
+ start: () => {
24
+ console.log(' ');
25
+
26
+ console.log(' 🏥 ' + chalk.bold('Pré-requis globaux'));
27
+ console.log(' ');
28
+ const myTasks = [
36
29
  {
37
- id: 'bun',
38
- displayName: 'Bun',
39
- command: 'bun --version',
40
- detectors: ['bun.lock', 'bun.lockb'],
30
+ title: 'Homebrew',
31
+ task: () => checkInstallation('brew --version'),
41
32
  },
42
33
  {
43
- id: 'pnpm',
44
- displayName: 'pnpm',
45
- command: 'pnpm -v',
46
- detectors: ['pnpm-lock.yaml', 'pnpm-lock.yml'],
34
+ title: 'Pnpm',
35
+ task: () => checkInstallation('pnpm -v'),
47
36
  },
48
37
  {
49
- id: 'yarn',
50
- displayName: 'Yarn',
51
- command: 'yarn -v',
52
- detectors: ['yarn.lock'],
38
+ title: 'Git',
39
+ task: () => checkInstallation('git --version'),
53
40
  },
54
41
  {
55
- id: 'npm',
56
- displayName: 'npm',
57
- command: 'npm -v',
58
- detectors: ['package-lock.json'],
59
- },
60
- ];
61
- const findCadricielContext = () => {
62
- let currentPath = process.cwd();
63
- const rootPath = path.parse(currentPath).root;
64
- while (true) {
65
- const versionPath = path.join(currentPath, '.cadriciel', 'version.json');
66
- if (fs.existsSync(versionPath)) {
67
- try {
68
- const version = JSON.parse(fs.readFileSync(versionPath, 'utf8'));
69
- return {
70
- architecture: typeof version.name === 'string' ? version.name : null,
71
- projectRoot: currentPath,
72
- };
73
- } catch (error) {
74
- return {
75
- architecture: null,
76
- projectRoot: currentPath,
77
- };
78
- }
79
- }
80
- if (currentPath === rootPath) break;
81
- currentPath = path.dirname(currentPath);
82
- }
83
- return {
84
- architecture: null,
85
- projectRoot: null,
86
- };
87
- };
88
- const detectPackageManager = (projectRoot) => {
89
- const searchRoot = projectRoot || process.cwd();
90
- const packageJsonPath = path.join(searchRoot, 'package.json');
91
- if (fs.existsSync(packageJsonPath)) {
92
- try {
93
- const pkgJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
94
- if (pkgJson.packageManager && typeof pkgJson.packageManager === 'string') {
95
- const requested = pkgJson.packageManager.split('@')[0].trim().toLowerCase();
96
- const pm = PACKAGE_MANAGERS.find((el) => el.id === requested);
97
- if (pm) return pm;
98
- }
99
- } catch (error) { }
100
- }
101
- for (const pm of PACKAGE_MANAGERS) {
102
- if (
103
- pm.detectors.some((fileName) =>
104
- fs.existsSync(path.join(searchRoot, fileName))
105
- )
106
- ) {
107
- return pm;
108
- }
109
- }
110
- return PACKAGE_MANAGERS.find((el) => el.id === 'npm');
111
- };
112
- const createNodeArchitectureTasks = (projectRoot) => {
113
- const detectedManager = detectPackageManager(projectRoot);
114
- const tasks = [
115
- {
116
- title: 'Node ou Bun',
117
- task: () =>
118
- checkOneOfInstallations(
119
- [
120
- { command: 'node -v', label: 'Node' },
121
- { command: 'bun --version', label: 'Bun' },
122
- ],
123
- 'Node ou Bun'
124
- ),
125
- },
126
- {
127
- title: 'npm ou Bun',
128
- task: () =>
129
- checkOneOfInstallations(
130
- [
131
- { command: 'npm -v', label: 'npm' },
132
- { command: 'bun --version', label: 'Bun' },
133
- ],
134
- 'npm ou Bun'
135
- ),
136
- },
137
- ];
138
- if (detectedManager) {
139
- tasks.push({
140
- title: `Gestionnaire (${detectedManager.displayName})`,
141
- task: () => checkInstallation(detectedManager.command),
142
- });
143
- }
144
- tasks.push(
145
- {
146
- title: 'Docker',
147
- task: () => checkInstallation('docker ps'),
148
- },
149
- {
150
- title: 'Docker Compose',
151
- task: () => checkInstallation('docker-compose --version'),
152
- }
153
- );
154
- return tasks;
155
- };
156
- const runArchitectureDoctor = ({ architecture, projectRoot }) => {
157
- if (!architecture) {
158
- if (!projectRoot) {
159
- console.log(' ');
160
- console.log(
161
- chalk.yellow(
162
- " ⚠️ Impossible de détecter l'architecture du cadriciel (.cadriciel/version.json introuvable)."
163
- )
164
- );
165
- }
166
- return Promise.resolve();
167
- }
168
- const normalized = architecture.toLowerCase();
169
- if (normalized === 'node') {
170
- console.log(' ');
171
- console.log(' 🧱 ' + chalk.bold('Pré-requis architecture Node'));
172
- console.log(' ');
173
- const nodeTasks = new Listr(createNodeArchitectureTasks(projectRoot), {
174
- concurrent: true,
175
- exitOnError: false,
176
- });
177
- return nodeTasks.run().catch(() => { });
178
- }
179
- console.log(' ');
180
- console.log(' 🧱 ' + chalk.bold('Pré-requis architecture'));
181
- console.log(' ');
182
- console.log(
183
- chalk.yellow(
184
- ` Architecture '${architecture}' détectée mais non prise en charge par ce docteur.`
185
- )
186
- );
187
- return Promise.resolve();
188
- };
189
- return {
190
- info: {
191
- title: 'doctor',
192
- description: `Affichage des pré-requis du cadriciel.`,
42
+ title: 'Java',
43
+ task: () => checkInstallation('java --version'),
193
44
  },
194
- start: () => {
195
- console.log(' ');
45
+ ];
46
+ if (platform === 'linux ') myTasks.splice(0, 1);
47
+ if (platform === 'win32') myTasks.splice(0, 1);
196
48
 
197
- console.log(' 🏥 ' + chalk.bold('Pré-requis globaux'));
198
- console.log(' ');
199
- const myTasks = [
200
- {
201
- title: 'Git',
202
- task: () => checkInstallation('git --version'),
203
- },
204
- {
205
- title: 'Java',
206
- task: () => checkInstallation('java --version'),
207
- },
208
- ];
209
- if (platform === 'linux') myTasks.splice(0, 1);
210
- if (platform === 'win32') myTasks.splice(0, 1);
49
+ const tasks = new Listr(myTasks, {
50
+ concurrent: true,
51
+ exitOnError: false,
52
+ });
211
53
 
212
- const tasks = new Listr(myTasks, {
213
- concurrent: true,
214
- exitOnError: false,
215
- });
54
+ const dockerTasks = new Listr(
55
+ [
56
+ {
57
+ title: 'Docker',
58
+ task: () => checkInstallation('docker ps'),
59
+ },
60
+ {
61
+ title: 'Docker Compose',
62
+ task: () => checkInstallation('docker-compose --version'),
63
+ },
64
+ ],
65
+ { concurrent: true, exitOnError: false }
66
+ );
216
67
 
217
- const architectureContext = findCadricielContext();
218
-
219
- tasks
220
- .run()
221
- .catch(() => { })
222
- .then(() => runArchitectureDoctor(architectureContext))
223
- .catch(() => { });
224
- },
225
- };
68
+ tasks
69
+ .run()
70
+ .then(() => {
71
+ console.log(' ');
72
+ console.log(
73
+ ' 📦 ' +
74
+ chalk.bold('Pré-requis Docker') +
75
+ ' (si vous utilisez docker)'
76
+ );
77
+ console.log(' ');
78
+ dockerTasks.run().then(()=>{
79
+ }).catch((err) => {});
80
+ })
81
+ .catch((err) => {
82
+ console.log(' ');
83
+ console.log(
84
+ ' 📦 ' +
85
+ chalk.bold('Pré-requis Docker') +
86
+ ' (si vous utilisez docker)'
87
+ );
88
+ console.log(' ');
89
+ dockerTasks.run().then(()=>{
90
+ }).catch((err) => {});
91
+ });
92
+ },
93
+ };
226
94
  };