@axiomatic-labs/claudeflow 2.49.23 → 2.49.25

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 +147 -16
  2. package/package.json +1 -1
package/lib/install.js CHANGED
@@ -27,7 +27,7 @@ const PER_PROJECT_CONFIGS = ['brand.json', 'risk-signals.json', 'test-context.js
27
27
  // Paths appended to the project's .gitignore (add-if-absent). These hold TEST credentials / Playwright
28
28
  // storage state — they must NEVER be committed. test-context.json is per-project runtime state; .auth/
29
29
  // holds storageState files the orchestrator seeds for auth-gated functional verification.
30
- const GITIGNORE_LINES = ['__pycache__/', '*.pyc', '.claudeflow/test-context.json', '.claudeflow/.auth/', '.claudeflow/telemetry/', '.claudeflow/certification/', '.claudeflow/tools/'];
30
+ const GITIGNORE_LINES = ['__pycache__/', '*.pyc', '.claudeflow/test-context.json', '.claudeflow/.auth/', '.claudeflow/telemetry/', '.claudeflow/certification/', '.claudeflow/tools/', '.claudeflow/backups/'];
31
31
  const SHARED_CONFIGS = ['playbook-map.json', 'agent-map.json']; // always refreshed (shared system)
32
32
  const CODEX_DOCS = ['claudeflow-codex-migration.md', 'codexflow-codex-native-workflow.md', 'codexflow-local-actions.md'];
33
33
 
@@ -272,6 +272,110 @@ function copyPath(src, dst) {
272
272
  }
273
273
  }
274
274
 
275
+ function safeRelative(base, target) {
276
+ const rel = path.relative(base, target);
277
+ if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) return path.basename(target);
278
+ return rel;
279
+ }
280
+
281
+ function uniqueBackupPath(backupRoot, rel) {
282
+ const parsed = path.parse(rel);
283
+ let candidate = path.join(backupRoot, rel);
284
+ let i = 1;
285
+ while (fs.existsSync(candidate)) {
286
+ candidate = path.join(backupRoot, parsed.dir, `${parsed.name}.${i}${parsed.ext}`);
287
+ i++;
288
+ }
289
+ return candidate;
290
+ }
291
+
292
+ function moveAsideManagedPath(target, backupRoot, relBase) {
293
+ const rel = safeRelative(relBase, target);
294
+ const backup = uniqueBackupPath(backupRoot, rel);
295
+ fs.mkdirSync(path.dirname(backup), { recursive: true });
296
+ fs.renameSync(target, backup);
297
+ return backup;
298
+ }
299
+
300
+ function makeWritableTree(target) {
301
+ if (!fs.existsSync(target)) return;
302
+ const st = fs.lstatSync(target);
303
+ if (st.isDirectory()) {
304
+ try { fs.chmodSync(target, 0o755); } catch {}
305
+ for (const name of fs.readdirSync(target)) makeWritableTree(path.join(target, name));
306
+ try { fs.chmodSync(target, 0o755); } catch {}
307
+ return;
308
+ }
309
+ if (st.isFile()) {
310
+ try { fs.chmodSync(target, 0o644); } catch {}
311
+ }
312
+ }
313
+
314
+ function replaceManagedPath(src, dst, backupRoot, relBase) {
315
+ let movedAside = null;
316
+ if (fs.existsSync(dst)) {
317
+ try {
318
+ rmrf(dst);
319
+ } catch {
320
+ // Recover user-owned managed paths that lost write bits. Root-owned paths still require
321
+ // a one-time chown; the caller reports that explicitly instead of hiding the failure.
322
+ try { makeWritableTree(dst); rmrf(dst); }
323
+ catch { movedAside = moveAsideManagedPath(dst, backupRoot, relBase || path.dirname(dst)); }
324
+ }
325
+ }
326
+ copyPath(src, dst);
327
+ return { movedAside };
328
+ }
329
+
330
+ function chownTree(target, uid, gid) {
331
+ if (!fs.existsSync(target)) return 0;
332
+ let count = 0;
333
+ const st = fs.lstatSync(target);
334
+ if (st.isDirectory() && !st.isSymbolicLink()) {
335
+ for (const name of fs.readdirSync(target)) count += chownTree(path.join(target, name), uid, gid);
336
+ }
337
+ try { fs.chownSync(target, uid, gid); count++; } catch {}
338
+ return count;
339
+ }
340
+
341
+ function addExisting(paths, p) {
342
+ if (fs.existsSync(p)) paths.push(p);
343
+ }
344
+
345
+ function addExistingChildren(paths, dir, predicate) {
346
+ if (!fs.existsSync(dir)) return;
347
+ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
348
+ if (predicate(e)) paths.push(path.join(dir, e.name));
349
+ }
350
+ }
351
+
352
+ function repairSudoOwnership(cwd, options = {}) {
353
+ const isRoot = typeof process.getuid === 'function' && process.getuid() === 0;
354
+ if (!isRoot && options.forceSudoOwnershipRepair !== true) return 0;
355
+ const uid = parseInt(options.sudoUid || process.env.SUDO_UID || '', 10);
356
+ const gid = parseInt(options.sudoGid || process.env.SUDO_GID || '', 10);
357
+ if (!Number.isInteger(uid) || uid <= 0 || !Number.isInteger(gid) || gid < 0) return 0;
358
+
359
+ const paths = [];
360
+ addExisting(paths, path.join(cwd, '.claudeflow'));
361
+ addExisting(paths, path.join(cwd, 'scripts', 'claudeflow'));
362
+ addExisting(paths, path.join(cwd, '.claude', 'settings.json'));
363
+ addExistingChildren(paths, path.join(cwd, '.claude', 'skills'), e => e.isDirectory() && e.name.startsWith('claudeflow-'));
364
+ addExistingChildren(paths, path.join(cwd, '.claude', 'agents'), e => e.isFile() && e.name.startsWith('claudeflow-') && e.name.endsWith('.md'));
365
+ addExistingChildren(paths, path.join(cwd, '.agents', 'skills'), e => e.isDirectory() && e.name.startsWith('claudeflow-'));
366
+ addExisting(paths, path.join(cwd, '.agents', 'plugins', 'marketplace.json'));
367
+ addExisting(paths, path.join(cwd, '.codex', 'hooks.json'));
368
+ addExisting(paths, path.join(cwd, '.codex', 'config.toml'));
369
+ addExistingChildren(paths, path.join(cwd, '.codex', 'agents'), e => e.isFile() && e.name.startsWith('claudeflow-') && e.name.endsWith('.toml'));
370
+ addExisting(paths, path.join(cwd, 'plugins', 'codexflow'));
371
+ for (const doc of CODEX_DOCS) addExisting(paths, path.join(cwd, 'docs', doc));
372
+ for (const f of ['CLAUDE.md', 'AGENTS.md', '.mcp.json', '.gitignore']) addExisting(paths, path.join(cwd, f));
373
+
374
+ let count = 0;
375
+ for (const p of [...new Set(paths)]) count += chownTree(p, uid, gid);
376
+ return count;
377
+ }
378
+
275
379
  function pathHash(p) {
276
380
  if (!fs.existsSync(p)) return null;
277
381
  const h = crypto.createHash('sha256');
@@ -306,8 +410,8 @@ function syncManagedGlobalDir(srcDir, dstDir, backupRoot, filter) {
306
410
  copyPath(d, path.join(backupRoot, path.basename(dstDir), e.name));
307
411
  out.backups++;
308
412
  }
309
- rmrf(d);
310
- copyPath(s, d);
413
+ const r = replaceManagedPath(s, d, backupRoot, dstDir);
414
+ if (r.movedAside) out.backups++;
311
415
  out.changed++;
312
416
  } catch {
313
417
  out.failed++;
@@ -606,6 +710,8 @@ function installFromPayload(src, cwd, options = {}) {
606
710
  globalAgents: 0,
607
711
  globalChanged: 0,
608
712
  globalBackups: 0,
713
+ localBackups: 0,
714
+ ownershipRepaired: 0,
609
715
  codexSkills: 0,
610
716
  codexAgents: 0,
611
717
  codexHooks: false,
@@ -627,15 +733,26 @@ function installFromPayload(src, cwd, options = {}) {
627
733
  // two steps a user most needs (the env block wires agent-teams + the autocompact window, etc.). Before this,
628
734
  // any throw in steps 1–4 silently left settings/CLAUDE.md unconfigured. Each step records its name on failure
629
735
  // so `run()` can warn instead of reporting a clean success over a partial install.
630
- const step = (name, fn) => { try { fn(); } catch { out.failed.push(name); } };
736
+ const step = (name, fn) => { try { fn(); } catch (err) { out.failed.push(`${name}${err && err.code ? ':' + err.code : ''}`); } };
737
+ const stamp = new Date().toISOString().replace(/\D/g, '').slice(0, 14) + '-' + process.pid;
738
+ const backupRoot = path.join(cwd, '.claudeflow', 'backups', `install-${stamp}`);
739
+ const replace = (s, d, relBase, label) => {
740
+ try {
741
+ const r = replaceManagedPath(s, d, backupRoot, relBase || cwd);
742
+ if (r.movedAside) out.localBackups++;
743
+ return true;
744
+ } catch (err) {
745
+ out.failed.push(`${label || path.relative(cwd, d)}${err && err.code ? ':' + err.code : ''}`);
746
+ return false;
747
+ }
748
+ };
631
749
 
632
750
  // 1) Playbooks — always refreshed (shared system).
633
751
  step('playbooks', () => {
634
752
  const srcPlaybooks = path.join(src, '.claudeflow', 'playbooks');
635
753
  if (fs.existsSync(srcPlaybooks)) {
636
754
  const dst = path.join(cwd, '.claudeflow', 'playbooks');
637
- rmrf(dst); copyDir(srcPlaybooks, dst);
638
- out.playbooks = countFiles(dst);
755
+ if (replace(srcPlaybooks, dst, cwd, 'playbooks')) out.playbooks = countFiles(dst);
639
756
  }
640
757
  });
641
758
 
@@ -648,7 +765,7 @@ function installFromPayload(src, cwd, options = {}) {
648
765
  }
649
766
  for (const c of SHARED_CONFIGS) {
650
767
  const s = path.join(src, '.claudeflow', c);
651
- if (fs.existsSync(s)) fs.copyFileSync(s, path.join(cwd, '.claudeflow', c));
768
+ if (fs.existsSync(s)) replace(s, path.join(cwd, '.claudeflow', c), cwd, `configs:${c}`);
652
769
  }
653
770
  });
654
771
 
@@ -671,7 +788,7 @@ function installFromPayload(src, cwd, options = {}) {
671
788
  const dst = path.join(cwd, 'scripts', 'claudeflow');
672
789
  fs.mkdirSync(dst, { recursive: true });
673
790
  for (const e of fs.readdirSync(srcScripts)) {
674
- try { fs.copyFileSync(path.join(srcScripts, e), path.join(dst, e)); out.scripts++; } catch {}
791
+ if (replace(path.join(srcScripts, e), path.join(dst, e), cwd, `scripts:${e}`)) out.scripts++;
675
792
  }
676
793
  }
677
794
  });
@@ -685,7 +802,7 @@ function installFromPayload(src, cwd, options = {}) {
685
802
  for (const e of fs.readdirSync(srcSkills, { withFileTypes: true })) {
686
803
  if (!e.isDirectory() || !e.name.startsWith('claudeflow-')) continue;
687
804
  const d = path.join(dst, e.name);
688
- try { rmrf(d); copyDir(path.join(srcSkills, e.name), d); out.skills++; } catch {}
805
+ if (replace(path.join(srcSkills, e.name), d, cwd, `skills:${e.name}`)) out.skills++;
689
806
  }
690
807
  }
691
808
  });
@@ -698,7 +815,7 @@ function installFromPayload(src, cwd, options = {}) {
698
815
  fs.mkdirSync(dst, { recursive: true });
699
816
  for (const e of fs.readdirSync(srcAgents, { withFileTypes: true })) {
700
817
  if (!e.isFile() || !e.name.startsWith('claudeflow-') || !e.name.endsWith('.md')) continue;
701
- try { fs.copyFileSync(path.join(srcAgents, e.name), path.join(dst, e.name)); out.agents++; } catch {}
818
+ if (replace(path.join(srcAgents, e.name), path.join(dst, e.name), cwd, `agents:${e.name}`)) out.agents++;
702
819
  }
703
820
  }
704
821
  });
@@ -725,7 +842,7 @@ function installFromPayload(src, cwd, options = {}) {
725
842
  for (const e of fs.readdirSync(srcSkills, { withFileTypes: true })) {
726
843
  if (!e.isDirectory() || !e.name.startsWith('claudeflow-')) continue;
727
844
  const d = path.join(dst, e.name);
728
- try { rmrf(d); copyDir(path.join(srcSkills, e.name), d); out.codexSkills++; } catch {}
845
+ if (replace(path.join(srcSkills, e.name), d, cwd, `codex-skills:${e.name}`)) out.codexSkills++;
729
846
  }
730
847
  }
731
848
  });
@@ -738,7 +855,7 @@ function installFromPayload(src, cwd, options = {}) {
738
855
  fs.mkdirSync(dst, { recursive: true });
739
856
  for (const e of fs.readdirSync(srcAgents, { withFileTypes: true })) {
740
857
  if (!e.isFile() || !e.name.startsWith('claudeflow-') || !e.name.endsWith('.toml')) continue;
741
- try { fs.copyFileSync(path.join(srcAgents, e.name), path.join(dst, e.name)); out.codexAgents++; } catch {}
858
+ if (replace(path.join(srcAgents, e.name), path.join(dst, e.name), cwd, `codex-agents:${e.name}`)) out.codexAgents++;
742
859
  }
743
860
  }
744
861
  });
@@ -803,8 +920,7 @@ function installFromPayload(src, cwd, options = {}) {
803
920
  const srcPlugin = path.join(src, 'plugins', 'codexflow');
804
921
  if (fs.existsSync(srcPlugin)) {
805
922
  const dst = path.join(cwd, 'plugins', 'codexflow');
806
- rmrf(dst); copyDir(srcPlugin, dst);
807
- out.codexPlugin = countFiles(dst);
923
+ if (replace(srcPlugin, dst, cwd, 'codex-plugin')) out.codexPlugin = countFiles(dst);
808
924
  }
809
925
  });
810
926
 
@@ -817,7 +933,7 @@ function installFromPayload(src, cwd, options = {}) {
817
933
  for (const doc of CODEX_DOCS) {
818
934
  const s = path.join(srcDocs, doc);
819
935
  if (!fs.existsSync(s)) continue;
820
- try { fs.copyFileSync(s, path.join(dstDocs, doc)); out.docs++; } catch {}
936
+ if (replace(s, path.join(dstDocs, doc), cwd, `docs:${doc}`)) out.docs++;
821
937
  }
822
938
  }
823
939
  });
@@ -825,6 +941,11 @@ function installFromPayload(src, cwd, options = {}) {
825
941
  // 12) .gitignore — ensure runtime and credential paths are ignored (add-if-absent; never commit creds).
826
942
  step('gitignore', () => { out.gitignore = mergeGitignore(cwd, GITIGNORE_LINES); });
827
943
 
944
+ // 13) If the user ran `sudo npx ... install`, avoid leaving the managed framework owned by root.
945
+ step('sudo-ownership', () => {
946
+ if (options.repairSudoOwnership === true) out.ownershipRepaired = repairSudoOwnership(cwd, options);
947
+ });
948
+
828
949
  return out;
829
950
  }
830
951
 
@@ -884,6 +1005,7 @@ async function run() {
884
1005
  syncGlobalClaude: process.env.CLAUDEFLOW_NO_GLOBAL_SYNC !== '1',
885
1006
  configureTools: true,
886
1007
  configureSerena: true,
1008
+ repairSudoOwnership: true,
887
1009
  });
888
1010
  writeLocalVersion(`v${version}`);
889
1011
 
@@ -892,10 +1014,12 @@ async function run() {
892
1014
  ui.success(`claudeflow v${version} installed`);
893
1015
  ui.info(` ${summary.playbooks} playbooks · ${summary.scripts} scripts · ${summary.skills} skills`
894
1016
  + (summary.globalSkills || summary.globalAgents
895
- ? ` · global Claude repaired (${summary.globalSkills} skills, ${summary.globalAgents} agents`
1017
+ ? ` · global Claude repaired (${summary.globalSkills} skills, ${summary.globalAgents} agents`
896
1018
  + (summary.globalBackups ? `, ${summary.globalBackups} backups` : '')
897
1019
  + ')'
898
1020
  : '')
1021
+ + (summary.localBackups ? ` · ${summary.localBackups} local stale path${summary.localBackups > 1 ? 's' : ''} moved to backup` : '')
1022
+ + (summary.ownershipRepaired ? ` · ownership restored on ${summary.ownershipRepaired} path${summary.ownershipRepaired > 1 ? 's' : ''}` : '')
899
1023
  + (summary.codexSkills ? ` · ${summary.codexSkills} Codex skills` : '')
900
1024
  + (summary.codexAgents ? ` · ${summary.codexAgents} Codex agents` : '')
901
1025
  + (summary.codexHooks ? ' · Codex hooks merged' : '')
@@ -909,6 +1033,12 @@ async function run() {
909
1033
  + (summary.agentsMd ? ' · AGENTS.md guidance synced' : ''));
910
1034
  if (summary.failed && summary.failed.length) {
911
1035
  ui.warn(` some steps did not complete: ${summary.failed.join(', ')}. Re-run \`npx @axiomatic-labs/claudeflow@latest install\` (the @latest avoids a stale npx cache).`);
1036
+ if (summary.failed.some(f => /:EACCES|:EPERM/.test(f))) {
1037
+ ui.warn(' Permission repair: one or more managed claudeflow paths are not writable by your user.');
1038
+ ui.warn(' From the project root, run once:');
1039
+ ui.warn(' sudo chown -R "$(whoami):$(id -gn)" .claudeflow scripts/claudeflow .claude/skills/claudeflow-* .claude/agents/claudeflow-* .agents/skills/claudeflow-* .codex/agents/claudeflow-* plugins/codexflow');
1040
+ ui.warn(' Then re-run: npx @axiomatic-labs/claudeflow@latest install');
1041
+ }
912
1042
  }
913
1043
  ui.done(`Describe a change and the ${ui.BOLD}claudeflow-implementer${ui.RESET} agent will build it.`);
914
1044
  }
@@ -928,5 +1058,6 @@ module.exports.optionalSerenaCommand = optionalSerenaCommand;
928
1058
  module.exports.serenaMcpServer = serenaMcpServer;
929
1059
  module.exports.serenaCodexToml = serenaCodexToml;
930
1060
  module.exports.ensureManagedTools = ensureManagedTools;
1061
+ module.exports.repairSudoOwnership = repairSudoOwnership;
931
1062
  module.exports.ensureSerenaTool = ensureSerenaTool;
932
1063
  module.exports.ensureAstGrepTool = ensureAstGrepTool;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axiomatic-labs/claudeflow",
3
- "version": "2.49.23",
3
+ "version": "2.49.25",
4
4
  "description": "Claudeflow — AI-powered development toolkit for Claude Code and Codex. Skills, agents, hooks, and quality gates that ship production apps.",
5
5
  "bin": {
6
6
  "claudeflow": "./bin/cli.js"