@claude-pw/framework 0.3.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.es.md +173 -0
- package/README.ja.md +173 -0
- package/README.md +173 -0
- package/README.pt-br.md +173 -0
- package/README.zh-cn.md +173 -0
- package/RELEASES.md +66 -0
- package/install.js +593 -0
- package/package.json +35 -0
- package/templates/CHANGELOG.md +6 -0
- package/templates/CLAUDE.md.tpl +38 -0
- package/templates/Makefile +37 -0
- package/templates/PLAN.md.tpl +18 -0
- package/templates/STATUS.md.tpl +15 -0
- package/templates/claude/agents/codebase-mapper.md +105 -0
- package/templates/claude/agents/debugger.md +90 -0
- package/templates/claude/agents/decision-impact.md +36 -0
- package/templates/claude/agents/implementer.md +73 -0
- package/templates/claude/agents/interface-reviewer.md +22 -0
- package/templates/claude/agents/learning-extractor.md +104 -0
- package/templates/claude/agents/phase-validator.md +108 -0
- package/templates/claude/agents/plan-checker.md +18 -0
- package/templates/claude/agents/researcher.md +85 -0
- package/templates/claude/agents/session-recovery.md +127 -0
- package/templates/claude/agents/spike-explorer.md +34 -0
- package/templates/claude/commands/cpw-debug.md +116 -0
- package/templates/claude/commands/cpw-discuss.md +70 -0
- package/templates/claude/commands/cpw-health.md +67 -0
- package/templates/claude/commands/cpw-impact.md +22 -0
- package/templates/claude/commands/cpw-next-step.md +492 -0
- package/templates/claude/commands/cpw-pause.md +49 -0
- package/templates/claude/commands/cpw-quick.md +83 -0
- package/templates/claude/commands/cpw-reflect.md +209 -0
- package/templates/claude/commands/cpw-startup.md +321 -0
- package/templates/claude/commands/cpw-todos.md +100 -0
- package/templates/claude/hooks/cpw-context-monitor.js +59 -0
- package/templates/claude/hooks/cpw-statusline.js +36 -0
- package/templates/claude/rules/git.md +27 -0
- package/templates/claude/rules/interfaces.md +9 -0
- package/templates/claude/rules/testing.md +8 -0
- package/templates/claude/settings.json +42 -0
- package/templates/docs/architecture.md +4 -0
- package/templates/docs/codebase-map.md +3 -0
- package/templates/docs/conventions.md +3 -0
- package/templates/docs/interfaces.md +10 -0
- package/templates/docs/tech-debt.md +3 -0
- package/templates/docs/tooling.md +15 -0
- package/templates/gitignore +17 -0
- package/templates/husky/pre-commit +23 -0
- package/templates/planning/config.json +11 -0
- package/templates/planning/learnings/applied.md +3 -0
- package/templates/planning/learnings/queue.md +5 -0
- package/templates/planning/quick/log.md +4 -0
- package/templates/plans/decisions.md +9 -0
- package/templates/plans/phase-0.md +57 -0
- package/templates/plans/phase-1.md +49 -0
package/install.js
ADDED
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ============================================================
|
|
3
|
+
// claude-pw — Structured Project Workflow for Claude Code
|
|
4
|
+
// https://github.com/dcdeve/claude-pw
|
|
5
|
+
//
|
|
6
|
+
// Usage:
|
|
7
|
+
// npx @claude-pw/framework my-project (npm)
|
|
8
|
+
// npx @claude-pw/framework --update (update existing)
|
|
9
|
+
// node install.js my-project (local)
|
|
10
|
+
// node install.js --update (update .claude/ only)
|
|
11
|
+
// ============================================================
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const readline = require('readline');
|
|
16
|
+
const { execSync } = require('child_process');
|
|
17
|
+
|
|
18
|
+
const VERSION = '0.3.0';
|
|
19
|
+
|
|
20
|
+
// --- Colors ---
|
|
21
|
+
const c = {
|
|
22
|
+
red: (s) => `\x1b[31m${s}\x1b[0m`,
|
|
23
|
+
green: (s) => `\x1b[32m${s}\x1b[0m`,
|
|
24
|
+
yellow: (s) => `\x1b[33m${s}\x1b[0m`,
|
|
25
|
+
bold: (s) => `\x1b[1m${s}\x1b[0m`,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const info = (msg) => console.log(`${c.green('>')} ${msg}`);
|
|
29
|
+
const warn = (msg) => console.log(`${c.yellow('!')} ${msg}`);
|
|
30
|
+
const error = (msg) => console.error(`${c.red('x')} ${msg}`);
|
|
31
|
+
const header = (msg) => console.log(`\n${c.bold(msg)}`);
|
|
32
|
+
|
|
33
|
+
// --- Version comparison ---
|
|
34
|
+
function versionLt(a, b) {
|
|
35
|
+
if (a === b) return false;
|
|
36
|
+
const pa = a.split('.').map(Number);
|
|
37
|
+
const pb = b.split('.').map(Number);
|
|
38
|
+
for (let i = 0; i < 3; i++) {
|
|
39
|
+
if ((pa[i] || 0) < (pb[i] || 0)) return true;
|
|
40
|
+
if ((pa[i] || 0) > (pb[i] || 0)) return false;
|
|
41
|
+
}
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// --- Changelog display ---
|
|
46
|
+
function showChangelog(from, to, releasesFile) {
|
|
47
|
+
if (!fs.existsSync(releasesFile)) return;
|
|
48
|
+
const lines = fs.readFileSync(releasesFile, 'utf8').split('\n');
|
|
49
|
+
let inRange = false;
|
|
50
|
+
console.log('');
|
|
51
|
+
for (const line of lines) {
|
|
52
|
+
const match = line.match(/^## \[(\d+\.\d+\.\d+)\]/);
|
|
53
|
+
if (match) {
|
|
54
|
+
const ver = match[1];
|
|
55
|
+
inRange = versionLt(from, ver) && !versionLt(to, ver);
|
|
56
|
+
}
|
|
57
|
+
if (inRange) console.log(` ${line}`);
|
|
58
|
+
}
|
|
59
|
+
console.log('');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// --- File utilities ---
|
|
63
|
+
function mkdirp(dir) {
|
|
64
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function copyFile(src, dest) {
|
|
68
|
+
mkdirp(path.dirname(dest));
|
|
69
|
+
fs.copyFileSync(src, dest);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function copyTemplate(src, dest, vars) {
|
|
73
|
+
mkdirp(path.dirname(dest));
|
|
74
|
+
if (src.endsWith('.tpl')) {
|
|
75
|
+
let content = fs.readFileSync(src, 'utf8');
|
|
76
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
77
|
+
content = content.replace(new RegExp(`\\{\\{${key}\\}\\}`, 'g'), value);
|
|
78
|
+
}
|
|
79
|
+
fs.writeFileSync(dest, content);
|
|
80
|
+
} else {
|
|
81
|
+
fs.copyFileSync(src, dest);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function copyGlob(srcDir, destDir, ext) {
|
|
86
|
+
mkdirp(destDir);
|
|
87
|
+
const files = fs.readdirSync(srcDir).filter((f) => f.endsWith(ext));
|
|
88
|
+
for (const f of files) {
|
|
89
|
+
fs.copyFileSync(path.join(srcDir, f), path.join(destDir, f));
|
|
90
|
+
}
|
|
91
|
+
return files;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function copyIfMissing(src, dest) {
|
|
95
|
+
if (!fs.existsSync(dest) && fs.existsSync(src)) {
|
|
96
|
+
mkdirp(path.dirname(dest));
|
|
97
|
+
fs.copyFileSync(src, dest);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// --- Model profile application ---
|
|
102
|
+
function applyModelProfile(agentsDir, configFile) {
|
|
103
|
+
if (!fs.existsSync(configFile)) return;
|
|
104
|
+
|
|
105
|
+
let config;
|
|
106
|
+
try {
|
|
107
|
+
config = JSON.parse(fs.readFileSync(configFile, 'utf8'));
|
|
108
|
+
} catch {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const profile = config.modelProfile || 'balanced';
|
|
113
|
+
const overrides = config.modelOverrides || {};
|
|
114
|
+
|
|
115
|
+
const profiles = {
|
|
116
|
+
quality: {
|
|
117
|
+
'phase-validator': 'opus', 'session-recovery': 'opus', debugger: 'opus',
|
|
118
|
+
researcher: 'opus', 'codebase-mapper': 'opus', 'plan-checker': 'opus',
|
|
119
|
+
'spike-explorer': 'opus', 'decision-impact': 'opus', 'interface-reviewer': 'opus',
|
|
120
|
+
'learning-extractor': 'opus', implementer: 'opus',
|
|
121
|
+
},
|
|
122
|
+
budget: {
|
|
123
|
+
'phase-validator': 'sonnet', 'session-recovery': 'haiku', debugger: 'haiku',
|
|
124
|
+
researcher: 'sonnet', 'codebase-mapper': 'haiku', 'plan-checker': 'haiku',
|
|
125
|
+
'spike-explorer': 'haiku', 'decision-impact': 'haiku', 'interface-reviewer': 'haiku',
|
|
126
|
+
'learning-extractor': 'haiku', implementer: 'sonnet',
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// Apply profile (skip balanced — it's the template default)
|
|
131
|
+
const profileMap = profiles[profile];
|
|
132
|
+
if (profileMap) {
|
|
133
|
+
for (const [agent, model] of Object.entries(profileMap)) {
|
|
134
|
+
const file = path.join(agentsDir, `${agent}.md`);
|
|
135
|
+
if (fs.existsSync(file)) {
|
|
136
|
+
let content = fs.readFileSync(file, 'utf8');
|
|
137
|
+
content = content.replace(/^model: .*/m, `model: ${model}`);
|
|
138
|
+
fs.writeFileSync(file, content);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Apply per-agent overrides
|
|
144
|
+
for (const [agent, model] of Object.entries(overrides)) {
|
|
145
|
+
const file = path.join(agentsDir, `${agent}.md`);
|
|
146
|
+
if (fs.existsSync(file)) {
|
|
147
|
+
let content = fs.readFileSync(file, 'utf8');
|
|
148
|
+
content = content.replace(/^model: .*/m, `model: ${model}`);
|
|
149
|
+
fs.writeFileSync(file, content);
|
|
150
|
+
info(`Model override: ${agent} -> ${model}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (profile !== 'balanced') {
|
|
155
|
+
info(`Model profile aplicado: ${profile}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// --- Settings.json merge ---
|
|
160
|
+
function mergeSettings(templateFile, projectFile) {
|
|
161
|
+
try {
|
|
162
|
+
const tpl = JSON.parse(fs.readFileSync(templateFile, 'utf8'));
|
|
163
|
+
const proj = JSON.parse(fs.readFileSync(projectFile, 'utf8'));
|
|
164
|
+
|
|
165
|
+
// Start with template, overlay project top-level keys (except hooks)
|
|
166
|
+
const merged = { ...tpl, ...proj };
|
|
167
|
+
|
|
168
|
+
// Merge hooks: template as base, add project custom hooks that aren't in template
|
|
169
|
+
const tplHooks = tpl.hooks || {};
|
|
170
|
+
const projHooks = proj.hooks || {};
|
|
171
|
+
merged.hooks = {};
|
|
172
|
+
|
|
173
|
+
const allHookTypes = new Set([...Object.keys(tplHooks), ...Object.keys(projHooks)]);
|
|
174
|
+
for (const hookType of allHookTypes) {
|
|
175
|
+
const tplArr = tplHooks[hookType] || [];
|
|
176
|
+
const projArr = projHooks[hookType] || [];
|
|
177
|
+
const tplMatchers = new Set(tplArr.map((h) => h.matcher || ''));
|
|
178
|
+
const customHooks = projArr.filter((h) => !tplMatchers.has(h.matcher || ''));
|
|
179
|
+
merged.hooks[hookType] = [...tplArr, ...customHooks];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Preserve statusLine from template if project doesn't have one
|
|
183
|
+
if (tpl.statusLine && !proj.statusLine) {
|
|
184
|
+
merged.statusLine = tpl.statusLine;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
fs.writeFileSync(projectFile, JSON.stringify(merged, null, 2) + '\n');
|
|
188
|
+
info('settings.json mergeado (hooks custom preservados)');
|
|
189
|
+
return true;
|
|
190
|
+
} catch (e) {
|
|
191
|
+
warn(`settings.json: merge fallo — no modificado (${e.message})`);
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// --- Gitignore merge ---
|
|
197
|
+
function mergeGitignore(templateFile, projectFile) {
|
|
198
|
+
if (!fs.existsSync(templateFile)) return;
|
|
199
|
+
const tplLines = fs.readFileSync(templateFile, 'utf8').split('\n');
|
|
200
|
+
const existing = fs.existsSync(projectFile)
|
|
201
|
+
? fs.readFileSync(projectFile, 'utf8')
|
|
202
|
+
: '';
|
|
203
|
+
const existingLines = new Set(existing.split('\n'));
|
|
204
|
+
|
|
205
|
+
const toAdd = tplLines.filter(
|
|
206
|
+
(line) => line && !line.startsWith('#') && !existingLines.has(line)
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
if (toAdd.length > 0) {
|
|
210
|
+
const append = (existing && !existing.endsWith('\n') ? '\n' : '') + toAdd.join('\n') + '\n';
|
|
211
|
+
fs.appendFileSync(projectFile, append);
|
|
212
|
+
for (const line of toAdd) info(`.gitignore: agregado '${line}'`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// --- Prompt user for input ---
|
|
217
|
+
function askQuestion(question) {
|
|
218
|
+
return new Promise((resolve) => {
|
|
219
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
220
|
+
rl.question(question, (answer) => {
|
|
221
|
+
rl.close();
|
|
222
|
+
resolve(answer.trim());
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// --- Detect custom files (not from template) ---
|
|
228
|
+
function detectCustomFiles(dir, templateDir, subdir) {
|
|
229
|
+
const projDir = path.join(dir, '.claude', subdir);
|
|
230
|
+
const tplDir = path.join(templateDir, 'claude', subdir);
|
|
231
|
+
if (!fs.existsSync(projDir) || !fs.existsSync(tplDir)) return;
|
|
232
|
+
|
|
233
|
+
const tplFiles = new Set(fs.readdirSync(tplDir));
|
|
234
|
+
for (const f of fs.readdirSync(projDir)) {
|
|
235
|
+
if (!tplFiles.has(f)) {
|
|
236
|
+
// Only warn about cpw-* files not in template (likely renamed/removed framework files)
|
|
237
|
+
// Non-cpw files are user's custom files — don't warn
|
|
238
|
+
if (f.startsWith('cpw-')) {
|
|
239
|
+
warn(`.claude/${subdir}/${f} has cpw- prefix but is not in the template (was it renamed?)`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// --- Detect modified framework files ---
|
|
246
|
+
function detectModifiedFiles(templateDir, destDir, ext) {
|
|
247
|
+
const tplFiles = fs.readdirSync(templateDir).filter((f) => f.endsWith(ext));
|
|
248
|
+
const modified = [];
|
|
249
|
+
for (const f of tplFiles) {
|
|
250
|
+
const destFile = path.join(destDir, f);
|
|
251
|
+
if (fs.existsSync(destFile)) {
|
|
252
|
+
const tplContent = fs.readFileSync(path.join(templateDir, f), 'utf8');
|
|
253
|
+
const destContent = fs.readFileSync(destFile, 'utf8');
|
|
254
|
+
if (tplContent !== destContent) {
|
|
255
|
+
modified.push(f);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return modified;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// --- Merge new fields into config.json ---
|
|
263
|
+
function mergeConfig(templateFile, projectFile) {
|
|
264
|
+
if (!fs.existsSync(projectFile) || !fs.existsSync(templateFile)) return;
|
|
265
|
+
try {
|
|
266
|
+
const tpl = JSON.parse(fs.readFileSync(templateFile, 'utf8'));
|
|
267
|
+
const proj = JSON.parse(fs.readFileSync(projectFile, 'utf8'));
|
|
268
|
+
let added = 0;
|
|
269
|
+
for (const [key, value] of Object.entries(tpl)) {
|
|
270
|
+
if (!(key in proj)) {
|
|
271
|
+
proj[key] = value;
|
|
272
|
+
added++;
|
|
273
|
+
info(`config.json: added new field '${key}' (default: ${JSON.stringify(value)})`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if (added > 0) {
|
|
277
|
+
fs.writeFileSync(projectFile, JSON.stringify(proj, null, 2) + '\n');
|
|
278
|
+
}
|
|
279
|
+
} catch (e) {
|
|
280
|
+
warn(`config.json merge failed: ${e.message}`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ============================================================
|
|
285
|
+
// MAIN
|
|
286
|
+
// ============================================================
|
|
287
|
+
async function main() {
|
|
288
|
+
const args = process.argv.slice(2);
|
|
289
|
+
let updateOnly = false;
|
|
290
|
+
let projectName = '';
|
|
291
|
+
|
|
292
|
+
// Parse arguments
|
|
293
|
+
for (const arg of args) {
|
|
294
|
+
if (arg === '--update') updateOnly = true;
|
|
295
|
+
else if (arg === '--version') { console.log(`claude-pw ${VERSION}`); process.exit(0); }
|
|
296
|
+
else if (arg === '--help' || arg === '-h') {
|
|
297
|
+
console.log('claude-pw — Structured Project Workflow for Claude Code\n');
|
|
298
|
+
console.log('Usage:');
|
|
299
|
+
console.log(' claude-pw [project-name] Create new project');
|
|
300
|
+
console.log(' claude-pw --update Update .claude/ in existing project');
|
|
301
|
+
console.log(' claude-pw --version Show version');
|
|
302
|
+
process.exit(0);
|
|
303
|
+
} else if (arg.startsWith('-')) {
|
|
304
|
+
error(`Unknown flag: ${arg}`);
|
|
305
|
+
process.exit(1);
|
|
306
|
+
} else {
|
|
307
|
+
projectName = arg;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Locate templates directory (next to this script)
|
|
312
|
+
const scriptDir = __dirname;
|
|
313
|
+
const templatesDir = path.join(scriptDir, 'templates');
|
|
314
|
+
if (!fs.existsSync(templatesDir)) {
|
|
315
|
+
error('Templates directory not found');
|
|
316
|
+
process.exit(1);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
320
|
+
|
|
321
|
+
// ============================================================
|
|
322
|
+
// UPDATE MODE
|
|
323
|
+
// ============================================================
|
|
324
|
+
if (updateOnly) {
|
|
325
|
+
header('Actualizando .claude/ ...');
|
|
326
|
+
|
|
327
|
+
if (!fs.existsSync('.claude')) {
|
|
328
|
+
error('No hay .claude/ en el directorio actual. Estas en un proyecto claude-pw?');
|
|
329
|
+
process.exit(1);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Read installed version
|
|
333
|
+
const versionFile = path.join('.claude', '.claude-pw-version');
|
|
334
|
+
const installedVersion = fs.existsSync(versionFile)
|
|
335
|
+
? fs.readFileSync(versionFile, 'utf8').trim()
|
|
336
|
+
: '0.0.0';
|
|
337
|
+
|
|
338
|
+
if (installedVersion === VERSION) {
|
|
339
|
+
info(`Ya actualizado a claude-pw v${VERSION}`);
|
|
340
|
+
process.exit(0);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
info(`Actualizando v${installedVersion} → v${VERSION}`);
|
|
344
|
+
|
|
345
|
+
// Show changelog
|
|
346
|
+
const releasesFile = path.join(scriptDir, 'RELEASES.md');
|
|
347
|
+
if (fs.existsSync(releasesFile)) {
|
|
348
|
+
header('Cambios:');
|
|
349
|
+
showChangelog(installedVersion, VERSION, releasesFile);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Detect modified framework files before overwriting
|
|
353
|
+
const dirs = [
|
|
354
|
+
{ tpl: path.join(templatesDir, 'claude', 'commands'), dest: path.join('.claude', 'commands'), ext: '.md', label: 'commands' },
|
|
355
|
+
{ tpl: path.join(templatesDir, 'claude', 'agents'), dest: path.join('.claude', 'agents'), ext: '.md', label: 'agents' },
|
|
356
|
+
{ tpl: path.join(templatesDir, 'claude', 'rules'), dest: path.join('.claude', 'rules'), ext: '.md', label: 'rules' },
|
|
357
|
+
{ tpl: path.join(templatesDir, 'claude', 'hooks'), dest: path.join('.claude', 'hooks'), ext: '.js', label: 'hooks' },
|
|
358
|
+
];
|
|
359
|
+
|
|
360
|
+
const allModified = [];
|
|
361
|
+
for (const d of dirs) {
|
|
362
|
+
const modified = detectModifiedFiles(d.tpl, d.dest, d.ext);
|
|
363
|
+
for (const f of modified) allModified.push(`.claude/${d.label}/${f}`);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Show summary and confirm
|
|
367
|
+
const tplCounts = dirs.map((d) => {
|
|
368
|
+
const files = fs.existsSync(d.tpl) ? fs.readdirSync(d.tpl).filter((f) => f.endsWith(d.ext)).length : 0;
|
|
369
|
+
return `${files} ${d.label}`;
|
|
370
|
+
});
|
|
371
|
+
console.log(`\n ${tplCounts.join(', ')} (overwrite)`);
|
|
372
|
+
console.log(' settings.json (merge, custom hooks preserved)');
|
|
373
|
+
console.log(' config.json (merge new fields)');
|
|
374
|
+
if (allModified.length > 0) {
|
|
375
|
+
console.log(`\n ${c.yellow('Modified by you (will be overwritten):')}`);
|
|
376
|
+
for (const f of allModified) console.log(` ${f}`);
|
|
377
|
+
}
|
|
378
|
+
console.log(`\n Backup: .claude.backup.* (created before changes)\n`);
|
|
379
|
+
|
|
380
|
+
const confirm = await askQuestion('Apply? [y/N]: ');
|
|
381
|
+
if (!/^[yYsS]$/.test(confirm)) {
|
|
382
|
+
info('Cancelled.');
|
|
383
|
+
process.exit(0);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Backup
|
|
387
|
+
const backupDir = `.claude.backup.${Date.now()}`;
|
|
388
|
+
fs.cpSync('.claude', backupDir, { recursive: true });
|
|
389
|
+
info(`Backup creado en ${backupDir}`);
|
|
390
|
+
|
|
391
|
+
// Copy commands, agents, rules, hooks
|
|
392
|
+
for (const d of dirs) {
|
|
393
|
+
mkdirp(d.dest);
|
|
394
|
+
copyGlob(d.tpl, d.dest, d.ext);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Apply model profile
|
|
398
|
+
applyModelProfile(path.join('.claude', 'agents'), path.join('.planning', 'config.json'));
|
|
399
|
+
|
|
400
|
+
// Merge settings.json
|
|
401
|
+
const projSettings = path.join('.claude', 'settings.json');
|
|
402
|
+
const tplSettings = path.join(templatesDir, 'claude', 'settings.json');
|
|
403
|
+
if (fs.existsSync(projSettings)) {
|
|
404
|
+
mergeSettings(tplSettings, projSettings);
|
|
405
|
+
} else {
|
|
406
|
+
copyFile(tplSettings, projSettings);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Merge .gitignore
|
|
410
|
+
mergeGitignore(path.join(templatesDir, 'gitignore'), '.gitignore');
|
|
411
|
+
|
|
412
|
+
// Create missing directories/files
|
|
413
|
+
mkdirp(path.join('.planning', 'quick'));
|
|
414
|
+
mkdirp(path.join('.planning', 'debug', 'resolved'));
|
|
415
|
+
mkdirp(path.join('.planning', 'learnings'));
|
|
416
|
+
|
|
417
|
+
copyIfMissing(path.join(templatesDir, 'planning', 'quick', 'log.md'), path.join('.planning', 'quick', 'log.md'));
|
|
418
|
+
copyIfMissing(path.join(templatesDir, 'planning', 'learnings', 'queue.md'), path.join('.planning', 'learnings', 'queue.md'));
|
|
419
|
+
copyIfMissing(path.join(templatesDir, 'planning', 'learnings', 'applied.md'), path.join('.planning', 'learnings', 'applied.md'));
|
|
420
|
+
copyIfMissing(path.join(templatesDir, 'planning', 'config.json'), path.join('.planning', 'config.json'));
|
|
421
|
+
|
|
422
|
+
// Merge new config fields from template
|
|
423
|
+
mergeConfig(path.join(templatesDir, 'planning', 'config.json'), path.join('.planning', 'config.json'));
|
|
424
|
+
|
|
425
|
+
// Detect custom files
|
|
426
|
+
detectCustomFiles('.', templatesDir, 'commands');
|
|
427
|
+
detectCustomFiles('.', templatesDir, 'agents');
|
|
428
|
+
|
|
429
|
+
// Write version
|
|
430
|
+
fs.writeFileSync(versionFile, VERSION);
|
|
431
|
+
info(`Actualizado a claude-pw v${VERSION}`);
|
|
432
|
+
process.exit(0);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// ============================================================
|
|
436
|
+
// NEW PROJECT MODE
|
|
437
|
+
// ============================================================
|
|
438
|
+
|
|
439
|
+
// Interactive prompt if no project name
|
|
440
|
+
if (!projectName) {
|
|
441
|
+
console.log(`${c.bold('claude-pw')} v${VERSION}\n`);
|
|
442
|
+
projectName = await askQuestion('Nombre del proyecto: ');
|
|
443
|
+
if (!projectName) {
|
|
444
|
+
error('Nombre requerido');
|
|
445
|
+
process.exit(1);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const projectDir = projectName;
|
|
450
|
+
projectName = path.basename(path.resolve(projectName));
|
|
451
|
+
|
|
452
|
+
header(`claude-pw v${VERSION} — ${projectName}`);
|
|
453
|
+
|
|
454
|
+
// Create directory
|
|
455
|
+
if (!fs.existsSync(projectDir)) {
|
|
456
|
+
mkdirp(projectDir);
|
|
457
|
+
info(`Directorio creado: ${projectDir}`);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
process.chdir(projectDir);
|
|
461
|
+
|
|
462
|
+
// Check for existing project
|
|
463
|
+
if (fs.existsSync('CLAUDE.md') || fs.existsSync(path.join('.claude', 'commands'))) {
|
|
464
|
+
const confirm = await askQuestion('Este directorio ya tiene archivos de claude-pw. Sobreescribir? [y/N]: ');
|
|
465
|
+
if (!/^[yYsS]$/.test(confirm)) {
|
|
466
|
+
info('Cancelado. Usa --update para actualizar solo .claude/');
|
|
467
|
+
process.exit(0);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Create directory structure
|
|
472
|
+
info('Creando estructura...');
|
|
473
|
+
for (const dir of [
|
|
474
|
+
'.claude/rules', '.claude/commands', '.claude/agents', '.claude/hooks',
|
|
475
|
+
'.husky', 'plans', 'docs', 'docs/research',
|
|
476
|
+
'src/interfaces',
|
|
477
|
+
'tests/unit', 'tests/integration', 'tests/contracts',
|
|
478
|
+
'.planning/quick', '.planning/debug/resolved', '.planning/learnings',
|
|
479
|
+
]) {
|
|
480
|
+
mkdirp(dir);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Copy templates
|
|
484
|
+
info('Copiando templates...');
|
|
485
|
+
|
|
486
|
+
// .claude/
|
|
487
|
+
copyGlob(path.join(templatesDir, 'claude', 'commands'), path.join('.claude', 'commands'), '.md');
|
|
488
|
+
copyGlob(path.join(templatesDir, 'claude', 'agents'), path.join('.claude', 'agents'), '.md');
|
|
489
|
+
copyGlob(path.join(templatesDir, 'claude', 'rules'), path.join('.claude', 'rules'), '.md');
|
|
490
|
+
copyGlob(path.join(templatesDir, 'claude', 'hooks'), path.join('.claude', 'hooks'), '.js');
|
|
491
|
+
copyFile(path.join(templatesDir, 'claude', 'settings.json'), path.join('.claude', 'settings.json'));
|
|
492
|
+
fs.writeFileSync(path.join('.claude', '.claude-pw-version'), VERSION);
|
|
493
|
+
|
|
494
|
+
// Apply model profile
|
|
495
|
+
applyModelProfile(path.join('.claude', 'agents'), path.join('.planning', 'config.json'));
|
|
496
|
+
|
|
497
|
+
// Husky
|
|
498
|
+
copyFile(path.join(templatesDir, 'husky', 'pre-commit'), path.join('.husky', 'pre-commit'));
|
|
499
|
+
try { fs.chmodSync(path.join('.husky', 'pre-commit'), 0o755); } catch { /* Windows */ }
|
|
500
|
+
|
|
501
|
+
// Plans and docs (only if missing)
|
|
502
|
+
for (const f of ['plans/phase-0.md', 'plans/phase-1.md', 'plans/decisions.md']) {
|
|
503
|
+
copyIfMissing(path.join(templatesDir, f), f);
|
|
504
|
+
}
|
|
505
|
+
for (const f of [
|
|
506
|
+
'docs/architecture.md', 'docs/interfaces.md', 'docs/tooling.md',
|
|
507
|
+
'docs/conventions.md', 'docs/codebase-map.md', 'docs/tech-debt.md',
|
|
508
|
+
]) {
|
|
509
|
+
copyIfMissing(path.join(templatesDir, f), f);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Planning state (local, gitignored)
|
|
513
|
+
copyIfMissing(path.join(templatesDir, 'planning', 'quick', 'log.md'), path.join('.planning', 'quick', 'log.md'));
|
|
514
|
+
copyIfMissing(path.join(templatesDir, 'planning', 'learnings', 'queue.md'), path.join('.planning', 'learnings', 'queue.md'));
|
|
515
|
+
copyIfMissing(path.join(templatesDir, 'planning', 'learnings', 'applied.md'), path.join('.planning', 'learnings', 'applied.md'));
|
|
516
|
+
copyIfMissing(path.join(templatesDir, 'planning', 'config.json'), path.join('.planning', 'config.json'));
|
|
517
|
+
|
|
518
|
+
// Root files with template variable substitution
|
|
519
|
+
const vars = { PROJECT_NAME: projectName, DATE: date };
|
|
520
|
+
if (!fs.existsSync('CLAUDE.md')) copyTemplate(path.join(templatesDir, 'CLAUDE.md.tpl'), 'CLAUDE.md', vars);
|
|
521
|
+
if (!fs.existsSync('PLAN.md')) copyTemplate(path.join(templatesDir, 'PLAN.md.tpl'), 'PLAN.md', vars);
|
|
522
|
+
if (!fs.existsSync('STATUS.md')) copyTemplate(path.join(templatesDir, 'STATUS.md.tpl'), 'STATUS.md', vars);
|
|
523
|
+
copyIfMissing(path.join(templatesDir, 'CHANGELOG.md'), 'CHANGELOG.md');
|
|
524
|
+
copyIfMissing(path.join(templatesDir, 'Makefile'), 'Makefile');
|
|
525
|
+
|
|
526
|
+
// .gitignore — handle commitPlanning setting
|
|
527
|
+
let commitPlanning = false;
|
|
528
|
+
const configPath = path.join('.planning', 'config.json');
|
|
529
|
+
if (fs.existsSync(configPath)) {
|
|
530
|
+
try {
|
|
531
|
+
commitPlanning = JSON.parse(fs.readFileSync(configPath, 'utf8')).commitPlanning === true;
|
|
532
|
+
} catch { /* ignore */ }
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const tplGitignore = path.join(templatesDir, 'gitignore');
|
|
536
|
+
if (fs.existsSync(tplGitignore)) {
|
|
537
|
+
let gitignoreContent = fs.readFileSync(tplGitignore, 'utf8');
|
|
538
|
+
if (commitPlanning) {
|
|
539
|
+
gitignoreContent = gitignoreContent.split('\n').filter((l) => !l.startsWith('.planning/')).join('\n');
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
if (fs.existsSync('.gitignore')) {
|
|
543
|
+
const existing = fs.readFileSync('.gitignore', 'utf8');
|
|
544
|
+
if (!existing.includes('claude-pw')) {
|
|
545
|
+
fs.appendFileSync('.gitignore', '\n' + gitignoreContent);
|
|
546
|
+
info('.gitignore updated');
|
|
547
|
+
}
|
|
548
|
+
} else {
|
|
549
|
+
fs.writeFileSync('.gitignore', gitignoreContent);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Git init
|
|
554
|
+
if (!fs.existsSync('.git')) {
|
|
555
|
+
try {
|
|
556
|
+
execSync('git init -q', { stdio: 'ignore' });
|
|
557
|
+
info('Git initialized');
|
|
558
|
+
} catch {
|
|
559
|
+
warn('git init failed — initialize manually');
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Summary
|
|
564
|
+
header('Listo');
|
|
565
|
+
console.log('');
|
|
566
|
+
console.log(` ${projectName}/`);
|
|
567
|
+
console.log(' ├── .claude/commands/ 10 commands (/cpw-startup, /cpw-next-step, /cpw-quick, ...)');
|
|
568
|
+
console.log(' ├── .claude/agents/ 11 agentes (plan-checker, debugger, researcher, implementer, ...)');
|
|
569
|
+
console.log(' ├── .claude/rules/ 3 reglas (interfaces, testing, git)');
|
|
570
|
+
console.log(' ├── .claude/hooks/ 2 hooks (statusline, context monitor)');
|
|
571
|
+
console.log(' ├── plans/ Sub-planes (phase-0, phase-1, decisions)');
|
|
572
|
+
console.log(' ├── docs/ Arquitectura, interfaces, tooling');
|
|
573
|
+
console.log(' ├── .planning/ Estado local (quick tasks, debug sessions)');
|
|
574
|
+
console.log(' ├── CLAUDE.md Instrucciones para Claude Code');
|
|
575
|
+
console.log(' ├── PLAN.md Indice del proyecto');
|
|
576
|
+
console.log(' ├── STATUS.md Puntero de sesion');
|
|
577
|
+
console.log(' └── Makefile Comandos estandarizados');
|
|
578
|
+
console.log('');
|
|
579
|
+
|
|
580
|
+
header('Siguiente paso');
|
|
581
|
+
console.log('');
|
|
582
|
+
if (projectDir !== '.') {
|
|
583
|
+
console.log(` cd ${projectDir}`);
|
|
584
|
+
}
|
|
585
|
+
console.log(' claude');
|
|
586
|
+
console.log(' /cpw-startup');
|
|
587
|
+
console.log('');
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
main().catch((e) => {
|
|
591
|
+
error(e.message);
|
|
592
|
+
process.exit(1);
|
|
593
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@claude-pw/framework",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Structured Project Workflow for Claude Code — adaptive pipeline, context management, quality gates",
|
|
5
|
+
"bin": {
|
|
6
|
+
"claude-pw": "./install.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"install.js",
|
|
10
|
+
"templates",
|
|
11
|
+
"RELEASES.md"
|
|
12
|
+
],
|
|
13
|
+
"keywords": [
|
|
14
|
+
"claude",
|
|
15
|
+
"claude-code",
|
|
16
|
+
"workflow",
|
|
17
|
+
"project-management",
|
|
18
|
+
"ai",
|
|
19
|
+
"context-engineering",
|
|
20
|
+
"meta-prompting"
|
|
21
|
+
],
|
|
22
|
+
"author": "dcdeve",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/dcdeve/claude-pw.git"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://github.com/dcdeve/claude-pw",
|
|
29
|
+
"scripts": {
|
|
30
|
+
"test": "node --test"
|
|
31
|
+
},
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=18.0.0"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# {{PROJECT_NAME}}
|
|
2
|
+
|
|
3
|
+
## Stack
|
|
4
|
+
TODO: Phase 0
|
|
5
|
+
|
|
6
|
+
## Context rules
|
|
7
|
+
- FIRST: Read STATUS.md — it tells you the current phase, step, stage, and which files to load
|
|
8
|
+
- Load ONLY files listed in "Required context" from STATUS.md — nothing else
|
|
9
|
+
- If `.planning/config.json` exists, load it for workflow settings
|
|
10
|
+
- If `plans/phase-N-context.md` exists for the current phase, load it (discussion decisions)
|
|
11
|
+
- Do NOT load files from other phases, previous steps, or unrelated docs
|
|
12
|
+
- Do not modify files outside the scope of the current step
|
|
13
|
+
- Do not modify src/interfaces/ without an RFC in plans/decisions.md
|
|
14
|
+
- Use `make` for all commands
|
|
15
|
+
- After each stage, update STATUS.md
|
|
16
|
+
- Adaptive pipeline: evaluate which stages apply at the start of the step
|
|
17
|
+
- Mandatory stages: DESIGN, ACCEPTANCE, CLOSE
|
|
18
|
+
- If there is technical uncertainty -> SPIKE (explore and discard code)
|
|
19
|
+
- At phase start, tooling is audited: search for skills/MCPs matching phase technologies, propose disabling tools no longer needed
|
|
20
|
+
- NEVER git commit --no-verify
|
|
21
|
+
- Big decisions -> /cpw-impact -> context reset
|
|
22
|
+
- Ad-hoc tasks that don't affect the plan -> /cpw-quick
|
|
23
|
+
- Debug that may take several sessions -> /cpw-debug (persists state)
|
|
24
|
+
- If something seems broken or inconsistent -> /cpw-health
|
|
25
|
+
- If the user corrects you, capture the correction in .planning/learnings/queue.md (include the active command in the Command field)
|
|
26
|
+
- Learnings are auto-processed based on config (autoReflect: off/remind/auto)
|
|
27
|
+
- When you discover something non-obvious (debug, workaround), evaluate for skill extraction → .claude/skills/
|
|
28
|
+
- Context monitor: statusline tracks usage, warns at 65% (WARNING) and 80% (CRITICAL) — run /cpw-pause at CRITICAL
|
|
29
|
+
|
|
30
|
+
## Structure
|
|
31
|
+
- STATUS.md — Session pointer. Read FIRST. Tells you what to load.
|
|
32
|
+
- PLAN.md — Project index (phases overview)
|
|
33
|
+
- plans/phase-N.md — Sub-plan for phase N (load only current phase)
|
|
34
|
+
- plans/phase-N-context.md — Discussion context (load if exists)
|
|
35
|
+
- plans/decisions.md — Decisions and RFCs
|
|
36
|
+
- src/interfaces/ — Contracts (RFC required to modify)
|
|
37
|
+
- .planning/ — Project state (gitignored by default, set commitPlanning: true to track)
|
|
38
|
+
- .planning/config.json — Workflow config (autoAdvance, granularity, modelProfile, modelOverrides, gitStrategy, commitPlanning, toolingSources, maxConsecutiveFailures, autoReflect)
|