fix(proxy): installer — no prompts during dry-run; never pin service to agent-bundled node

- askConfirm short-circuits non-interactively in --dry-run (prints what would be asked)
- systemd node resolution order: existing unit's pinned binary > newest nvm node >
  PATH lookup excluding hermes-agent runtimes > process.execPath. On the live box
  this keeps the unit byte-identical (true idempotency for the service section).
This commit is contained in:
tke committed 2026-09-23 15:15:33 +02:00
1 parent 3f9be30784
commit 753fac9fe9
1 file changed
+25 -1
+25 -1
View File
@@ -94,6 +94,7 @@ function askSecret(question) {
} }
async function askConfirm(question, defaultYes = true) { async function askConfirm(question, defaultYes = true) {
if (FLAGS.dryRun) { console.log(`(dry-run) would ask: ${question} -> default ${defaultYes ? 'y' : 'n'}`); return defaultYes; }
if (FLAGS.nonInteractive) return defaultYes; if (FLAGS.nonInteractive) return defaultYes;
const a = (await ask(`${question} (y/n)`, defaultYes ? 'y' : 'n')).toLowerCase(); const a = (await ask(`${question} (y/n)`, defaultYes ? 'y' : 'n')).toLowerCase();
return a === 'y'; return a === 'y';
@@ -399,7 +400,30 @@ function sectionUnit() {
const S = 'systemd service'; const S = 'systemd service';
if (FLAGS.noSystemd) { report(S, 'SKIPPED', '--no-systemd'); return true; } if (FLAGS.noSystemd) { report(S, 'SKIPPED', '--no-systemd'); return true; }
const unitPath = path.join(SYSTEMD_USER_DIR, 'proxy-bridge.service'); const unitPath = path.join(SYSTEMD_USER_DIR, 'proxy-bridge.service');
const nodeBin = process.execPath; // The service must not run on an agent-bundled runtime: prefer (1) the node
// already pinned in an existing unit (stable, known-good), (2) the newest
// nvm-installed node, (3) PATH lookup that does NOT resolve into the hermes
// agent dir, (4) process.execPath as a last resort.
let nodeBin = null;
try {
const existing = fs.readFileSync(unitPath, 'utf8');
const m = existing.match(/^ExecStart=(\S+)\s+/m);
if (m && fs.existsSync(m[1])) nodeBin = m[1];
} catch {}
if (!nodeBin) {
const nvmRoot = path.join(os.homedir(), '.nvm', 'versions', 'node');
try {
const versions = fs.readdirSync(nvmRoot).filter(v => /^v\d+/.test(v)).sort((a, b) =>
a.localeCompare(b, undefined, { numeric: true }));
if (versions.length > 0) nodeBin = path.join(nvmRoot, versions[versions.length - 1], 'bin', 'node');
} catch {}
}
if (!nodeBin) {
const loginNode = run('bash', ['--noprofile', '--norc', '-c', 'command -v node'], { readOnly: true });
const cand = loginNode.status === 0 ? loginNode.stdout.trim() : '';
if (cand && !fs.realpathSync(cand).includes('/.hermes/')) nodeBin = cand;
}
if (!nodeBin) nodeBin = process.execPath;
const want = `[Unit]\nDescription=Local Proxy Bridge (Keyring Auth)\nAfter=network.target\n\n[Service]\nType=simple\nExecStart=${nodeBin} ${BRIDGE_DIR}/bridge.js\nRestart=on-failure\nRestartSec=5\n\n[Install]\nWantedBy=default.target\n`; const want = `[Unit]\nDescription=Local Proxy Bridge (Keyring Auth)\nAfter=network.target\n\n[Service]\nType=simple\nExecStart=${nodeBin} ${BRIDGE_DIR}/bridge.js\nRestart=on-failure\nRestartSec=5\n\n[Install]\nWantedBy=default.target\n`;
let current = null; let current = null;
try { current = fs.readFileSync(unitPath, 'utf8'); } catch {} try { current = fs.readFileSync(unitPath, 'utf8'); } catch {}