import fs from 'node:fs'; import path from 'node:path'; import { createRequire } from 'node:module'; import { PACKAGE_ROOT } from './config-loader.mjs'; const require = createRequire(import.meta.url); const { outputsRoot } = require(path.join(PACKAGE_ROOT, 'mcp', 'src', 'core', 'output-paths.js')); const SECRET_KEY = /(token|secret|private.?key|authorization|credential|password)/i; function redactState(value) { if (Array.isArray(value)) return value.map(redactState); if (!value || typeof value !== 'object') return value; const output = {}; for (const [key, item] of Object.entries(value)) { if (SECRET_KEY.test(key)) continue; output[key] = redactState(item); } return output; } export function runtimeStatePath(customPath = '') { return customPath || path.join(outputsRoot(), 'runtime', 'qiwei-runtime.json'); } export function runtimeStopRequestPath(customPath = '') { return runtimeStatePath(customPath).replace(/\.json$/i, '.stop.json'); } export function readRuntimeState(customPath = '') { const filePath = runtimeStatePath(customPath); try { return JSON.parse(fs.readFileSync(filePath, 'utf8').replace(/^\uFEFF/, '')); } catch { return {}; } } export function writeRuntimeState(state, customPath = '') { const filePath = runtimeStatePath(customPath); fs.mkdirSync(path.dirname(filePath), { recursive: true }); const safe = redactState({ ...state, updatedAt: new Date().toISOString() }); const temporary = `${filePath}.${process.pid}.tmp`; fs.writeFileSync(temporary, `${JSON.stringify(safe, null, 2)}\n`, 'utf8'); fs.renameSync(temporary, filePath); return safe; } export function isProcessAlive(pid) { const numericPid = Number(pid); if (!Number.isInteger(numericPid) || numericPid <= 0) return false; try { process.kill(numericPid, 0); return true; } catch { return false; } } export function assertRuntimeAvailable(customPath = '') { const current = readRuntimeState(customPath); if (current.pid && current.pid !== process.pid && current.status !== 'stopped' && isProcessAlive(current.pid)) { throw new Error(`Qiwei runtime is already running (pid=${current.pid}, mode=${current.mode || 'unknown'}).`); } return current; } export function readRuntimeStopRequest(customPath = '') { try { return JSON.parse(fs.readFileSync(runtimeStopRequestPath(customPath), 'utf8').replace(/^\uFEFF/, '')); } catch { return {}; } } export function clearRuntimeStopRequest(customPath = '') { try { fs.unlinkSync(runtimeStopRequestPath(customPath)); } catch {} } export function requestRuntimeStop(customPath = '') { const current = readRuntimeState(customPath); const filePath = runtimeStopRequestPath(customPath); fs.mkdirSync(path.dirname(filePath), { recursive: true }); fs.writeFileSync(filePath, `${JSON.stringify({ targetPid: Number(current.pid) || null, requestedAt: new Date().toISOString(), }, null, 2)}\n`, 'utf8'); return { requested: true, pid: Number(current.pid) || null }; } export function stopRuntimeProcess(customPath = '') { const current = readRuntimeState(customPath); if (!isProcessAlive(current.pid)) { writeRuntimeState({ ...current, status: 'stopped', stoppedAt: new Date().toISOString() }, customPath); return { stopped: false, reason: 'not-running', pid: current.pid || null }; } return requestRuntimeStop(customPath); } export { redactState };