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.
- package/LICENSE +21 -0
- package/README.md +88 -0
- package/bin/cli.js +529 -0
- package/bin/wrapper.js +32 -0
- package/config/install-config.yaml +167 -0
- package/package.json +42 -0
- package/src/base/.cad/config.yaml.tpl +25 -0
- package/src/base/.cad/workflow-status.yaml.tpl +18 -0
- package/src/base/.claude/settings.local.json.tpl +8 -0
- package/src/base/CLAUDE.md +69 -0
- package/src/base/commands/cad.md +547 -0
- package/src/base/commands/commit.md +103 -0
- package/src/base/commands/comprendre.md +96 -0
- package/src/base/commands/concevoir.md +121 -0
- package/src/base/commands/documenter.md +97 -0
- package/src/base/commands/e2e.md +79 -0
- package/src/base/commands/implementer.md +98 -0
- package/src/base/commands/review.md +85 -0
- package/src/base/commands/status.md +55 -0
- package/src/base/skills/clean-code/SKILL.md +92 -0
- package/src/base/skills/tdd/SKILL.md +132 -0
- package/src/integrations/jira/.mcp.json.tpl +19 -0
- package/src/integrations/jira/commands/jira-setup.md +34 -0
- package/src/stacks/backend-only/agents/backend-developer.md +167 -0
- package/src/stacks/backend-only/agents/backend-reviewer.md +89 -0
- package/src/stacks/backend-only/agents/orchestrator.md +69 -0
- package/src/stacks/backend-only/skills/clean-hexa-backend/SKILL.md +187 -0
- package/src/stacks/backend-only/skills/clean-hexa-backend/templates/adapter.template.ts +75 -0
- package/src/stacks/backend-only/skills/clean-hexa-backend/templates/controller.template.ts +131 -0
- package/src/stacks/backend-only/skills/clean-hexa-backend/templates/entity.template.ts +87 -0
- package/src/stacks/backend-only/skills/clean-hexa-backend/templates/port.template.ts +62 -0
- package/src/stacks/backend-only/skills/clean-hexa-backend/templates/use-case.template.ts +77 -0
- package/src/stacks/backend-only/skills/mutation-testing/SKILL.md +129 -0
- package/src/stacks/mobile/agents/backend-developer.md +167 -0
- package/src/stacks/mobile/agents/backend-reviewer.md +89 -0
- package/src/stacks/mobile/agents/mobile-developer.md +70 -0
- package/src/stacks/mobile/agents/mobile-reviewer.md +175 -0
- package/src/stacks/mobile/agents/orchestrator.md +69 -0
- package/src/stacks/mobile/skills/clean-hexa-backend/SKILL.md +187 -0
- package/src/stacks/mobile/skills/clean-hexa-backend/templates/adapter.template.ts +75 -0
- package/src/stacks/mobile/skills/clean-hexa-backend/templates/controller.template.ts +131 -0
- package/src/stacks/mobile/skills/clean-hexa-backend/templates/entity.template.ts +87 -0
- package/src/stacks/mobile/skills/clean-hexa-backend/templates/port.template.ts +62 -0
- package/src/stacks/mobile/skills/clean-hexa-backend/templates/use-case.template.ts +77 -0
- package/src/stacks/mobile/skills/clean-hexa-mobile/SKILL.md +984 -0
- package/src/stacks/mobile/skills/mutation-testing/SKILL.md +129 -0
- package/src/stacks/web/agents/backend-developer.md +167 -0
- package/src/stacks/web/agents/backend-reviewer.md +89 -0
- package/src/stacks/web/agents/frontend-developer.md +65 -0
- package/src/stacks/web/agents/frontend-reviewer.md +92 -0
- package/src/stacks/web/agents/orchestrator.md +69 -0
- package/src/stacks/web/skills/clean-hexa-backend/SKILL.md +187 -0
- package/src/stacks/web/skills/clean-hexa-backend/templates/adapter.template.ts +75 -0
- package/src/stacks/web/skills/clean-hexa-backend/templates/controller.template.ts +131 -0
- package/src/stacks/web/skills/clean-hexa-backend/templates/entity.template.ts +87 -0
- package/src/stacks/web/skills/clean-hexa-backend/templates/port.template.ts +62 -0
- package/src/stacks/web/skills/clean-hexa-backend/templates/use-case.template.ts +77 -0
- package/src/stacks/web/skills/clean-hexa-frontend/SKILL.md +172 -0
- 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
|
+
}
|