397777089e
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
201 lines
7.7 KiB
JavaScript
201 lines
7.7 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* ruflo H7 patch re-apply tool.
|
|
*
|
|
* H7: `ruflo memory store` routes through the AgentDB-v3 bridge
|
|
* (memory-bridge.js:bridgeStoreEntry) which inserts into an in-RAM sql.js DB
|
|
* and never flushes to disk — entries are lost between CLI invocations.
|
|
*
|
|
* Fix: force getBridge() in @claude-flow/cli's memory-initializer.js to
|
|
* return null, so memory ops fall through to the raw sql.js path that DOES
|
|
* persist (db.export() + writeFileRestricted).
|
|
*
|
|
* Both operations live in the GLOBAL npm install and are overwritten by
|
|
* `ruflo update` — re-run this script after any ruflo upgrade.
|
|
* Each apply/revert/check now covers BOTH the getBridge patch AND the
|
|
* onnxruntime-node dedupe (disabling the nested @xenova/transformers copy).
|
|
*
|
|
* Usage:
|
|
* node tools/ruflo-h7-patch.mjs apply both fixes (idempotent)
|
|
* node tools/ruflo-h7-patch.mjs --check exit 0 if both applied, 1 if not
|
|
* node tools/ruflo-h7-patch.mjs --revert remove both fixes
|
|
*
|
|
* See docs/superpowers/specs/2026-05-15-ruflo-memory-h7-fix-and-advisory-hook-design.md §4
|
|
*/
|
|
import { execSync } from 'node:child_process';
|
|
import { existsSync, readFileSync, writeFileSync, renameSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { pathToFileURL } from 'node:url';
|
|
|
|
export const PATCH_MARKER = 'LIDERRA-H7-PATCH';
|
|
export const PATCH_LINE =
|
|
' return null; /* LIDERRA-H7-PATCH: AgentDB bridge writes in-RAM sql.js, never flushes — force raw persisting path */';
|
|
const ANCHOR = 'async function getBridge() {';
|
|
|
|
/** True if the file content already contains the patch. */
|
|
export function isPatched(content) {
|
|
return content.includes(PATCH_MARKER);
|
|
}
|
|
|
|
/**
|
|
* Insert the patch line as the first statement of getBridge().
|
|
* Idempotent. Throws if the anchor is absent (ruflo upstream changed).
|
|
*/
|
|
export function applyPatch(content) {
|
|
if (isPatched(content)) return { content, changed: false };
|
|
const idx = content.indexOf(ANCHOR);
|
|
if (idx === -1) {
|
|
throw new Error(
|
|
`anchor "${ANCHOR}" not found — ruflo upstream changed getBridge(); revise this patch manually`,
|
|
);
|
|
}
|
|
const insertAt = idx + ANCHOR.length;
|
|
const patched = content.slice(0, insertAt) + '\n' + PATCH_LINE + content.slice(insertAt);
|
|
return { content: patched, changed: true };
|
|
}
|
|
|
|
/** Remove the patch line (the line carrying PATCH_MARKER). */
|
|
export function revertPatch(content) {
|
|
if (!isPatched(content)) return { content, changed: false };
|
|
const lines = content.split('\n');
|
|
const kept = lines.filter((l) => !l.includes(PATCH_MARKER));
|
|
return { content: kept.join('\n'), changed: true };
|
|
}
|
|
|
|
/**
|
|
* Resolve the path to @claude-flow/cli's memory-initializer.js inside the
|
|
* global npm tree. Handles both nested (under ruflo/) and hoisted layouts.
|
|
*/
|
|
export function resolveTargetPath(npmRoot) {
|
|
const tail = ['@claude-flow', 'cli', 'dist', 'src', 'memory', 'memory-initializer.js'];
|
|
const candidates = [
|
|
join(npmRoot, 'ruflo', 'node_modules', ...tail),
|
|
join(npmRoot, ...tail),
|
|
];
|
|
return candidates.find((c) => existsSync(c)) ?? null;
|
|
}
|
|
|
|
// --- onnxruntime-node dedupe (H7 fix, part 2) ---
|
|
// ruflo's dependency tree carries two incompatible onnxruntime-node versions:
|
|
// ruflo/node_modules/onnxruntime-node (1.24.3)
|
|
// ruflo/node_modules/@xenova/transformers/node_modules/onnxruntime-node (1.14.0)
|
|
// Both ship onnxruntime.dll; loading both into one process => DLL name
|
|
// collision => ERR_DLOPEN_FAILED. Disabling the nested duplicate makes
|
|
// @xenova/transformers resolve to the single hoisted 1.24.3.
|
|
const DEDUPE_DISABLED_SUFFIX = '.h7disabled';
|
|
|
|
/** Resolve { active, disabled } paths for the duplicate nested onnxruntime-node. */
|
|
export function resolveDedupePaths(npmRoot) {
|
|
const active = join(
|
|
npmRoot, 'ruflo', 'node_modules', '@xenova', 'transformers',
|
|
'node_modules', 'onnxruntime-node',
|
|
);
|
|
return { active, disabled: active + DEDUPE_DISABLED_SUFFIX };
|
|
}
|
|
|
|
/**
|
|
* Dedupe status:
|
|
* 'deduped' — disabled dir exists, active does not (fix applied)
|
|
* 'not-deduped' — active dir exists, disabled does not (fix not applied)
|
|
* 'absent' — neither exists (no nested duplicate — nothing to dedupe)
|
|
* 'inconsistent' — both exist (manual intervention required)
|
|
*/
|
|
export function dedupeStatus(active, disabled) {
|
|
const a = existsSync(active);
|
|
const d = existsSync(disabled);
|
|
if (a && d) return 'inconsistent';
|
|
if (d) return 'deduped';
|
|
if (a) return 'not-deduped';
|
|
return 'absent';
|
|
}
|
|
|
|
/** Disable the nested duplicate (rename active -> disabled). Idempotent. */
|
|
export function applyDedupe(active, disabled) {
|
|
const status = dedupeStatus(active, disabled);
|
|
if (status === 'deduped' || status === 'absent') return { changed: false, status };
|
|
if (status === 'inconsistent') {
|
|
throw new Error(
|
|
`dedupe inconsistent — both "${active}" and "${disabled}" exist; resolve manually`,
|
|
);
|
|
}
|
|
renameSync(active, disabled);
|
|
return { changed: true, status: 'deduped' };
|
|
}
|
|
|
|
/** Re-enable the nested duplicate (rename disabled -> active). */
|
|
export function revertDedupe(active, disabled) {
|
|
const status = dedupeStatus(active, disabled);
|
|
if (status === 'not-deduped' || status === 'absent') return { changed: false, status };
|
|
if (status === 'inconsistent') {
|
|
throw new Error(
|
|
`dedupe inconsistent — both "${active}" and "${disabled}" exist; resolve manually`,
|
|
);
|
|
}
|
|
renameSync(disabled, active);
|
|
return { changed: true, status: 'not-deduped' };
|
|
}
|
|
|
|
function main() {
|
|
const mode = process.argv[2];
|
|
const VALID_MODES = [undefined, '--check', '--revert'];
|
|
if (!VALID_MODES.includes(mode)) {
|
|
console.error(`[ruflo-h7-patch] unknown flag: ${mode}`);
|
|
console.error('Usage: node tools/ruflo-h7-patch.mjs [--check | --revert]');
|
|
process.exit(2);
|
|
}
|
|
let npmRoot;
|
|
try {
|
|
npmRoot = execSync('npm root -g', { encoding: 'utf8' }).trim();
|
|
} catch (e) {
|
|
console.error('[ruflo-h7-patch] failed to run `npm root -g`: ' + e.message);
|
|
process.exit(2);
|
|
}
|
|
const target = resolveTargetPath(npmRoot);
|
|
if (!target) {
|
|
console.error('[ruflo-h7-patch] memory-initializer.js not found under ' + npmRoot);
|
|
process.exit(2);
|
|
}
|
|
const { active, disabled } = resolveDedupePaths(npmRoot);
|
|
|
|
if (mode === '--check') {
|
|
const patched = isPatched(readFileSync(target, 'utf8'));
|
|
const dStatus = dedupeStatus(active, disabled);
|
|
const dedupeOk = dStatus === 'deduped' || dStatus === 'absent';
|
|
console.log(`[ruflo-h7-patch] ${target}`);
|
|
console.log(` getBridge patch: ${patched ? 'PATCHED' : 'UNPATCHED'}`);
|
|
console.log(` onnxruntime dedupe: ${dStatus}`);
|
|
process.exit(patched && dedupeOk ? 0 : 1);
|
|
}
|
|
|
|
if (mode === '--revert') {
|
|
const r1 = revertPatch(readFileSync(target, 'utf8'));
|
|
if (r1.changed) writeFileSync(target, r1.content);
|
|
console.log(`[ruflo-h7-patch] revert getBridge: ${r1.changed ? 'patch removed' : 'was not patched'}`);
|
|
try {
|
|
const r2 = revertDedupe(active, disabled);
|
|
console.log(`[ruflo-h7-patch] revert dedupe: ${r2.changed ? 're-enabled nested onnxruntime-node' : r2.status}`);
|
|
} catch (e) {
|
|
console.error('[ruflo-h7-patch] ' + e.message);
|
|
process.exit(3);
|
|
}
|
|
process.exit(0);
|
|
}
|
|
|
|
// default: apply
|
|
try {
|
|
const r1 = applyPatch(readFileSync(target, 'utf8'));
|
|
if (r1.changed) writeFileSync(target, r1.content);
|
|
console.log(`[ruflo-h7-patch] apply getBridge: ${r1.changed ? 'patched' : 'already patched'}`);
|
|
const r2 = applyDedupe(active, disabled);
|
|
console.log(`[ruflo-h7-patch] apply dedupe: ${r2.changed ? 'disabled nested onnxruntime-node' : r2.status}`);
|
|
process.exit(0);
|
|
} catch (e) {
|
|
console.error('[ruflo-h7-patch] ' + e.message);
|
|
process.exit(3);
|
|
}
|
|
}
|
|
|
|
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
main();
|
|
}
|