gm-cc 2.0.727 → 2.0.1064
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/agents/gm.md +1 -3
- package/agents/memorize.md +22 -2
- package/agents/research-worker.md +36 -0
- package/agents/textprocessing.md +47 -0
- package/bin/bootstrap.js +624 -34
- package/bin/plugkit.js +95 -53
- package/bin/plugkit.sha256 +6 -6
- package/bin/plugkit.version +1 -1
- package/bin/rtk.sha256 +6 -0
- package/bin/rtk.version +1 -0
- package/hooks/hooks.json +2 -46
- package/hooks/hooks.spec.json +48 -0
- package/package.json +2 -2
- package/plugin.json +1 -1
- package/skills/browser/SKILL.md +18 -16
- package/skills/code-search/SKILL.md +15 -15
- package/skills/create-lang-plugin/SKILL.md +22 -26
- package/skills/gm/SKILL.md +31 -66
- package/skills/gm-cc/SKILL.md +19 -0
- package/skills/gm-codex/SKILL.md +19 -0
- package/skills/gm-complete/SKILL.md +52 -69
- package/skills/gm-copilot-cli/SKILL.md +19 -0
- package/skills/gm-cursor/SKILL.md +19 -0
- package/skills/gm-emit/SKILL.md +44 -61
- package/skills/gm-execute/SKILL.md +42 -84
- package/skills/gm-gc/SKILL.md +19 -0
- package/skills/gm-jetbrains/SKILL.md +19 -0
- package/skills/gm-kilo/SKILL.md +19 -0
- package/skills/gm-oc/SKILL.md +19 -0
- package/skills/gm-vscode/SKILL.md +19 -0
- package/skills/gm-zed/SKILL.md +19 -0
- package/skills/governance/SKILL.md +24 -23
- package/skills/pages/SKILL.md +42 -92
- package/skills/planning/SKILL.md +83 -80
- package/skills/research/SKILL.md +43 -0
- package/skills/ssh/SKILL.md +15 -9
- package/skills/textprocessing/SKILL.md +40 -0
- package/skills/update-docs/SKILL.md +27 -21
- package/.github/workflows/publish-npm.yml +0 -44
- package/hooks/post-tool-use-hook.js +0 -34
- package/hooks/pre-tool-use-hook.js +0 -45
- package/hooks/prompt-submit-hook.js +0 -19
- package/hooks/session-start-hook.js +0 -23
package/bin/bootstrap.js
CHANGED
|
@@ -9,16 +9,70 @@ const crypto = require('crypto');
|
|
|
9
9
|
const { URL } = require('url');
|
|
10
10
|
|
|
11
11
|
const RELEASE_REPO = 'AnEntrypoint/plugkit-bin';
|
|
12
|
-
const ATTEMPT_TIMEOUT_MS = 60 * 1000;
|
|
13
|
-
const STALL_TIMEOUT_MS =
|
|
12
|
+
const ATTEMPT_TIMEOUT_MS = 5 * 60 * 1000;
|
|
13
|
+
const STALL_TIMEOUT_MS = 15 * 1000;
|
|
14
14
|
const MAX_ATTEMPTS = 5;
|
|
15
15
|
const BACKOFF_MS = [2000, 5000, 15000, 30000];
|
|
16
|
-
|
|
16
|
+
// Worst case: a slow link downloading 140MB at 1MB/s = ~140s. Allow 30 minutes
|
|
17
|
+
// before another bootstrap process treats this lock as abandoned. Below this,
|
|
18
|
+
// concurrent bootstrap calls would wipe an in-progress download mid-stream
|
|
19
|
+
// (see the v0.1.294 incident where a race between two wrappers blew away the
|
|
20
|
+
// .partial during a 10-minute fetch).
|
|
21
|
+
const LOCK_STALE_MS = 30 * 60 * 1000;
|
|
17
22
|
|
|
18
23
|
function log(msg) {
|
|
19
24
|
try { process.stderr.write(`[plugkit-bootstrap] ${msg}\n`); } catch (_) {}
|
|
20
25
|
}
|
|
21
26
|
|
|
27
|
+
function probeBinaryVersion(binPath) {
|
|
28
|
+
try {
|
|
29
|
+
const { spawnSync } = require('child_process');
|
|
30
|
+
const r = spawnSync(binPath, ['--version'], { timeout: 3000, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true });
|
|
31
|
+
if (r.error) return null;
|
|
32
|
+
const text = `${r.stdout || ''} ${r.stderr || ''}`.trim();
|
|
33
|
+
const m = text.match(/(\d+\.\d+\.\d+)/);
|
|
34
|
+
return m ? m[1] : null;
|
|
35
|
+
} catch (_) { return null; }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function writeBootstrapError(spec) {
|
|
39
|
+
try {
|
|
40
|
+
const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
41
|
+
const spoolDir = path.join(projectDir, '.gm', 'exec-spool');
|
|
42
|
+
fs.mkdirSync(spoolDir, { recursive: true });
|
|
43
|
+
const out = path.join(spoolDir, '.bootstrap-error.json');
|
|
44
|
+
fs.writeFileSync(out, JSON.stringify({ ts: new Date().toISOString(), ...spec }, null, 2));
|
|
45
|
+
} catch (_) {}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function clearBootstrapError() {
|
|
49
|
+
try {
|
|
50
|
+
const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
51
|
+
const out = path.join(projectDir, '.gm', 'exec-spool', '.bootstrap-error.json');
|
|
52
|
+
fs.unlinkSync(out);
|
|
53
|
+
} catch (_) {}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function obsEvent(subsystem, event, fields) {
|
|
57
|
+
if (process.env.GM_LOG_DISABLE) return;
|
|
58
|
+
try {
|
|
59
|
+
const root = process.env.GM_LOG_DIR
|
|
60
|
+
|| path.join(os.homedir(), '.claude', 'gm-log');
|
|
61
|
+
const day = new Date().toISOString().slice(0, 10);
|
|
62
|
+
const dir = path.join(root, day);
|
|
63
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
64
|
+
const line = JSON.stringify({
|
|
65
|
+
ts: new Date().toISOString(),
|
|
66
|
+
sub: subsystem,
|
|
67
|
+
event,
|
|
68
|
+
pid: process.pid,
|
|
69
|
+
sess: process.env.CLAUDE_SESSION_ID || process.env.GM_SESSION_ID || '',
|
|
70
|
+
...fields,
|
|
71
|
+
});
|
|
72
|
+
fs.appendFileSync(path.join(dir, `${subsystem}.jsonl`), line + '\n');
|
|
73
|
+
} catch (_) {}
|
|
74
|
+
}
|
|
75
|
+
|
|
22
76
|
function platformKey() {
|
|
23
77
|
const p = os.platform();
|
|
24
78
|
const a = os.arch();
|
|
@@ -32,6 +86,11 @@ function binaryName() {
|
|
|
32
86
|
return key.startsWith('win32') ? `plugkit-${key}.exe` : `plugkit-${key}`;
|
|
33
87
|
}
|
|
34
88
|
|
|
89
|
+
function rtkBinaryName() {
|
|
90
|
+
const key = platformKey();
|
|
91
|
+
return key.startsWith('win32') ? `rtk-${key}.exe` : `rtk-${key}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
35
94
|
function cacheRoot() {
|
|
36
95
|
const home = os.homedir();
|
|
37
96
|
if (process.env.PLUGKIT_CACHE_DIR) return process.env.PLUGKIT_CACHE_DIR;
|
|
@@ -48,6 +107,115 @@ function fallbackCacheRoot() {
|
|
|
48
107
|
return path.join(os.tmpdir(), 'plugkit-cache', 'bin');
|
|
49
108
|
}
|
|
50
109
|
|
|
110
|
+
function gmToolsDir() {
|
|
111
|
+
const home = process.env.USERPROFILE || process.env.HOME || os.homedir();
|
|
112
|
+
return path.join(home, '.claude', 'gm-tools');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Copy the freshly-resolved plugkit binary + its version+sha manifests to
|
|
116
|
+
// ~/.claude/gm-tools so hooks.json can invoke plugkit directly without going
|
|
117
|
+
// through node. Self-update inside the Rust binary keeps gm-tools fresh from
|
|
118
|
+
// here on. Skipped silently on any error — the next session-start hook will
|
|
119
|
+
// retry via ensure_tools_current.
|
|
120
|
+
function killHoldersOfPath(targetPath) {
|
|
121
|
+
if (process.platform !== 'win32') return 0;
|
|
122
|
+
try {
|
|
123
|
+
const { spawnSync } = require('child_process');
|
|
124
|
+
const norm = path.resolve(targetPath).replace(/\//g, '\\');
|
|
125
|
+
const r = spawnSync('wmic', ['process', 'where', `ExecutablePath='${norm.replace(/\\/g, '\\\\')}'`, 'get', 'ProcessId', '/format:value'], { encoding: 'utf8', windowsHide: true, timeout: 5000 });
|
|
126
|
+
if (r.status !== 0 || !r.stdout) return 0;
|
|
127
|
+
const pids = [];
|
|
128
|
+
for (const line of r.stdout.split(/\r?\n/)) {
|
|
129
|
+
const m = line.match(/ProcessId=(\d+)/);
|
|
130
|
+
if (m) {
|
|
131
|
+
const pid = parseInt(m[1], 10);
|
|
132
|
+
if (Number.isFinite(pid) && pid !== process.pid) pids.push(pid);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
for (const pid of pids) {
|
|
136
|
+
try { spawnSync('taskkill', ['/F', '/PID', String(pid)], { windowsHide: true, timeout: 3000 }); } catch (_) {}
|
|
137
|
+
}
|
|
138
|
+
return pids.length;
|
|
139
|
+
} catch (_) { return 0; }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function cleanOrphanNewFiles(dst, exeName) {
|
|
143
|
+
try {
|
|
144
|
+
for (const name of fs.readdirSync(dst)) {
|
|
145
|
+
if (name === exeName + '.new') continue;
|
|
146
|
+
if (/^plugkit(\.\d+\.\d+\.\d+)?\.new$/i.test(name)) {
|
|
147
|
+
try { fs.unlinkSync(path.join(dst, name)); } catch (_) {}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
} catch (_) {}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function renameWithRetry(src, dst, attempts) {
|
|
154
|
+
for (let i = 0; i < attempts; i++) {
|
|
155
|
+
try { fs.renameSync(src, dst); return true; }
|
|
156
|
+
catch (err) {
|
|
157
|
+
if (err.code !== 'EEXIST' && err.code !== 'EPERM' && err.code !== 'EBUSY' && err.code !== 'EACCES') throw err;
|
|
158
|
+
if (i === Math.floor(attempts / 2)) killHoldersOfPath(dst);
|
|
159
|
+
try { const { spawnSync } = require('child_process'); spawnSync(process.execPath, ['-e', 'setTimeout(()=>{}, 200)'], { timeout: 400, killSignal: 'SIGKILL', stdio: 'ignore', windowsHide: true }); } catch (_) {}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function copyToGmTools(finalPath, wrapperDir, version) {
|
|
166
|
+
const dst = gmToolsDir();
|
|
167
|
+
fs.mkdirSync(dst, { recursive: true });
|
|
168
|
+
const exeName = process.platform === 'win32' ? 'plugkit.exe' : 'plugkit';
|
|
169
|
+
const target = path.join(dst, exeName);
|
|
170
|
+
const targetTmp = target + '.new';
|
|
171
|
+
cleanOrphanNewFiles(dst, exeName);
|
|
172
|
+
if (fs.existsSync(target)) {
|
|
173
|
+
let needsRefresh = true;
|
|
174
|
+
try {
|
|
175
|
+
const cur = sha256OfFileSync(target);
|
|
176
|
+
const src = sha256OfFileSync(finalPath);
|
|
177
|
+
if (cur === src) needsRefresh = false;
|
|
178
|
+
} catch (_) {}
|
|
179
|
+
if (!needsRefresh) {
|
|
180
|
+
try { fs.writeFileSync(path.join(dst, 'plugkit.version'), version); } catch (_) {}
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
try { killHoldersOfPath(target); } catch (_) {}
|
|
184
|
+
}
|
|
185
|
+
fs.copyFileSync(finalPath, targetTmp);
|
|
186
|
+
if (!renameWithRetry(targetTmp, target, 8)) {
|
|
187
|
+
try { killHoldersOfPath(target); } catch (_) {}
|
|
188
|
+
try { fs.unlinkSync(target); } catch (_) {}
|
|
189
|
+
try { fs.renameSync(targetTmp, target); }
|
|
190
|
+
catch (_) {
|
|
191
|
+
try { fs.unlinkSync(targetTmp); } catch (_) {}
|
|
192
|
+
obsEvent('bootstrap', 'gmtools.rename.failed', { target });
|
|
193
|
+
writeBootstrapError({
|
|
194
|
+
expected_version: version,
|
|
195
|
+
cached_version: null,
|
|
196
|
+
error_phase: 'gmtools-rename',
|
|
197
|
+
error_message: `cannot replace ${target} after kill+unlink retry; orphan removed`,
|
|
198
|
+
});
|
|
199
|
+
throw new Error(`gm-tools update blocked: cannot replace ${target} (held open by running plugkit and kill-retry exhausted)`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
try {
|
|
203
|
+
for (const name of fs.readdirSync(dst)) {
|
|
204
|
+
if (/^plugkit(\.\d+\.\d+\.\d+)?\.new$/i.test(name)) {
|
|
205
|
+
try { fs.unlinkSync(path.join(dst, name)); } catch (_) {}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
} catch (_) {}
|
|
209
|
+
if (process.platform !== 'win32') {
|
|
210
|
+
try { fs.chmodSync(target, 0o755); } catch (_) {}
|
|
211
|
+
}
|
|
212
|
+
fs.writeFileSync(path.join(dst, 'plugkit.version'), version);
|
|
213
|
+
try {
|
|
214
|
+
const srcSha = path.join(wrapperDir, 'plugkit.sha256');
|
|
215
|
+
if (fs.existsSync(srcSha)) fs.copyFileSync(srcSha, path.join(dst, 'plugkit.sha256'));
|
|
216
|
+
} catch (_) {}
|
|
217
|
+
}
|
|
218
|
+
|
|
51
219
|
function ensureDir(dir) {
|
|
52
220
|
fs.mkdirSync(dir, { recursive: true });
|
|
53
221
|
}
|
|
@@ -58,8 +226,45 @@ function readVersionFile(wrapperDir) {
|
|
|
58
226
|
return fs.readFileSync(p, 'utf8').trim();
|
|
59
227
|
}
|
|
60
228
|
|
|
61
|
-
function
|
|
62
|
-
const p = path.join(wrapperDir, '
|
|
229
|
+
function readRtkVersion(wrapperDir) {
|
|
230
|
+
const p = path.join(wrapperDir, 'rtk.version');
|
|
231
|
+
if (!fs.existsSync(p)) return null;
|
|
232
|
+
const v = fs.readFileSync(p, 'utf8').trim();
|
|
233
|
+
return v || null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function sha256OfFileSync(filePath) {
|
|
237
|
+
const h = crypto.createHash('sha256');
|
|
238
|
+
const fd = fs.openSync(filePath, 'r');
|
|
239
|
+
try {
|
|
240
|
+
const buf = Buffer.alloc(1024 * 1024);
|
|
241
|
+
for (;;) {
|
|
242
|
+
const n = fs.readSync(fd, buf, 0, buf.length, null);
|
|
243
|
+
if (n <= 0) break;
|
|
244
|
+
h.update(buf.subarray(0, n));
|
|
245
|
+
}
|
|
246
|
+
} finally { try { fs.closeSync(fd); } catch (_) {} }
|
|
247
|
+
return h.digest('hex');
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function healIfShaMatches(binPath, expectedSha, sentinelPath, partialPath, kind) {
|
|
251
|
+
if (!fs.existsSync(binPath)) return false;
|
|
252
|
+
if (partialPath) { try { if (fs.existsSync(partialPath)) fs.unlinkSync(partialPath); } catch (_) {} }
|
|
253
|
+
if (!expectedSha) return false;
|
|
254
|
+
let got;
|
|
255
|
+
try { got = sha256OfFileSync(binPath); }
|
|
256
|
+
catch (_) { return false; }
|
|
257
|
+
if (got !== expectedSha) {
|
|
258
|
+
try { fs.unlinkSync(binPath); } catch (_) {}
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
try { fs.writeFileSync(sentinelPath, new Date().toISOString()); } catch (_) { return false; }
|
|
262
|
+
obsEvent('bootstrap', 'cache.heal', { path: binPath, kind });
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function readShaManifest(wrapperDir, manifestName) {
|
|
267
|
+
const p = path.join(wrapperDir, manifestName || 'plugkit.sha256');
|
|
63
268
|
if (!fs.existsSync(p)) return null;
|
|
64
269
|
const out = {};
|
|
65
270
|
for (const line of fs.readFileSync(p, 'utf8').split(/\r?\n/)) {
|
|
@@ -95,9 +300,7 @@ function acquireLock(lockPath) {
|
|
|
95
300
|
continue;
|
|
96
301
|
}
|
|
97
302
|
if (Date.now() - start > ATTEMPT_TIMEOUT_MS) throw new Error(`lock wait timeout: ${lockPath}`);
|
|
98
|
-
const
|
|
99
|
-
const deadline = Date.now() + waitMs;
|
|
100
|
-
while (Date.now() < deadline) {}
|
|
303
|
+
try { const { spawnSync } = require('child_process'); spawnSync(process.execPath, ['-e', 'setTimeout(()=>{}, 2000)'], { timeout: 2500, killSignal: 'SIGKILL', stdio: 'ignore', windowsHide: true }); } catch (_) {}
|
|
101
304
|
}
|
|
102
305
|
}
|
|
103
306
|
}
|
|
@@ -145,10 +348,17 @@ function fetchToFile(url, destPath, expectedTotal) {
|
|
|
145
348
|
return reject(new Error(`HTTP ${res.statusCode} for ${url}`));
|
|
146
349
|
}
|
|
147
350
|
const append = res.statusCode === 206 && existing > 0;
|
|
351
|
+
// Ensure parent dir exists — a concurrent prune may have removed it
|
|
352
|
+
// between lock-acquire and now. Recreating is cheap and avoids a
|
|
353
|
+
// confusing ENOENT later.
|
|
354
|
+
try { ensureDir(path.dirname(destPath)); } catch (_) {}
|
|
148
355
|
const out = fs.createWriteStream(destPath, { flags: append ? 'a' : 'w' });
|
|
149
356
|
let bytes = append ? existing : 0;
|
|
150
|
-
let
|
|
357
|
+
let lastStderr = Date.now();
|
|
151
358
|
let lastByte = Date.now();
|
|
359
|
+
const fetchStart = Date.now();
|
|
360
|
+
const safeUrl = (() => { try { const p = new URL(url); return p.hostname + p.pathname; } catch(_) { return url.split('?')[0]; } })();
|
|
361
|
+
obsEvent('bootstrap', 'fetch.start', { url: safeUrl, resume_from: existing, status: res.statusCode });
|
|
152
362
|
const stallTimer = setInterval(() => {
|
|
153
363
|
if (Date.now() - lastByte > STALL_TIMEOUT_MS) {
|
|
154
364
|
clearInterval(stallTimer);
|
|
@@ -158,15 +368,19 @@ function fetchToFile(url, destPath, expectedTotal) {
|
|
|
158
368
|
res.on('data', c => {
|
|
159
369
|
bytes += c.length;
|
|
160
370
|
lastByte = Date.now();
|
|
161
|
-
if (Date.now() -
|
|
371
|
+
if (Date.now() - lastStderr > 5000) {
|
|
162
372
|
const pct = expectedTotal ? ` ${Math.floor(bytes / expectedTotal * 100)}%` : '';
|
|
163
|
-
|
|
164
|
-
|
|
373
|
+
try { process.stderr.write(`[plugkit-bootstrap] downloading: ${(bytes / 1048576).toFixed(1)} MiB${pct}\n`); } catch (_) {}
|
|
374
|
+
lastStderr = Date.now();
|
|
165
375
|
}
|
|
166
376
|
});
|
|
167
377
|
res.pipe(out);
|
|
168
|
-
out.on('finish', () => {
|
|
169
|
-
|
|
378
|
+
out.on('finish', () => {
|
|
379
|
+
clearInterval(stallTimer);
|
|
380
|
+
obsEvent('bootstrap', 'fetch.end', { url: safeUrl, bytes, dur_ms: Date.now() - fetchStart, ok: true });
|
|
381
|
+
out.close(() => resolve(bytes));
|
|
382
|
+
});
|
|
383
|
+
out.on('error', err => { clearInterval(stallTimer); obsEvent('bootstrap', 'fetch.end', { url: safeUrl, bytes, dur_ms: Date.now() - fetchStart, ok: false, err: String(err.message || err) }); reject(err); });
|
|
170
384
|
res.on('error', err => { clearInterval(stallTimer); reject(err); });
|
|
171
385
|
res.on('end', () => clearInterval(stallTimer));
|
|
172
386
|
});
|
|
@@ -186,6 +400,7 @@ async function downloadWithRetry(url, destPath) {
|
|
|
186
400
|
} catch (err) {
|
|
187
401
|
lastErr = err;
|
|
188
402
|
log(`attempt ${attempt} failed: ${err.message}`);
|
|
403
|
+
obsEvent('bootstrap', 'fetch.attempt_failed', { url, attempt, max: MAX_ATTEMPTS, err: String(err.message || err) });
|
|
189
404
|
if (attempt < MAX_ATTEMPTS) {
|
|
190
405
|
const wait = BACKOFF_MS[attempt - 1] || 120000;
|
|
191
406
|
log(`backing off ${wait}ms`);
|
|
@@ -196,17 +411,31 @@ async function downloadWithRetry(url, destPath) {
|
|
|
196
411
|
throw lastErr;
|
|
197
412
|
}
|
|
198
413
|
|
|
199
|
-
function
|
|
414
|
+
function isLockStale(lockPath) {
|
|
415
|
+
try {
|
|
416
|
+
const st = fs.statSync(lockPath);
|
|
417
|
+
if (Date.now() - st.mtimeMs > LOCK_STALE_MS) return true;
|
|
418
|
+
const owner = parseInt(fs.readFileSync(lockPath, 'utf8').trim(), 10);
|
|
419
|
+
if (Number.isFinite(owner) && !pidAlive(owner)) return true;
|
|
420
|
+
} catch (_) { return true; }
|
|
421
|
+
return false;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function pruneOldVersions(root, keepVersion, keepRtkVersion) {
|
|
200
425
|
try {
|
|
201
426
|
const entries = fs.readdirSync(root);
|
|
202
427
|
for (const e of entries) {
|
|
203
|
-
|
|
204
|
-
|
|
428
|
+
const isPlugkit = e.startsWith('v') && !e.startsWith('rtk-');
|
|
429
|
+
const isRtk = e.startsWith('rtk-v');
|
|
430
|
+
if (!isPlugkit && !isRtk) continue;
|
|
431
|
+
if (isPlugkit && e === `v${keepVersion}`) continue;
|
|
432
|
+
if (isRtk && keepRtkVersion && e === `rtk-v${keepRtkVersion}`) continue;
|
|
205
433
|
const dir = path.join(root, e);
|
|
206
434
|
const lock = path.join(dir, '.lock');
|
|
207
|
-
if (fs.existsSync(lock)) continue;
|
|
435
|
+
if (fs.existsSync(lock) && !isLockStale(lock)) continue;
|
|
436
|
+
if (fs.existsSync(lock)) { try { fs.unlinkSync(lock); } catch (_) {} }
|
|
208
437
|
try {
|
|
209
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
438
|
+
fs.rmSync(dir, { recursive: true, force: true, maxRetries: 1, retryDelay: 50 });
|
|
210
439
|
log(`pruned ${dir}`);
|
|
211
440
|
} catch (err) { log(`prune skip ${dir}: ${err.message}`); }
|
|
212
441
|
}
|
|
@@ -230,10 +459,39 @@ async function bootstrap(opts) {
|
|
|
230
459
|
|
|
231
460
|
const finalPath = path.join(verDir, binName);
|
|
232
461
|
const okSentinel = path.join(verDir, '.ok');
|
|
462
|
+
const partialPath = `${finalPath}.partial`;
|
|
233
463
|
|
|
234
464
|
if (fs.existsSync(finalPath) && fs.existsSync(okSentinel)) {
|
|
235
|
-
if (
|
|
236
|
-
|
|
465
|
+
if (expectedSha) {
|
|
466
|
+
const actualSha = sha256OfFileSync(finalPath);
|
|
467
|
+
if (actualSha === expectedSha) {
|
|
468
|
+
obsEvent('bootstrap', 'decision.hit', { reason: 'sha-match', version, path: finalPath });
|
|
469
|
+
copyToGmTools(finalPath, wrapperDir, version);
|
|
470
|
+
clearBootstrapError();
|
|
471
|
+
return finalPath;
|
|
472
|
+
}
|
|
473
|
+
log(`decision: fetch reason: cache-hit-sha-mismatch (dir=v${version} expected ${expectedSha.slice(0,12)}… got ${(actualSha||'').slice(0,12)}…)`);
|
|
474
|
+
writeBootstrapError({
|
|
475
|
+
expected_version: version,
|
|
476
|
+
cached_version: null,
|
|
477
|
+
error_phase: 'cache-hit-sha-mismatch',
|
|
478
|
+
error_message: `cached binary at ${finalPath} sha=${actualSha} but manifest expects ${expectedSha}`,
|
|
479
|
+
});
|
|
480
|
+
try { fs.unlinkSync(finalPath); } catch (_) {}
|
|
481
|
+
try { fs.unlinkSync(okSentinel); } catch (_) {}
|
|
482
|
+
} else {
|
|
483
|
+
obsEvent('bootstrap', 'decision.hit', { reason: 'sentinel+no-sha-manifest', path: finalPath });
|
|
484
|
+
copyToGmTools(finalPath, wrapperDir, version);
|
|
485
|
+
clearBootstrapError();
|
|
486
|
+
return finalPath;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (healIfShaMatches(finalPath, expectedSha, okSentinel, partialPath, 'plugkit')) {
|
|
491
|
+
obsEvent('bootstrap', 'decision.heal', { reason: 'sha-match', path: finalPath });
|
|
492
|
+
spawnDetachedRtkFetch(wrapperDir);
|
|
493
|
+
copyToGmTools(finalPath, wrapperDir, version);
|
|
494
|
+
clearBootstrapError();
|
|
237
495
|
return finalPath;
|
|
238
496
|
}
|
|
239
497
|
|
|
@@ -241,18 +499,51 @@ async function bootstrap(opts) {
|
|
|
241
499
|
acquireLock(lockPath);
|
|
242
500
|
try {
|
|
243
501
|
if (fs.existsSync(finalPath) && fs.existsSync(okSentinel)) {
|
|
244
|
-
|
|
502
|
+
obsEvent('bootstrap', 'decision.hit', { reason: 'lock-race-resolved', path: finalPath });
|
|
503
|
+
copyToGmTools(finalPath, wrapperDir, version);
|
|
504
|
+
clearBootstrapError();
|
|
505
|
+
return finalPath;
|
|
506
|
+
}
|
|
507
|
+
if (healIfShaMatches(finalPath, expectedSha, okSentinel, partialPath, 'plugkit')) {
|
|
508
|
+
obsEvent('bootstrap', 'decision.heal', { reason: 'sha-match-under-lock', path: finalPath });
|
|
509
|
+
spawnDetachedRtkFetch(wrapperDir);
|
|
510
|
+
copyToGmTools(finalPath, wrapperDir, version);
|
|
511
|
+
clearBootstrapError();
|
|
245
512
|
return finalPath;
|
|
246
513
|
}
|
|
247
514
|
|
|
248
|
-
|
|
515
|
+
if (fs.existsSync(partialPath)) {
|
|
516
|
+
try {
|
|
517
|
+
const st = fs.statSync(partialPath);
|
|
518
|
+
if (Date.now() - st.mtimeMs > LOCK_STALE_MS) {
|
|
519
|
+
fs.unlinkSync(partialPath);
|
|
520
|
+
log(`cleared stale partial: ${partialPath}`);
|
|
521
|
+
}
|
|
522
|
+
} catch (_) {}
|
|
523
|
+
}
|
|
249
524
|
const url = `https://github.com/${RELEASE_REPO}/releases/download/v${version}/${binName}`;
|
|
250
|
-
|
|
525
|
+
try {
|
|
526
|
+
await downloadWithRetry(url, partialPath);
|
|
527
|
+
} catch (fetchErr) {
|
|
528
|
+
writeBootstrapError({
|
|
529
|
+
expected_version: version,
|
|
530
|
+
cached_version: null,
|
|
531
|
+
error_phase: 'download',
|
|
532
|
+
error_message: fetchErr && fetchErr.message ? fetchErr.message : String(fetchErr),
|
|
533
|
+
});
|
|
534
|
+
throw fetchErr;
|
|
535
|
+
}
|
|
251
536
|
|
|
252
537
|
if (expectedSha) {
|
|
253
|
-
const got = await sha256OfFile(
|
|
538
|
+
const got = await sha256OfFile(partialPath);
|
|
254
539
|
if (got !== expectedSha) {
|
|
255
|
-
try { fs.unlinkSync(
|
|
540
|
+
try { fs.unlinkSync(partialPath); } catch (_) {}
|
|
541
|
+
writeBootstrapError({
|
|
542
|
+
expected_version: version,
|
|
543
|
+
cached_version: null,
|
|
544
|
+
error_phase: 'sha256-mismatch',
|
|
545
|
+
error_message: `sha256 mismatch for ${binName}: expected ${expectedSha}, got ${got}`,
|
|
546
|
+
});
|
|
256
547
|
throw new Error(`sha256 mismatch for ${binName}: expected ${expectedSha}, got ${got}`);
|
|
257
548
|
}
|
|
258
549
|
log('sha256 verified');
|
|
@@ -260,11 +551,11 @@ async function bootstrap(opts) {
|
|
|
260
551
|
log('no sha256 manifest — skipping verify');
|
|
261
552
|
}
|
|
262
553
|
|
|
263
|
-
try { fs.renameSync(
|
|
554
|
+
try { fs.renameSync(partialPath, finalPath); }
|
|
264
555
|
catch (err) {
|
|
265
556
|
if (err.code === 'EEXIST' || err.code === 'EPERM') {
|
|
266
557
|
try { fs.unlinkSync(finalPath); } catch (_) {}
|
|
267
|
-
fs.renameSync(
|
|
558
|
+
fs.renameSync(partialPath, finalPath);
|
|
268
559
|
} else throw err;
|
|
269
560
|
}
|
|
270
561
|
|
|
@@ -273,14 +564,97 @@ async function bootstrap(opts) {
|
|
|
273
564
|
}
|
|
274
565
|
|
|
275
566
|
fs.writeFileSync(okSentinel, new Date().toISOString());
|
|
276
|
-
log(`
|
|
277
|
-
|
|
567
|
+
log(`decision: fetch reason: install-complete (${finalPath})`);
|
|
568
|
+
obsEvent('bootstrap', 'install.done', { path: finalPath, version, kind: 'plugkit' });
|
|
569
|
+
proactiveKillForNewInstall(version, finalPath);
|
|
570
|
+
pruneOldVersions(root, version, readRtkVersion(wrapperDir));
|
|
571
|
+
spawnDetachedRtkFetch(wrapperDir);
|
|
572
|
+
copyToGmTools(finalPath, wrapperDir, version);
|
|
573
|
+
clearBootstrapError();
|
|
278
574
|
return finalPath;
|
|
279
575
|
} finally {
|
|
280
576
|
releaseLock(lockPath);
|
|
281
577
|
}
|
|
282
578
|
}
|
|
283
579
|
|
|
580
|
+
function spawnDetachedRtkFetch(wrapperDir) {
|
|
581
|
+
try {
|
|
582
|
+
const { spawn } = require('child_process');
|
|
583
|
+
const child = spawn(process.execPath, [__filename, '--rtk-only', '--wrapper-dir', wrapperDir], {
|
|
584
|
+
detached: true,
|
|
585
|
+
stdio: 'ignore',
|
|
586
|
+
windowsHide: true,
|
|
587
|
+
});
|
|
588
|
+
child.unref();
|
|
589
|
+
obsEvent('bootstrap', 'rtk.detached.spawned', { pid: child.pid, wrapperDir });
|
|
590
|
+
} catch (err) {
|
|
591
|
+
log(`rtk detach spawn failed: ${err.message}`);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function rtkCacheDir(root, wrapperDir, plugkitVerDir) {
|
|
596
|
+
const rtkVer = readRtkVersion(wrapperDir);
|
|
597
|
+
if (!rtkVer) return plugkitVerDir;
|
|
598
|
+
const dir = path.join(root, `rtk-v${rtkVer}`);
|
|
599
|
+
ensureDir(dir);
|
|
600
|
+
return dir;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
async function bootstrapRtk(plugkitVerDir, plugkitVersion, wrapperDir, silent, root) {
|
|
604
|
+
const rtkName = rtkBinaryName();
|
|
605
|
+
const cacheDir = rtkCacheDir(root || cacheRoot(), wrapperDir, plugkitVerDir);
|
|
606
|
+
const rtkPath = path.join(cacheDir, rtkName);
|
|
607
|
+
const rtkOk = path.join(cacheDir, '.rtk-ok');
|
|
608
|
+
if (fs.existsSync(rtkPath) && fs.existsSync(rtkOk)) {
|
|
609
|
+
if (!silent) log(`rtk cache hit: ${rtkPath}`);
|
|
610
|
+
return rtkPath;
|
|
611
|
+
}
|
|
612
|
+
const rtkSha = readShaManifest(wrapperDir, 'rtk.sha256');
|
|
613
|
+
const expected = rtkSha ? rtkSha[rtkName] : null;
|
|
614
|
+
const tmp = `${rtkPath}.partial`;
|
|
615
|
+
if (healIfShaMatches(rtkPath, expected, rtkOk, tmp, 'rtk')) {
|
|
616
|
+
if (!silent) log(`rtk cache heal (sha match): ${rtkPath}`);
|
|
617
|
+
return rtkPath;
|
|
618
|
+
}
|
|
619
|
+
const url = `https://github.com/${RELEASE_REPO}/releases/download/v${plugkitVersion}/${rtkName}`;
|
|
620
|
+
await downloadWithRetry(url, tmp);
|
|
621
|
+
if (expected) {
|
|
622
|
+
const got = await sha256OfFile(tmp);
|
|
623
|
+
if (got !== expected) {
|
|
624
|
+
try { fs.unlinkSync(tmp); } catch (_) {}
|
|
625
|
+
throw new Error(`rtk sha256 mismatch: expected ${expected}, got ${got}`);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
try { fs.renameSync(tmp, rtkPath); }
|
|
629
|
+
catch (err) {
|
|
630
|
+
if (err.code === 'EEXIST' || err.code === 'EPERM') {
|
|
631
|
+
try { fs.unlinkSync(rtkPath); } catch (_) {}
|
|
632
|
+
fs.renameSync(tmp, rtkPath);
|
|
633
|
+
} else throw err;
|
|
634
|
+
}
|
|
635
|
+
if (os.platform() !== 'win32') { try { fs.chmodSync(rtkPath, 0o755); } catch (_) {} }
|
|
636
|
+
fs.writeFileSync(rtkOk, new Date().toISOString());
|
|
637
|
+
log(`installed ${rtkPath}`);
|
|
638
|
+
obsEvent('bootstrap', 'install.done', { path: rtkPath, plugkit_version: plugkitVersion, rtk_version: readRtkVersion(wrapperDir) || plugkitVersion, kind: 'rtk' });
|
|
639
|
+
return rtkPath;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function resolveCachedRtk(opts) {
|
|
643
|
+
opts = opts || {};
|
|
644
|
+
const wrapperDir = opts.wrapperDir || __dirname;
|
|
645
|
+
const version = opts.version || readVersionFile(wrapperDir);
|
|
646
|
+
const root = (() => {
|
|
647
|
+
try { const r = cacheRoot(); ensureDir(r); return r; }
|
|
648
|
+
catch (_) { const r = fallbackCacheRoot(); ensureDir(r); return r; }
|
|
649
|
+
})();
|
|
650
|
+
const plugkitVerDir = path.join(root, `v${version}`);
|
|
651
|
+
const cacheDir = rtkCacheDir(root, wrapperDir, plugkitVerDir);
|
|
652
|
+
const rtkPath = path.join(cacheDir, rtkBinaryName());
|
|
653
|
+
const rtkOk = path.join(cacheDir, '.rtk-ok');
|
|
654
|
+
if (fs.existsSync(rtkPath) && fs.existsSync(rtkOk)) return rtkPath;
|
|
655
|
+
return null;
|
|
656
|
+
}
|
|
657
|
+
|
|
284
658
|
function resolveCachedBinary(opts) {
|
|
285
659
|
opts = opts || {};
|
|
286
660
|
const wrapperDir = opts.wrapperDir || __dirname;
|
|
@@ -296,10 +670,226 @@ function resolveCachedBinary(opts) {
|
|
|
296
670
|
return null;
|
|
297
671
|
}
|
|
298
672
|
|
|
299
|
-
|
|
673
|
+
// ---------------------------------------------------------------------------
|
|
674
|
+
// Daemon kill on version change.
|
|
675
|
+
//
|
|
676
|
+
// The plugin tarball pins `plugkit.version`. When that pin advances and we
|
|
677
|
+
// install a newer cached binary, any long-running daemon (the runner) holds
|
|
678
|
+
// stale code and serves stale RPCs until killed. We track which version the
|
|
679
|
+
// daemon was last started under via `.daemon-version`; on every wrapper
|
|
680
|
+
// invocation, if the wrapper-pinned version differs, we kill the daemon so
|
|
681
|
+
// the next exec spawns it fresh under the new binary.
|
|
682
|
+
// ---------------------------------------------------------------------------
|
|
683
|
+
|
|
684
|
+
function daemonVersionSentinel() {
|
|
685
|
+
const root = (() => {
|
|
686
|
+
try { const r = cacheRoot(); ensureDir(r); return r; }
|
|
687
|
+
catch (_) { const r = fallbackCacheRoot(); ensureDir(r); return r; }
|
|
688
|
+
})();
|
|
689
|
+
return path.join(root, '.daemon-version');
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function readDaemonVersion() {
|
|
693
|
+
try { return fs.readFileSync(daemonVersionSentinel(), 'utf8').trim(); }
|
|
694
|
+
catch (_) { return null; }
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function writeDaemonVersion(v) {
|
|
698
|
+
try { fs.writeFileSync(daemonVersionSentinel(), String(v)); } catch (_) {}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function killPid(pid) {
|
|
702
|
+
if (!Number.isFinite(pid) || pid === process.pid || !pidAlive(pid)) return false;
|
|
703
|
+
try { process.kill(pid, 'SIGTERM'); }
|
|
704
|
+
catch (_) { try { process.kill(pid); } catch (_) {} }
|
|
705
|
+
if (os.platform() === 'win32' && pidAlive(pid)) {
|
|
706
|
+
try {
|
|
707
|
+
const { spawnSync } = require('child_process');
|
|
708
|
+
spawnSync('taskkill', ['/F', '/PID', String(pid)], { stdio: 'ignore', windowsHide: true, timeout: 3000, killSignal: 'SIGKILL' });
|
|
709
|
+
} catch (_) {}
|
|
710
|
+
}
|
|
711
|
+
return true;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
function killRunningDaemons(reason) {
|
|
715
|
+
const tmp = os.tmpdir();
|
|
716
|
+
const killedPids = [];
|
|
717
|
+
for (const pidFile of ['glootie-runner.pid', 'plugkit-runner.pid']) {
|
|
718
|
+
const pidPath = path.join(tmp, pidFile);
|
|
719
|
+
if (!fs.existsSync(pidPath)) continue;
|
|
720
|
+
try {
|
|
721
|
+
const pid = parseInt(fs.readFileSync(pidPath, 'utf8').trim(), 10);
|
|
722
|
+
if (killPid(pid)) {
|
|
723
|
+
killedPids.push(pid);
|
|
724
|
+
obsEvent('bootstrap', 'daemon.killed', { pid, pidFile, reason });
|
|
725
|
+
}
|
|
726
|
+
try { fs.unlinkSync(pidPath); } catch (_) {}
|
|
727
|
+
} catch (_) {}
|
|
728
|
+
}
|
|
729
|
+
return killedPids;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function listRunningPlugkitImagePaths() {
|
|
733
|
+
const out = [];
|
|
734
|
+
try {
|
|
735
|
+
const { spawnSync } = require('child_process');
|
|
736
|
+
if (os.platform() === 'win32') {
|
|
737
|
+
let parsed = null;
|
|
738
|
+
try {
|
|
739
|
+
const p = spawnSync('powershell', ['-NoProfile', '-NonInteractive', '-Command', "Get-Process plugkit* -ErrorAction SilentlyContinue | Select-Object Id,Path | ConvertTo-Json -Compress"], { windowsHide: true, encoding: 'utf8', timeout: 5000, killSignal: 'SIGKILL', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
740
|
+
const text = ((p && p.stdout) || '').trim();
|
|
741
|
+
if (text) {
|
|
742
|
+
const j = JSON.parse(text);
|
|
743
|
+
parsed = Array.isArray(j) ? j : [j];
|
|
744
|
+
}
|
|
745
|
+
} catch (_) {}
|
|
746
|
+
if (parsed) {
|
|
747
|
+
for (const item of parsed) {
|
|
748
|
+
if (!item) continue;
|
|
749
|
+
const pid = parseInt(item.Id, 10);
|
|
750
|
+
if (!Number.isFinite(pid)) continue;
|
|
751
|
+
out.push({ pid, path: (item.Path || '').trim() });
|
|
752
|
+
}
|
|
753
|
+
} else {
|
|
754
|
+
const r = spawnSync('tasklist', ['/FI', 'IMAGENAME eq plugkit*', '/FO', 'CSV', '/NH'], { windowsHide: true, encoding: 'utf8', timeout: 5000, killSignal: 'SIGKILL', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
755
|
+
const text = (r && r.stdout) || '';
|
|
756
|
+
const seen = new Set();
|
|
757
|
+
for (const line of text.split(/\r?\n/)) {
|
|
758
|
+
const m = line.match(/^"([^"]+)","(\d+)"/);
|
|
759
|
+
if (!m) continue;
|
|
760
|
+
const pid = parseInt(m[2], 10);
|
|
761
|
+
if (!Number.isFinite(pid) || seen.has(pid)) continue;
|
|
762
|
+
seen.add(pid);
|
|
763
|
+
out.push({ pid, path: '' });
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
} else if (os.platform() === 'linux') {
|
|
767
|
+
let entries = [];
|
|
768
|
+
try { entries = fs.readdirSync('/proc'); } catch (_) {}
|
|
769
|
+
for (const e of entries) {
|
|
770
|
+
if (!/^\d+$/.test(e)) continue;
|
|
771
|
+
const pid = parseInt(e, 10);
|
|
772
|
+
let comm = '';
|
|
773
|
+
try { comm = fs.readFileSync(`/proc/${pid}/comm`, 'utf8').trim(); } catch (_) { continue; }
|
|
774
|
+
if (!/^plugkit/i.test(comm)) continue;
|
|
775
|
+
let imagePath = '';
|
|
776
|
+
try { imagePath = fs.readlinkSync(`/proc/${pid}/exe`); } catch (_) {}
|
|
777
|
+
out.push({ pid, path: imagePath });
|
|
778
|
+
}
|
|
779
|
+
} else {
|
|
780
|
+
const r = spawnSync('ps', ['-axo', 'pid=,comm='], { encoding: 'utf8', timeout: 5000, killSignal: 'SIGKILL', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
781
|
+
const text = (r && r.stdout) || '';
|
|
782
|
+
for (const line of text.split(/\r?\n/)) {
|
|
783
|
+
const m = line.match(/^\s*(\d+)\s+(.+?)\s*$/);
|
|
784
|
+
if (!m) continue;
|
|
785
|
+
if (!/plugkit/i.test(m[2])) continue;
|
|
786
|
+
const pid = parseInt(m[1], 10);
|
|
787
|
+
let imagePath = '';
|
|
788
|
+
try {
|
|
789
|
+
const p = spawnSync('ps', ['-p', String(pid), '-o', 'command='], { encoding: 'utf8', timeout: 3000, killSignal: 'SIGKILL', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
790
|
+
imagePath = ((p && p.stdout) || '').trim().split(/\s+/)[0] || '';
|
|
791
|
+
} catch (_) {}
|
|
792
|
+
out.push({ pid, path: imagePath });
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
} catch (_) {}
|
|
796
|
+
return out;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
function killSpoolWatcherInCwd(reason) {
|
|
800
|
+
try {
|
|
801
|
+
const pidPath = path.join(process.cwd(), '.gm', 'exec-spool', '.watcher.pid');
|
|
802
|
+
if (!fs.existsSync(pidPath)) return null;
|
|
803
|
+
const pid = parseInt(fs.readFileSync(pidPath, 'utf8').trim(), 10);
|
|
804
|
+
if (killPid(pid)) {
|
|
805
|
+
obsEvent('bootstrap', 'watcher.killed', { pid, reason });
|
|
806
|
+
try { fs.unlinkSync(pidPath); } catch (_) {}
|
|
807
|
+
return pid;
|
|
808
|
+
}
|
|
809
|
+
try { fs.unlinkSync(pidPath); } catch (_) {}
|
|
810
|
+
} catch (_) {}
|
|
811
|
+
return null;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
function proactiveKillForNewInstall(installedVersion, finalPath) {
|
|
815
|
+
try {
|
|
816
|
+
const reason = `install:v${installedVersion}`;
|
|
817
|
+
const target = finalPath ? path.resolve(finalPath).toLowerCase() : null;
|
|
818
|
+
const cacheRootNorm = (() => {
|
|
819
|
+
try { return path.resolve(cacheRoot()).toLowerCase(); } catch (_) { return null; }
|
|
820
|
+
})();
|
|
821
|
+
const procs = listRunningPlugkitImagePaths();
|
|
822
|
+
for (const { pid, path: imagePath } of procs) {
|
|
823
|
+
if (!Number.isFinite(pid) || pid === process.pid) continue;
|
|
824
|
+
if (!imagePath) continue;
|
|
825
|
+
const norm = path.resolve(imagePath).toLowerCase();
|
|
826
|
+
if (target && norm === target) continue;
|
|
827
|
+
if (!cacheRootNorm || !norm.startsWith(cacheRootNorm + path.sep.toLowerCase())) continue;
|
|
828
|
+
if (killPid(pid)) {
|
|
829
|
+
try { process.stderr.write(`[bootstrap] killed stale daemon pid=${pid} path=${imagePath} (current install: v${installedVersion})\n`); } catch (_) {}
|
|
830
|
+
obsEvent('bootstrap', 'daemon.killed', { pid, oldPath: imagePath, installedVersion, mechanism: 'process-path' });
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
killRunningDaemons(reason);
|
|
834
|
+
killSpoolWatcherInCwd(reason);
|
|
835
|
+
writeDaemonVersion(installedVersion);
|
|
836
|
+
} catch (_) {}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// Compare wrapper-pinned version against last-recorded daemon version. If
|
|
840
|
+
// they differ, kill the daemon so it respawns under the new binary.
|
|
841
|
+
function killStaleDaemonIfVersionChanged(wrapperDir) {
|
|
842
|
+
let currentVersion;
|
|
843
|
+
try { currentVersion = readVersionFile(wrapperDir); } catch (_) { return; }
|
|
844
|
+
const cached = resolveCachedBinary({ wrapperDir, version: currentVersion });
|
|
845
|
+
if (cached) {
|
|
846
|
+
proactiveKillForNewInstall(currentVersion, cached);
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
const recorded = readDaemonVersion();
|
|
850
|
+
if (recorded === currentVersion) return;
|
|
851
|
+
if (recorded) killRunningDaemons(`version_change:${recorded}->${currentVersion}`);
|
|
852
|
+
writeDaemonVersion(currentVersion);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
module.exports = { bootstrap, resolveCachedBinary, resolveCachedRtk, platformKey, binaryName, rtkBinaryName, cacheRoot, obsEvent, killRunningDaemons, killStaleDaemonIfVersionChanged, killSpoolWatcherInCwd, proactiveKillForNewInstall };
|
|
300
856
|
|
|
301
857
|
if (require.main === module) {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
858
|
+
const argv = process.argv.slice(2);
|
|
859
|
+
if (argv.includes('--rtk-only')) {
|
|
860
|
+
const wIdx = argv.indexOf('--wrapper-dir');
|
|
861
|
+
const wrapperDir = wIdx >= 0 ? argv[wIdx + 1] : __dirname;
|
|
862
|
+
(async () => {
|
|
863
|
+
try {
|
|
864
|
+
const version = readVersionFile(wrapperDir);
|
|
865
|
+
let root = cacheRoot();
|
|
866
|
+
try { ensureDir(root); }
|
|
867
|
+
catch (_) { root = fallbackCacheRoot(); ensureDir(root); }
|
|
868
|
+
const verDir = path.join(root, `v${version}`);
|
|
869
|
+
ensureDir(verDir);
|
|
870
|
+
await bootstrapRtk(verDir, version, wrapperDir, true, root);
|
|
871
|
+
process.exit(0);
|
|
872
|
+
} catch (err) {
|
|
873
|
+
obsEvent('bootstrap', 'rtk.detached.failed', { err: String(err.message || err) });
|
|
874
|
+
process.exit(1);
|
|
875
|
+
}
|
|
876
|
+
})();
|
|
877
|
+
} else {
|
|
878
|
+
bootstrap({ silent: false })
|
|
879
|
+
.then(p => { process.stdout.write(p + '\n'); process.exit(0); })
|
|
880
|
+
.catch(err => {
|
|
881
|
+
log(`FATAL: ${err.message}`);
|
|
882
|
+
obsEvent('bootstrap', 'fatal', { err: String(err.message || err) });
|
|
883
|
+
try {
|
|
884
|
+
const pinned = (() => { try { return readVersionFile(__dirname); } catch (_) { return null; } })();
|
|
885
|
+
writeBootstrapError({
|
|
886
|
+
expected_version: pinned,
|
|
887
|
+
cached_version: null,
|
|
888
|
+
error_phase: 'fatal',
|
|
889
|
+
error_message: String(err && err.message || err),
|
|
890
|
+
});
|
|
891
|
+
} catch (_) {}
|
|
892
|
+
process.exit(1);
|
|
893
|
+
});
|
|
894
|
+
}
|
|
305
895
|
}
|