gm-plugkit 2.0.1074
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/bootstrap.js +896 -0
- package/cli.js +48 -0
- package/index.js +12 -0
- package/package.json +33 -0
- package/plugkit-wasm-wrapper.js +190 -0
- package/plugkit.sha256 +6 -0
- package/plugkit.version +1 -0
package/bootstrap.js
ADDED
|
@@ -0,0 +1,896 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const crypto = require('crypto');
|
|
8
|
+
const { spawn, spawnSync } = require('child_process');
|
|
9
|
+
|
|
10
|
+
const NPM_PACKAGE = '@anentrypoint/plugkit-wasm';
|
|
11
|
+
const ATTEMPT_TIMEOUT_MS = 10 * 60 * 1000;
|
|
12
|
+
const MAX_ATTEMPTS = 3;
|
|
13
|
+
const BACKOFF_MS = [5000, 15000];
|
|
14
|
+
const LOCK_STALE_MS = 30 * 60 * 1000;
|
|
15
|
+
|
|
16
|
+
const wrapperDir = __dirname;
|
|
17
|
+
|
|
18
|
+
function log(msg) {
|
|
19
|
+
try { process.stderr.write(`[gm-plugkit] ${msg}\n`); } catch (_) {}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function obsEvent(subsystem, event, fields) {
|
|
23
|
+
if (process.env.GM_LOG_DISABLE) return;
|
|
24
|
+
try {
|
|
25
|
+
const root = process.env.GM_LOG_DIR || path.join(os.homedir(), '.claude', 'gm-log');
|
|
26
|
+
const day = new Date().toISOString().slice(0, 10);
|
|
27
|
+
const dir = path.join(root, day);
|
|
28
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
29
|
+
const line = JSON.stringify({
|
|
30
|
+
ts: new Date().toISOString(), sub: subsystem, event,
|
|
31
|
+
pid: process.pid, sess: process.env.CLAUDE_SESSION_ID || process.env.GM_SESSION_ID || '',
|
|
32
|
+
...fields,
|
|
33
|
+
});
|
|
34
|
+
fs.appendFileSync(path.join(dir, `${subsystem}.jsonl`), line + '\n');
|
|
35
|
+
} catch (_) {}
|
|
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
|
+
fs.writeFileSync(path.join(spoolDir, '.bootstrap-error.json'), JSON.stringify({ ts: new Date().toISOString(), ...spec }, null, 2));
|
|
44
|
+
} catch (_) {}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function clearBootstrapError() {
|
|
48
|
+
try {
|
|
49
|
+
const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
50
|
+
fs.unlinkSync(path.join(projectDir, '.gm', 'exec-spool', '.bootstrap-error.json'));
|
|
51
|
+
} catch (_) {}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function platformKey() {
|
|
55
|
+
const p = os.platform();
|
|
56
|
+
const a = os.arch();
|
|
57
|
+
if (p === 'win32') return a === 'arm64' ? 'win32-arm64' : 'win32-x64';
|
|
58
|
+
if (p === 'darwin') return a === 'arm64' ? 'darwin-arm64' : 'darwin-x64';
|
|
59
|
+
return (a === 'arm64' || a === 'aarch64') ? 'linux-arm64' : 'linux-x64';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function binaryName() {
|
|
63
|
+
const key = platformKey();
|
|
64
|
+
return key.startsWith('win32') ? `plugkit-${key}.exe` : `plugkit-${key}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function cacheRoot() {
|
|
68
|
+
const home = os.homedir();
|
|
69
|
+
if (process.env.PLUGKIT_CACHE_DIR) return process.env.PLUGKIT_CACHE_DIR;
|
|
70
|
+
if (os.platform() === 'win32') {
|
|
71
|
+
const base = process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
|
|
72
|
+
return path.join(base, 'plugkit', 'bin');
|
|
73
|
+
}
|
|
74
|
+
if (os.platform() === 'darwin') return path.join(home, 'Library', 'Caches', 'plugkit', 'bin');
|
|
75
|
+
const xdg = process.env.XDG_CACHE_HOME || path.join(home, '.cache');
|
|
76
|
+
return path.join(xdg, 'plugkit', 'bin');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function fallbackCacheRoot() {
|
|
80
|
+
return path.join(os.tmpdir(), 'plugkit-cache', 'bin');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function gmToolsDir() {
|
|
84
|
+
const home = process.env.USERPROFILE || process.env.HOME || os.homedir();
|
|
85
|
+
return path.join(home, '.claude', 'gm-tools');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function readVersionFile() {
|
|
89
|
+
const p = path.join(wrapperDir, 'plugkit.version');
|
|
90
|
+
if (!fs.existsSync(p)) throw new Error(`plugkit.version not found at ${p}`);
|
|
91
|
+
return fs.readFileSync(p, 'utf8').trim();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function readShaManifest() {
|
|
95
|
+
const p = path.join(wrapperDir, 'plugkit.sha256');
|
|
96
|
+
if (!fs.existsSync(p)) return null;
|
|
97
|
+
const out = {};
|
|
98
|
+
for (const line of fs.readFileSync(p, 'utf8').split(/\r?\n/)) {
|
|
99
|
+
const m = line.match(/^([0-9a-f]{64})\s+(\S+)\s*$/i);
|
|
100
|
+
if (m) out[m[2]] = m[1].toLowerCase();
|
|
101
|
+
}
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function sha256OfFileSync(filePath) {
|
|
106
|
+
const h = crypto.createHash('sha256');
|
|
107
|
+
const fd = fs.openSync(filePath, 'r');
|
|
108
|
+
try {
|
|
109
|
+
const buf = Buffer.alloc(1024 * 1024);
|
|
110
|
+
for (;;) {
|
|
111
|
+
const n = fs.readSync(fd, buf, 0, buf.length, null);
|
|
112
|
+
if (n <= 0) break;
|
|
113
|
+
h.update(buf.subarray(0, n));
|
|
114
|
+
}
|
|
115
|
+
} finally { try { fs.closeSync(fd); } catch (_) {} }
|
|
116
|
+
return h.digest('hex');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function ensureDir(dir) {
|
|
120
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function pidAlive(pid) {
|
|
124
|
+
try { process.kill(pid, 0); return true; } catch (e) { return e.code === 'EPERM'; }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function acquireLock(lockPath) {
|
|
128
|
+
const start = Date.now();
|
|
129
|
+
for (;;) {
|
|
130
|
+
try {
|
|
131
|
+
const fd = fs.openSync(lockPath, 'wx');
|
|
132
|
+
fs.writeSync(fd, String(process.pid));
|
|
133
|
+
fs.closeSync(fd);
|
|
134
|
+
return true;
|
|
135
|
+
} catch (err) {
|
|
136
|
+
if (err.code !== 'EEXIST') throw err;
|
|
137
|
+
let stale = false;
|
|
138
|
+
try {
|
|
139
|
+
const st = fs.statSync(lockPath);
|
|
140
|
+
if (Date.now() - st.mtimeMs > LOCK_STALE_MS) stale = true;
|
|
141
|
+
const owner = parseInt(fs.readFileSync(lockPath, 'utf8').trim(), 10);
|
|
142
|
+
if (Number.isFinite(owner) && owner !== process.pid && !pidAlive(owner)) stale = true;
|
|
143
|
+
} catch (_) { stale = true; }
|
|
144
|
+
if (stale) {
|
|
145
|
+
try { fs.unlinkSync(lockPath); } catch (_) {}
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (Date.now() - start > ATTEMPT_TIMEOUT_MS) throw new Error(`lock wait timeout: ${lockPath}`);
|
|
149
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 2000);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function releaseLock(lockPath) {
|
|
155
|
+
try { fs.unlinkSync(lockPath); } catch (_) {}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function sha256OfFile(filePath) {
|
|
159
|
+
return new Promise((resolve, reject) => {
|
|
160
|
+
const h = crypto.createHash('sha256');
|
|
161
|
+
const s = fs.createReadStream(filePath);
|
|
162
|
+
s.on('data', c => h.update(c));
|
|
163
|
+
s.on('end', () => resolve(h.digest('hex')));
|
|
164
|
+
s.on('error', reject);
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function extractNpmPackageWasm(destPath, version) {
|
|
169
|
+
const tempDir = path.join(path.dirname(destPath), '.npm-extract-' + Date.now());
|
|
170
|
+
try {
|
|
171
|
+
ensureDir(tempDir);
|
|
172
|
+
const startMs = Date.now();
|
|
173
|
+
log(`extracting npm package ${NPM_PACKAGE}@${version} to ${tempDir}`);
|
|
174
|
+
obsEvent('bootstrap', 'npm.extract.start', { package: NPM_PACKAGE, version });
|
|
175
|
+
|
|
176
|
+
const result = spawnSync(
|
|
177
|
+
process.platform === 'win32' ? 'npx.cmd' : 'npx',
|
|
178
|
+
[NPM_PACKAGE + '@' + version, '--prefix', tempDir],
|
|
179
|
+
{
|
|
180
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
181
|
+
timeout: ATTEMPT_TIMEOUT_MS,
|
|
182
|
+
encoding: 'utf8',
|
|
183
|
+
windowsHide: true,
|
|
184
|
+
}
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
if (result.error) throw result.error;
|
|
188
|
+
if (result.status !== 0) {
|
|
189
|
+
throw new Error(`npx extraction failed: ${result.stderr || result.stdout || 'unknown error'}`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const nodeModulesPath = path.join(tempDir, 'node_modules', NPM_PACKAGE, 'plugkit.wasm');
|
|
193
|
+
if (!fs.existsSync(nodeModulesPath)) {
|
|
194
|
+
throw new Error(`plugkit.wasm not found in extracted npm package at ${nodeModulesPath}`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
fs.copyFileSync(nodeModulesPath, destPath);
|
|
198
|
+
log(`extracted ${nodeModulesPath} → ${destPath}`);
|
|
199
|
+
obsEvent('bootstrap', 'npm.extract.end', { dur_ms: Date.now() - startMs, ok: true });
|
|
200
|
+
} finally {
|
|
201
|
+
try { fs.rmSync(tempDir, { recursive: true, force: true, maxRetries: 1, retryDelay: 50 }); } catch (_) {}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function extractNpmPackageWithRetry(destPath, version) {
|
|
206
|
+
let lastErr;
|
|
207
|
+
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
208
|
+
try {
|
|
209
|
+
log(`npm extract attempt ${attempt}/${MAX_ATTEMPTS}: ${NPM_PACKAGE}@${version}`);
|
|
210
|
+
await extractNpmPackageWasm(destPath, version);
|
|
211
|
+
return;
|
|
212
|
+
} catch (err) {
|
|
213
|
+
lastErr = err;
|
|
214
|
+
log(`attempt ${attempt} failed: ${err.message}`);
|
|
215
|
+
obsEvent('bootstrap', 'npm.extract.attempt_failed', { package: NPM_PACKAGE, attempt, max: MAX_ATTEMPTS, err: String(err.message || err) });
|
|
216
|
+
if (attempt < MAX_ATTEMPTS) {
|
|
217
|
+
const wait = BACKOFF_MS[attempt - 1] || 120000;
|
|
218
|
+
log(`backing off ${wait}ms`);
|
|
219
|
+
await new Promise(r => setTimeout(r, wait));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
throw lastErr;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function killHoldersOfPath(targetPath) {
|
|
227
|
+
if (process.platform !== 'win32') return 0;
|
|
228
|
+
try {
|
|
229
|
+
const norm = path.resolve(targetPath).replace(/\//g, '\\');
|
|
230
|
+
const r = spawnSync('wmic', ['process', 'where', `ExecutablePath='${norm.replace(/\\/g, '\\\\')}'`, 'get', 'ProcessId', '/format:value'], { encoding: 'utf8', windowsHide: true, timeout: 5000 });
|
|
231
|
+
if (r.status !== 0 || !r.stdout) return 0;
|
|
232
|
+
const pids = [];
|
|
233
|
+
for (const line of r.stdout.split(/\r?\n/)) {
|
|
234
|
+
const m = line.match(/ProcessId=(\d+)/);
|
|
235
|
+
if (m) {
|
|
236
|
+
const pid = parseInt(m[1], 10);
|
|
237
|
+
if (Number.isFinite(pid) && pid !== process.pid) pids.push(pid);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
for (const pid of pids) {
|
|
241
|
+
try { spawnSync('taskkill', ['/F', '/PID', String(pid)], { windowsHide: true, timeout: 3000 }); } catch (_) {}
|
|
242
|
+
}
|
|
243
|
+
return pids.length;
|
|
244
|
+
} catch (_) { return 0; }
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function renameWithRetry(src, dst, attempts) {
|
|
248
|
+
for (let i = 0; i < attempts; i++) {
|
|
249
|
+
try { fs.renameSync(src, dst); return true; }
|
|
250
|
+
catch (err) {
|
|
251
|
+
if (err.code !== 'EEXIST' && err.code !== 'EPERM' && err.code !== 'EBUSY' && err.code !== 'EACCES') throw err;
|
|
252
|
+
if (i === Math.floor(attempts / 2)) killHoldersOfPath(dst);
|
|
253
|
+
try { spawnSync(process.execPath, ['-e', 'setTimeout(()=>{}, 200)'], { timeout: 400, killSignal: 'SIGKILL', stdio: 'ignore', windowsHide: true }); } catch (_) {}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function copyToGmTools(finalPath, version) {
|
|
260
|
+
const dst = gmToolsDir();
|
|
261
|
+
fs.mkdirSync(dst, { recursive: true });
|
|
262
|
+
const exeName = process.platform === 'win32' ? 'plugkit.exe' : 'plugkit';
|
|
263
|
+
const target = path.join(dst, exeName);
|
|
264
|
+
const targetTmp = target + '.new';
|
|
265
|
+
|
|
266
|
+
if (fs.existsSync(target)) {
|
|
267
|
+
let needsRefresh = true;
|
|
268
|
+
try {
|
|
269
|
+
const cur = sha256OfFileSync(target);
|
|
270
|
+
const src = sha256OfFileSync(finalPath);
|
|
271
|
+
if (cur === src) needsRefresh = false;
|
|
272
|
+
} catch (_) {}
|
|
273
|
+
if (!needsRefresh) {
|
|
274
|
+
try { fs.writeFileSync(path.join(dst, 'plugkit.version'), version); } catch (_) {}
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
try { killHoldersOfPath(target); } catch (_) {}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
fs.copyFileSync(finalPath, targetTmp);
|
|
281
|
+
if (!renameWithRetry(targetTmp, target, 8)) {
|
|
282
|
+
try { killHoldersOfPath(target); } catch (_) {}
|
|
283
|
+
try { fs.unlinkSync(target); } catch (_) {}
|
|
284
|
+
try { fs.renameSync(targetTmp, target); }
|
|
285
|
+
catch (_) {
|
|
286
|
+
try { fs.unlinkSync(targetTmp); } catch (_) {}
|
|
287
|
+
throw new Error(`gm-tools update blocked: cannot replace ${target}`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (process.platform !== 'win32') {
|
|
291
|
+
try { fs.chmodSync(target, 0o755); } catch (_) {}
|
|
292
|
+
}
|
|
293
|
+
fs.writeFileSync(path.join(dst, 'plugkit.version'), version);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function rtkBinaryName() {
|
|
297
|
+
const key = platformKey();
|
|
298
|
+
return key.startsWith('win32') ? `rtk-${key}.exe` : `rtk-${key}`;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function readRtkVersion() {
|
|
302
|
+
const p = path.join(wrapperDir, 'rtk.version');
|
|
303
|
+
if (!fs.existsSync(p)) return null;
|
|
304
|
+
const v = fs.readFileSync(p, 'utf8').trim();
|
|
305
|
+
return v || null;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function rtkCacheDir(root, plugkitVerDir) {
|
|
309
|
+
const rtkVer = readRtkVersion();
|
|
310
|
+
if (!rtkVer) return plugkitVerDir;
|
|
311
|
+
const dir = path.join(root, `rtk-v${rtkVer}`);
|
|
312
|
+
ensureDir(dir);
|
|
313
|
+
return dir;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function healIfShaMatches(binPath, expectedSha, sentinelPath, partialPath, kind) {
|
|
317
|
+
if (!fs.existsSync(binPath)) return false;
|
|
318
|
+
if (partialPath) { try { if (fs.existsSync(partialPath)) fs.unlinkSync(partialPath); } catch (_) {} }
|
|
319
|
+
if (!expectedSha) return false;
|
|
320
|
+
let got;
|
|
321
|
+
try { got = sha256OfFileSync(binPath); }
|
|
322
|
+
catch (_) { return false; }
|
|
323
|
+
if (got !== expectedSha) {
|
|
324
|
+
try { fs.unlinkSync(binPath); } catch (_) {}
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
try { fs.writeFileSync(sentinelPath, new Date().toISOString()); } catch (_) { return false; }
|
|
328
|
+
obsEvent('bootstrap', 'cache.heal', { path: binPath, kind });
|
|
329
|
+
return true;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function daemonVersionSentinel() {
|
|
333
|
+
const root = (() => {
|
|
334
|
+
try { const r = cacheRoot(); ensureDir(r); return r; }
|
|
335
|
+
catch (_) { const r = fallbackCacheRoot(); ensureDir(r); return r; }
|
|
336
|
+
})();
|
|
337
|
+
return path.join(root, '.daemon-version');
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function readDaemonVersion() {
|
|
341
|
+
try { return fs.readFileSync(daemonVersionSentinel(), 'utf8').trim(); }
|
|
342
|
+
catch (_) { return null; }
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function writeDaemonVersion(v) {
|
|
346
|
+
try { fs.writeFileSync(daemonVersionSentinel(), String(v)); } catch (_) {}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function killPid(pid) {
|
|
350
|
+
if (!Number.isFinite(pid) || pid === process.pid || !pidAlive(pid)) return false;
|
|
351
|
+
try { process.kill(pid, 'SIGTERM'); }
|
|
352
|
+
catch (_) { try { process.kill(pid); } catch (_) {} }
|
|
353
|
+
if (os.platform() === 'win32' && pidAlive(pid)) {
|
|
354
|
+
try { spawnSync('taskkill', ['/F', '/PID', String(pid)], { stdio: 'ignore', windowsHide: true, timeout: 3000, killSignal: 'SIGKILL' }); } catch (_) {}
|
|
355
|
+
}
|
|
356
|
+
return true;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function killRunningDaemons(reason) {
|
|
360
|
+
const tmp = os.tmpdir();
|
|
361
|
+
const killedPids = [];
|
|
362
|
+
for (const pidFile of ['glootie-runner.pid', 'plugkit-runner.pid']) {
|
|
363
|
+
const pidPath = path.join(tmp, pidFile);
|
|
364
|
+
if (!fs.existsSync(pidPath)) continue;
|
|
365
|
+
try {
|
|
366
|
+
const pid = parseInt(fs.readFileSync(pidPath, 'utf8').trim(), 10);
|
|
367
|
+
if (killPid(pid)) {
|
|
368
|
+
killedPids.push(pid);
|
|
369
|
+
obsEvent('bootstrap', 'daemon.killed', { pid, pidFile, reason });
|
|
370
|
+
}
|
|
371
|
+
try { fs.unlinkSync(pidPath); } catch (_) {}
|
|
372
|
+
} catch (_) {}
|
|
373
|
+
}
|
|
374
|
+
return killedPids;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function killSpoolWatcherInCwd(reason) {
|
|
378
|
+
try {
|
|
379
|
+
const pidPath = path.join(process.cwd(), '.gm', 'exec-spool', '.watcher.pid');
|
|
380
|
+
if (!fs.existsSync(pidPath)) return null;
|
|
381
|
+
const pid = parseInt(fs.readFileSync(pidPath, 'utf8').trim(), 10);
|
|
382
|
+
if (killPid(pid)) {
|
|
383
|
+
obsEvent('bootstrap', 'watcher.killed', { pid, reason });
|
|
384
|
+
try { fs.unlinkSync(pidPath); } catch (_) {}
|
|
385
|
+
return pid;
|
|
386
|
+
}
|
|
387
|
+
try { fs.unlinkSync(pidPath); } catch (_) {}
|
|
388
|
+
} catch (_) {}
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function listRunningPlugkitImagePaths() {
|
|
393
|
+
const out = [];
|
|
394
|
+
try {
|
|
395
|
+
if (os.platform() === 'win32') {
|
|
396
|
+
let parsed = null;
|
|
397
|
+
try {
|
|
398
|
+
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'] });
|
|
399
|
+
const text = ((p && p.stdout) || '').trim();
|
|
400
|
+
if (text) {
|
|
401
|
+
const j = JSON.parse(text);
|
|
402
|
+
parsed = Array.isArray(j) ? j : [j];
|
|
403
|
+
}
|
|
404
|
+
} catch (_) {}
|
|
405
|
+
if (parsed) {
|
|
406
|
+
for (const item of parsed) {
|
|
407
|
+
if (!item) continue;
|
|
408
|
+
const pid = parseInt(item.Id, 10);
|
|
409
|
+
if (!Number.isFinite(pid)) continue;
|
|
410
|
+
out.push({ pid, path: (item.Path || '').trim() });
|
|
411
|
+
}
|
|
412
|
+
} else {
|
|
413
|
+
const r = spawnSync('tasklist', ['/FI', 'IMAGENAME eq plugkit*', '/FO', 'CSV', '/NH'], { windowsHide: true, encoding: 'utf8', timeout: 5000, killSignal: 'SIGKILL', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
414
|
+
const text = (r && r.stdout) || '';
|
|
415
|
+
const seen = new Set();
|
|
416
|
+
for (const line of text.split(/\r?\n/)) {
|
|
417
|
+
const m = line.match(/^"([^"]+)","(\d+)"/);
|
|
418
|
+
if (!m) continue;
|
|
419
|
+
const pid = parseInt(m[2], 10);
|
|
420
|
+
if (!Number.isFinite(pid) || seen.has(pid)) continue;
|
|
421
|
+
seen.add(pid);
|
|
422
|
+
out.push({ pid, path: '' });
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
} else if (os.platform() === 'linux') {
|
|
426
|
+
let entries = [];
|
|
427
|
+
try { entries = fs.readdirSync('/proc'); } catch (_) {}
|
|
428
|
+
for (const e of entries) {
|
|
429
|
+
if (!/^\d+$/.test(e)) continue;
|
|
430
|
+
const pid = parseInt(e, 10);
|
|
431
|
+
let comm = '';
|
|
432
|
+
try { comm = fs.readFileSync(`/proc/${pid}/comm`, 'utf8').trim(); } catch (_) { continue; }
|
|
433
|
+
if (!/^plugkit/i.test(comm)) continue;
|
|
434
|
+
let imagePath = '';
|
|
435
|
+
try { imagePath = fs.readlinkSync(`/proc/${pid}/exe`); } catch (_) {}
|
|
436
|
+
out.push({ pid, path: imagePath });
|
|
437
|
+
}
|
|
438
|
+
} else {
|
|
439
|
+
const r = spawnSync('ps', ['-axo', 'pid=,comm='], { encoding: 'utf8', timeout: 5000, killSignal: 'SIGKILL', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
440
|
+
const text = (r && r.stdout) || '';
|
|
441
|
+
for (const line of text.split(/\r?\n/)) {
|
|
442
|
+
const m = line.match(/^\s*(\d+)\s+(.+?)\s*$/);
|
|
443
|
+
if (!m) continue;
|
|
444
|
+
if (!/plugkit/i.test(m[2])) continue;
|
|
445
|
+
const pid = parseInt(m[1], 10);
|
|
446
|
+
let imagePath = '';
|
|
447
|
+
try {
|
|
448
|
+
const p = spawnSync('ps', ['-p', String(pid), '-o', 'command='], { encoding: 'utf8', timeout: 3000, killSignal: 'SIGKILL', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
449
|
+
imagePath = ((p && p.stdout) || '').trim().split(/\s+/)[0] || '';
|
|
450
|
+
} catch (_) {}
|
|
451
|
+
out.push({ pid, path: imagePath });
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
} catch (_) {}
|
|
455
|
+
return out;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function isLockStale(lockPath) {
|
|
459
|
+
try {
|
|
460
|
+
const st = fs.statSync(lockPath);
|
|
461
|
+
if (Date.now() - st.mtimeMs > LOCK_STALE_MS) return true;
|
|
462
|
+
const owner = parseInt(fs.readFileSync(lockPath, 'utf8').trim(), 10);
|
|
463
|
+
if (Number.isFinite(owner) && !pidAlive(owner)) return true;
|
|
464
|
+
} catch (_) { return true; }
|
|
465
|
+
return false;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function pruneOldVersions(root, keepVersion, keepRtkVersion) {
|
|
469
|
+
try {
|
|
470
|
+
const entries = fs.readdirSync(root);
|
|
471
|
+
for (const e of entries) {
|
|
472
|
+
const isPlugkit = e.startsWith('v') && !e.startsWith('rtk-');
|
|
473
|
+
const isRtk = e.startsWith('rtk-v');
|
|
474
|
+
if (!isPlugkit && !isRtk) continue;
|
|
475
|
+
if (isPlugkit && e === `v${keepVersion}`) continue;
|
|
476
|
+
if (isRtk && keepRtkVersion && e === `rtk-v${keepRtkVersion}`) continue;
|
|
477
|
+
const dir = path.join(root, e);
|
|
478
|
+
const lock = path.join(dir, '.lock');
|
|
479
|
+
if (fs.existsSync(lock) && !isLockStale(lock)) continue;
|
|
480
|
+
if (fs.existsSync(lock)) { try { fs.unlinkSync(lock); } catch (_) {} }
|
|
481
|
+
try {
|
|
482
|
+
fs.rmSync(dir, { recursive: true, force: true, maxRetries: 1, retryDelay: 50 });
|
|
483
|
+
log(`pruned ${dir}`);
|
|
484
|
+
} catch (err) { log(`prune skip ${dir}: ${err.message}`); }
|
|
485
|
+
}
|
|
486
|
+
} catch (_) {}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function proactiveKillForNewInstall(installedVersion, finalPath) {
|
|
490
|
+
try {
|
|
491
|
+
const reason = `install:v${installedVersion}`;
|
|
492
|
+
const target = finalPath ? path.resolve(finalPath).toLowerCase() : null;
|
|
493
|
+
const cacheRootNorm = (() => {
|
|
494
|
+
try { return path.resolve(cacheRoot()).toLowerCase(); } catch (_) { return null; }
|
|
495
|
+
})();
|
|
496
|
+
const procs = listRunningPlugkitImagePaths();
|
|
497
|
+
for (const { pid, path: imagePath } of procs) {
|
|
498
|
+
if (!Number.isFinite(pid) || pid === process.pid) continue;
|
|
499
|
+
if (!imagePath) continue;
|
|
500
|
+
const norm = path.resolve(imagePath).toLowerCase();
|
|
501
|
+
if (target && norm === target) continue;
|
|
502
|
+
if (!cacheRootNorm || !norm.startsWith(cacheRootNorm + path.sep.toLowerCase())) continue;
|
|
503
|
+
if (killPid(pid)) {
|
|
504
|
+
try { process.stderr.write(`[bootstrap] killed stale daemon pid=${pid} path=${imagePath} (current install: v${installedVersion})\n`); } catch (_) {}
|
|
505
|
+
obsEvent('bootstrap', 'daemon.killed', { pid, oldPath: imagePath, installedVersion, mechanism: 'process-path' });
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
killRunningDaemons(reason);
|
|
509
|
+
killSpoolWatcherInCwd(reason);
|
|
510
|
+
writeDaemonVersion(installedVersion);
|
|
511
|
+
} catch (_) {}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function killStaleDaemonIfVersionChanged() {
|
|
515
|
+
let currentVersion;
|
|
516
|
+
try { currentVersion = readVersionFile(); } catch (_) { return; }
|
|
517
|
+
const cached = resolveCachedBinary({ version: currentVersion });
|
|
518
|
+
if (cached) {
|
|
519
|
+
proactiveKillForNewInstall(currentVersion, cached);
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
const recorded = readDaemonVersion();
|
|
523
|
+
if (recorded === currentVersion) return;
|
|
524
|
+
if (recorded) killRunningDaemons(`version_change:${recorded}->${currentVersion}`);
|
|
525
|
+
writeDaemonVersion(currentVersion);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function resolveCachedBinary(opts) {
|
|
529
|
+
opts = opts || {};
|
|
530
|
+
const version = opts.version || readVersionFile();
|
|
531
|
+
const root = (() => {
|
|
532
|
+
try { const r = cacheRoot(); ensureDir(r); return r; }
|
|
533
|
+
catch (_) { const r = fallbackCacheRoot(); ensureDir(r); return r; }
|
|
534
|
+
})();
|
|
535
|
+
const verDir = path.join(root, `v${version}`);
|
|
536
|
+
const finalPath = path.join(verDir, binaryName());
|
|
537
|
+
const okSentinel = path.join(verDir, '.ok');
|
|
538
|
+
if (fs.existsSync(finalPath) && fs.existsSync(okSentinel)) return finalPath;
|
|
539
|
+
return null;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function resolveCachedRtk() {
|
|
543
|
+
const version = readVersionFile();
|
|
544
|
+
const root = (() => {
|
|
545
|
+
try { const r = cacheRoot(); ensureDir(r); return r; }
|
|
546
|
+
catch (_) { const r = fallbackCacheRoot(); ensureDir(r); return r; }
|
|
547
|
+
})();
|
|
548
|
+
const plugkitVerDir = path.join(root, `v${version}`);
|
|
549
|
+
const cacheDir = rtkCacheDir(root, plugkitVerDir);
|
|
550
|
+
const rtkPath = path.join(cacheDir, rtkBinaryName());
|
|
551
|
+
const rtkOk = path.join(cacheDir, '.rtk-ok');
|
|
552
|
+
if (fs.existsSync(rtkPath) && fs.existsSync(rtkOk)) return rtkPath;
|
|
553
|
+
return null;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
async function bootstrapRtk(plugkitVerDir, plugkitVersion, silent, root) {
|
|
557
|
+
const rtkName = rtkBinaryName();
|
|
558
|
+
const cacheDir = rtkCacheDir(root || cacheRoot(), plugkitVerDir);
|
|
559
|
+
const rtkPath = path.join(cacheDir, rtkName);
|
|
560
|
+
const rtkOk = path.join(cacheDir, '.rtk-ok');
|
|
561
|
+
if (fs.existsSync(rtkPath) && fs.existsSync(rtkOk)) {
|
|
562
|
+
if (!silent) log(`rtk cache hit: ${rtkPath}`);
|
|
563
|
+
return rtkPath;
|
|
564
|
+
}
|
|
565
|
+
const rtkSha = readShaManifest();
|
|
566
|
+
// rtk.sha256 may be in a separate file
|
|
567
|
+
const rtkShaPath = path.join(wrapperDir, 'rtk.sha256');
|
|
568
|
+
let expected = null;
|
|
569
|
+
if (fs.existsSync(rtkShaPath)) {
|
|
570
|
+
expected = fs.readFileSync(rtkShaPath, 'utf8').trim();
|
|
571
|
+
}
|
|
572
|
+
const tmp = `${rtkPath}.partial`;
|
|
573
|
+
if (healIfShaMatches(rtkPath, expected, rtkOk, tmp, 'rtk')) {
|
|
574
|
+
if (!silent) log(`rtk cache heal (sha match): ${rtkPath}`);
|
|
575
|
+
return rtkPath;
|
|
576
|
+
}
|
|
577
|
+
const RTKS_RELEASE_REPO = 'AnEntrypoint/plugkit-bin';
|
|
578
|
+
const url = `https://github.com/${RTKS_RELEASE_REPO}/releases/download/v${plugkitVersion}/${rtkName}`;
|
|
579
|
+
const startMs = Date.now();
|
|
580
|
+
let lastErr;
|
|
581
|
+
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
582
|
+
try {
|
|
583
|
+
log(`rtk download attempt ${attempt}/${MAX_ATTEMPTS}: ${url}`);
|
|
584
|
+
const result = spawnSync(
|
|
585
|
+
'curl',
|
|
586
|
+
['-fSL', '--max-time', String(Math.floor(ATTEMPT_TIMEOUT_MS / 1000)), '-o', tmp, url],
|
|
587
|
+
{ stdio: 'pipe', timeout: ATTEMPT_TIMEOUT_MS + 5000, windowsHide: true }
|
|
588
|
+
);
|
|
589
|
+
if (result.error) throw result.error;
|
|
590
|
+
if (result.status !== 0) throw new Error(`curl failed with status ${result.status}`);
|
|
591
|
+
break;
|
|
592
|
+
} catch (err) {
|
|
593
|
+
lastErr = err;
|
|
594
|
+
log(`rtk attempt ${attempt} failed: ${err.message}`);
|
|
595
|
+
obsEvent('bootstrap', 'rtk.download.attempt_failed', { attempt, max: MAX_ATTEMPTS, err: String(err.message || err) });
|
|
596
|
+
if (attempt < MAX_ATTEMPTS) {
|
|
597
|
+
const wait = BACKOFF_MS[attempt - 1] || 120000;
|
|
598
|
+
log(`backing off ${wait}ms`);
|
|
599
|
+
await new Promise(r => setTimeout(r, wait));
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
if (lastErr) throw lastErr;
|
|
604
|
+
if (expected) {
|
|
605
|
+
const got = await sha256OfFile(tmp);
|
|
606
|
+
if (got !== expected) {
|
|
607
|
+
try { fs.unlinkSync(tmp); } catch (_) {}
|
|
608
|
+
throw new Error(`rtk sha256 mismatch: expected ${expected}, got ${got}`);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
try { fs.renameSync(tmp, rtkPath); }
|
|
612
|
+
catch (err) {
|
|
613
|
+
if (err.code === 'EEXIST' || err.code === 'EPERM') {
|
|
614
|
+
try { fs.unlinkSync(rtkPath); } catch (_) {}
|
|
615
|
+
fs.renameSync(tmp, rtkPath);
|
|
616
|
+
} else throw err;
|
|
617
|
+
}
|
|
618
|
+
if (os.platform() !== 'win32') { try { fs.chmodSync(rtkPath, 0o755); } catch (_) {} }
|
|
619
|
+
fs.writeFileSync(rtkOk, new Date().toISOString());
|
|
620
|
+
log(`installed ${rtkPath}`);
|
|
621
|
+
obsEvent('bootstrap', 'install.done', { path: rtkPath, plugkit_version: plugkitVersion, rtk_version: readRtkVersion() || plugkitVersion, kind: 'rtk', dur_ms: Date.now() - startMs });
|
|
622
|
+
return rtkPath;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function spawnDetachedRtkFetch() {
|
|
626
|
+
try {
|
|
627
|
+
const child = spawn(process.execPath, [__filename, '--rtk-only'], {
|
|
628
|
+
detached: true, stdio: 'ignore', windowsHide: true,
|
|
629
|
+
});
|
|
630
|
+
child.unref();
|
|
631
|
+
obsEvent('bootstrap', 'rtk.detached.spawned', { pid: child.pid });
|
|
632
|
+
} catch (err) {
|
|
633
|
+
log(`rtk detach spawn failed: ${err.message}`);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
async function bootstrap(opts) {
|
|
638
|
+
opts = opts || {};
|
|
639
|
+
const version = readVersionFile();
|
|
640
|
+
const shaManifest = readShaManifest();
|
|
641
|
+
const binName = binaryName();
|
|
642
|
+
const expectedSha = shaManifest ? shaManifest[binName] : null;
|
|
643
|
+
|
|
644
|
+
let root = cacheRoot();
|
|
645
|
+
try { ensureDir(root); }
|
|
646
|
+
catch (_) { root = fallbackCacheRoot(); ensureDir(root); }
|
|
647
|
+
|
|
648
|
+
const verDir = path.join(root, `v${version}`);
|
|
649
|
+
ensureDir(verDir);
|
|
650
|
+
|
|
651
|
+
const finalPath = path.join(verDir, binName);
|
|
652
|
+
const okSentinel = path.join(verDir, '.ok');
|
|
653
|
+
const partialPath = `${finalPath}.partial`;
|
|
654
|
+
|
|
655
|
+
if (fs.existsSync(finalPath) && fs.existsSync(okSentinel)) {
|
|
656
|
+
if (expectedSha) {
|
|
657
|
+
const actualSha = sha256OfFileSync(finalPath);
|
|
658
|
+
if (actualSha === expectedSha) {
|
|
659
|
+
obsEvent('bootstrap', 'decision.hit', { reason: 'sha-match', version, path: finalPath });
|
|
660
|
+
copyToGmTools(finalPath, version);
|
|
661
|
+
clearBootstrapError();
|
|
662
|
+
return finalPath;
|
|
663
|
+
}
|
|
664
|
+
log(`decision: fetch reason: cache-hit-sha-mismatch (dir=v${version} expected ${expectedSha.slice(0,12)}... got ${(actualSha||'').slice(0,12)}...)`);
|
|
665
|
+
writeBootstrapError({
|
|
666
|
+
expected_version: version, cached_version: null,
|
|
667
|
+
error_phase: 'cache-hit-sha-mismatch',
|
|
668
|
+
error_message: `cached binary at ${finalPath} sha=${actualSha} but manifest expects ${expectedSha}`,
|
|
669
|
+
});
|
|
670
|
+
try { fs.unlinkSync(finalPath); } catch (_) {}
|
|
671
|
+
try { fs.unlinkSync(okSentinel); } catch (_) {}
|
|
672
|
+
} else {
|
|
673
|
+
obsEvent('bootstrap', 'decision.hit', { reason: 'sentinel+no-sha-manifest', path: finalPath });
|
|
674
|
+
copyToGmTools(finalPath, version);
|
|
675
|
+
clearBootstrapError();
|
|
676
|
+
return finalPath;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
if (healIfShaMatches(finalPath, expectedSha, okSentinel, partialPath, 'plugkit')) {
|
|
681
|
+
obsEvent('bootstrap', 'decision.heal', { reason: 'sha-match', path: finalPath });
|
|
682
|
+
spawnDetachedRtkFetch();
|
|
683
|
+
copyToGmTools(finalPath, version);
|
|
684
|
+
clearBootstrapError();
|
|
685
|
+
return finalPath;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
const lockPath = path.join(verDir, '.lock');
|
|
689
|
+
acquireLock(lockPath);
|
|
690
|
+
try {
|
|
691
|
+
if (fs.existsSync(finalPath) && fs.existsSync(okSentinel)) {
|
|
692
|
+
obsEvent('bootstrap', 'decision.hit', { reason: 'lock-race-resolved', path: finalPath });
|
|
693
|
+
copyToGmTools(finalPath, version);
|
|
694
|
+
clearBootstrapError();
|
|
695
|
+
return finalPath;
|
|
696
|
+
}
|
|
697
|
+
if (healIfShaMatches(finalPath, expectedSha, okSentinel, partialPath, 'plugkit')) {
|
|
698
|
+
obsEvent('bootstrap', 'decision.heal', { reason: 'sha-match-under-lock', path: finalPath });
|
|
699
|
+
spawnDetachedRtkFetch();
|
|
700
|
+
copyToGmTools(finalPath, version);
|
|
701
|
+
clearBootstrapError();
|
|
702
|
+
return finalPath;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
if (fs.existsSync(partialPath)) {
|
|
706
|
+
try {
|
|
707
|
+
const st = fs.statSync(partialPath);
|
|
708
|
+
if (Date.now() - st.mtimeMs > LOCK_STALE_MS) {
|
|
709
|
+
fs.unlinkSync(partialPath);
|
|
710
|
+
log(`cleared stale partial: ${partialPath}`);
|
|
711
|
+
}
|
|
712
|
+
} catch (_) {}
|
|
713
|
+
}
|
|
714
|
+
try {
|
|
715
|
+
await extractNpmPackageWithRetry(partialPath, version);
|
|
716
|
+
} catch (extractErr) {
|
|
717
|
+
writeBootstrapError({
|
|
718
|
+
expected_version: version, cached_version: null,
|
|
719
|
+
error_phase: 'npm-extract',
|
|
720
|
+
error_message: extractErr && extractErr.message ? extractErr.message : String(extractErr),
|
|
721
|
+
});
|
|
722
|
+
throw extractErr;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
if (expectedSha) {
|
|
726
|
+
const got = await sha256OfFile(partialPath);
|
|
727
|
+
if (got !== expectedSha) {
|
|
728
|
+
try { fs.unlinkSync(partialPath); } catch (_) {}
|
|
729
|
+
writeBootstrapError({
|
|
730
|
+
expected_version: version, cached_version: null,
|
|
731
|
+
error_phase: 'sha256-mismatch',
|
|
732
|
+
error_message: `sha256 mismatch for ${binName}: expected ${expectedSha}, got ${got}`,
|
|
733
|
+
});
|
|
734
|
+
throw new Error(`sha256 mismatch for ${binName}: expected ${expectedSha}, got ${got}`);
|
|
735
|
+
}
|
|
736
|
+
log('sha256 verified');
|
|
737
|
+
} else {
|
|
738
|
+
log('no sha256 manifest — skipping verify');
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
try { fs.renameSync(partialPath, finalPath); }
|
|
742
|
+
catch (err) {
|
|
743
|
+
if (err.code === 'EEXIST' || err.code === 'EPERM') {
|
|
744
|
+
try { fs.unlinkSync(finalPath); } catch (_) {}
|
|
745
|
+
fs.renameSync(partialPath, finalPath);
|
|
746
|
+
} else throw err;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
if (os.platform() !== 'win32') {
|
|
750
|
+
try { fs.chmodSync(finalPath, 0o755); } catch (_) {}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
fs.writeFileSync(okSentinel, new Date().toISOString());
|
|
754
|
+
log(`decision: fetch reason: install-complete (${finalPath})`);
|
|
755
|
+
obsEvent('bootstrap', 'install.done', { path: finalPath, version, kind: 'plugkit' });
|
|
756
|
+
proactiveKillForNewInstall(version, finalPath);
|
|
757
|
+
pruneOldVersions(root, version, readRtkVersion());
|
|
758
|
+
spawnDetachedRtkFetch();
|
|
759
|
+
copyToGmTools(finalPath, version);
|
|
760
|
+
clearBootstrapError();
|
|
761
|
+
return finalPath;
|
|
762
|
+
} finally {
|
|
763
|
+
releaseLock(lockPath);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function getBinaryPath() {
|
|
768
|
+
const home = process.env.USERPROFILE || process.env.HOME || os.homedir();
|
|
769
|
+
const exe = process.platform === 'win32' ? 'plugkit.exe' : 'plugkit';
|
|
770
|
+
return path.join(home, '.claude', 'gm-tools', exe);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
function isReady() {
|
|
774
|
+
const bin = getBinaryPath();
|
|
775
|
+
if (!fs.existsSync(bin)) return false;
|
|
776
|
+
try {
|
|
777
|
+
const r = spawnSync(bin, ['--version'], { timeout: 3000, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true });
|
|
778
|
+
return r.status === 0;
|
|
779
|
+
} catch (_) { return false; }
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
function startSpoolDaemon() {
|
|
783
|
+
const bin = getBinaryPath();
|
|
784
|
+
if (!fs.existsSync(bin)) return { ok: false, error: 'binary not found' };
|
|
785
|
+
|
|
786
|
+
const pidFile = path.join(os.tmpdir(), 'gm-plugkit-spool.pid');
|
|
787
|
+
if (fs.existsSync(pidFile)) {
|
|
788
|
+
const pid = parseInt(fs.readFileSync(pidFile, 'utf8').trim(), 10);
|
|
789
|
+
if (Number.isFinite(pid) && pidAlive(pid)) {
|
|
790
|
+
return { ok: true, pid, status: 'already-running' };
|
|
791
|
+
}
|
|
792
|
+
try { fs.unlinkSync(pidFile); } catch (_) {}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
const child = spawn(bin, ['spool'], {
|
|
796
|
+
detached: true,
|
|
797
|
+
stdio: 'ignore',
|
|
798
|
+
windowsHide: true,
|
|
799
|
+
});
|
|
800
|
+
child.unref();
|
|
801
|
+
|
|
802
|
+
try { fs.writeFileSync(pidFile, String(child.pid)); } catch (_) {}
|
|
803
|
+
return { ok: true, pid: child.pid, status: 'started' };
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
async function ensureReady() {
|
|
807
|
+
if (isReady()) {
|
|
808
|
+
return { ok: true, binaryPath: getBinaryPath(), status: 'already-ready' };
|
|
809
|
+
}
|
|
810
|
+
const binaryPath = await bootstrap();
|
|
811
|
+
return { ok: true, binaryPath, status: 'bootstrapped' };
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
module.exports = {
|
|
815
|
+
bootstrap,
|
|
816
|
+
ensureReady,
|
|
817
|
+
startSpoolDaemon,
|
|
818
|
+
getBinaryPath,
|
|
819
|
+
isReady,
|
|
820
|
+
platformKey,
|
|
821
|
+
binaryName,
|
|
822
|
+
rtkBinaryName,
|
|
823
|
+
cacheRoot,
|
|
824
|
+
obsEvent,
|
|
825
|
+
killRunningDaemons,
|
|
826
|
+
killStaleDaemonIfVersionChanged,
|
|
827
|
+
killSpoolWatcherInCwd,
|
|
828
|
+
proactiveKillForNewInstall,
|
|
829
|
+
resolveCachedBinary,
|
|
830
|
+
resolveCachedRtk,
|
|
831
|
+
bootstrapRtk,
|
|
832
|
+
readDaemonVersion,
|
|
833
|
+
writeDaemonVersion,
|
|
834
|
+
daemonVersionSentinel,
|
|
835
|
+
};
|
|
836
|
+
|
|
837
|
+
if (require.main === module) {
|
|
838
|
+
(async () => {
|
|
839
|
+
try {
|
|
840
|
+
const args = process.argv.slice(2);
|
|
841
|
+
if (args.includes('--rtk-only')) {
|
|
842
|
+
const version = readVersionFile();
|
|
843
|
+
let root = cacheRoot();
|
|
844
|
+
try { ensureDir(root); }
|
|
845
|
+
catch (_) { root = fallbackCacheRoot(); ensureDir(root); }
|
|
846
|
+
const verDir = path.join(root, `v${version}`);
|
|
847
|
+
ensureDir(verDir);
|
|
848
|
+
await bootstrapRtk(verDir, version, true, root);
|
|
849
|
+
process.exit(0);
|
|
850
|
+
} else if (args.includes('--daemon')) {
|
|
851
|
+
const result = await ensureReady();
|
|
852
|
+
if (!result.ok) {
|
|
853
|
+
console.error('Bootstrap failed:', result.error);
|
|
854
|
+
process.exit(1);
|
|
855
|
+
}
|
|
856
|
+
const daemon = startSpoolDaemon();
|
|
857
|
+
console.log(JSON.stringify({ bootstrap: result, daemon }));
|
|
858
|
+
process.exit(0);
|
|
859
|
+
} else if (args.includes('--binary')) {
|
|
860
|
+
const result = await ensureReady();
|
|
861
|
+
if (result.ok) {
|
|
862
|
+
console.log(result.binaryPath);
|
|
863
|
+
process.exit(0);
|
|
864
|
+
} else {
|
|
865
|
+
console.error('Bootstrap failed:', result.error);
|
|
866
|
+
process.exit(1);
|
|
867
|
+
}
|
|
868
|
+
} else if (args.includes('--status')) {
|
|
869
|
+
console.log(JSON.stringify({
|
|
870
|
+
ready: isReady(),
|
|
871
|
+
binaryPath: getBinaryPath(),
|
|
872
|
+
platform: platformKey(),
|
|
873
|
+
daemonVersion: readDaemonVersion(),
|
|
874
|
+
cachedRtk: resolveCachedRtk(),
|
|
875
|
+
}));
|
|
876
|
+
process.exit(0);
|
|
877
|
+
} else {
|
|
878
|
+
const result = await ensureReady();
|
|
879
|
+
const daemon = startSpoolDaemon();
|
|
880
|
+
console.log(JSON.stringify({ bootstrap: result, daemon }));
|
|
881
|
+
process.exit(result.ok && daemon.ok ? 0 : 1);
|
|
882
|
+
}
|
|
883
|
+
} catch (err) {
|
|
884
|
+
obsEvent('bootstrap', 'fatal', { err: String(err.message || err) });
|
|
885
|
+
try {
|
|
886
|
+
const pinned = (() => { try { return readVersionFile(); } catch (_) { return null; } })();
|
|
887
|
+
writeBootstrapError({
|
|
888
|
+
expected_version: pinned, cached_version: null,
|
|
889
|
+
error_phase: 'fatal', error_message: String(err && err.message || err),
|
|
890
|
+
});
|
|
891
|
+
} catch (_) {}
|
|
892
|
+
console.error('gm-plugkit bootstrap failed:', err.message);
|
|
893
|
+
process.exit(1);
|
|
894
|
+
}
|
|
895
|
+
})();
|
|
896
|
+
}
|
package/cli.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { ensureReady, startSpoolDaemon, getBinaryPath, isReady } = require('./bootstrap');
|
|
5
|
+
|
|
6
|
+
const usage = `gm-plugkit — Bootstrap and daemon-spawn for gm plugkit binary.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
bun x gm-plugkit@latest Bootstrap + start spool daemon
|
|
10
|
+
bun x gm-plugkit@latest --daemon Same as default
|
|
11
|
+
bun x gm-plugkit@latest --binary Print binary path only
|
|
12
|
+
bun x gm-plugkit@latest --status JSON status check
|
|
13
|
+
bun x gm-plugkit@latest --help Show this help
|
|
14
|
+
`;
|
|
15
|
+
|
|
16
|
+
(async () => {
|
|
17
|
+
const args = process.argv.slice(2);
|
|
18
|
+
|
|
19
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
20
|
+
console.log(usage);
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const result = await ensureReady();
|
|
26
|
+
if (!result.ok) {
|
|
27
|
+
console.error('Bootstrap failed:', result.error);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const daemon = startSpoolDaemon();
|
|
32
|
+
if (!daemon.ok) {
|
|
33
|
+
console.error('Daemon start failed:', daemon.error);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
console.log(JSON.stringify({
|
|
38
|
+
ok: true,
|
|
39
|
+
binary: result.binaryPath,
|
|
40
|
+
daemon: daemon,
|
|
41
|
+
message: 'plugkit ready, spool watcher running'
|
|
42
|
+
}));
|
|
43
|
+
process.exit(0);
|
|
44
|
+
} catch (err) {
|
|
45
|
+
console.error('gm-plugkit failed:', err.message);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
})();
|
package/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "gm-plugkit",
|
|
3
|
+
"version": "2.0.1074",
|
|
4
|
+
"description": "Bootstrap and daemon-spawn tool for gm plugkit binary. Downloads the correct platform binary, verifies SHA256, and starts the spool watcher daemon. Includes plugkit-wasm-wrapper for WASM-based spool watching.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"gm-plugkit": "./cli.js",
|
|
8
|
+
"plugkit-wasm-wrapper": "./plugkit-wasm-wrapper.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"cli.js",
|
|
12
|
+
"index.js",
|
|
13
|
+
"bootstrap.js",
|
|
14
|
+
"plugkit-wasm-wrapper.js",
|
|
15
|
+
"plugkit.version",
|
|
16
|
+
"plugkit.sha256"
|
|
17
|
+
],
|
|
18
|
+
"keywords": [
|
|
19
|
+
"gm",
|
|
20
|
+
"plugkit",
|
|
21
|
+
"bootstrap",
|
|
22
|
+
"daemon",
|
|
23
|
+
"spool",
|
|
24
|
+
"wasm"
|
|
25
|
+
],
|
|
26
|
+
"author": "AnEntrypoint",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "https://github.com/AnEntrypoint/gm.git",
|
|
31
|
+
"directory": "gm-starter/gm-plugkit"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import { watch } from 'fs';
|
|
5
|
+
|
|
6
|
+
function createWasiShim() {
|
|
7
|
+
const shim = new Proxy({}, {
|
|
8
|
+
get(target, prop) {
|
|
9
|
+
if (prop === 'proc_exit') return (code) => process.exit(code);
|
|
10
|
+
return () => 0;
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
return shim;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function memWriteStr(memory, ptr, len, str) {
|
|
17
|
+
const bytes = new TextEncoder().encode(str);
|
|
18
|
+
const buf = new Uint8Array(memory.buffer, ptr, len);
|
|
19
|
+
buf.set(bytes.slice(0, len));
|
|
20
|
+
return Math.min(bytes.length, len);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function memReadStr(memory, ptr, len) {
|
|
24
|
+
const buf = new Uint8Array(memory.buffer, ptr, len);
|
|
25
|
+
return new TextDecoder().decode(buf);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let nextMemPtr = 0x10000;
|
|
29
|
+
function allocInMemory(memory, data) {
|
|
30
|
+
const ptr = nextMemPtr;
|
|
31
|
+
const buf = new Uint8Array(memory.buffer, ptr, data.length);
|
|
32
|
+
buf.set(data);
|
|
33
|
+
nextMemPtr += data.length;
|
|
34
|
+
if (nextMemPtr > memory.buffer.byteLength - 4096) nextMemPtr = 0x10000;
|
|
35
|
+
return { ptr, len: data.length };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function packResult(str) {
|
|
39
|
+
const bytes = new TextEncoder().encode(str);
|
|
40
|
+
const ptr = Math.random() * 0x100000 | 0;
|
|
41
|
+
const len = bytes.length;
|
|
42
|
+
return (BigInt(ptr) & 0xffffffffn) | (BigInt(len) << 32n);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function runSpoolWatcher(instance, spoolDir) {
|
|
46
|
+
const inDir = path.join(spoolDir, 'in');
|
|
47
|
+
const outDir = path.join(spoolDir, 'out');
|
|
48
|
+
fs.mkdirSync(inDir, { recursive: true });
|
|
49
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
50
|
+
|
|
51
|
+
console.log(`[plugkit-wasm] watching ${inDir}`);
|
|
52
|
+
|
|
53
|
+
const processed = new Set();
|
|
54
|
+
const dispatch = instance.exports.dispatch_verb;
|
|
55
|
+
if (!dispatch) throw new Error('dispatch_verb not exported');
|
|
56
|
+
|
|
57
|
+
const processFile = async (filePath) => {
|
|
58
|
+
const key = path.relative(inDir, filePath);
|
|
59
|
+
if (processed.has(key)) return;
|
|
60
|
+
processed.add(key);
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
64
|
+
const relPath = path.relative(inDir, filePath);
|
|
65
|
+
const dir = path.dirname(relPath);
|
|
66
|
+
const verb = dir === '.' ? path.basename(filePath, path.extname(filePath)) : dir;
|
|
67
|
+
const body = content.trim() || '{}';
|
|
68
|
+
|
|
69
|
+
const verbBytes = new TextEncoder().encode(verb);
|
|
70
|
+
const bodyBytes = new TextEncoder().encode(body);
|
|
71
|
+
|
|
72
|
+
const { ptr: verbPtr, len: verbLen } = allocInMemory(instance.exports.memory, verbBytes);
|
|
73
|
+
const { ptr: bodyPtr, len: bodyLen } = allocInMemory(instance.exports.memory, bodyBytes);
|
|
74
|
+
|
|
75
|
+
const result = dispatch(verbPtr, verbLen, bodyPtr, bodyLen);
|
|
76
|
+
|
|
77
|
+
const ptr = Number(result & 0xffffffffn);
|
|
78
|
+
const len = Number(result >> 32n);
|
|
79
|
+
const resultStr = memReadStr(instance.exports.memory, ptr, len);
|
|
80
|
+
|
|
81
|
+
const taskId = Math.random().toString(36).slice(2);
|
|
82
|
+
fs.writeFileSync(path.join(outDir, `${taskId}.json`), resultStr);
|
|
83
|
+
|
|
84
|
+
fs.unlinkSync(filePath);
|
|
85
|
+
processed.delete(key);
|
|
86
|
+
} catch (e) {
|
|
87
|
+
console.error(`[plugkit-wasm] error processing ${key}: ${e.message}`);
|
|
88
|
+
try { fs.unlinkSync(filePath); } catch (_) {}
|
|
89
|
+
processed.delete(key);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
function walkDir(dir) {
|
|
94
|
+
const files = [];
|
|
95
|
+
try {
|
|
96
|
+
for (const entry of fs.readdirSync(dir)) {
|
|
97
|
+
const fullPath = path.join(dir, entry);
|
|
98
|
+
const stat = fs.statSync(fullPath);
|
|
99
|
+
if (stat.isFile()) {
|
|
100
|
+
files.push(fullPath);
|
|
101
|
+
} else if (stat.isDirectory()) {
|
|
102
|
+
files.push(...walkDir(fullPath));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
} catch (e) {
|
|
106
|
+
console.error(`[plugkit-wasm] error walking ${dir}: ${e.message}`);
|
|
107
|
+
}
|
|
108
|
+
return files;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const existing = walkDir(inDir);
|
|
112
|
+
for (const fullPath of existing) {
|
|
113
|
+
await processFile(fullPath);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
let debounce = {};
|
|
117
|
+
watch(inDir, { recursive: true }, (eventType, filename) => {
|
|
118
|
+
if (!filename) return;
|
|
119
|
+
const fullPath = path.join(inDir, filename);
|
|
120
|
+
|
|
121
|
+
clearTimeout(debounce[fullPath]);
|
|
122
|
+
debounce[fullPath] = setTimeout(async () => {
|
|
123
|
+
try {
|
|
124
|
+
if (fs.existsSync(fullPath) && fs.statSync(fullPath).isFile()) {
|
|
125
|
+
await processFile(fullPath);
|
|
126
|
+
}
|
|
127
|
+
} catch (_) {}
|
|
128
|
+
delete debounce[fullPath];
|
|
129
|
+
}, 50);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
console.log('[plugkit-wasm] spool watcher running');
|
|
133
|
+
await new Promise(() => {});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
(async () => {
|
|
137
|
+
try {
|
|
138
|
+
const wasmPath = path.join(os.homedir(), '.claude', 'gm-tools', 'plugkit.wasm');
|
|
139
|
+
const wasmBuffer = fs.readFileSync(wasmPath);
|
|
140
|
+
const wasmModule = new WebAssembly.Module(wasmBuffer);
|
|
141
|
+
|
|
142
|
+
const memory = new WebAssembly.Memory({ initial: 256, maximum: 512 });
|
|
143
|
+
|
|
144
|
+
const hostFunctions = {
|
|
145
|
+
host_fs_read: () => 0,
|
|
146
|
+
host_fs_write: () => 0,
|
|
147
|
+
host_fs_readdir: () => 0,
|
|
148
|
+
host_fs_stat: () => 0,
|
|
149
|
+
host_kv_get: () => 0,
|
|
150
|
+
host_kv_put: () => 0,
|
|
151
|
+
host_kv_query: () => 0,
|
|
152
|
+
host_fetch: () => 0,
|
|
153
|
+
host_vec_search: () => 0,
|
|
154
|
+
host_vec_embed: () => 0,
|
|
155
|
+
host_browser_spawn: () => 0,
|
|
156
|
+
host_browser_eval: () => 0,
|
|
157
|
+
host_browser_close: () => 0,
|
|
158
|
+
host_exec_js: () => 0,
|
|
159
|
+
host_log: (ptr, len) => { console.log('[host_log]'); return 0; },
|
|
160
|
+
host_now_ms: () => BigInt(Date.now()),
|
|
161
|
+
host_env_get: () => 0,
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const importObject = {
|
|
165
|
+
env: { memory, ...hostFunctions },
|
|
166
|
+
wasi_snapshot_preview1: createWasiShim(),
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const instance = new WebAssembly.Instance(wasmModule, importObject);
|
|
170
|
+
|
|
171
|
+
const args = process.argv.slice(2);
|
|
172
|
+
if (args.includes('--version')) {
|
|
173
|
+
console.log('plugkit v0.1.366 (wasm)');
|
|
174
|
+
process.exit(0);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (args[0] === 'spool') {
|
|
178
|
+
const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
179
|
+
const spoolDir = path.join(projectDir, '.gm', 'exec-spool');
|
|
180
|
+
await runSpoolWatcher(instance, spoolDir);
|
|
181
|
+
} else {
|
|
182
|
+
console.log('[plugkit-wasm] args:', args.join(' '));
|
|
183
|
+
process.exit(0);
|
|
184
|
+
}
|
|
185
|
+
} catch (e) {
|
|
186
|
+
console.error('[plugkit-wasm] fatal:', e.message);
|
|
187
|
+
if (process.env.DEBUG) console.error(e.stack);
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
})();
|
package/plugkit.sha256
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
e5569efe81e4ef06c8349678253ca2845571495e619babb0a79be9268ea83c2a plugkit-win32-x64.exe
|
|
2
|
+
ce2f09f8ea0dd522345a9d9c3e5b04ba44bc37b2046d311d7ff1737f1b3fbf1a plugkit-win32-arm64.exe
|
|
3
|
+
a1a1d376986551828e5a39e4ae931accf66f00663aceac1439b2778cb4fffd27 plugkit-darwin-x64
|
|
4
|
+
7c36d730edab5cddf678211146ca670c9ce1def17d8b454234ce4bc04a4d7e85 plugkit-darwin-arm64
|
|
5
|
+
c9db60a399caf53c490dc08705713c7d83a1f62db057585a3950d64ab8fa449a plugkit-linux-x64
|
|
6
|
+
b9ebabaace995b1768d1d96ae13ca18a6dc5e2ca65b774fcdd457f069a7d115c plugkit-linux-arm64
|
package/plugkit.version
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.1.366
|