#!/usr/bin/env node /** * W7 补丁编排:恢复 co-chat storage → Cursor 未运行时 headless 打补丁; * Cursor 运行中则排队(不强杀),Hub watcher 在退出后自动续跑。 */ import fs from 'node:fs'; import path from 'node:path'; import { spawn, spawnSync } from 'node:child_process'; import { fileURLToPath } from 'node:url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const HOME = process.env.HOME || ''; const PCHAT_ROOT = path.join(HOME, '.persistent-chat-local'); const PENDING_FILE = path.join(PCHAT_ROOT, 'pending-w7.json'); const COCHAT_STORAGE = path.join( HOME, 'Library/Application Support/Cursor/User/globalStorage/co-chat.co-chat-panel/storage.json', ); const CURSOR_APP = '/Applications/Cursor.app'; const WORKBENCH = path.join(CURSOR_APP, 'Contents/Resources/app/out/vs/workbench/workbench.desktop.main.js'); const EXTHOST = path.join(CURSOR_APP, 'Contents/Resources/app/out/vs/workbench/api/node/extensionHostProcess.js'); const PATCH_ACTIVATE = path.join(__dirname, 'patch-activate.mjs'); const PATCH_FLAGS = { 'kc.corePatchBundle': true, 'kc.agentSortPerfPatch': true, 'kc.seamlessPatch': true, 'kc.nameTabHook': true, 'kc.noQuotaMcp': true, 'kc.extendStallTimeout': true, 'kc.mcpSelfHealing': true, 'kc.agentRetries': true, 'kc.endlessRetries': true, 'kc.extensionProtect': true, 'kc.stripeOverride': true, 'kc.disableTelemetry': true, 'kc.cursorCleanAuto': true, 'kc.bgKeepalive': true, 'kc.loopReminderHook': true, 'kc.noQuotaTogglePort': true, 'kc.disableCursorUpdate': true, }; function emit(obj) { console.log(JSON.stringify(obj)); } function readJson(p, fb = {}) { try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return fb; } } function writeJson(p, obj) { fs.mkdirSync(path.dirname(p), { recursive: true }); fs.writeFileSync(p, JSON.stringify(obj, null, 2)); } function verifyMarkers() { const wb = fs.existsSync(WORKBENCH) ? fs.readFileSync(WORKBENCH, 'utf8') : ''; const eh = fs.existsSync(EXTHOST) ? fs.readFileSync(EXTHOST, 'utf8') : ''; const checks = [ { id: 'noQuotaMcp', start: 'CO_NO_QUOTA_MCP_V1_START', src: wb }, { id: 'composerBridge', start: 'CO_COMPOSER_BRIDGE_START', src: wb }, { id: 'exthost', start: 'CO_NO_QUOTA_EXTHOST_V1_START', src: eh }, ]; const found = checks.filter((m) => m.src.includes(m.start)).map((m) => m.id); return { patchOk: found.length >= 3, found, total: checks.length, details: checks.map((m) => ({ id: m.id, start: m.start, found: m.src.includes(m.start) })), }; } function isCursorRunning() { const r = spawnSync('ps', ['aux'], { encoding: 'utf8', maxBuffer: 4 * 1024 * 1024 }); return /\/Applications\/Cursor\.app\/Contents\/MacOS\/Cursor(\s|$)/.test(r.stdout || ''); } function restoreCoChatStorage() { const now = new Date().toISOString(); const cur = readJson(COCHAT_STORAGE, {}); const merged = { ...cur, ...PATCH_FLAGS, 'coMcp.aclSetupComplete': true, 'coMcp.aclGrantedPath': WORKBENCH, 'kc.noQuotaMcp.lastMode': cur['kc.noQuotaMcp.lastMode'] || 'extremely_low', 'kc.noQuotaMcp.lastEnabled': true, 'kc.noQuotaMcp.lastExtVersion': cur['kc.noQuotaMcp.lastExtVersion'] || '3.3.34', 'kc.noQuotaMcp.reloadStamps': now, 'kc.noQuotaMcp.reloadReasonTs': now, 'kc.corePatchBundle.reapplyPending': now, }; writeJson(COCHAT_STORAGE, merged); return { storagePath: COCHAT_STORAGE, keys: Object.keys(PATCH_FLAGS).length }; } function queuePending(reason) { writeJson(PENDING_FILE, { queuedAt: new Date().toISOString(), reason }); } function clearPending() { try { fs.unlinkSync(PENDING_FILE); } catch {} } function runPatchActivate(timeoutMs = 28000) { return new Promise((resolve) => { if (!fs.existsSync(PATCH_ACTIVATE)) { resolve({ ok: false, error: 'patch-activate.mjs 未找到' }); return; } const child = spawn(process.execPath, [PATCH_ACTIVATE, 'apply'], { env: { ...process.env, CO_SKIP_LICENSE: '1', PCHAT_NO_LICENSE: '1' }, }); let out = ''; let err = ''; const timer = setTimeout(() => { try { child.kill('SIGTERM'); } catch {} }, timeoutMs); child.stdout.on('data', (c) => { out += c; }); child.stderr.on('data', (c) => { err += c; }); child.on('close', () => { clearTimeout(timer); let parsed = null; try { parsed = JSON.parse(out.trim().split('\n').filter(Boolean).pop()); } catch {} if (parsed) return resolve(parsed); resolve({ ok: false, stdout: out.slice(-2000), stderr: err.slice(-2000) }); }); }); } async function applyPatchFlow() { const before = verifyMarkers(); if (before.patchOk) { clearPending(); return { ok: true, ...before, method: 'already', hint: 'W8 已通过' }; } const storage = restoreCoChatStorage(); if (isCursorRunning()) { queuePending('cursor_running'); return { ok: false, queued: true, needsQuit: true, needsCoChatToggle: true, storage, ...before, method: 'queued', hint: '已写入 co-chat 补丁配置。请在 Cursor 内:co-chat 面板 → 设置 → 关闭再打开「核心增强包」→ 再 Cmd+Q 完全退出 → 重开 Cursor → Hub 点「诊断」。Hub 不会强杀 Cursor。', }; } const result = await runPatchActivate(); const after = verifyMarkers(); const ok = after.patchOk || result.patchOk; if (ok) clearPending(); else queuePending('patch_activate_failed'); return { ok, queued: !ok, storage, patchResult: result, ...after, method: ok ? 'patch-activate' : 'patch-activate-failed', hint: ok ? 'W8 OK — 请重新打开 Cursor 使补丁生效' : result.hint || '补丁未写入:请 Cmd+Q 退出 Cursor 后 Hub 将自动重试,或重开 Cursor 让 co-chat 扩展激活时打补丁', }; } async function tryPendingPatch() { if (!fs.existsSync(PENDING_FILE)) return null; if (isCursorRunning()) return { waiting: true, reason: 'cursor_still_running' }; restoreCoChatStorage(); const result = await runPatchActivate(); const after = verifyMarkers(); if (after.patchOk) { clearPending(); return { ok: true, applied: true, ...after, patchResult: result }; } return { ok: false, applied: false, ...after, patchResult: result }; } async function main() { const mode = process.argv[2] || 'apply'; if (mode === 'verify') { emit({ ok: true, ...verifyMarkers(), method: 'verify' }); return; } if (mode === 'pending') { emit({ ...(await tryPendingPatch()), method: 'pending' }); return; } emit(await applyPatchFlow()); } main().catch((e) => { emit({ ok: false, error: String(e.stack || e.message || e) }); process.exitCode = 1; });