@sparkleideas/claude-flow-patch 3.1.0-alpha.44.patch.10

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 (120) hide show
  1. package/AGENTS.md +162 -0
  2. package/CLAUDE.md +506 -0
  3. package/README.md +351 -0
  4. package/bin/claude-flow-patch.mjs +148 -0
  5. package/check-patches.sh +195 -0
  6. package/lib/categories.json +15 -0
  7. package/lib/common.py +97 -0
  8. package/lib/discover.mjs +181 -0
  9. package/lib/discover.sh +160 -0
  10. package/package.json +86 -0
  11. package/patch/010-CF-001-doctor-yaml/README.md +11 -0
  12. package/patch/010-CF-001-doctor-yaml/fix.py +20 -0
  13. package/patch/010-CF-001-doctor-yaml/sentinel +1 -0
  14. package/patch/020-CF-002-config-export-yaml/README.md +11 -0
  15. package/patch/020-CF-002-config-export-yaml/fix.py +130 -0
  16. package/patch/020-CF-002-config-export-yaml/sentinel +1 -0
  17. package/patch/030-DM-001-daemon-log-zero/README.md +12 -0
  18. package/patch/030-DM-001-daemon-log-zero/fix.py +37 -0
  19. package/patch/030-DM-001-daemon-log-zero/sentinel +1 -0
  20. package/patch/040-DM-002-cpu-load-threshold/README.md +11 -0
  21. package/patch/040-DM-002-cpu-load-threshold/fix.py +6 -0
  22. package/patch/040-DM-002-cpu-load-threshold/sentinel +1 -0
  23. package/patch/050-DM-003-macos-freemem/README.md +11 -0
  24. package/patch/050-DM-003-macos-freemem/fix.py +7 -0
  25. package/patch/050-DM-003-macos-freemem/sentinel +1 -0
  26. package/patch/060-DM-004-preload-worker-stub/README.md +11 -0
  27. package/patch/060-DM-004-preload-worker-stub/fix.py +34 -0
  28. package/patch/060-DM-004-preload-worker-stub/sentinel +1 -0
  29. package/patch/070-DM-005-consolidation-worker-stub/README.md +11 -0
  30. package/patch/070-DM-005-consolidation-worker-stub/fix.py +46 -0
  31. package/patch/070-DM-005-consolidation-worker-stub/sentinel +1 -0
  32. package/patch/080-EM-001-embedding-ignores-config/README.md +11 -0
  33. package/patch/080-EM-001-embedding-ignores-config/fix.py +111 -0
  34. package/patch/080-EM-001-embedding-ignores-config/sentinel +1 -0
  35. package/patch/090-EM-002-transformers-cache-eacces/README.md +11 -0
  36. package/patch/090-EM-002-transformers-cache-eacces/fix.sh +12 -0
  37. package/patch/090-EM-002-transformers-cache-eacces/sentinel +1 -0
  38. package/patch/100-GV-001-hnsw-ghost-vectors/README.md +11 -0
  39. package/patch/100-GV-001-hnsw-ghost-vectors/fix.py +34 -0
  40. package/patch/100-GV-001-hnsw-ghost-vectors/sentinel +1 -0
  41. package/patch/110-HK-001-post-edit-file-path/README.md +44 -0
  42. package/patch/110-HK-001-post-edit-file-path/fix.py +23 -0
  43. package/patch/110-HK-001-post-edit-file-path/sentinel +1 -0
  44. package/patch/120-HK-002-hooks-tools-stub/README.md +36 -0
  45. package/patch/120-HK-002-hooks-tools-stub/fix.py +155 -0
  46. package/patch/120-HK-002-hooks-tools-stub/sentinel +1 -0
  47. package/patch/130-HK-003-metrics-hardcoded/README.md +30 -0
  48. package/patch/130-HK-003-metrics-hardcoded/fix.py +82 -0
  49. package/patch/130-HK-003-metrics-hardcoded/sentinel +1 -0
  50. package/patch/135-HK-004-respect-daemon-autostart/README.md +11 -0
  51. package/patch/135-HK-004-respect-daemon-autostart/fix.py +14 -0
  52. package/patch/135-HK-004-respect-daemon-autostart/sentinel +1 -0
  53. package/patch/137-HK-005-daemon-pid-guard/README.md +11 -0
  54. package/patch/137-HK-005-daemon-pid-guard/fix.py +53 -0
  55. package/patch/137-HK-005-daemon-pid-guard/sentinel +2 -0
  56. package/patch/140-HW-001-stdin-hang/README.md +11 -0
  57. package/patch/140-HW-001-stdin-hang/fix.py +6 -0
  58. package/patch/140-HW-001-stdin-hang/sentinel +1 -0
  59. package/patch/150-HW-002-failures-swallowed/README.md +11 -0
  60. package/patch/150-HW-002-failures-swallowed/fix.py +42 -0
  61. package/patch/150-HW-002-failures-swallowed/sentinel +1 -0
  62. package/patch/160-HW-003-aggressive-intervals/README.md +11 -0
  63. package/patch/160-HW-003-aggressive-intervals/fix.py +52 -0
  64. package/patch/160-HW-003-aggressive-intervals/sentinel +3 -0
  65. package/patch/170-IN-001-intelligence-stub/README.md +64 -0
  66. package/patch/170-IN-001-intelligence-stub/fix.py +63 -0
  67. package/patch/170-IN-001-intelligence-stub/sentinel +1 -0
  68. package/patch/180-MM-001-memory-persist-path/README.md +27 -0
  69. package/patch/180-MM-001-memory-persist-path/fix.py +54 -0
  70. package/patch/180-MM-001-memory-persist-path/sentinel +1 -0
  71. package/patch/190-NS-001-discovery-default-namespace/README.md +16 -0
  72. package/patch/190-NS-001-discovery-default-namespace/fix.py +68 -0
  73. package/patch/190-NS-001-discovery-default-namespace/sentinel +2 -0
  74. package/patch/200-NS-002-targeted-require-namespace/README.md +19 -0
  75. package/patch/200-NS-002-targeted-require-namespace/fix.py +158 -0
  76. package/patch/200-NS-002-targeted-require-namespace/sentinel +2 -0
  77. package/patch/210-NS-003-namespace-typo-pattern/README.md +15 -0
  78. package/patch/210-NS-003-namespace-typo-pattern/fix.py +23 -0
  79. package/patch/210-NS-003-namespace-typo-pattern/sentinel +1 -0
  80. package/patch/220-RS-001-better-sqlite3-node24/README.md +54 -0
  81. package/patch/220-RS-001-better-sqlite3-node24/fix.py +27 -0
  82. package/patch/220-RS-001-better-sqlite3-node24/rebuild.sh +31 -0
  83. package/patch/220-RS-001-better-sqlite3-node24/sentinel +2 -0
  84. package/patch/230-RV-001-force-learn-tick/README.md +31 -0
  85. package/patch/230-RV-001-force-learn-tick/fix.py +14 -0
  86. package/patch/230-RV-001-force-learn-tick/sentinel +2 -0
  87. package/patch/240-RV-002-trajectory-load/README.md +28 -0
  88. package/patch/240-RV-002-trajectory-load/fix.py +14 -0
  89. package/patch/240-RV-002-trajectory-load/sentinel +2 -0
  90. package/patch/250-RV-003-trajectory-stats-sync/README.md +31 -0
  91. package/patch/250-RV-003-trajectory-stats-sync/fix.py +18 -0
  92. package/patch/250-RV-003-trajectory-stats-sync/sentinel +2 -0
  93. package/patch/260-SG-001-init-settings/README.md +29 -0
  94. package/patch/260-SG-001-init-settings/fix.py +143 -0
  95. package/patch/260-SG-001-init-settings/sentinel +4 -0
  96. package/patch/270-SG-003-init-helpers-all-paths/README.md +60 -0
  97. package/patch/270-SG-003-init-helpers-all-paths/fix.py +165 -0
  98. package/patch/270-SG-003-init-helpers-all-paths/sentinel +3 -0
  99. package/patch/280-UI-001-intelligence-stats-crash/README.md +11 -0
  100. package/patch/280-UI-001-intelligence-stats-crash/fix.py +57 -0
  101. package/patch/280-UI-001-intelligence-stats-crash/sentinel +1 -0
  102. package/patch/290-UI-002-neural-status-not-loaded/README.md +11 -0
  103. package/patch/290-UI-002-neural-status-not-loaded/fix.py +19 -0
  104. package/patch/290-UI-002-neural-status-not-loaded/sentinel +1 -0
  105. package/patch/300-DM-006-log-rotation/README.md +12 -0
  106. package/patch/300-DM-006-log-rotation/fix.py +72 -0
  107. package/patch/300-DM-006-log-rotation/sentinel +2 -0
  108. package/patch/310-HW-004-runwithtimeout-orphan/README.md +11 -0
  109. package/patch/310-HW-004-runwithtimeout-orphan/fix.py +10 -0
  110. package/patch/310-HW-004-runwithtimeout-orphan/sentinel +1 -0
  111. package/patch/320-SG-004-wizard-parity/README.md +40 -0
  112. package/patch/320-SG-004-wizard-parity/fix.py +208 -0
  113. package/patch/320-SG-004-wizard-parity/sentinel +3 -0
  114. package/patch/330-SG-005-start-all-subcommand/README.md +32 -0
  115. package/patch/330-SG-005-start-all-subcommand/fix.py +58 -0
  116. package/patch/330-SG-005-start-all-subcommand/sentinel +1 -0
  117. package/patch-all.sh +199 -0
  118. package/repair-post-init.sh +263 -0
  119. package/scripts/preflight.mjs +249 -0
  120. package/scripts/upstream-log.mjs +257 -0
@@ -0,0 +1,195 @@
1
+ #!/bin/bash
2
+ # check-patches.sh — Dynamic sentinel checker
3
+ # Reads patch/*/sentinel files to verify patches are still applied.
4
+ # On session start: detects wipes, auto-reapplies, warns user.
5
+ #
6
+ # Usage:
7
+ # bash check-patches.sh [--global] [--target <dir>]
8
+ #
9
+ # If neither flag is given, --global is assumed.
10
+
11
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
12
+
13
+ # ── Parse arguments ──
14
+ DO_GLOBAL=0
15
+ TARGET_DIR=""
16
+ while [[ $# -gt 0 ]]; do
17
+ case $1 in
18
+ --global) DO_GLOBAL=1; shift ;;
19
+ --target) TARGET_DIR="${2:-}"; shift 2 ;;
20
+ *) shift ;; # ignore unknown (e.g. filter env handled by caller)
21
+ esac
22
+ done
23
+
24
+ if [[ $DO_GLOBAL -eq 0 && -z "$TARGET_DIR" ]]; then
25
+ DO_GLOBAL=1
26
+ fi
27
+
28
+ # ── Shared discovery ──
29
+ . "$SCRIPT_DIR/lib/discover.sh"
30
+
31
+ # ── Collect installs ──
32
+ INSTALLS=()
33
+
34
+ if [[ $DO_GLOBAL -eq 1 ]]; then
35
+ while IFS= read -r line; do
36
+ [ -n "$line" ] && INSTALLS+=("$line")
37
+ done < <(discover_all_cf_installs)
38
+ fi
39
+
40
+ if [[ -n "$TARGET_DIR" && -d "$TARGET_DIR" ]]; then
41
+ while IFS= read -r line; do
42
+ [ -n "$line" ] && INSTALLS+=("$line")
43
+ done < <(discover_target_installs "$TARGET_DIR")
44
+ fi
45
+
46
+ if [[ ${#INSTALLS[@]} -eq 0 ]]; then
47
+ echo "[PATCHES] WARN: Cannot find claude-flow CLI files"
48
+ exit 0
49
+ fi
50
+
51
+ # ── Path resolver ──
52
+
53
+ resolve_path() {
54
+ local base="$1"
55
+ local rv_base="$2"
56
+ local rs_base="$3"
57
+ local pkg="$4"
58
+ local relpath="$5"
59
+ case "$pkg" in
60
+ ruvector) echo "$rv_base/$relpath" ;;
61
+ ruv-swarm) echo "$rs_base/$relpath" ;;
62
+ *) echo "$base/$relpath" ;;
63
+ esac
64
+ }
65
+
66
+ # ── Check sentinels for a single install ──
67
+ # Returns 0 if all OK, 1 if any failed.
68
+
69
+ check_sentinels_for_install() {
70
+ local base="$1"
71
+ local rv_cli="$2"
72
+ local rs_root="$3"
73
+
74
+ # Derive ruvector base from cli path
75
+ local rv_base=""
76
+ if [ -n "$rv_cli" ]; then
77
+ rv_base="$(cd "$(dirname "$rv_cli")/.." 2>/dev/null && pwd)"
78
+ fi
79
+
80
+ local all_ok=true
81
+
82
+ for sentinel_file in "$SCRIPT_DIR"/patch/*/sentinel; do
83
+ [ -f "$sentinel_file" ] || continue
84
+
85
+ # PATCH_INCLUDE / PATCH_EXCLUDE env vars filter by directory name regex
86
+ local dirname
87
+ dirname=$(basename "$(dirname "$sentinel_file")")
88
+ local matchname="${dirname#[0-9][0-9][0-9]-}" # strip NNN- prefix for pattern matching
89
+ if [ -n "${PATCH_INCLUDE:-}" ] && ! echo "$matchname" | grep -qE "$PATCH_INCLUDE"; then
90
+ continue
91
+ fi
92
+ if [ -n "${PATCH_EXCLUDE:-}" ] && echo "$matchname" | grep -qE "$PATCH_EXCLUDE"; then
93
+ continue
94
+ fi
95
+
96
+ # Read package line (default: claude-flow)
97
+ local pkg="claude-flow"
98
+ local pkg_line
99
+ pkg_line=$(grep -m1 '^package:' "$sentinel_file" 2>/dev/null || true)
100
+ if [ -n "$pkg_line" ]; then
101
+ pkg="${pkg_line#package:}"
102
+ pkg="${pkg#"${pkg%%[![:space:]]*}"}" # trim leading whitespace
103
+ pkg="${pkg%%[[:space:]]*}" # trim trailing whitespace
104
+ fi
105
+
106
+ # Skip if required package not installed
107
+ case "$pkg" in
108
+ ruvector) [ -z "$rv_cli" ] && continue ;;
109
+ ruv-swarm) [ -z "$rs_root" ] && continue ;;
110
+ esac
111
+
112
+ # Process each line
113
+ while IFS= read -r line; do
114
+ line="${line#"${line%%[![:space:]]*}"}" # trim leading whitespace
115
+ [[ -z "$line" ]] && continue
116
+ [[ "$line" == package:* ]] && continue
117
+
118
+ if [[ "$line" == "none" ]]; then
119
+ continue
120
+
121
+ elif [[ "$line" =~ ^absent\ \"(.+)\"\ (.+)$ ]]; then
122
+ local pattern="${BASH_REMATCH[1]}"
123
+ local filepath
124
+ filepath=$(resolve_path "$base" "$rv_base" "$rs_root" "$pkg" "${BASH_REMATCH[2]}")
125
+ if grep -q "$pattern" "$filepath" 2>/dev/null; then
126
+ all_ok=false
127
+ fi
128
+
129
+ elif [[ "$line" =~ ^grep\ \"(.+)\"\ (.+)$ ]]; then
130
+ local pattern="${BASH_REMATCH[1]}"
131
+ local filepath
132
+ filepath=$(resolve_path "$base" "$rv_base" "$rs_root" "$pkg" "${BASH_REMATCH[2]}")
133
+ if ! grep -q "$pattern" "$filepath" 2>/dev/null; then
134
+ all_ok=false
135
+ fi
136
+ fi
137
+ done < "$sentinel_file"
138
+ done
139
+
140
+ $all_ok
141
+ }
142
+
143
+ # ── Check all installs ──
144
+
145
+ any_failed=false
146
+ first_version=""
147
+
148
+ for entry in "${INSTALLS[@]}"; do
149
+ IFS=$'\t' read -r dist_src version rv_cli rs_root writable <<< "$entry"
150
+ # "-" is the placeholder for empty fields (bash IFS collapses consecutive tabs)
151
+ [ "$rv_cli" = "-" ] && rv_cli=""
152
+ [ "$rs_root" = "-" ] && rs_root=""
153
+ [ -z "$first_version" ] && first_version="$version"
154
+
155
+ if ! check_sentinels_for_install "$dist_src" "$rv_cli" "$rs_root"; then
156
+ any_failed=true
157
+ break # One failure is enough to trigger reapply
158
+ fi
159
+ done
160
+
161
+ VERSION="${first_version:-unknown}"
162
+
163
+ if ! $any_failed; then
164
+ echo "[PATCHES] OK: All patches verified (v$VERSION)"
165
+ exit 0
166
+ fi
167
+
168
+ # ── Patches wiped — auto-reapply and warn ──
169
+
170
+ echo ""
171
+ echo "============================================"
172
+ echo " WARNING: claude-flow patches were wiped!"
173
+ echo " Likely cause: npx cache update (v$VERSION)"
174
+ echo "============================================"
175
+ echo ""
176
+
177
+ if [ -x "$SCRIPT_DIR/patch-all.sh" ]; then
178
+ REAPPLY_ARGS=()
179
+ if [[ $DO_GLOBAL -eq 1 ]]; then REAPPLY_ARGS+=(--global); fi
180
+ if [[ -n "$TARGET_DIR" ]]; then REAPPLY_ARGS+=(--target "$TARGET_DIR"); fi
181
+ bash "$SCRIPT_DIR/patch-all.sh" "${REAPPLY_ARGS[@]}"
182
+ echo ""
183
+ echo "[PATCHES] Auto-reapplied. Stopping existing daemons..."
184
+ npx @claude-flow/cli@latest daemon stop 2>/dev/null
185
+ # Fallback: kill by PID file if daemon stop missed an orphan (project-scoped, not global)
186
+ _pid=$(cat .claude-flow/daemon.pid 2>/dev/null)
187
+ if [ -n "$_pid" ]; then kill "$_pid" 2>/dev/null || true; rm -f .claude-flow/daemon.pid; fi
188
+ sleep 1
189
+ npx @claude-flow/cli@latest daemon start 2>/dev/null
190
+ echo "[PATCHES] Daemon restarted in background (PID: $(cat .claude-flow/daemon.pid 2>/dev/null || echo 'unknown'))"
191
+ echo ""
192
+ else
193
+ echo "[PATCHES] ERROR: patch-all.sh not found at $SCRIPT_DIR"
194
+ echo "[PATCHES] Run manually: bash ~/src/claude-flow-patch/patch-all.sh"
195
+ fi
@@ -0,0 +1,15 @@
1
+ {
2
+ "HW": "Headless Worker",
3
+ "DM": "Daemon & Workers",
4
+ "CF": "Config & Doctor",
5
+ "EM": "Embeddings & HNSW",
6
+ "UI": "Display & Cosmetic",
7
+ "NS": "Memory Namespace",
8
+ "GV": "Ghost Vectors",
9
+ "IN": "Intelligence",
10
+ "SG": "Settings Generator",
11
+ "MM": "Memory Management",
12
+ "HK": "Hooks",
13
+ "RV": "RuVector Intelligence",
14
+ "RS": "ruv-swarm"
15
+ }
package/lib/common.py ADDED
@@ -0,0 +1,97 @@
1
+ # common.py — shared patch infrastructure
2
+ # Extracted from apply-patches.sh. Provides patch()/patch_all() + path variables.
3
+
4
+ import sys, os, re
5
+
6
+ base = os.environ.get("BASE", "")
7
+ if not base or base == "/dev/null":
8
+ base = "" # No claude-flow/cli, paths will be invalid (patch() will skip gracefully)
9
+ services = base + "/services" if base else ""
10
+ commands = base + "/commands" if base else ""
11
+ memory = base + "/memory" if base else ""
12
+
13
+ applied = 0
14
+ skipped = 0
15
+
16
+ def patch(label, filepath, old, new):
17
+ global applied, skipped
18
+ if not filepath:
19
+ return # Skip if path is empty (package not found)
20
+ try:
21
+ with open(filepath, 'r') as f:
22
+ code = f.read()
23
+ if new in code:
24
+ skipped += 1
25
+ return
26
+ if old not in code:
27
+ print(f" WARN: {label} — pattern not found (code may have changed)")
28
+ return
29
+ code = code.replace(old, new, 1)
30
+ with open(filepath, 'w') as f:
31
+ f.write(code)
32
+ print(f" Applied: {label}")
33
+ applied += 1
34
+ except FileNotFoundError:
35
+ pass # Silently skip if file doesn't exist (package not installed)
36
+ except Exception as e:
37
+ print(f" ERROR: {label} — {e}")
38
+
39
+ def patch_all(label, filepath, old, new):
40
+ """Replace ALL occurrences"""
41
+ global applied, skipped
42
+ if not filepath:
43
+ return # Skip if path is empty (package not found)
44
+ try:
45
+ with open(filepath, 'r') as f:
46
+ code = f.read()
47
+ if new in code and old not in code:
48
+ skipped += 1
49
+ return
50
+ if old not in code:
51
+ print(f" WARN: {label} — pattern not found")
52
+ return
53
+ code = code.replace(old, new)
54
+ with open(filepath, 'w') as f:
55
+ f.write(code)
56
+ print(f" Applied: {label}")
57
+ applied += 1
58
+ except FileNotFoundError:
59
+ pass # Silently skip if file doesn't exist (package not installed)
60
+ except Exception as e:
61
+ print(f" ERROR: {label} — {e}")
62
+
63
+ # ── Target file paths ──
64
+ # These may be empty strings if base is not set (no claude-flow/cli found)
65
+ HWE = services + "/headless-worker-executor.js" if services else ""
66
+ WD = services + "/worker-daemon.js" if services else ""
67
+ DJ = commands + "/daemon.js" if commands else ""
68
+ DOC = commands + "/doctor.js" if commands else ""
69
+ MI = memory + "/memory-initializer.js" if memory else ""
70
+
71
+ MCP_MEMORY = base + "/mcp-tools/memory-tools.js" if base else ""
72
+ MCP_HOOKS = base + "/mcp-tools/hooks-tools.js" if base else ""
73
+ CLI_MEMORY = commands + "/memory.js" if commands else ""
74
+ CONF = commands + "/config.js" if commands else ""
75
+ HOOKS_CMD = commands + "/hooks.js" if commands else ""
76
+ NEURAL = commands + "/neural.js" if commands else ""
77
+ EMB_TOOLS = base + "/mcp-tools/embeddings-tools.js" if base else ""
78
+
79
+ # Init module
80
+ init = base + "/init" if base else ""
81
+ SETTINGS_GEN = init + "/settings-generator.js" if init else ""
82
+ HELPERS_GEN = init + "/helpers-generator.js" if init else ""
83
+ EXECUTOR = init + "/executor.js" if init else ""
84
+ TYPES = init + "/types.js" if init else ""
85
+ INIT_CMD = commands + "/init.js" if commands else ""
86
+ START_CMD = commands + "/start.js" if commands else ""
87
+ CMDS_INDEX = commands + "/index.js" if commands else ""
88
+
89
+ # Source helpers (shipped with package, copied by writeHelpers when source dir found)
90
+ _pkg_root = os.path.dirname(os.path.dirname(base)) if base else ""
91
+ SRC_HOOK_HANDLER = os.path.join(_pkg_root, ".claude", "helpers", "hook-handler.cjs") if _pkg_root else ""
92
+
93
+ # RuVector (separate package, path set by patch-all.sh)
94
+ ruvector_cli = os.environ.get("RUVECTOR_CLI", "")
95
+
96
+ # ruv-swarm root (separate package, path set by patch-all.sh via discover.sh)
97
+ ruv_swarm_root = os.environ.get("RUV_SWARM_ROOT", "")
@@ -0,0 +1,181 @@
1
+ #!/usr/bin/env node
2
+ // lib/discover.mjs — Dynamic patch discovery
3
+ // Scans patch/*/ directories, parses README.md + fix.py headers for metadata.
4
+ // Single source of truth for scripts, sentinel checks, and documentation.
5
+
6
+ import { readdirSync, readFileSync, existsSync } from 'node:fs';
7
+ import { resolve, dirname } from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
9
+
10
+ const __dirname = dirname(fileURLToPath(import.meta.url));
11
+ const ROOT = resolve(__dirname, '..');
12
+ const PATCH_DIR = resolve(ROOT, 'patch');
13
+
14
+ // Category labels from lib/categories.json
15
+ const CATEGORY_MAP = JSON.parse(
16
+ readFileSync(resolve(__dirname, 'categories.json'), 'utf-8')
17
+ );
18
+
19
+ /**
20
+ * Parse a patch README.md for doc metadata.
21
+ * Expected format:
22
+ * # {ID}: {title}
23
+ * **Severity**: {severity}
24
+ * **GitHub**: [{label}]({url})
25
+ * ## Files Patched
26
+ * - {file}
27
+ * ## Ops
28
+ * {N} op(s) in fix.py
29
+ */
30
+ function parseReadme(readmePath, dirName) {
31
+ const text = readFileSync(readmePath, 'utf-8');
32
+ const lines = text.split('\n');
33
+
34
+ const titleMatch = lines[0]?.match(/^#\s+(\S+):\s+(.+)/);
35
+ const stripped = dirName.replace(/^\d+-/, '');
36
+ const id = titleMatch?.[1] ?? stripped.split('-').slice(0, 2).join('-');
37
+ const title = titleMatch?.[2]?.trim() ?? '';
38
+
39
+ const sevLine = lines.find(l => l.startsWith('**Severity**'));
40
+ const severity = sevLine?.match(/\*\*Severity\*\*:\s*(\S+)/)?.[1] ?? 'Unknown';
41
+
42
+ const ghLine = lines.find(l => l.startsWith('**GitHub**'));
43
+ const ghMatch = ghLine?.match(/\[([^\]]+)\]\(([^)]+)\)/);
44
+ const github = ghMatch?.[1] ?? '';
45
+ const githubUrl = ghMatch?.[2] ?? '';
46
+
47
+ const filesIdx = lines.findIndex(l => /^##\s+Files Patched/i.test(l));
48
+ const files = [];
49
+ if (filesIdx >= 0) {
50
+ for (let i = filesIdx + 1; i < lines.length; i++) {
51
+ const m = lines[i].match(/^-\s+`?([^`\n]+)`?\s*$/);
52
+ if (m) files.push(m[1].trim());
53
+ else if (lines[i].startsWith('#') || (lines[i].trim() === '' && files.length > 0)) break;
54
+ }
55
+ }
56
+
57
+ const opsLine = lines.find(l => /^\d+\s+ops?\b/i.test(l));
58
+ const ops = opsLine ? parseInt(opsLine, 10) : 0;
59
+
60
+ return { id, title, severity, github, githubUrl, files, ops };
61
+ }
62
+
63
+ /**
64
+ * Parse a sentinel file for verification metadata.
65
+ * Each line is one of:
66
+ * package: <name> — target package (default: @claude-flow/cli)
67
+ * grep "<pattern>" <file> — pass if pattern found
68
+ * absent "<pattern>" <file> — pass if pattern NOT found
69
+ * none — skip verification
70
+ */
71
+ function parseSentinels(sentinelPath) {
72
+ const text = readFileSync(sentinelPath, 'utf-8');
73
+ const lines = text.split('\n');
74
+
75
+ let pkg = null;
76
+ const sentinels = [];
77
+
78
+ for (const line of lines) {
79
+ const trimmed = line.trim();
80
+ if (!trimmed) continue;
81
+
82
+ if (trimmed.startsWith('package:')) {
83
+ pkg = trimmed.replace('package:', '').trim();
84
+ } else if (trimmed === 'none') {
85
+ sentinels.push({ type: 'none' });
86
+ } else {
87
+ const absentMatch = trimmed.match(/^absent\s+"(.+)"\s+(.+)$/);
88
+ if (absentMatch) {
89
+ sentinels.push({ type: 'absent', pattern: absentMatch[1], file: absentMatch[2] });
90
+ continue;
91
+ }
92
+ const grepMatch = trimmed.match(/^grep\s+"(.+)"\s+(.+)$/);
93
+ if (grepMatch) {
94
+ sentinels.push({ type: 'grep', pattern: grepMatch[1], file: grepMatch[2] });
95
+ }
96
+ }
97
+ }
98
+
99
+ return { package: pkg, sentinels };
100
+ }
101
+
102
+ /**
103
+ * Discover all patches. Returns structured JSON with everything needed
104
+ * by scripts, sentinel checks, and documentation.
105
+ */
106
+ export function discover() {
107
+ let dirs;
108
+ try {
109
+ dirs = readdirSync(PATCH_DIR, { withFileTypes: true })
110
+ .filter(d => d.isDirectory())
111
+ .map(d => d.name)
112
+ .sort();
113
+ } catch {
114
+ return { patches: [], categories: {}, stats: { total: 0, categories: 0 } };
115
+ }
116
+
117
+ const patches = [];
118
+ const categorySet = new Set();
119
+
120
+ for (const dirName of dirs) {
121
+ const readmePath = resolve(PATCH_DIR, dirName, 'README.md');
122
+ if (!existsSync(readmePath)) continue;
123
+
124
+ const meta = parseReadme(readmePath, dirName);
125
+ const prefix = meta.id.split('-')[0];
126
+ const stripped = dirName.replace(/^\d+-/, '');
127
+ const slug = stripped.replace(/^[A-Z]+-\d+-/, '');
128
+ const orderMatch = dirName.match(/^(\d+)-/);
129
+ const order = orderMatch ? parseInt(orderMatch[1], 10) : null;
130
+ const hasPy = existsSync(resolve(PATCH_DIR, dirName, 'fix.py'));
131
+ const hasSh = existsSync(resolve(PATCH_DIR, dirName, 'fix.sh'));
132
+ const fixType = hasPy ? 'python' : hasSh ? 'shell' : 'unknown';
133
+ const category = CATEGORY_MAP[prefix] ?? prefix;
134
+ categorySet.add(category);
135
+
136
+ // Parse sentinel metadata from dedicated sentinel file
137
+ const sentinelPath = resolve(PATCH_DIR, dirName, 'sentinel');
138
+ const sentinel = existsSync(sentinelPath) ? parseSentinels(sentinelPath) : { package: null, sentinels: [] };
139
+
140
+ patches.push({
141
+ id: meta.id,
142
+ order,
143
+ slug,
144
+ dir: dirName,
145
+ title: meta.title,
146
+ severity: meta.severity,
147
+ github: meta.github,
148
+ githubUrl: meta.githubUrl,
149
+ category,
150
+ prefix,
151
+ type: fixType,
152
+ files: meta.files,
153
+ ops: meta.ops,
154
+ package: sentinel.package,
155
+ sentinels: sentinel.sentinels,
156
+ });
157
+ }
158
+
159
+ // Build categories object: { prefix: label } for active categories
160
+ const categories = {};
161
+ for (const p of patches) {
162
+ if (!categories[p.prefix]) {
163
+ categories[p.prefix] = CATEGORY_MAP[p.prefix] ?? p.prefix;
164
+ }
165
+ }
166
+
167
+ return {
168
+ patches,
169
+ categories,
170
+ stats: {
171
+ total: patches.length,
172
+ categories: categorySet.size,
173
+ },
174
+ };
175
+ }
176
+
177
+ // CLI: `node lib/discover.mjs` prints JSON to stdout
178
+ const thisFile = resolve(__dirname, 'discover.mjs');
179
+ if (process.argv[1] && resolve(process.argv[1]) === thisFile) {
180
+ console.log(JSON.stringify(discover(), null, 2));
181
+ }
@@ -0,0 +1,160 @@
1
+ #!/bin/bash
2
+ # lib/discover.sh — Shared install discovery for patch-all.sh, check-patches.sh, repair-post-init.sh
3
+ #
4
+ # Finds ALL @claude-flow/cli installations across three patterns:
5
+ # 1. Direct npx: {npx_cache}/{hash}/node_modules/@claude-flow/cli/dist/src/
6
+ # 2. Umbrella npx: {npx_cache}/{hash}/node_modules/claude-flow/v3/@claude-flow/cli/dist/src/
7
+ # 3. Umbrella global: {npm_prefix}/lib/node_modules/claude-flow/v3/@claude-flow/cli/dist/src/
8
+ #
9
+ # Output: tab-separated lines:
10
+ # dist_src \t version \t ruvector_cli \t ruv_swarm_root \t writable(yes|no)
11
+
12
+ # ── npx cache roots ──
13
+
14
+ _cfp_npx_cache_roots() {
15
+ # Linux / macOS
16
+ local home_npx="$HOME/.npm/_npx"
17
+ [ -d "$home_npx" ] && echo "$home_npx"
18
+
19
+ # Windows (Git Bash / MSYS2): $LOCALAPPDATA/npm-cache/_npx
20
+ if [ -n "${LOCALAPPDATA:-}" ]; then
21
+ local win_npx
22
+ if command -v cygpath >/dev/null 2>&1; then
23
+ win_npx="$(cygpath "$LOCALAPPDATA")/npm-cache/_npx"
24
+ else
25
+ win_npx="$LOCALAPPDATA/npm-cache/_npx"
26
+ fi
27
+ [ -d "$win_npx" ] && echo "$win_npx"
28
+ fi
29
+ }
30
+
31
+ # ── Probe a single node_modules directory ──
32
+ # Args: <node_modules_dir>
33
+ # Outputs tab-separated lines for each valid install found.
34
+
35
+ _cfp_probe_node_modules() {
36
+ local nm_dir="$1"
37
+ [ -d "$nm_dir" ] || return 0
38
+
39
+ local layouts=(
40
+ "@claude-flow/cli/dist/src"
41
+ "claude-flow/v3/@claude-flow/cli/dist/src"
42
+ )
43
+
44
+ local _seen_real=()
45
+
46
+ for layout in "${layouts[@]}"; do
47
+ local dist_src="$nm_dir/$layout"
48
+ [ -f "$dist_src/memory/memory-initializer.js" ] || continue
49
+
50
+ # Deduplicate by realpath
51
+ local real
52
+ real="$(realpath "$dist_src" 2>/dev/null || echo "$dist_src")"
53
+ local dup=0
54
+ for s in "${_seen_real[@]}"; do
55
+ [ "$s" = "$real" ] && { dup=1; break; }
56
+ done
57
+ [ "$dup" -eq 1 ] && continue
58
+ _seen_real+=("$real")
59
+
60
+ # Version
61
+ local pkg_json="$dist_src/../../package.json"
62
+ local version
63
+ version=$(grep -o '"version": *"[^"]*"' "$pkg_json" 2>/dev/null | head -1 | cut -d'"' -f4)
64
+ [ -z "$version" ] && version="unknown"
65
+
66
+ # Companion: ruvector — search sibling node_modules and umbrella's v3/node_modules
67
+ local rv_cli="-"
68
+ local search_dirs=("$nm_dir")
69
+ # If umbrella layout, also search inside v3/node_modules
70
+ if [[ "$layout" == claude-flow/* ]]; then
71
+ local v3_nm="$nm_dir/claude-flow/v3/node_modules"
72
+ [ -d "$v3_nm" ] && search_dirs+=("$v3_nm")
73
+ fi
74
+ for sd in "${search_dirs[@]}"; do
75
+ if [ -f "$sd/ruvector/bin/cli.js" ]; then
76
+ rv_cli="$(cd "$sd/ruvector/bin" 2>/dev/null && pwd)/cli.js"
77
+ break
78
+ fi
79
+ done
80
+
81
+ # Companion: ruv-swarm root
82
+ local rs_root="-"
83
+ for sd in "${search_dirs[@]}"; do
84
+ if [ -f "$sd/ruv-swarm/package.json" ]; then
85
+ rs_root="$(cd "$sd/ruv-swarm" 2>/dev/null && pwd)"
86
+ break
87
+ fi
88
+ done
89
+
90
+ # Writable check
91
+ local writable="yes"
92
+ [ -w "$dist_src/memory/memory-initializer.js" ] || writable="no"
93
+
94
+ # Use "-" for empty fields to prevent bash IFS collapsing consecutive delimiters
95
+ printf '%s\t%s\t%s\t%s\t%s\n' "$dist_src" "$version" "$rv_cli" "$rs_root" "$writable"
96
+ done
97
+ }
98
+
99
+ # ── Discover all global installs ──
100
+ # Outputs tab-separated lines (same format as _cfp_probe_node_modules).
101
+
102
+ discover_all_cf_installs() {
103
+ local _global_seen=()
104
+
105
+ # 1. npx cache directories
106
+ while IFS= read -r cache_root; do
107
+ [ -n "$cache_root" ] || continue
108
+ for hash_dir in "$cache_root"/*/; do
109
+ [ -d "$hash_dir/node_modules" ] || continue
110
+ while IFS= read -r line; do
111
+ [ -n "$line" ] || continue
112
+ local ds="${line%% *}"
113
+ local real
114
+ real="$(realpath "$ds" 2>/dev/null || echo "$ds")"
115
+ local dup=0
116
+ for s in "${_global_seen[@]}"; do
117
+ [ "$s" = "$real" ] && { dup=1; break; }
118
+ done
119
+ [ "$dup" -eq 1 ] && continue
120
+ _global_seen+=("$real")
121
+ echo "$line"
122
+ done < <(_cfp_probe_node_modules "$hash_dir/node_modules")
123
+ done
124
+ done < <(_cfp_npx_cache_roots)
125
+
126
+ # 2. Global npm prefix
127
+ local npm_prefix
128
+ npm_prefix="$(npm config get prefix 2>/dev/null)" || true
129
+ if [ -n "$npm_prefix" ]; then
130
+ # Linux/macOS: {prefix}/lib/node_modules
131
+ # Windows: {prefix}/node_modules (no /lib/)
132
+ local prefix_dirs=("$npm_prefix/lib/node_modules" "$npm_prefix/node_modules")
133
+ for pdir in "${prefix_dirs[@]}"; do
134
+ [ -d "$pdir" ] || continue
135
+ while IFS= read -r line; do
136
+ [ -n "$line" ] || continue
137
+ local ds="${line%% *}"
138
+ local real
139
+ real="$(realpath "$ds" 2>/dev/null || echo "$ds")"
140
+ local dup=0
141
+ for s in "${_global_seen[@]}"; do
142
+ [ "$s" = "$real" ] && { dup=1; break; }
143
+ done
144
+ [ "$dup" -eq 1 ] && continue
145
+ _global_seen+=("$real")
146
+ echo "$line"
147
+ done < <(_cfp_probe_node_modules "$pdir")
148
+ done
149
+ fi
150
+ }
151
+
152
+ # ── Discover installs in a --target directory ──
153
+ # Args: <target_dir>
154
+ # Now handles both direct and umbrella layouts.
155
+
156
+ discover_target_installs() {
157
+ local dir="$1"
158
+ [ -d "$dir/node_modules" ] || return 0
159
+ _cfp_probe_node_modules "$dir/node_modules"
160
+ }