@axiomatic-labs/claudeflow 2.10.71 → 2.10.73

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/lib/install.js +273 -1
  2. package/package.json +1 -1
package/lib/install.js CHANGED
@@ -9,11 +9,17 @@ const ui = require('./ui.js');
9
9
  // Template skills are shipped inside the ZIP. Copy every claudeflow-* skill
10
10
  // from the release so template subskills stay in sync, while preserving any
11
11
  // user-generated skills that are not part of the shipped asset.
12
+ const TEMPLATE_AGENTS = new Set(['claudeflow-explorer', 'claudeflow-planner', 'example-agent']);
13
+ const TEMPLATE_MANAGED_MANIFEST = path.join('.claude', 'config', 'template-managed-manifest.json');
14
+ const CLAUDE_BACKUP_ROOT = '.claudeflow-backups';
12
15
 
13
16
  async function run() {
14
17
  const current = readLocalVersion();
15
18
  const isUpdate = !!current;
16
19
  let copiedTemplateSkillCount = 0;
20
+ let staleRemovedCount = 0;
21
+ let managedPathCount = 0;
22
+ let backupPath = null;
17
23
 
18
24
  ui.banner(current || undefined);
19
25
 
@@ -41,11 +47,27 @@ async function run() {
41
47
  fs.mkdirSync(tmpExtract, { recursive: true });
42
48
 
43
49
  const cwd = process.cwd();
50
+ backupPath = createPreInstallBackup(cwd, {
51
+ isUpdate,
52
+ currentVersion: current,
53
+ });
44
54
 
45
55
  try {
46
56
  ui.step('Extracting template files...');
47
57
  execSync(`unzip -o "${tmpZip}" -d "${tmpExtract}"`, { stdio: 'pipe' });
48
58
  const srcClaude = path.join(tmpExtract, '.claude');
59
+ const nextManagedPaths = collectManagedPathsFromRelease(tmpExtract);
60
+ const previousManifest = readTemplateManagedManifest(cwd);
61
+ const previousManagedPaths = previousManifest
62
+ ? previousManifest.managedPaths
63
+ : collectLegacyManagedPathsFromDisk(cwd);
64
+
65
+ if (isUpdate && !previousManifest) {
66
+ ui.info('No template-managed manifest found. Running aggressive legacy prune for managed paths.');
67
+ }
68
+
69
+ const staleManagedPaths = [...previousManagedPaths].filter((relPath) => !nextManagedPaths.has(relPath));
70
+ staleRemovedCount = pruneStaleManagedPaths(cwd, staleManagedPaths);
49
71
 
50
72
  // Copy settings.json
51
73
  const srcSettings = path.join(srcClaude, 'settings.json');
@@ -91,7 +113,6 @@ async function run() {
91
113
  }
92
114
 
93
115
  // Copy template agents (only template-managed, preserve user agents)
94
- const TEMPLATE_AGENTS = new Set(['claudeflow-explorer', 'claudeflow-planner', 'example-agent']);
95
116
  const srcAgents = path.join(srcClaude, 'agents');
96
117
  const dstAgents = path.join(cwd, '.claude', 'agents');
97
118
  if (fs.existsSync(srcAgents)) {
@@ -128,6 +149,11 @@ async function run() {
128
149
 
129
150
  // Write version file
130
151
  writeLocalVersion(version);
152
+ nextManagedPaths.add(normalizeRelativePath(path.join('.claude', '.claudeflow-version')));
153
+
154
+ // Persist managed path manifest for deterministic stale-file pruning on update
155
+ writeTemplateManagedManifest(cwd, version, nextManagedPaths);
156
+ managedPathCount = nextManagedPaths.size;
131
157
 
132
158
  } finally {
133
159
  fs.unlinkSync(tmpZip);
@@ -161,6 +187,11 @@ async function run() {
161
187
  ui.success(`${skillCount} skills (${copiedTemplateSkillCount} template skills updated, user skills preserved)`);
162
188
  ui.success(`${hookCount} hooks updated`);
163
189
  ui.success(`${docCount} docs updated`);
190
+ ui.success(`${staleRemovedCount} stale template-managed files removed`);
191
+ ui.success(`Template-managed manifest updated (${managedPathCount} paths)`);
192
+ if (backupPath) {
193
+ ui.success(`Backup created: ${path.relative(cwd, backupPath)}`);
194
+ }
164
195
 
165
196
  if (current === version) {
166
197
  ui.done(`Claudeflow ${version} reinstalled.`);
@@ -171,6 +202,10 @@ async function run() {
171
202
  ui.success(`${skillCount} skills installed`);
172
203
  ui.success(`${hookCount} hooks installed`);
173
204
  ui.success(`${docCount} docs installed`);
205
+ ui.success(`Template-managed manifest written (${managedPathCount} paths)`);
206
+ if (backupPath) {
207
+ ui.success(`Backup created: ${path.relative(cwd, backupPath)}`);
208
+ }
174
209
 
175
210
  ui.done(`Claudeflow ${version} installed.`);
176
211
 
@@ -387,6 +422,243 @@ function homedir() {
387
422
  return require('os').homedir();
388
423
  }
389
424
 
425
+ function createPreInstallBackup(projectRoot, options = {}) {
426
+ const sourceClaudeDir = path.join(projectRoot, '.claude');
427
+ if (!fs.existsSync(sourceClaudeDir) || !fs.statSync(sourceClaudeDir).isDirectory()) {
428
+ return null;
429
+ }
430
+
431
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
432
+ const modeLabel = options.isUpdate ? 'update' : 'install';
433
+ const fromVersion = sanitizeForPath(options.currentVersion || 'none');
434
+ const backupDir = path.join(
435
+ projectRoot,
436
+ CLAUDE_BACKUP_ROOT,
437
+ `${timestamp}-${modeLabel}-from-${fromVersion}`,
438
+ '.claude',
439
+ );
440
+
441
+ ui.step('Creating safety backup of .claude...');
442
+ fs.mkdirSync(path.dirname(backupDir), { recursive: true });
443
+ copyDirSync(sourceClaudeDir, backupDir);
444
+ return path.dirname(backupDir);
445
+ }
446
+
447
+ function sanitizeForPath(value) {
448
+ return String(value || 'none').replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/-+/g, '-');
449
+ }
450
+
451
+ function normalizeRelativePath(value) {
452
+ return String(value || '').replace(/\\/g, '/').replace(/^\/+/, '');
453
+ }
454
+
455
+ function isPathInside(candidatePath, parentPath) {
456
+ const relative = path.relative(parentPath, candidatePath);
457
+ return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
458
+ }
459
+
460
+ function collectFilesRecursive(rootDir) {
461
+ if (!fs.existsSync(rootDir)) return [];
462
+ const files = [];
463
+ const stack = [rootDir];
464
+ while (stack.length > 0) {
465
+ const current = stack.pop();
466
+ const entries = fs.readdirSync(current, { withFileTypes: true });
467
+ for (const entry of entries) {
468
+ const fullPath = path.join(current, entry.name);
469
+ if (entry.isDirectory()) {
470
+ stack.push(fullPath);
471
+ } else if (entry.isFile()) {
472
+ files.push(fullPath);
473
+ }
474
+ }
475
+ }
476
+ return files;
477
+ }
478
+
479
+ function addFilesFromDir(set, rootBase, targetDir) {
480
+ for (const filePath of collectFilesRecursive(targetDir)) {
481
+ const relPath = normalizeRelativePath(path.relative(rootBase, filePath));
482
+ if (relPath.startsWith('.claude/')) {
483
+ set.add(relPath);
484
+ }
485
+ }
486
+ }
487
+
488
+ function collectManagedPathsFromRelease(extractRoot) {
489
+ const srcClaude = path.join(extractRoot, '.claude');
490
+ const managed = new Set();
491
+
492
+ const settingsPath = path.join(srcClaude, 'settings.json');
493
+ if (fs.existsSync(settingsPath)) {
494
+ managed.add('.claude/settings.json');
495
+ }
496
+
497
+ ['runtime', 'hooks', 'templates', 'docs'].forEach((folder) => {
498
+ addFilesFromDir(managed, extractRoot, path.join(srcClaude, folder));
499
+ });
500
+
501
+ const skillsDir = path.join(srcClaude, 'skills');
502
+ if (fs.existsSync(skillsDir)) {
503
+ const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
504
+ for (const entry of entries) {
505
+ if (entry.isDirectory() && entry.name.startsWith('claudeflow-')) {
506
+ addFilesFromDir(managed, extractRoot, path.join(skillsDir, entry.name));
507
+ }
508
+ }
509
+ }
510
+
511
+ const agentsDir = path.join(srcClaude, 'agents');
512
+ if (fs.existsSync(agentsDir)) {
513
+ for (const agentName of TEMPLATE_AGENTS) {
514
+ const filePath = path.join(agentsDir, `${agentName}.md`);
515
+ if (fs.existsSync(filePath)) {
516
+ managed.add(normalizeRelativePath(path.join('.claude', 'agents', `${agentName}.md`)));
517
+ }
518
+ }
519
+ }
520
+
521
+ const tmpDir = path.join(srcClaude, 'tmp');
522
+ if (fs.existsSync(tmpDir)) {
523
+ const entries = fs.readdirSync(tmpDir, { withFileTypes: true });
524
+ for (const entry of entries) {
525
+ if (entry.isFile()) {
526
+ managed.add(normalizeRelativePath(path.join('.claude', 'tmp', entry.name)));
527
+ }
528
+ }
529
+ }
530
+
531
+ const versionFile = path.join(srcClaude, '.claudeflow-version');
532
+ if (fs.existsSync(versionFile)) {
533
+ managed.add('.claude/.claudeflow-version');
534
+ }
535
+
536
+ return managed;
537
+ }
538
+
539
+ function collectLegacyManagedPathsFromDisk(projectRoot) {
540
+ const claudeflowRoot = path.join(projectRoot, '.claude');
541
+ const managed = new Set();
542
+
543
+ const settingsPath = path.join(claudeflowRoot, 'settings.json');
544
+ if (fs.existsSync(settingsPath)) {
545
+ managed.add('.claude/settings.json');
546
+ }
547
+
548
+ ['runtime', 'hooks', 'templates', 'docs'].forEach((folder) => {
549
+ addFilesFromDir(managed, projectRoot, path.join(claudeflowRoot, folder));
550
+ });
551
+
552
+ const skillsDir = path.join(claudeflowRoot, 'skills');
553
+ if (fs.existsSync(skillsDir)) {
554
+ const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
555
+ for (const entry of entries) {
556
+ if (entry.isDirectory() && entry.name.startsWith('claudeflow-')) {
557
+ addFilesFromDir(managed, projectRoot, path.join(skillsDir, entry.name));
558
+ }
559
+ }
560
+ }
561
+
562
+ const agentsDir = path.join(claudeflowRoot, 'agents');
563
+ if (fs.existsSync(agentsDir)) {
564
+ for (const agentName of TEMPLATE_AGENTS) {
565
+ const filePath = path.join(agentsDir, `${agentName}.md`);
566
+ if (fs.existsSync(filePath)) {
567
+ managed.add(normalizeRelativePath(path.join('.claude', 'agents', `${agentName}.md`)));
568
+ }
569
+ }
570
+ }
571
+
572
+ const tmpDir = path.join(claudeflowRoot, 'tmp');
573
+ if (fs.existsSync(tmpDir)) {
574
+ const entries = fs.readdirSync(tmpDir, { withFileTypes: true });
575
+ for (const entry of entries) {
576
+ if (entry.isFile() && entry.name.endsWith('.py')) {
577
+ managed.add(normalizeRelativePath(path.join('.claude', 'tmp', entry.name)));
578
+ }
579
+ }
580
+ }
581
+
582
+ const versionFile = path.join(claudeflowRoot, '.claudeflow-version');
583
+ if (fs.existsSync(versionFile)) {
584
+ managed.add('.claude/.claudeflow-version');
585
+ }
586
+
587
+ return managed;
588
+ }
589
+
590
+ function readTemplateManagedManifest(projectRoot) {
591
+ const manifestPath = path.join(projectRoot, TEMPLATE_MANAGED_MANIFEST);
592
+ if (!fs.existsSync(manifestPath)) return null;
593
+
594
+ try {
595
+ const parsed = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
596
+ if (!Array.isArray(parsed.managed_paths)) return null;
597
+ const managedPaths = new Set(
598
+ parsed.managed_paths
599
+ .filter((entry) => typeof entry === 'string')
600
+ .map((entry) => normalizeRelativePath(entry))
601
+ .filter((entry) => entry.startsWith('.claude/')),
602
+ );
603
+ return {
604
+ version: typeof parsed.version === 'string' ? parsed.version : null,
605
+ managedPaths,
606
+ };
607
+ } catch {
608
+ return null;
609
+ }
610
+ }
611
+
612
+ function writeTemplateManagedManifest(projectRoot, version, managedPaths) {
613
+ const manifestPath = path.join(projectRoot, TEMPLATE_MANAGED_MANIFEST);
614
+ fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
615
+ const payload = {
616
+ schema_version: 1,
617
+ version,
618
+ updated_at: new Date().toISOString(),
619
+ managed_paths: [...managedPaths].sort(),
620
+ };
621
+ fs.writeFileSync(manifestPath, `${JSON.stringify(payload, null, 2)}\n`);
622
+ }
623
+
624
+ function pruneStaleManagedPaths(projectRoot, staleRelativePaths) {
625
+ if (!Array.isArray(staleRelativePaths) || staleRelativePaths.length === 0) {
626
+ return 0;
627
+ }
628
+
629
+ const claudeflowRoot = path.resolve(projectRoot, '.claude');
630
+ let removedCount = 0;
631
+ const uniquePaths = [...new Set(staleRelativePaths.map((entry) => normalizeRelativePath(entry)))]
632
+ .filter((entry) => entry.startsWith('.claude/'))
633
+ .sort((a, b) => b.length - a.length);
634
+
635
+ for (const relPath of uniquePaths) {
636
+ const absolutePath = path.resolve(projectRoot, relPath);
637
+ if (!isPathInside(absolutePath, claudeflowRoot)) continue;
638
+ if (!fs.existsSync(absolutePath)) continue;
639
+
640
+ fs.rmSync(absolutePath, { recursive: true, force: true });
641
+ removedCount += 1;
642
+ pruneEmptyAncestors(path.dirname(absolutePath), claudeflowRoot);
643
+ }
644
+
645
+ return removedCount;
646
+ }
647
+
648
+ function pruneEmptyAncestors(startDir, stopDir) {
649
+ let current = startDir;
650
+ while (isPathInside(current, stopDir) && current !== stopDir) {
651
+ if (!fs.existsSync(current)) {
652
+ current = path.dirname(current);
653
+ continue;
654
+ }
655
+ const entries = fs.readdirSync(current);
656
+ if (entries.length > 0) break;
657
+ fs.rmdirSync(current);
658
+ current = path.dirname(current);
659
+ }
660
+ }
661
+
390
662
  // Recursively copy a directory, creating parents as needed
391
663
  function copyDirSync(src, dst) {
392
664
  fs.mkdirSync(dst, { recursive: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axiomatic-labs/claudeflow",
3
- "version": "2.10.71",
3
+ "version": "2.10.73",
4
4
  "description": "Claudeflow — AI-powered development toolkit for Claude Code. Skills, agents, hooks, and quality gates that ship production apps.",
5
5
  "bin": {
6
6
  "claudeflow": "./bin/cli.js"