#!/usr/bin/env node /** * Static guard for ruvnet/ruflo#2127 (and the family of #1147 / #2018). * * The reporter hit `TypeError: Invalid Version: (empty)` inside arborist's * `canDedupe` while installing `ruflo@3.8.0`. Two reviewers could not * reproduce, but the published `ruflo` wrapper still pinned * `"@claude-flow/cli": "^3.7.0-alpha.11"` long after the project moved to * stable semver. That pre-release range widens the resolution space the * dedupe pass has to walk and gives more chances for an upstream * malformed dep to surface as an empty version comparison. * * This audit asserts: * * 1. The `ruflo` wrapper's `@claude-flow/cli` dep range INCLUDES the * version that `v3/@claude-flow/cli` currently publishes. * * 2. The root `claude-flow` umbrella's sibling deps that we maintain * (`@claude-flow/cli-core`, `@claude-flow/mcp`, `@claude-flow/neural`, * `@claude-flow/shared`) likewise include their actual published * versions (best-effort — only when the corresponding workspace * package.json is present locally). * * 3. The `ruflo` wrapper does NOT carry a pre-release range * (`-alpha.N` / `-beta.N`) for `@claude-flow/cli` once that package * is publishing stable versions. Pre-release ranges on stable deps * are the specific shape that caused #2127. * * Failure modes return non-zero with a clear remediation. Runs in CI on * every PR via v3-ci.yml `wrapper-dep-ranges-audit` job. */ import { readFileSync, existsSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { dirname, join, resolve } from 'node:path'; import semver from 'semver'; const __dirname = dirname(fileURLToPath(import.meta.url)); const REPO_ROOT = resolve(__dirname, '..'); function readPkg(relPath) { const p = join(REPO_ROOT, relPath); if (!existsSync(p)) return null; try { return JSON.parse(readFileSync(p, 'utf8')); } catch (err) { return null; } } const violations = []; const checks = []; // ── 1. ruflo wrapper's @claude-flow/cli dep range ──────────────────────────── const rufloPkg = readPkg('ruflo/package.json'); const cliPkg = readPkg('v3/@claude-flow/cli/package.json'); if (!rufloPkg) { violations.push('ruflo/package.json not found'); } else if (!cliPkg) { violations.push('v3/@claude-flow/cli/package.json not found'); } else { const cliVersion = cliPkg.version; const rufloDepRange = rufloPkg.dependencies?.['@claude-flow/cli']; if (!rufloDepRange) { violations.push( `ruflo/package.json does not declare @claude-flow/cli — wrapper must depend on the CLI it wraps` ); } else { checks.push(`ruflo wraps @claude-flow/cli with range "${rufloDepRange}" — cli published as ${cliVersion}`); // 1a. Range must include the current cli version if (!semver.satisfies(cliVersion, rufloDepRange, { includePrerelease: true })) { violations.push( `ruflo's "@claude-flow/cli": "${rufloDepRange}" does NOT include the cli's actual ` + `version ${cliVersion}. Bump the range to "^${cliVersion}" or wider that covers it.` ); } // 1b. If the cli is on stable semver (no pre-release), the dep must not be on a pre-release range const cliPrerelease = semver.prerelease(cliVersion); const rangeUsesPrerelease = /-alpha\.|-beta\.|-rc\.|alpha\.\d+|beta\.\d+|rc\.\d+/.test(rufloDepRange); if (!cliPrerelease && rangeUsesPrerelease) { violations.push( `ruflo's "@claude-flow/cli": "${rufloDepRange}" carries a pre-release tag but cli ${cliVersion} ` + `is on stable semver. Pre-release ranges widen the dedupe walk and have caused real-world ` + `crashes (see #1147 / #2018 / #2127). Replace with a plain caret range like "^${cliVersion}".` ); } } } // ── 2. root claude-flow umbrella sibling deps ──────────────────────────────── const rootPkg = readPkg('package.json'); const siblingsToCheck = [ { dep: '@claude-flow/cli-core', workspace: 'v3/@claude-flow/cli-core/package.json' }, { dep: '@claude-flow/mcp', workspace: 'v3/@claude-flow/mcp/package.json' }, { dep: '@claude-flow/neural', workspace: 'v3/@claude-flow/neural/package.json' }, { dep: '@claude-flow/shared', workspace: 'v3/@claude-flow/shared/package.json' }, ]; for (const { dep, workspace } of siblingsToCheck) { const wsPkg = readPkg(workspace); if (!wsPkg) continue; // best-effort — skip if workspace missing const range = rootPkg?.dependencies?.[dep]; if (!range) continue; checks.push(`claude-flow umbrella → ${dep}: range "${range}" — workspace at ${wsPkg.version}`); if (!semver.satisfies(wsPkg.version, range, { includePrerelease: true })) { violations.push( `claude-flow's "${dep}": "${range}" does NOT include the workspace's actual ` + `version ${wsPkg.version}. Bump the range.` ); } } // ── 3. report ──────────────────────────────────────────────────────────────── console.log(`wrapper-dep-ranges audit — scanned ${checks.length} declaration(s)`); for (const c of checks) console.log(` ${c}`); if (violations.length === 0) { console.log(' ok: all wrapper ranges include their published target versions'); console.log(' ok: no pre-release ranges pointing at stable deps'); process.exit(0); } console.error('\nviolations:'); for (const v of violations) console.error(` ✗ ${v}`); console.error(`\n${violations.length} violation(s) — see remediation hints above.`); console.error('Reference: ruvnet/ruflo#2127 (Invalid Version dedupe crash).'); process.exit(1);