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 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 copyDirRecursive(src, dest) {
22
- if (!fs.existsSync(dest)) {
23
- fs.mkdirSync(dest, { recursive: true });
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 downloadFramework(targetDir) {
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
- // Copy .claude/ folder
50
- var sourceClaudeDir = path.join(tmpDir, '.claude');
51
- var targetClaudeDir = path.join(targetDir, '.claude');
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
- if (fs.existsSync(sourceClaudeDir)) {
54
- console.log('Installing .claude/ framework structure...');
55
- copyDirRecursive(sourceClaudeDir, targetClaudeDir);
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
- // Copy docs/ folder
59
- var sourceDocsDir = path.join(tmpDir, 'docs');
60
- var targetDocsDir = path.join(targetDir, 'docs');
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
- if (fs.existsSync(sourceDocsDir)) {
63
- console.log('Installing docs/...');
64
- copyDirRecursive(sourceDocsDir, targetDocsDir);
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
- return true;
68
- } catch (err) {
69
- console.error('Download failed: ' + err.message);
70
- console.log('');
71
- console.log('Falling back to local copy...');
72
- return false;
73
- } finally {
74
- // Cleanup tmp
75
- try {
76
- fs.rmSync(tmpDir, { recursive: true, force: true });
77
- } catch (e) {
78
- // ignore cleanup errors
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 read errors
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 from GitHub (latest version)
154
- var downloaded = downloadFramework(targetDir);
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
- var localDocsDir = path.join(frameworkDir, 'docs');
165
- if (fs.existsSync(localDocsDir)) {
166
- copyDirRecursive(localDocsDir, path.join(targetDir, 'docs'));
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 local framework files found. Please check your installation.');
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('Installed:');
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
- // Files that are project-specific and should NOT be overwritten
10
- var PROTECTED_FILES = [
11
- '.claude/agents/architect-agent/index.md',
12
- '.claude/agents/architect-agent/shared/patterns.md',
13
- '.claude/agents/architect-agent/decisions/log.md',
14
- '.claude/agents/tester-agent/test-patterns.md',
15
- '.claude/agents/tester-agent/auth-state.md',
16
- '.claude/agents/mobile-tester-agent/screen-patterns.md',
17
- '.claude/references/code-patterns.md',
18
- '.claude/settings.local.json',
19
- ];
20
-
21
- // Directories with project-specific content that should NOT be overwritten
22
- var PROTECTED_DIRS = [
23
- '.claude/agents/architect-agent/modules',
24
- '.claude/agents/architect-agent/frontend',
25
- ];
26
-
27
- function copyDirRecursive(src, dest, protectedFiles, protectedDirs) {
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 relativePath = path.relative(process.cwd(), destPath);
46
+ var projectRelPath = toProjectRelative(destPath, projectRoot);
37
47
 
38
48
  if (entry.isDirectory()) {
39
- // Skip protected directories entirely
40
- if (protectedDirs.some(function (d) { return relativePath.startsWith(d); })) {
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
- copyDirRecursive(srcPath, destPath, protectedFiles, protectedDirs);
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
- // Skip protected files
46
- if (protectedFiles.indexOf(relativePath) !== -1) {
47
- console.log(' Skipped (project-specific): ' + relativePath);
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(process.cwd(), '.claude');
155
+ var targetClaudeDir = path.join(projectRoot, '.claude');
76
156
 
77
157
  if (fs.existsSync(sourceClaudeDir)) {
78
- console.log('Updating .claude/ (preserving project-specific files)...');
79
- copyDirRecursive(sourceClaudeDir, targetClaudeDir, PROTECTED_FILES, PROTECTED_DIRS);
80
- }
158
+ console.log('');
159
+ console.log('Updating .claude/ ...');
160
+ console.log('');
161
+ var stats = copyDirWithProtection(sourceClaudeDir, targetClaudeDir, projectRoot, customizedAction);
81
162
 
82
- // Update docs (but not docs/plans/ which contains project plans)
83
- var sourceDocsDir = path.join(tmpDir, 'docs');
84
- var targetDocsDir = path.join(process.cwd(), 'docs');
85
- if (fs.existsSync(sourceDocsDir)) {
86
- console.log('Updating docs/...');
87
- var docEntries = fs.readdirSync(sourceDocsDir, { withFileTypes: true });
88
- for (var i = 0; i < docEntries.length; i++) {
89
- var entry = docEntries[i];
90
- if (entry.isFile()) {
91
- fs.copyFileSync(
92
- path.join(sourceDocsDir, entry.name),
93
- path.join(targetDocsDir, entry.name)
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
- console.log('');
101
- console.log('Update complete!');
102
- console.log('');
103
- console.log('Updated: commands, agent protocols, rules templates, hooks, skills, docs');
104
- console.log('Preserved: agent knowledge bases, test patterns, auth state, code patterns, settings, plans');
105
- console.log('');
106
- console.log('Run /setup to check if new plugins are required.');
107
- console.log('');
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 cleanup errors
204
+ // ignore
116
205
  }
206
+ rl.close();
117
207
  }
118
208
  }
119
209
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-development-framework",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "The system around the AI that makes the AI reliable. Structured AI-assisted development with the PIV+E loop.",
5
5
  "bin": {
6
6
  "ai-framework": "cli/index.js"