cad-workflow 1.1.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.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +88 -0
  3. package/bin/cli.js +529 -0
  4. package/bin/wrapper.js +32 -0
  5. package/config/install-config.yaml +167 -0
  6. package/package.json +42 -0
  7. package/src/base/.cad/config.yaml.tpl +25 -0
  8. package/src/base/.cad/workflow-status.yaml.tpl +18 -0
  9. package/src/base/.claude/settings.local.json.tpl +8 -0
  10. package/src/base/CLAUDE.md +69 -0
  11. package/src/base/commands/cad.md +547 -0
  12. package/src/base/commands/commit.md +103 -0
  13. package/src/base/commands/comprendre.md +96 -0
  14. package/src/base/commands/concevoir.md +121 -0
  15. package/src/base/commands/documenter.md +97 -0
  16. package/src/base/commands/e2e.md +79 -0
  17. package/src/base/commands/implementer.md +98 -0
  18. package/src/base/commands/review.md +85 -0
  19. package/src/base/commands/status.md +55 -0
  20. package/src/base/skills/clean-code/SKILL.md +92 -0
  21. package/src/base/skills/tdd/SKILL.md +132 -0
  22. package/src/integrations/jira/.mcp.json.tpl +19 -0
  23. package/src/integrations/jira/commands/jira-setup.md +34 -0
  24. package/src/stacks/backend-only/agents/backend-developer.md +167 -0
  25. package/src/stacks/backend-only/agents/backend-reviewer.md +89 -0
  26. package/src/stacks/backend-only/agents/orchestrator.md +69 -0
  27. package/src/stacks/backend-only/skills/clean-hexa-backend/SKILL.md +187 -0
  28. package/src/stacks/backend-only/skills/clean-hexa-backend/templates/adapter.template.ts +75 -0
  29. package/src/stacks/backend-only/skills/clean-hexa-backend/templates/controller.template.ts +131 -0
  30. package/src/stacks/backend-only/skills/clean-hexa-backend/templates/entity.template.ts +87 -0
  31. package/src/stacks/backend-only/skills/clean-hexa-backend/templates/port.template.ts +62 -0
  32. package/src/stacks/backend-only/skills/clean-hexa-backend/templates/use-case.template.ts +77 -0
  33. package/src/stacks/backend-only/skills/mutation-testing/SKILL.md +129 -0
  34. package/src/stacks/mobile/agents/backend-developer.md +167 -0
  35. package/src/stacks/mobile/agents/backend-reviewer.md +89 -0
  36. package/src/stacks/mobile/agents/mobile-developer.md +70 -0
  37. package/src/stacks/mobile/agents/mobile-reviewer.md +175 -0
  38. package/src/stacks/mobile/agents/orchestrator.md +69 -0
  39. package/src/stacks/mobile/skills/clean-hexa-backend/SKILL.md +187 -0
  40. package/src/stacks/mobile/skills/clean-hexa-backend/templates/adapter.template.ts +75 -0
  41. package/src/stacks/mobile/skills/clean-hexa-backend/templates/controller.template.ts +131 -0
  42. package/src/stacks/mobile/skills/clean-hexa-backend/templates/entity.template.ts +87 -0
  43. package/src/stacks/mobile/skills/clean-hexa-backend/templates/port.template.ts +62 -0
  44. package/src/stacks/mobile/skills/clean-hexa-backend/templates/use-case.template.ts +77 -0
  45. package/src/stacks/mobile/skills/clean-hexa-mobile/SKILL.md +984 -0
  46. package/src/stacks/mobile/skills/mutation-testing/SKILL.md +129 -0
  47. package/src/stacks/web/agents/backend-developer.md +167 -0
  48. package/src/stacks/web/agents/backend-reviewer.md +89 -0
  49. package/src/stacks/web/agents/frontend-developer.md +65 -0
  50. package/src/stacks/web/agents/frontend-reviewer.md +92 -0
  51. package/src/stacks/web/agents/orchestrator.md +69 -0
  52. package/src/stacks/web/skills/clean-hexa-backend/SKILL.md +187 -0
  53. package/src/stacks/web/skills/clean-hexa-backend/templates/adapter.template.ts +75 -0
  54. package/src/stacks/web/skills/clean-hexa-backend/templates/controller.template.ts +131 -0
  55. package/src/stacks/web/skills/clean-hexa-backend/templates/entity.template.ts +87 -0
  56. package/src/stacks/web/skills/clean-hexa-backend/templates/port.template.ts +62 -0
  57. package/src/stacks/web/skills/clean-hexa-backend/templates/use-case.template.ts +77 -0
  58. package/src/stacks/web/skills/clean-hexa-frontend/SKILL.md +172 -0
  59. package/src/stacks/web/skills/mutation-testing/SKILL.md +129 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Taha K
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,88 @@
1
+ # CAD Workflow - Clean Agentic Dev
2
+
3
+ Workflow Claude Code pour le developpement assiste par IA avec **Clean Architecture Hexagonale** et **TDD**.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npx cad-workflow install
9
+ ```
10
+
11
+ L'installateur interactif vous pose quelques questions :
12
+
13
+ 1. **Nom du projet**
14
+ 2. **Stack** : Mobile (React Native Expo + NestJS), Web (Angular/React + NestJS), ou Backend-only (NestJS)
15
+ 3. **Gestion des tickets** : Jira (connexion automatique) ou Local (stories en markdown)
16
+
17
+ Fichiers installes dans votre projet :
18
+
19
+ - `CLAUDE.md` - Instructions pour Claude Code
20
+ - `.claude/agents/` - Agents specialises selon votre stack
21
+ - `.claude/commands/` - 9 commandes de workflow (+ jira-setup si Jira)
22
+ - `.claude/skills/` - Skills (clean code, hexa, TDD, mutation testing)
23
+ - `.cad/config.yaml` - Configuration du workflow
24
+ - `.cad/workflow-status.yaml` - Etat du workflow
25
+ - `docs/` - Structure de documentation
26
+
27
+ Si Jira est selectionne :
28
+ - `.mcp.json` - Configuration MCP Atlassian
29
+ - `.claude/settings.local.json` - Credentials Jira (non commite)
30
+
31
+ ## Mise a jour
32
+
33
+ ```bash
34
+ npx cad-workflow update
35
+ ```
36
+
37
+ Met a jour les fichiers CAD (agents, commands, skills) sans toucher a vos fichiers personnalises (CLAUDE.md, config, docs/).
38
+
39
+ ## Status
40
+
41
+ ```bash
42
+ npx cad-workflow status
43
+ ```
44
+
45
+ Affiche la version installee, la stack, le mode de tickets, et verifie les mises a jour.
46
+
47
+ ## Workflow
48
+
49
+ ```
50
+ /cad [description de la feature]
51
+ ```
52
+
53
+ Repondez `OK` apres chaque phase pour continuer. Si interrompu :
54
+
55
+ ```
56
+ /cad continue
57
+ ```
58
+
59
+ ## Commandes
60
+
61
+ | Commande | Phase | Description |
62
+ |----------|-------|-------------|
63
+ | `/status` | - | Afficher le status actuel |
64
+ | `/cad` | 1-7 | Workflow complet orchestre |
65
+ | `/comprendre` | 1 | Analyse et clarification du besoin |
66
+ | `/concevoir` | 2 | Design et plan d'implementation |
67
+ | `/implementer` | 3 | Implementation TDD |
68
+ | `/review` | 4 | Review code + mutation tests |
69
+ | `/e2e` | 5 | Tests end-to-end |
70
+ | `/documenter` | 6 | Documentation |
71
+ | `/commit` | 7 | Formatage, linting, commit |
72
+
73
+ ## Stacks
74
+
75
+ | Stack | Agents | Skills |
76
+ |-------|--------|--------|
77
+ | **Mobile** | backend-developer, backend-reviewer, mobile-developer, mobile-reviewer, orchestrator | clean-hexa-backend, clean-hexa-mobile, mutation-testing |
78
+ | **Web** | backend-developer, backend-reviewer, frontend-developer, frontend-reviewer, orchestrator | clean-hexa-backend, clean-hexa-frontend, mutation-testing |
79
+ | **Backend-only** | backend-developer, backend-reviewer, orchestrator | clean-hexa-backend, mutation-testing |
80
+
81
+ ## MCPs recommandes
82
+
83
+ - **context7** : Documentation a jour (ajouter "use context7" dans vos prompts)
84
+ - **playwright** : Automatisation tests E2E
85
+
86
+ ## Licence
87
+
88
+ MIT - Voir [LICENSE](LICENSE)
package/bin/cli.js ADDED
@@ -0,0 +1,529 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs-extra');
4
+ const path = require('path');
5
+ const chalk = require('chalk');
6
+ const yaml = require('js-yaml');
7
+ const Mustache = require('mustache');
8
+ const { prompt } = require('enquirer');
9
+
10
+ const pkg = require('../package.json');
11
+
12
+ // Resolve the user's project directory
13
+ const PROJECT_DIR = process.env.CAD_CWD || process.cwd();
14
+ const PACKAGE_ROOT = path.resolve(__dirname, '..');
15
+ const SRC_DIR = path.join(PACKAGE_ROOT, 'src');
16
+ const CONFIG_PATH = path.join(PACKAGE_ROOT, 'config', 'install-config.yaml');
17
+ const CLAUDE_DIR = path.join(PROJECT_DIR, '.claude');
18
+ const MANIFEST_PATH = path.join(CLAUDE_DIR, '.cad-manifest.json');
19
+
20
+ // Disable Mustache HTML escaping (we deal with plain text, not HTML)
21
+ Mustache.escape = (text) => text;
22
+
23
+ // ─── Helpers ────────────────────────────────────────────────────────
24
+
25
+ function readManifest() {
26
+ if (fs.existsSync(MANIFEST_PATH)) {
27
+ return fs.readJSONSync(MANIFEST_PATH);
28
+ }
29
+ return null;
30
+ }
31
+
32
+ function writeManifest(data) {
33
+ fs.ensureDirSync(CLAUDE_DIR);
34
+ fs.writeJSONSync(MANIFEST_PATH, data, { spaces: 2 });
35
+ }
36
+
37
+ function loadConfig() {
38
+ const content = fs.readFileSync(CONFIG_PATH, 'utf8');
39
+ return yaml.load(content);
40
+ }
41
+
42
+ function collectFiles(dir, base) {
43
+ const results = [];
44
+ if (!fs.existsSync(dir)) return results;
45
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
46
+ for (const entry of entries) {
47
+ const rel = path.join(base, entry.name);
48
+ if (entry.isDirectory()) {
49
+ results.push(...collectFiles(path.join(dir, entry.name), rel));
50
+ } else {
51
+ results.push(rel);
52
+ }
53
+ }
54
+ return results;
55
+ }
56
+
57
+ function evaluateCondition(condition, answers) {
58
+ if (!condition) return true;
59
+ const match = condition.match(/(\w+)\s*==\s*(\w+)/);
60
+ if (match) {
61
+ const [, key, value] = match;
62
+ return answers[key] === value;
63
+ }
64
+ return true;
65
+ }
66
+
67
+ function interpolate(template, values) {
68
+ return template.replace(/\{(\w+)\}/g, (_, key) => values[key] || `{${key}}`);
69
+ }
70
+
71
+ function addToGitignore(targetDir, filePath) {
72
+ const gitignorePath = path.join(targetDir, '.gitignore');
73
+ let content = '';
74
+ if (fs.existsSync(gitignorePath)) {
75
+ content = fs.readFileSync(gitignorePath, 'utf8');
76
+ }
77
+ if (!content.includes(filePath)) {
78
+ content = content.trimEnd() + `\n\n# CAD Workflow - credentials (ne pas commiter)\n${filePath}\n`;
79
+ fs.writeFileSync(gitignorePath, content, 'utf8');
80
+ }
81
+ }
82
+
83
+ // ─── Install Logic ──────────────────────────────────────────────────
84
+
85
+ async function runInstall() {
86
+ const config = loadConfig();
87
+
88
+ // Check if already installed
89
+ const existing = readManifest();
90
+ if (existing) {
91
+ console.log(
92
+ chalk.yellow(`\n CAD v${existing.version} is already installed (${existing.installedAt}).`)
93
+ );
94
+ console.log(chalk.yellow(' Use `cad-workflow update` to update.\n'));
95
+ return;
96
+ }
97
+
98
+ // Welcome message
99
+ console.log(chalk.cyan(config.welcome_message));
100
+
101
+ // ── Interactive prompts ──
102
+
103
+ const answers = {
104
+ directory_name: path.basename(PROJECT_DIR),
105
+ };
106
+
107
+ // Project name
108
+ const projectNameAnswer = await prompt({
109
+ type: 'input',
110
+ name: 'project_name',
111
+ message: config.project_name.prompt,
112
+ initial: config.project_name.default.replace('{directory_name}', answers.directory_name),
113
+ });
114
+ answers.project_name = projectNameAnswer.project_name;
115
+
116
+ // Stack selection
117
+ const stackAnswer = await prompt({
118
+ type: 'select',
119
+ name: 'stack',
120
+ message: config.stack.prompt,
121
+ choices: config.stack['single-select'].map((opt) => ({
122
+ name: opt.value,
123
+ message: opt.label,
124
+ hint: opt.description,
125
+ })),
126
+ initial: config.stack['single-select'].findIndex((s) => s.value === config.stack.default),
127
+ });
128
+ answers.stack = stackAnswer.stack;
129
+
130
+ // Ticket management
131
+ const ticketAnswer = await prompt({
132
+ type: 'select',
133
+ name: 'ticket_management',
134
+ message: config.ticket_management.prompt,
135
+ choices: config.ticket_management['single-select'].map((opt) => ({
136
+ name: opt.value,
137
+ message: opt.label,
138
+ hint: opt.description,
139
+ })),
140
+ initial: config.ticket_management['single-select'].findIndex(
141
+ (t) => t.value === config.ticket_management.default
142
+ ),
143
+ });
144
+ answers.ticket_management = ticketAnswer.ticket_management;
145
+
146
+ // Jira prompts (conditional)
147
+ if (answers.ticket_management === 'jira') {
148
+ const jiraAnswers = await prompt([
149
+ {
150
+ type: 'input',
151
+ name: 'jira_url',
152
+ message: config.jira_url.prompt,
153
+ initial: config.jira_url.placeholder,
154
+ validate: (value) => {
155
+ const regex = new RegExp(config.jira_url.validation);
156
+ return regex.test(value) || config.jira_url.validation_error;
157
+ },
158
+ },
159
+ {
160
+ type: 'input',
161
+ name: 'jira_email',
162
+ message: config.jira_email.prompt,
163
+ validate: (value) => {
164
+ const regex = new RegExp(config.jira_email.validation);
165
+ return regex.test(value) || config.jira_email.validation_error;
166
+ },
167
+ },
168
+ {
169
+ type: 'password',
170
+ name: 'jira_token',
171
+ message:
172
+ config.jira_token.prompt +
173
+ ' (créer sur https://id.atlassian.com/manage-profile/security/api-tokens)',
174
+ },
175
+ {
176
+ type: 'input',
177
+ name: 'jira_project_key',
178
+ message: config.jira_project_key.prompt,
179
+ initial: config.jira_project_key.placeholder,
180
+ validate: (value) => {
181
+ const regex = new RegExp(config.jira_project_key.validation);
182
+ return regex.test(value) || config.jira_project_key.validation_error;
183
+ },
184
+ },
185
+ ]);
186
+ Object.assign(answers, jiraAnswers);
187
+ }
188
+
189
+ // Workflow config defaults
190
+ answers.min_coverage = config.workflow_config.min_coverage.default;
191
+ answers.min_mutation_score = config.workflow_config.min_mutation_score.default;
192
+ answers.timestamp = new Date().toISOString();
193
+
194
+ // ── File installation ──
195
+
196
+ console.log(chalk.gray('\n Installing CAD workflow...\n'));
197
+ const copied = [];
198
+
199
+ // 1. Create directories
200
+ for (const dir of config.directories) {
201
+ fs.ensureDirSync(path.join(PROJECT_DIR, dir));
202
+ }
203
+ for (const condDir of config.conditional_directories) {
204
+ if (evaluateCondition(condDir.condition, answers)) {
205
+ fs.ensureDirSync(path.join(PROJECT_DIR, condDir.path));
206
+ }
207
+ }
208
+
209
+ // 2. Copy base commands
210
+ const baseDir = path.join(SRC_DIR, 'base');
211
+ const commandsSrc = path.join(baseDir, 'commands');
212
+ const commandsDest = path.join(CLAUDE_DIR, 'commands');
213
+ if (fs.existsSync(commandsSrc)) {
214
+ fs.copySync(commandsSrc, commandsDest, { overwrite: true });
215
+ copied.push(...collectFiles(commandsDest, '.claude/commands'));
216
+ console.log(chalk.gray(' Copied commands'));
217
+ }
218
+
219
+ // 3. Copy base skills
220
+ const skillsSrc = path.join(baseDir, 'skills');
221
+ const skillsDest = path.join(CLAUDE_DIR, 'skills');
222
+ if (fs.existsSync(skillsSrc)) {
223
+ fs.copySync(skillsSrc, skillsDest, { overwrite: true });
224
+ copied.push(...collectFiles(skillsDest, '.claude/skills'));
225
+ console.log(chalk.gray(' Copied base skills'));
226
+ }
227
+
228
+ // 4. Copy CLAUDE.md
229
+ const claudeMdSrc = path.join(baseDir, 'CLAUDE.md');
230
+ const claudeMdDest = path.join(PROJECT_DIR, 'CLAUDE.md');
231
+ if (!fs.existsSync(claudeMdDest)) {
232
+ fs.copySync(claudeMdSrc, claudeMdDest);
233
+ copied.push('CLAUDE.md');
234
+ console.log(chalk.gray(' Copied CLAUDE.md'));
235
+ } else {
236
+ console.log(chalk.yellow(' CLAUDE.md already exists, skipped'));
237
+ }
238
+
239
+ // 5. Copy stack-specific files (agents + skills)
240
+ const stackConfig = config.stack['single-select'].find((s) => s.value === answers.stack);
241
+ if (stackConfig && stackConfig.installs) {
242
+ for (const installPath of stackConfig.installs) {
243
+ const sourcePath = path.join(SRC_DIR, installPath);
244
+ if (fs.existsSync(sourcePath)) {
245
+ // Copy agents
246
+ const agentsSrc = path.join(sourcePath, 'agents');
247
+ if (fs.existsSync(agentsSrc)) {
248
+ const agentsDest = path.join(CLAUDE_DIR, 'agents');
249
+ fs.copySync(agentsSrc, agentsDest, { overwrite: true });
250
+ copied.push(...collectFiles(agentsDest, '.claude/agents'));
251
+ console.log(chalk.gray(` Copied ${answers.stack} agents`));
252
+ }
253
+ // Copy skills
254
+ const stackSkillsSrc = path.join(sourcePath, 'skills');
255
+ if (fs.existsSync(stackSkillsSrc)) {
256
+ fs.copySync(stackSkillsSrc, skillsDest, { overwrite: false });
257
+ copied.push(...collectFiles(skillsDest, '.claude/skills'));
258
+ console.log(chalk.gray(` Copied ${answers.stack} skills`));
259
+ }
260
+ }
261
+ }
262
+ }
263
+
264
+ // 6. Copy integration files (Jira)
265
+ const ticketConfig = config.ticket_management['single-select'].find(
266
+ (t) => t.value === answers.ticket_management
267
+ );
268
+ if (ticketConfig && ticketConfig.installs) {
269
+ for (const installPath of ticketConfig.installs) {
270
+ const sourcePath = path.join(SRC_DIR, installPath);
271
+ if (fs.existsSync(sourcePath)) {
272
+ // Copy integration commands
273
+ const intCommandsSrc = path.join(sourcePath, 'commands');
274
+ if (fs.existsSync(intCommandsSrc)) {
275
+ fs.copySync(intCommandsSrc, commandsDest, { overwrite: true });
276
+ copied.push(...collectFiles(intCommandsSrc, '.claude/commands'));
277
+ console.log(chalk.gray(' Copied Jira commands'));
278
+ }
279
+ // Generate .mcp.json from template
280
+ const mcpTemplate = path.join(sourcePath, '.mcp.json.tpl');
281
+ if (fs.existsSync(mcpTemplate)) {
282
+ const tpl = fs.readFileSync(mcpTemplate, 'utf8');
283
+ const rendered = Mustache.render(tpl, answers);
284
+ fs.writeFileSync(path.join(PROJECT_DIR, '.mcp.json'), rendered, 'utf8');
285
+ copied.push('.mcp.json');
286
+ console.log(chalk.gray(' Generated .mcp.json'));
287
+ }
288
+ }
289
+ }
290
+ }
291
+
292
+ // 7. Generate files from Mustache templates
293
+ for (const fileConfig of config.files_to_generate) {
294
+ const templatePath = path.join(baseDir, fileConfig.template);
295
+ const outputPath = path.join(PROJECT_DIR, fileConfig.output);
296
+ if (fs.existsSync(templatePath)) {
297
+ const tpl = fs.readFileSync(templatePath, 'utf8');
298
+ const rendered = Mustache.render(tpl, answers);
299
+ fs.ensureDirSync(path.dirname(outputPath));
300
+ fs.writeFileSync(outputPath, rendered, 'utf8');
301
+ copied.push(fileConfig.output);
302
+ console.log(chalk.gray(` Generated ${fileConfig.output}`));
303
+ }
304
+ }
305
+
306
+ // 8. Generate sensitive files (Jira credentials)
307
+ for (const fileConfig of config.sensitive_files) {
308
+ if (evaluateCondition(fileConfig.condition, answers)) {
309
+ const templatePath = path.join(baseDir, fileConfig.template);
310
+ const outputPath = path.join(PROJECT_DIR, fileConfig.output);
311
+ if (fs.existsSync(templatePath)) {
312
+ const tpl = fs.readFileSync(templatePath, 'utf8');
313
+ const rendered = Mustache.render(tpl, answers);
314
+ fs.ensureDirSync(path.dirname(outputPath));
315
+ fs.writeFileSync(outputPath, rendered, 'utf8');
316
+ copied.push(fileConfig.output);
317
+ console.log(chalk.gray(` Generated ${fileConfig.output}`));
318
+ }
319
+ if (fileConfig.gitignore) {
320
+ addToGitignore(PROJECT_DIR, fileConfig.output);
321
+ }
322
+ }
323
+ }
324
+
325
+ // 9. Ensure .gitignore entries
326
+ const gitignorePath = path.join(PROJECT_DIR, '.gitignore');
327
+ let gitignoreContent = '';
328
+ if (fs.existsSync(gitignorePath)) {
329
+ gitignoreContent = fs.readFileSync(gitignorePath, 'utf8');
330
+ }
331
+ const defaultEntries = ['.claude/memory/', '.claude/settings.local.json'];
332
+ const toAdd = defaultEntries.filter((e) => !gitignoreContent.includes(e));
333
+ if (toAdd.length > 0) {
334
+ const section = '\n# Claude Code local state\n' + toAdd.join('\n') + '\n';
335
+ fs.writeFileSync(gitignorePath, gitignoreContent.trimEnd() + '\n' + section, 'utf8');
336
+ }
337
+
338
+ // 10. Write manifest
339
+ const manifest = {
340
+ version: pkg.version,
341
+ stack: answers.stack,
342
+ ticket_management: answers.ticket_management,
343
+ project_name: answers.project_name,
344
+ installedAt: new Date().toISOString(),
345
+ updatedAt: new Date().toISOString(),
346
+ files: [...new Set(copied)],
347
+ };
348
+ writeManifest(manifest);
349
+
350
+ // 11. Post-install message
351
+ const postMessage =
352
+ answers.ticket_management === 'jira'
353
+ ? config.post_install_message_jira
354
+ : config.post_install_message_local;
355
+ console.log(interpolate(postMessage, answers));
356
+ }
357
+
358
+ // ─── Update Logic ───────────────────────────────────────────────────
359
+
360
+ function runUpdate() {
361
+ console.log(chalk.cyan.bold('\n CAD Workflow - Update\n'));
362
+
363
+ const manifest = readManifest();
364
+ if (!manifest) {
365
+ console.log(chalk.red(' CAD is not installed. Run `cad-workflow install` first.\n'));
366
+ return;
367
+ }
368
+
369
+ console.log(chalk.gray(` Current version: ${manifest.version}`));
370
+ console.log(chalk.gray(` Package version: ${pkg.version}\n`));
371
+
372
+ const copied = [];
373
+ const stack = manifest.stack || 'mobile';
374
+
375
+ // Re-copy base commands
376
+ const baseDir = path.join(SRC_DIR, 'base');
377
+ const commandsDest = path.join(CLAUDE_DIR, 'commands');
378
+ const commandsSrc = path.join(baseDir, 'commands');
379
+ if (fs.existsSync(commandsSrc)) {
380
+ fs.copySync(commandsSrc, commandsDest, { overwrite: true });
381
+ copied.push(...collectFiles(commandsDest, '.claude/commands'));
382
+ }
383
+
384
+ // Re-copy base skills
385
+ const skillsDest = path.join(CLAUDE_DIR, 'skills');
386
+ const skillsSrc = path.join(baseDir, 'skills');
387
+ if (fs.existsSync(skillsSrc)) {
388
+ fs.copySync(skillsSrc, skillsDest, { overwrite: true });
389
+ copied.push(...collectFiles(skillsDest, '.claude/skills'));
390
+ }
391
+
392
+ // Re-copy stack agents and skills
393
+ const config = loadConfig();
394
+ const stackConfig = config.stack['single-select'].find((s) => s.value === stack);
395
+ if (stackConfig && stackConfig.installs) {
396
+ for (const installPath of stackConfig.installs) {
397
+ const sourcePath = path.join(SRC_DIR, installPath);
398
+ if (fs.existsSync(sourcePath)) {
399
+ const agentsSrc = path.join(sourcePath, 'agents');
400
+ if (fs.existsSync(agentsSrc)) {
401
+ fs.copySync(agentsSrc, path.join(CLAUDE_DIR, 'agents'), { overwrite: true });
402
+ copied.push(...collectFiles(path.join(CLAUDE_DIR, 'agents'), '.claude/agents'));
403
+ }
404
+ const stackSkillsSrc = path.join(sourcePath, 'skills');
405
+ if (fs.existsSync(stackSkillsSrc)) {
406
+ fs.copySync(stackSkillsSrc, skillsDest, { overwrite: true });
407
+ }
408
+ }
409
+ }
410
+ }
411
+
412
+ // Re-copy integration commands if Jira
413
+ if (manifest.ticket_management === 'jira') {
414
+ const jiraCommandsSrc = path.join(SRC_DIR, 'integrations', 'jira', 'commands');
415
+ if (fs.existsSync(jiraCommandsSrc)) {
416
+ fs.copySync(jiraCommandsSrc, commandsDest, { overwrite: true });
417
+ }
418
+ }
419
+
420
+ console.log(chalk.gray(' Updated agents, commands, skills...'));
421
+
422
+ // Update manifest
423
+ manifest.version = pkg.version;
424
+ manifest.updatedAt = new Date().toISOString();
425
+ manifest.files = [...new Set(copied)];
426
+ writeManifest(manifest);
427
+
428
+ console.log(chalk.green.bold('\n Update complete!\n'));
429
+ console.log(chalk.gray(` ${copied.length} files updated`));
430
+ console.log(
431
+ chalk.yellow(' Note: CLAUDE.md, config, and docs/ were preserved.\n')
432
+ );
433
+ }
434
+
435
+ // ─── Status Logic ───────────────────────────────────────────────────
436
+
437
+ async function runStatus() {
438
+ console.log(chalk.cyan.bold('\n CAD Workflow - Status\n'));
439
+
440
+ const manifest = readManifest();
441
+ if (!manifest) {
442
+ console.log(chalk.red(' CAD is not installed.\n'));
443
+ console.log(chalk.gray(' Run: npx cad-workflow install\n'));
444
+ return;
445
+ }
446
+
447
+ console.log(chalk.white(` Installed version: ${chalk.green(manifest.version)}`));
448
+ console.log(chalk.white(` Stack: ${chalk.cyan(manifest.stack || 'unknown')}`));
449
+ console.log(chalk.white(` Tickets: ${chalk.cyan(manifest.ticket_management || 'unknown')}`));
450
+ console.log(chalk.white(` Installed at: ${chalk.gray(manifest.installedAt)}`));
451
+ console.log(chalk.white(` Last updated: ${chalk.gray(manifest.updatedAt)}`));
452
+ console.log(chalk.white(` Files tracked: ${chalk.gray(manifest.files.length)}`));
453
+
454
+ // Check for updates via npm
455
+ console.log(chalk.gray('\n Checking for updates...'));
456
+ try {
457
+ const { execSync } = require('child_process');
458
+ const latest = execSync('npm view cad-workflow version 2>/dev/null', {
459
+ encoding: 'utf8',
460
+ }).trim();
461
+
462
+ if (latest && latest !== manifest.version) {
463
+ console.log(chalk.yellow(`\n Update available: ${manifest.version} -> ${latest}`));
464
+ console.log(chalk.gray(' Run: npx cad-workflow@latest update\n'));
465
+ } else if (latest) {
466
+ console.log(chalk.green(' You are up to date!\n'));
467
+ } else {
468
+ console.log(chalk.gray(' Package not published yet.\n'));
469
+ }
470
+ } catch {
471
+ console.log(chalk.gray(' Could not check for updates (package not published yet).\n'));
472
+ }
473
+ }
474
+
475
+ // ─── CLI Router ─────────────────────────────────────────────────────
476
+
477
+ async function main() {
478
+ const command = process.argv[2];
479
+
480
+ switch (command) {
481
+ case 'install':
482
+ await runInstall();
483
+ break;
484
+ case 'update':
485
+ runUpdate();
486
+ break;
487
+ case 'status':
488
+ await runStatus();
489
+ break;
490
+ case '--version':
491
+ case '-v':
492
+ console.log(pkg.version);
493
+ break;
494
+ case '--help':
495
+ case '-h':
496
+ case undefined:
497
+ console.log(`
498
+ ${chalk.cyan.bold('cad-workflow')} v${pkg.version}
499
+ ${chalk.gray('Clean Agentic Dev - Workflow IA avec Clean Architecture et TDD')}
500
+
501
+ ${chalk.white('Usage:')}
502
+ npx cad-workflow ${chalk.cyan('<command>')}
503
+
504
+ ${chalk.white('Commands:')}
505
+ ${chalk.cyan('install')} Install CAD workflow into the current project
506
+ ${chalk.cyan('update')} Update CAD workflow files (preserves CLAUDE.md, config, docs)
507
+ ${chalk.cyan('status')} Show installed CAD version and check for updates
508
+
509
+ ${chalk.white('Options:')}
510
+ ${chalk.cyan('-v, --version')} Show version
511
+ ${chalk.cyan('-h, --help')} Show help
512
+ `);
513
+ break;
514
+ default:
515
+ console.log(chalk.red(`\n Unknown command: ${command}`));
516
+ console.log(chalk.gray(' Run `cad-workflow --help` for available commands.\n'));
517
+ process.exit(1);
518
+ }
519
+ }
520
+
521
+ main().catch((err) => {
522
+ if (err === '') {
523
+ // User cancelled prompt (Ctrl+C)
524
+ console.log(chalk.yellow('\n Installation cancelled.\n'));
525
+ process.exit(0);
526
+ }
527
+ console.error(chalk.red(`\n Error: ${err.message}\n`));
528
+ process.exit(1);
529
+ });
package/bin/wrapper.js ADDED
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * npx entry point - preserves the user's working directory.
5
+ *
6
+ * When running via npx, __dirname points to a temp cache directory
7
+ * (contains `_npx` or `.npm`). In that case we spawn a child process
8
+ * with the correct cwd so that the CLI operates in the user's project.
9
+ */
10
+
11
+ const path = require('path');
12
+
13
+ const isNpx =
14
+ __dirname.includes('_npx') ||
15
+ __dirname.includes('.npm') ||
16
+ __dirname.includes('npx');
17
+
18
+ if (isNpx) {
19
+ const { spawn } = require('child_process');
20
+ const child = spawn(
21
+ process.execPath,
22
+ [path.join(__dirname, 'cli.js'), ...process.argv.slice(2)],
23
+ {
24
+ cwd: process.cwd(),
25
+ stdio: 'inherit',
26
+ env: { ...process.env, CAD_CWD: process.cwd() },
27
+ }
28
+ );
29
+ child.on('exit', (code) => process.exit(code || 0));
30
+ } else {
31
+ require('./cli.js');
32
+ }