|
5 | 5 | // `security defend` needs (dropped from the 3.28 tree — ruvnet/ruflo#2670). |
6 | 6 | import fs from 'node:fs'; |
7 | 7 | import path from 'node:path'; |
8 | | -import { rufloNodeModules, aqeRoot } from './paths.mjs'; |
| 8 | +import { rufloRoot, rufloNodeModules, aqeRoot } from './paths.mjs'; |
| 9 | +import { run } from './exec.mjs'; |
| 10 | +import { readJson } from './settings.mjs'; |
9 | 11 |
|
10 | 12 | /** agentdb locations under the global ruflo tree (mirrors ruflo-patch-native). */ |
11 | 13 | export function agentdbLocations() { |
@@ -48,6 +50,90 @@ export function nativesStatus() { |
48 | 50 | return { locations, aqe }; |
49 | 51 | } |
50 | 52 |
|
| 53 | +// The packages ruflo's memory RUNTIME resolves better-sqlite3 from — not the |
| 54 | +// agentdb copies above. @claude-flow/memory is the store; @claude-flow/cli is what |
| 55 | +// `npx ruflo memory` runs. #45: these can be WASM-only while the agentdb copy is |
| 56 | +// native, so the agentdb-only status was a false positive. Older ruflo trees may |
| 57 | +// lack either package — filter to what exists so heal/status skip silently. |
| 58 | +export function rufloMemoryContexts() { |
| 59 | + const nm = rufloNodeModules(); |
| 60 | + return [ |
| 61 | + { context: 'memory', dir: path.join(nm, '@claude-flow', 'memory') }, |
| 62 | + { context: 'cli', dir: path.join(nm, '@claude-flow', 'cli') }, |
| 63 | + ].filter((c) => fs.existsSync(c.dir)); |
| 64 | +} |
| 65 | + |
| 66 | +// A PLAIN semver range/version — the only override/dependency form npm install can |
| 67 | +// take by value. Reference forms (`$agentdb`) and protocols (workspace:/file:/link:/ |
| 68 | +// npm:/git+ssh:) are NOT installable specs, so they must be skipped during |
| 69 | +// derivation rather than emitted (they'd make npm error). Requires a version digit |
| 70 | +// so bare `*`/`latest`/`x` fall through to the caller's fallback. |
| 71 | +const isPlainSemver = (v) => |
| 72 | + typeof v === 'string' && /\d/.test(v) && !v.includes(':') && !v.trimStart().startsWith('$'); |
| 73 | + |
| 74 | +/** The install spec for better-sqlite3 in `dir`, derived from that tree's OWN |
| 75 | + * declarations so an npm `overrides` pin isn't fought with EOVERRIDE (verified |
| 76 | + * live: `install better-sqlite3@^12` under @claude-flow/cli, which pins ^12.9.0, |
| 77 | + * is rejected; the declared spec succeeds). Precedence: overrides → |
| 78 | + * optionalDependencies → dependencies → fallback; the first PLAIN-semver value |
| 79 | + * wins, non-semver forms are skipped. */ |
| 80 | +export function deriveBsq3Spec(dir, fallback = '^12') { |
| 81 | + const pkg = readJson(path.join(dir, 'package.json'), {}) ?? {}; |
| 82 | + for (const field of ['overrides', 'optionalDependencies', 'dependencies']) { |
| 83 | + const v = pkg[field]?.['better-sqlite3']; |
| 84 | + if (isPlainSemver(v)) return v; |
| 85 | + } |
| 86 | + return fallback; |
| 87 | +} |
| 88 | + |
| 89 | +// A truthful load-test of the binding as node resolution finds it FROM `dir`: an |
| 90 | +// ABI-mismatched or absent binding throws on `require` (exactly `ruflo doctor`'s |
| 91 | +// "Could not locate the bindings file"), so requiring it, opening :memory:, and |
| 92 | +// running SELECT 1 in a child process is the real WASM-vs-native answer — not a |
| 93 | +// file-existence guess. Kept in a child process so a broken addon can't crash ak. |
| 94 | +const RUNTIME_PROBE = |
| 95 | + "const D=require(require.resolve('better-sqlite3',{paths:[process.argv[1]]}));" |
| 96 | + + "const db=new D(':memory:');const r=db.prepare('SELECT 1 AS ok').get();db.close();" |
| 97 | + + 'process.exit(r&&r.ok===1?0:3);'; |
| 98 | + |
| 99 | +/** Load-test better-sqlite3 as resolved from `dir`. Injectable runner keeps the |
| 100 | + * test spawn-free. Returns {ok} or {ok:false, reason}. */ |
| 101 | +export async function probeBsq3Runtime(dir, { runner = run } = {}) { |
| 102 | + // Generous timeout for a cold `node` spawn on CI; a real native require returns |
| 103 | + // well under the status budget (probes run in parallel, see rufloRuntimeNatives). |
| 104 | + const r = await runner('node', ['-e', RUNTIME_PROBE, dir], { cwd: dir, timeout: 8000 }); |
| 105 | + if (r.code === 0) return { ok: true }; |
| 106 | + return { ok: false, reason: (r.stderr || `exit ${r.code}`).trim().split('\n').pop().slice(0, 160) }; |
| 107 | +} |
| 108 | + |
| 109 | +/** Per-context native-binding truth for ruflo's memory runtime. {installed:false} |
| 110 | + * when ruflo is absent (EC-1: status/pre-flight skip, never crash). Probes run in |
| 111 | + * parallel to stay inside the status time budget. */ |
| 112 | +export async function rufloRuntimeNatives({ runner = run } = {}) { |
| 113 | + let installed; |
| 114 | + try { installed = fs.existsSync(rufloRoot()); } catch { installed = false; } |
| 115 | + if (!installed) return { installed: false, contexts: [] }; |
| 116 | + const contexts = await Promise.all(rufloMemoryContexts().map(async ({ context, dir }) => { |
| 117 | + const res = await probeBsq3Runtime(dir, { runner }); |
| 118 | + return { context, dir, ok: res.ok, reason: res.reason }; |
| 119 | + })); |
| 120 | + return { installed: true, contexts }; |
| 121 | +} |
| 122 | + |
| 123 | +/** Drift for a CLAUDE_FLOW_DB_PATH pin in .claude/settings.local.json `env`: warn |
| 124 | + * when the pinned DB's directory is missing OR the path lies outside the project. |
| 125 | + * Warn-only (the pin may be deliberate); `sync` never touches it. path.relative |
| 126 | + * for containment so drive-letter/Windows paths compare correctly, not by prefix. |
| 127 | + * Returns null when there is no pin (EC-4: absent/unparseable settings). */ |
| 128 | +export function dbPathPinStatus({ settingsLocalFile, projectRoot }) { |
| 129 | + const pinned = readJson(settingsLocalFile)?.env?.CLAUDE_FLOW_DB_PATH; |
| 130 | + if (!pinned) return null; |
| 131 | + if (!fs.existsSync(path.dirname(pinned))) return { warn: true, pinned, reason: 'directory does not exist' }; |
| 132 | + const rel = path.relative(projectRoot, pinned); |
| 133 | + if (rel.startsWith('..') || path.isAbsolute(rel)) return { warn: true, pinned, reason: 'outside the project root' }; |
| 134 | + return { warn: false, pinned }; |
| 135 | +} |
| 136 | + |
51 | 137 | export const aidefencePresent = () => |
52 | 138 | fs.existsSync(path.join(rufloNodeModules(), '@claude-flow', 'aidefence', 'package.json')); |
53 | 139 |
|
|
0 commit comments