ai-development-framework 0.1.0 → 0.1.1
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/cli/init.js +307 -72
- package/cli/protected-files.js +65 -0
- package/cli/update.js +145 -55
- package/package.json +1 -1
package/cli/init.js
CHANGED
|
@@ -2,6 +2,7 @@ const fs = require('fs');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const { execFileSync } = require('child_process');
|
|
4
4
|
const readline = require('readline');
|
|
5
|
+
const { PROTECTED_FILES, PROTECTED_DIRS, CUSTOMIZABLE_FILES, toProjectRelative } = require('./protected-files');
|
|
5
6
|
|
|
6
7
|
const REPO = 'cristian-robert/AIDevelopmentFramework';
|
|
7
8
|
const BRANCH = 'main';
|
|
@@ -18,66 +19,193 @@ function ask(question) {
|
|
|
18
19
|
});
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
function
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
var entries = fs.readdirSync(src, { withFileTypes: true });
|
|
26
|
-
for (var i = 0; i < entries.length; i++) {
|
|
27
|
-
var entry = entries[i];
|
|
28
|
-
var srcPath = path.join(src, entry.name);
|
|
29
|
-
var destPath = path.join(dest, entry.name);
|
|
30
|
-
if (entry.isDirectory()) {
|
|
31
|
-
copyDirRecursive(srcPath, destPath);
|
|
32
|
-
} else {
|
|
33
|
-
fs.copyFileSync(srcPath, destPath);
|
|
34
|
-
}
|
|
22
|
+
function copyFileSimple(srcPath, destPath) {
|
|
23
|
+
var destDir = path.dirname(destPath);
|
|
24
|
+
if (!fs.existsSync(destDir)) {
|
|
25
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
35
26
|
}
|
|
27
|
+
fs.copyFileSync(srcPath, destPath);
|
|
36
28
|
}
|
|
37
29
|
|
|
38
|
-
function
|
|
39
|
-
var tmpDir = path.join(require('os').tmpdir(), 'ai-framework-' + Date.now());
|
|
40
|
-
fs.mkdirSync(tmpDir, { recursive: true });
|
|
41
|
-
|
|
30
|
+
function downloadAndExtract(tmpDir) {
|
|
42
31
|
console.log('Downloading latest framework from GitHub...');
|
|
43
|
-
|
|
44
32
|
try {
|
|
45
|
-
// Download and extract tarball
|
|
46
33
|
execFileSync('curl', ['-sL', TARBALL_URL, '-o', path.join(tmpDir, 'framework.tar.gz')]);
|
|
47
34
|
execFileSync('tar', ['-xzf', path.join(tmpDir, 'framework.tar.gz'), '-C', tmpDir, '--strip-components=1']);
|
|
35
|
+
return true;
|
|
36
|
+
} catch (err) {
|
|
37
|
+
console.error('Download failed: ' + err.message);
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
48
41
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
42
|
+
function getLocalFallbackDir() {
|
|
43
|
+
var frameworkDir = path.join(__dirname, '..');
|
|
44
|
+
if (fs.existsSync(path.join(frameworkDir, '.claude'))) {
|
|
45
|
+
return frameworkDir;
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
52
49
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
50
|
+
function isTemplateContent(filePath) {
|
|
51
|
+
try {
|
|
52
|
+
var content = fs.readFileSync(filePath, 'utf-8');
|
|
53
|
+
if (content.includes('> Populated by /create-rules')) return true;
|
|
54
|
+
if (content.includes('> Populated when /create-rules')) return true;
|
|
55
|
+
if (content.includes('> No decisions recorded yet')) return true;
|
|
56
|
+
if (content.includes('Run `/create-rules` to populate')) return true;
|
|
57
|
+
if (content.includes('> This section is populated as the project grows')) return true;
|
|
58
|
+
return false;
|
|
59
|
+
} catch (e) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function cleanupTmpDir(tmpDir) {
|
|
65
|
+
try {
|
|
66
|
+
if (fs.existsSync(tmpDir)) {
|
|
67
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
68
|
+
}
|
|
69
|
+
} catch (e) {
|
|
70
|
+
// Best-effort cleanup
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Collect all files from source, compute project-root-relative paths, classify them
|
|
75
|
+
function collectSourceFiles(sourceDir, projectRoot) {
|
|
76
|
+
var files = [];
|
|
77
|
+
|
|
78
|
+
function walk(dir) {
|
|
79
|
+
var entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
80
|
+
for (var i = 0; i < entries.length; i++) {
|
|
81
|
+
var entry = entries[i];
|
|
82
|
+
var srcPath = path.join(dir, entry.name);
|
|
83
|
+
if (entry.isDirectory()) {
|
|
84
|
+
walk(srcPath);
|
|
85
|
+
} else {
|
|
86
|
+
// Compute the project-root-relative path this file WOULD have
|
|
87
|
+
var relFromSource = path.relative(sourceDir, srcPath).split(path.sep).join('/');
|
|
88
|
+
files.push({ srcPath: srcPath, relPath: relFromSource });
|
|
89
|
+
}
|
|
56
90
|
}
|
|
91
|
+
}
|
|
57
92
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
93
|
+
walk(sourceDir);
|
|
94
|
+
return files;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function detectConflicts(sourceDir, targetDir, projectRoot) {
|
|
98
|
+
var conflicts = { protected: [], customized: [], safe: [] };
|
|
99
|
+
var files = collectSourceFiles(sourceDir, projectRoot);
|
|
100
|
+
|
|
101
|
+
for (var i = 0; i < files.length; i++) {
|
|
102
|
+
var relPath = files[i].relPath;
|
|
103
|
+
var srcPath = files[i].srcPath;
|
|
104
|
+
var destPath = path.join(targetDir, relPath);
|
|
61
105
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
106
|
+
// Compute project-root-relative path for matching against protection lists
|
|
107
|
+
var projectRelPath = toProjectRelative(destPath, projectRoot);
|
|
108
|
+
|
|
109
|
+
if (!fs.existsSync(destPath)) {
|
|
110
|
+
conflicts.safe.push(relPath);
|
|
111
|
+
} else if (PROTECTED_FILES.indexOf(projectRelPath) !== -1) {
|
|
112
|
+
if (!isTemplateContent(destPath)) {
|
|
113
|
+
conflicts.protected.push(relPath);
|
|
114
|
+
} else {
|
|
115
|
+
conflicts.safe.push(relPath);
|
|
116
|
+
}
|
|
117
|
+
} else if (CUSTOMIZABLE_FILES.indexOf(projectRelPath) !== -1) {
|
|
118
|
+
var srcContent = fs.readFileSync(srcPath, 'utf-8');
|
|
119
|
+
var destContent = fs.readFileSync(destPath, 'utf-8');
|
|
120
|
+
if (srcContent !== destContent) {
|
|
121
|
+
conflicts.customized.push(relPath);
|
|
122
|
+
} else {
|
|
123
|
+
conflicts.safe.push(relPath);
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
conflicts.safe.push(relPath);
|
|
65
127
|
}
|
|
128
|
+
}
|
|
66
129
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
130
|
+
return conflicts;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function smartCopy(sourceDir, targetDir, projectRoot, strategy, customizedAction) {
|
|
134
|
+
var stats = { created: 0, updated: 0, skipped: 0, backedUp: 0 };
|
|
135
|
+
|
|
136
|
+
function copy(src, dest) {
|
|
137
|
+
if (!fs.existsSync(dest)) {
|
|
138
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
139
|
+
}
|
|
140
|
+
var entries = fs.readdirSync(src, { withFileTypes: true });
|
|
141
|
+
for (var i = 0; i < entries.length; i++) {
|
|
142
|
+
var entry = entries[i];
|
|
143
|
+
var srcPath = path.join(src, entry.name);
|
|
144
|
+
var destPath = path.join(dest, entry.name);
|
|
145
|
+
var projectRelPath = toProjectRelative(destPath, projectRoot);
|
|
146
|
+
|
|
147
|
+
if (entry.isDirectory()) {
|
|
148
|
+
if (strategy === 'smart') {
|
|
149
|
+
var isProtectedDir = PROTECTED_DIRS.some(function (d) {
|
|
150
|
+
return projectRelPath === d || projectRelPath.startsWith(d + '/');
|
|
151
|
+
});
|
|
152
|
+
if (isProtectedDir && fs.existsSync(destPath)) {
|
|
153
|
+
// Count actual files in the directory for accurate reporting
|
|
154
|
+
try {
|
|
155
|
+
var dirFiles = fs.readdirSync(destPath, { recursive: true });
|
|
156
|
+
stats.skipped += dirFiles.length || 1;
|
|
157
|
+
} catch (e) {
|
|
158
|
+
stats.skipped++;
|
|
159
|
+
}
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
copy(srcPath, destPath);
|
|
164
|
+
} else {
|
|
165
|
+
var destExists = fs.existsSync(destPath);
|
|
166
|
+
|
|
167
|
+
if (strategy === 'smart' && destExists) {
|
|
168
|
+
// Protected file with real content — never overwrite
|
|
169
|
+
if (PROTECTED_FILES.indexOf(projectRelPath) !== -1 && !isTemplateContent(destPath)) {
|
|
170
|
+
stats.skipped++;
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Customized file — apply user's chosen action
|
|
175
|
+
if (CUSTOMIZABLE_FILES.indexOf(projectRelPath) !== -1) {
|
|
176
|
+
var srcContent = fs.readFileSync(srcPath, 'utf-8');
|
|
177
|
+
var destContent = fs.readFileSync(destPath, 'utf-8');
|
|
178
|
+
if (srcContent !== destContent) {
|
|
179
|
+
if (customizedAction === 'keep') {
|
|
180
|
+
stats.skipped++;
|
|
181
|
+
continue;
|
|
182
|
+
} else if (customizedAction === 'backup') {
|
|
183
|
+
// Use timestamped backup name to avoid overwriting previous backups
|
|
184
|
+
var timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
185
|
+
var backupPath = destPath + '.' + timestamp + '.backup';
|
|
186
|
+
fs.copyFileSync(destPath, backupPath);
|
|
187
|
+
fs.copyFileSync(srcPath, destPath);
|
|
188
|
+
stats.backedUp++;
|
|
189
|
+
stats.updated++;
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
// 'overwrite' falls through to normal copy
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
fs.copyFileSync(srcPath, destPath);
|
|
198
|
+
if (destExists) {
|
|
199
|
+
stats.updated++;
|
|
200
|
+
} else {
|
|
201
|
+
stats.created++;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
79
204
|
}
|
|
80
205
|
}
|
|
206
|
+
|
|
207
|
+
copy(sourceDir, targetDir);
|
|
208
|
+
return stats;
|
|
81
209
|
}
|
|
82
210
|
|
|
83
211
|
function detectTechStack() {
|
|
@@ -115,7 +243,7 @@ function detectTechStack() {
|
|
|
115
243
|
if (reqContent.includes('django')) detected.push('Django');
|
|
116
244
|
if (reqContent.includes('flask')) detected.push('Flask');
|
|
117
245
|
} catch (e) {
|
|
118
|
-
// ignore
|
|
246
|
+
// ignore
|
|
119
247
|
}
|
|
120
248
|
}
|
|
121
249
|
if (fs.existsSync('go.mod')) detected.push('Go');
|
|
@@ -133,45 +261,143 @@ async function main() {
|
|
|
133
261
|
var targetDir = process.cwd();
|
|
134
262
|
var hasGit = fs.existsSync('.git');
|
|
135
263
|
var hasClaudeDir = fs.existsSync('.claude');
|
|
136
|
-
|
|
137
|
-
// Check if .claude/ already exists
|
|
138
|
-
if (hasClaudeDir) {
|
|
139
|
-
var overwrite = await ask('.claude/ already exists. Overwrite? (yes/no): ');
|
|
140
|
-
if (overwrite.toLowerCase() !== 'yes' && overwrite.toLowerCase() !== 'y') {
|
|
141
|
-
console.log('Aborted. Use "npx ai-framework update" to update existing installation.');
|
|
142
|
-
rl.close();
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
264
|
+
var hasCLAUDEmd = fs.existsSync('CLAUDE.md');
|
|
146
265
|
|
|
147
266
|
// Detect tech stack
|
|
148
267
|
var stack = detectTechStack();
|
|
149
268
|
if (stack.length > 0) {
|
|
150
|
-
console.log('Detected tech stack: ' + stack.join(', '));
|
|
269
|
+
console.log(' Detected tech stack: ' + stack.join(', '));
|
|
270
|
+
console.log('');
|
|
151
271
|
}
|
|
152
272
|
|
|
153
|
-
// Download
|
|
154
|
-
var
|
|
155
|
-
|
|
156
|
-
// Fall back to local copy if download fails (when installed via npm)
|
|
157
|
-
if (!downloaded) {
|
|
158
|
-
var frameworkDir = path.join(__dirname, '..');
|
|
159
|
-
var localClaudeDir = path.join(frameworkDir, '.claude');
|
|
160
|
-
if (fs.existsSync(localClaudeDir)) {
|
|
161
|
-
console.log('Copying from local package...');
|
|
162
|
-
copyDirRecursive(localClaudeDir, path.join(targetDir, '.claude'));
|
|
273
|
+
// Download framework to temp dir
|
|
274
|
+
var tmpDir = path.join(require('os').tmpdir(), 'ai-framework-' + Date.now());
|
|
275
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
163
276
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
277
|
+
var sourceDir = null;
|
|
278
|
+
var downloaded = downloadAndExtract(tmpDir);
|
|
279
|
+
if (downloaded) {
|
|
280
|
+
sourceDir = tmpDir;
|
|
281
|
+
} else {
|
|
282
|
+
var fallback = getLocalFallbackDir();
|
|
283
|
+
if (fallback) {
|
|
284
|
+
console.log('Using local package as fallback...');
|
|
285
|
+
sourceDir = fallback;
|
|
168
286
|
} else {
|
|
169
|
-
console.error('No
|
|
287
|
+
console.error('No framework source available. Check your internet connection.');
|
|
288
|
+
cleanupTmpDir(tmpDir);
|
|
170
289
|
rl.close();
|
|
171
290
|
process.exit(1);
|
|
172
291
|
}
|
|
173
292
|
}
|
|
174
293
|
|
|
294
|
+
var strategy = 'fresh';
|
|
295
|
+
var customizedAction = 'overwrite';
|
|
296
|
+
|
|
297
|
+
if (hasClaudeDir || hasCLAUDEmd) {
|
|
298
|
+
console.log('Existing configuration detected. Scanning for conflicts...');
|
|
299
|
+
console.log('');
|
|
300
|
+
|
|
301
|
+
// Scan .claude/ directory for conflicts
|
|
302
|
+
var claudeConflicts = { protected: [], customized: [], safe: [] };
|
|
303
|
+
var sourceClaudeDir = path.join(sourceDir, '.claude');
|
|
304
|
+
if (fs.existsSync(sourceClaudeDir)) {
|
|
305
|
+
claudeConflicts = detectConflicts(sourceClaudeDir, path.join(targetDir, '.claude'), targetDir);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Check CLAUDE.md separately
|
|
309
|
+
if (hasCLAUDEmd && !isTemplateContent(path.join(targetDir, 'CLAUDE.md'))) {
|
|
310
|
+
claudeConflicts.protected.push('CLAUDE.md');
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (claudeConflicts.protected.length > 0) {
|
|
314
|
+
console.log(' Project-specific files (will be preserved):');
|
|
315
|
+
for (var i = 0; i < claudeConflicts.protected.length; i++) {
|
|
316
|
+
console.log(' ' + claudeConflicts.protected[i]);
|
|
317
|
+
}
|
|
318
|
+
console.log('');
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (claudeConflicts.customized.length > 0) {
|
|
322
|
+
console.log(' Modified files (differ from framework defaults):');
|
|
323
|
+
for (var j = 0; j < claudeConflicts.customized.length; j++) {
|
|
324
|
+
console.log(' ' + claudeConflicts.customized[j]);
|
|
325
|
+
}
|
|
326
|
+
console.log('');
|
|
327
|
+
|
|
328
|
+
var choice = await ask(
|
|
329
|
+
' How should modified files be handled?\n' +
|
|
330
|
+
' 1. Keep mine — preserve your customizations, skip framework updates\n' +
|
|
331
|
+
' 2. Use framework — overwrite with latest framework versions\n' +
|
|
332
|
+
' 3. Backup + update — save yours as .backup, install framework versions\n' +
|
|
333
|
+
' Choice (1/2/3): '
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
if (choice === '1') customizedAction = 'keep';
|
|
337
|
+
else if (choice === '2') customizedAction = 'overwrite';
|
|
338
|
+
else if (choice === '3') customizedAction = 'backup';
|
|
339
|
+
else customizedAction = 'keep'; // default to safe option
|
|
340
|
+
|
|
341
|
+
console.log('');
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (claudeConflicts.safe.length > 0) {
|
|
345
|
+
console.log(' New/unchanged files: ' + claudeConflicts.safe.length + ' (will be installed)');
|
|
346
|
+
console.log('');
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
strategy = 'smart';
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Install .claude/
|
|
353
|
+
console.log('Installing framework...');
|
|
354
|
+
var stats = smartCopy(
|
|
355
|
+
path.join(sourceDir, '.claude'),
|
|
356
|
+
path.join(targetDir, '.claude'),
|
|
357
|
+
targetDir,
|
|
358
|
+
strategy,
|
|
359
|
+
customizedAction
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
// Install CLAUDE.md from framework source (if not protected)
|
|
363
|
+
var claudeMdSource = path.join(sourceDir, 'CLAUDE.md');
|
|
364
|
+
if (fs.existsSync(claudeMdSource)) {
|
|
365
|
+
var claudeMdDest = path.join(targetDir, 'CLAUDE.md');
|
|
366
|
+
if (!fs.existsSync(claudeMdDest)) {
|
|
367
|
+
copyFileSimple(claudeMdSource, claudeMdDest);
|
|
368
|
+
stats.created++;
|
|
369
|
+
} else if (strategy === 'fresh') {
|
|
370
|
+
copyFileSimple(claudeMdSource, claudeMdDest);
|
|
371
|
+
stats.updated++;
|
|
372
|
+
}
|
|
373
|
+
// In 'smart' mode, CLAUDE.md is handled by the protection logic above
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Install docs/ (methodology guides only, not project-specific plans)
|
|
377
|
+
var docsSource = path.join(sourceDir, 'docs');
|
|
378
|
+
if (fs.existsSync(docsSource)) {
|
|
379
|
+
var docsTarget = path.join(targetDir, 'docs');
|
|
380
|
+
if (!fs.existsSync(docsTarget)) {
|
|
381
|
+
fs.mkdirSync(docsTarget, { recursive: true });
|
|
382
|
+
}
|
|
383
|
+
var docsEntries = fs.readdirSync(docsSource, { withFileTypes: true });
|
|
384
|
+
for (var k = 0; k < docsEntries.length; k++) {
|
|
385
|
+
var entry = docsEntries[k];
|
|
386
|
+
if (entry.isFile()) {
|
|
387
|
+
var srcPath = path.join(docsSource, entry.name);
|
|
388
|
+
var destPath = path.join(docsTarget, entry.name);
|
|
389
|
+
var existed = fs.existsSync(destPath);
|
|
390
|
+
fs.copyFileSync(srcPath, destPath);
|
|
391
|
+
if (existed) {
|
|
392
|
+
stats.updated++;
|
|
393
|
+
} else {
|
|
394
|
+
stats.created++;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
// Skip docs/plans/ and docs/superpowers/ — project-specific
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
175
401
|
// Create docs/plans directory
|
|
176
402
|
var plansDir = path.join(targetDir, 'docs', 'plans');
|
|
177
403
|
if (!fs.existsSync(plansDir)) {
|
|
@@ -180,6 +406,7 @@ async function main() {
|
|
|
180
406
|
|
|
181
407
|
// Init git if needed
|
|
182
408
|
if (!hasGit) {
|
|
409
|
+
console.log('');
|
|
183
410
|
var initGit = await ask('No git repo found. Initialize one? (yes/no): ');
|
|
184
411
|
if (initGit.toLowerCase() === 'yes' || initGit.toLowerCase() === 'y') {
|
|
185
412
|
try {
|
|
@@ -192,10 +419,17 @@ async function main() {
|
|
|
192
419
|
}
|
|
193
420
|
}
|
|
194
421
|
|
|
422
|
+
// Summary
|
|
195
423
|
console.log('');
|
|
196
424
|
console.log('Setup complete!');
|
|
197
425
|
console.log('');
|
|
198
|
-
console.log('
|
|
426
|
+
console.log(' Created: ' + stats.created + ' files');
|
|
427
|
+
console.log(' Updated: ' + stats.updated + ' files');
|
|
428
|
+
console.log(' Skipped: ' + stats.skipped + ' files (preserved your customizations)');
|
|
429
|
+
if (stats.backedUp > 0) {
|
|
430
|
+
console.log(' Backed up: ' + stats.backedUp + ' files (saved as .backup)');
|
|
431
|
+
}
|
|
432
|
+
console.log('');
|
|
199
433
|
console.log(' .claude/commands/ 10 pipeline commands');
|
|
200
434
|
console.log(' .claude/agents/ 4 specialist agents + template');
|
|
201
435
|
console.log(' .claude/skills/ 2 framework skills');
|
|
@@ -210,6 +444,7 @@ async function main() {
|
|
|
210
444
|
console.log(' 3. Run /start to begin');
|
|
211
445
|
console.log('');
|
|
212
446
|
|
|
447
|
+
cleanupTmpDir(tmpDir);
|
|
213
448
|
rl.close();
|
|
214
449
|
}
|
|
215
450
|
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Files that contain project-specific content and should never be blindly overwritten.
|
|
2
|
+
// All paths are relative to the PROJECT ROOT (not .claude/).
|
|
3
|
+
var PROTECTED_FILES = [
|
|
4
|
+
// Project CLAUDE.md (user may have their own)
|
|
5
|
+
'CLAUDE.md',
|
|
6
|
+
|
|
7
|
+
// Agent knowledge bases (populated per-project by /evolve and architect-agent RECORD)
|
|
8
|
+
'.claude/agents/architect-agent/index.md',
|
|
9
|
+
'.claude/agents/architect-agent/shared/patterns.md',
|
|
10
|
+
'.claude/agents/architect-agent/decisions/log.md',
|
|
11
|
+
|
|
12
|
+
// Tester agent project-specific config
|
|
13
|
+
'.claude/agents/tester-agent/test-patterns.md',
|
|
14
|
+
'.claude/agents/tester-agent/auth-state.md',
|
|
15
|
+
|
|
16
|
+
// Mobile tester project-specific config
|
|
17
|
+
'.claude/agents/mobile-tester-agent/screen-patterns.md',
|
|
18
|
+
|
|
19
|
+
// Project-specific code patterns (generated by /create-rules)
|
|
20
|
+
'.claude/references/code-patterns.md',
|
|
21
|
+
|
|
22
|
+
// Project-specific settings and permissions
|
|
23
|
+
'.claude/settings.local.json',
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
// Directories with project-specific content (architect-agent module knowledge).
|
|
27
|
+
// All paths are relative to the PROJECT ROOT.
|
|
28
|
+
var PROTECTED_DIRS = [
|
|
29
|
+
'.claude/agents/architect-agent/modules',
|
|
30
|
+
'.claude/agents/architect-agent/frontend',
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
// Files that users commonly customize (rules, hooks).
|
|
34
|
+
// These get special "merge" treatment — ask user what to do.
|
|
35
|
+
// All paths are relative to the PROJECT ROOT.
|
|
36
|
+
var CUSTOMIZABLE_FILES = [
|
|
37
|
+
'.claude/rules/_global.md',
|
|
38
|
+
'.claude/rules/backend.md',
|
|
39
|
+
'.claude/rules/frontend.md',
|
|
40
|
+
'.claude/rules/mobile.md',
|
|
41
|
+
'.claude/rules/database.md',
|
|
42
|
+
'.claude/rules/testing.md',
|
|
43
|
+
'.claude/hooks/branch-guard.sh',
|
|
44
|
+
'.claude/hooks/plan-required.sh',
|
|
45
|
+
'.claude/hooks/architect-sync.sh',
|
|
46
|
+
'.claude/hooks/evolve-reminder.sh',
|
|
47
|
+
'.claude/hooks/session-primer.sh',
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
// Helper: normalize a path to project-root-relative format.
|
|
51
|
+
// Call sites that compare against these lists MUST use this function
|
|
52
|
+
// to ensure the path prefix matches (e.g., '.claude/rules/backend.md').
|
|
53
|
+
function toProjectRelative(filePath, rootDir) {
|
|
54
|
+
var path = require('path');
|
|
55
|
+
var abs = path.resolve(filePath);
|
|
56
|
+
var root = path.resolve(rootDir);
|
|
57
|
+
return path.relative(root, abs).split(path.sep).join('/');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = {
|
|
61
|
+
PROTECTED_FILES: PROTECTED_FILES,
|
|
62
|
+
PROTECTED_DIRS: PROTECTED_DIRS,
|
|
63
|
+
CUSTOMIZABLE_FILES: CUSTOMIZABLE_FILES,
|
|
64
|
+
toProjectRelative: toProjectRelative,
|
|
65
|
+
};
|
package/cli/update.js
CHANGED
|
@@ -1,58 +1,123 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const { execFileSync } = require('child_process');
|
|
4
|
+
const readline = require('readline');
|
|
5
|
+
const { PROTECTED_FILES, PROTECTED_DIRS, CUSTOMIZABLE_FILES, toProjectRelative } = require('./protected-files');
|
|
4
6
|
|
|
5
7
|
const REPO = 'cristian-robert/AIDevelopmentFramework';
|
|
6
8
|
const BRANCH = 'main';
|
|
7
9
|
const TARBALL_URL = 'https://github.com/' + REPO + '/archive/refs/heads/' + BRANCH + '.tar.gz';
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
var
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
11
|
+
const rl = readline.createInterface({
|
|
12
|
+
input: process.stdin,
|
|
13
|
+
output: process.stdout,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
function ask(question) {
|
|
17
|
+
return new Promise(function (resolve) {
|
|
18
|
+
rl.question(question, resolve);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isTemplateContent(filePath) {
|
|
23
|
+
try {
|
|
24
|
+
var content = fs.readFileSync(filePath, 'utf-8');
|
|
25
|
+
if (content.includes('> Populated by /create-rules')) return true;
|
|
26
|
+
if (content.includes('> Populated when /create-rules')) return true;
|
|
27
|
+
if (content.includes('> No decisions recorded yet')) return true;
|
|
28
|
+
if (content.includes('Run `/create-rules` to populate')) return true;
|
|
29
|
+
if (content.includes('> This section is populated as the project grows')) return true;
|
|
30
|
+
return false;
|
|
31
|
+
} catch (e) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function copyDirWithProtection(src, dest, projectRoot, customizedAction) {
|
|
28
37
|
if (!fs.existsSync(dest)) {
|
|
29
38
|
fs.mkdirSync(dest, { recursive: true });
|
|
30
39
|
}
|
|
31
40
|
var entries = fs.readdirSync(src, { withFileTypes: true });
|
|
41
|
+
var stats = { updated: 0, skipped: 0, backedUp: 0 };
|
|
32
42
|
for (var i = 0; i < entries.length; i++) {
|
|
33
43
|
var entry = entries[i];
|
|
34
44
|
var srcPath = path.join(src, entry.name);
|
|
35
45
|
var destPath = path.join(dest, entry.name);
|
|
36
|
-
var
|
|
46
|
+
var projectRelPath = toProjectRelative(destPath, projectRoot);
|
|
37
47
|
|
|
38
48
|
if (entry.isDirectory()) {
|
|
39
|
-
|
|
40
|
-
|
|
49
|
+
var isProtectedDir = PROTECTED_DIRS.some(function (d) {
|
|
50
|
+
return projectRelPath === d || projectRelPath.startsWith(d + '/');
|
|
51
|
+
});
|
|
52
|
+
if (isProtectedDir && fs.existsSync(destPath)) {
|
|
53
|
+
console.log(' Skipped (project-specific): ' + projectRelPath + '/');
|
|
54
|
+
try {
|
|
55
|
+
var dirFiles = fs.readdirSync(destPath, { recursive: true });
|
|
56
|
+
stats.skipped += dirFiles.length || 1;
|
|
57
|
+
} catch (e) {
|
|
58
|
+
stats.skipped++;
|
|
59
|
+
}
|
|
41
60
|
continue;
|
|
42
61
|
}
|
|
43
|
-
|
|
62
|
+
var sub = copyDirWithProtection(srcPath, destPath, projectRoot, customizedAction);
|
|
63
|
+
stats.updated += sub.updated;
|
|
64
|
+
stats.skipped += sub.skipped;
|
|
65
|
+
stats.backedUp += sub.backedUp;
|
|
44
66
|
} else {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
67
|
+
if (!fs.existsSync(destPath)) {
|
|
68
|
+
// New file — always install
|
|
69
|
+
var destDir = path.dirname(destPath);
|
|
70
|
+
if (!fs.existsSync(destDir)) {
|
|
71
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
72
|
+
}
|
|
73
|
+
fs.copyFileSync(srcPath, destPath);
|
|
74
|
+
stats.updated++;
|
|
48
75
|
continue;
|
|
49
76
|
}
|
|
77
|
+
|
|
78
|
+
// Protected file — skip if it has real content, update if still template
|
|
79
|
+
if (PROTECTED_FILES.indexOf(projectRelPath) !== -1) {
|
|
80
|
+
if (!isTemplateContent(destPath)) {
|
|
81
|
+
console.log(' Skipped (project-specific): ' + projectRelPath);
|
|
82
|
+
stats.skipped++;
|
|
83
|
+
} else {
|
|
84
|
+
fs.copyFileSync(srcPath, destPath);
|
|
85
|
+
stats.updated++;
|
|
86
|
+
}
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Customizable file — apply chosen action
|
|
91
|
+
if (CUSTOMIZABLE_FILES.indexOf(projectRelPath) !== -1) {
|
|
92
|
+
var srcContent = fs.readFileSync(srcPath, 'utf-8');
|
|
93
|
+
var destContent = fs.readFileSync(destPath, 'utf-8');
|
|
94
|
+
if (srcContent !== destContent) {
|
|
95
|
+
if (customizedAction === 'keep') {
|
|
96
|
+
console.log(' Skipped (customized): ' + projectRelPath);
|
|
97
|
+
stats.skipped++;
|
|
98
|
+
continue;
|
|
99
|
+
} else if (customizedAction === 'backup') {
|
|
100
|
+
var timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
101
|
+
var backupPath = destPath + '.' + timestamp + '.backup';
|
|
102
|
+
fs.copyFileSync(destPath, backupPath);
|
|
103
|
+
console.log(' Backed up: ' + projectRelPath);
|
|
104
|
+
fs.copyFileSync(srcPath, destPath);
|
|
105
|
+
stats.backedUp++;
|
|
106
|
+
stats.updated++;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
// 'overwrite' falls through
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
50
113
|
fs.copyFileSync(srcPath, destPath);
|
|
114
|
+
stats.updated++;
|
|
51
115
|
}
|
|
52
116
|
}
|
|
117
|
+
return stats;
|
|
53
118
|
}
|
|
54
119
|
|
|
55
|
-
function main() {
|
|
120
|
+
async function main() {
|
|
56
121
|
console.log('');
|
|
57
122
|
console.log(' AIDevelopmentFramework — Update');
|
|
58
123
|
console.log('');
|
|
@@ -62,6 +127,7 @@ function main() {
|
|
|
62
127
|
process.exit(1);
|
|
63
128
|
}
|
|
64
129
|
|
|
130
|
+
var projectRoot = process.cwd();
|
|
65
131
|
var tmpDir = path.join(require('os').tmpdir(), 'ai-framework-update-' + Date.now());
|
|
66
132
|
fs.mkdirSync(tmpDir, { recursive: true });
|
|
67
133
|
|
|
@@ -71,40 +137,63 @@ function main() {
|
|
|
71
137
|
execFileSync('curl', ['-sL', TARBALL_URL, '-o', path.join(tmpDir, 'framework.tar.gz')]);
|
|
72
138
|
execFileSync('tar', ['-xzf', path.join(tmpDir, 'framework.tar.gz'), '-C', tmpDir, '--strip-components=1']);
|
|
73
139
|
|
|
140
|
+
// Ask about customized files
|
|
141
|
+
var customizedAction = 'keep';
|
|
142
|
+
console.log('');
|
|
143
|
+
var choice = await ask(
|
|
144
|
+
' How should customized rules/hooks be handled?\n' +
|
|
145
|
+
' 1. Keep mine — preserve your customizations (default)\n' +
|
|
146
|
+
' 2. Use framework — overwrite with latest framework versions\n' +
|
|
147
|
+
' 3. Backup + update — save yours as .backup, install new versions\n' +
|
|
148
|
+
' Choice (1/2/3): '
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
if (choice === '2') customizedAction = 'overwrite';
|
|
152
|
+
else if (choice === '3') customizedAction = 'backup';
|
|
153
|
+
|
|
74
154
|
var sourceClaudeDir = path.join(tmpDir, '.claude');
|
|
75
|
-
var targetClaudeDir = path.join(
|
|
155
|
+
var targetClaudeDir = path.join(projectRoot, '.claude');
|
|
76
156
|
|
|
77
157
|
if (fs.existsSync(sourceClaudeDir)) {
|
|
78
|
-
console.log('
|
|
79
|
-
|
|
80
|
-
|
|
158
|
+
console.log('');
|
|
159
|
+
console.log('Updating .claude/ ...');
|
|
160
|
+
console.log('');
|
|
161
|
+
var stats = copyDirWithProtection(sourceClaudeDir, targetClaudeDir, projectRoot, customizedAction);
|
|
81
162
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
)
|
|
163
|
+
// Update docs (but not docs/plans/ or docs/superpowers/)
|
|
164
|
+
var sourceDocsDir = path.join(tmpDir, 'docs');
|
|
165
|
+
var targetDocsDir = path.join(projectRoot, 'docs');
|
|
166
|
+
if (fs.existsSync(sourceDocsDir)) {
|
|
167
|
+
console.log('');
|
|
168
|
+
console.log('Updating docs/...');
|
|
169
|
+
if (!fs.existsSync(targetDocsDir)) {
|
|
170
|
+
fs.mkdirSync(targetDocsDir, { recursive: true });
|
|
171
|
+
}
|
|
172
|
+
var docEntries = fs.readdirSync(sourceDocsDir, { withFileTypes: true });
|
|
173
|
+
for (var i = 0; i < docEntries.length; i++) {
|
|
174
|
+
var entry = docEntries[i];
|
|
175
|
+
if (entry.isFile()) {
|
|
176
|
+
fs.copyFileSync(
|
|
177
|
+
path.join(sourceDocsDir, entry.name),
|
|
178
|
+
path.join(targetDocsDir, entry.name)
|
|
179
|
+
);
|
|
180
|
+
stats.updated++;
|
|
181
|
+
}
|
|
95
182
|
}
|
|
96
|
-
// Skip docs/plans/ and docs/superpowers/ — project-specific
|
|
97
183
|
}
|
|
98
|
-
}
|
|
99
184
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
185
|
+
console.log('');
|
|
186
|
+
console.log('Update complete!');
|
|
187
|
+
console.log('');
|
|
188
|
+
console.log(' Updated: ' + stats.updated + ' files');
|
|
189
|
+
console.log(' Skipped: ' + stats.skipped + ' files (preserved your customizations)');
|
|
190
|
+
if (stats.backedUp > 0) {
|
|
191
|
+
console.log(' Backed up: ' + stats.backedUp + ' files (saved as .backup)');
|
|
192
|
+
}
|
|
193
|
+
console.log('');
|
|
194
|
+
console.log('Run /setup to check if new plugins are required.');
|
|
195
|
+
console.log('');
|
|
196
|
+
}
|
|
108
197
|
} catch (err) {
|
|
109
198
|
console.error('Update failed: ' + err.message);
|
|
110
199
|
process.exit(1);
|
|
@@ -112,8 +201,9 @@ function main() {
|
|
|
112
201
|
try {
|
|
113
202
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
114
203
|
} catch (e) {
|
|
115
|
-
// ignore
|
|
204
|
+
// ignore
|
|
116
205
|
}
|
|
206
|
+
rl.close();
|
|
117
207
|
}
|
|
118
208
|
}
|
|
119
209
|
|
package/package.json
CHANGED