gm-skill 0.1.2 → 2.0.1080
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/AGENTS.md +1 -0
- package/LICENSE +21 -0
- package/README.md +20 -84
- package/agents/gm.md +22 -0
- package/agents/memorize.md +100 -0
- package/agents/research-worker.md +36 -0
- package/agents/textprocessing.md +47 -0
- package/bin/bootstrap.js +702 -0
- package/bin/plugkit.js +136 -0
- package/bin/plugkit.sha256 +7 -0
- package/bin/plugkit.version +1 -0
- package/bin/plugkit.wasm +0 -0
- package/bin/plugkit.wasm.sha256 +1 -0
- package/bin/rtk.sha256 +6 -0
- package/bin/rtk.version +1 -0
- package/gm-plugkit/bootstrap.js +694 -0
- package/gm-plugkit/cli.js +48 -0
- package/gm-plugkit/index.js +12 -0
- package/gm-plugkit/package.json +26 -0
- package/gm-plugkit/plugkit-wasm-wrapper.js +190 -0
- package/gm-plugkit/plugkit.sha256 +6 -0
- package/gm-plugkit/plugkit.version +1 -0
- package/gm.json +27 -0
- package/lang/browser.js +45 -0
- package/lang/ssh.js +166 -0
- package/lib/browser-spool-handler.js +130 -0
- package/lib/browser.js +131 -0
- package/lib/codeinsight.js +109 -0
- package/lib/daemon-bootstrap.js +253 -132
- package/lib/git.js +0 -1
- package/lib/learning.js +169 -0
- package/lib/skill-bootstrap.js +406 -0
- package/lib/spool-dispatch.js +100 -0
- package/lib/spool.js +87 -49
- package/lib/wasm-host.js +241 -0
- package/package.json +38 -20
- package/prompts/bash-deny.txt +22 -0
- package/prompts/pre-compact.txt +21 -0
- package/prompts/prompt-submit.txt +83 -0
- package/prompts/session-start.txt +15 -0
- package/scripts/run-hook.sh +7 -0
- package/scripts/watch-cascade.js +166 -0
- package/skills/browser/SKILL.md +80 -0
- package/skills/code-search/SKILL.md +48 -0
- package/skills/create-lang-plugin/SKILL.md +121 -0
- package/skills/gm/SKILL.md +10 -49
- package/skills/gm-complete/SKILL.md +16 -87
- package/skills/gm-emit/SKILL.md +17 -50
- package/skills/gm-execute/SKILL.md +18 -69
- package/skills/gm-skill/SKILL.md +43 -0
- package/skills/gm-skill/index.js +21 -0
- package/skills/governance/SKILL.md +97 -0
- package/skills/pages/SKILL.md +208 -0
- package/skills/planning/SKILL.md +21 -97
- package/skills/research/SKILL.md +43 -0
- package/skills/ssh/SKILL.md +71 -0
- package/skills/textprocessing/SKILL.md +40 -0
- package/skills/update-docs/SKILL.md +24 -43
- package/gm-complete.SKILL.md +0 -106
- package/gm-emit.SKILL.md +0 -70
- package/gm-execute.SKILL.md +0 -88
- package/gm.SKILL.md +0 -63
- package/index.js +0 -1
- package/lib/index.js +0 -37
- package/lib/loader.js +0 -66
- package/lib/manifest.js +0 -99
- package/lib/prepare.js +0 -14
- package/planning.SKILL.md +0 -118
- package/skills/gm/index.js +0 -113
- package/skills/gm-complete/index.js +0 -118
- package/skills/gm-complete.SKILL.md +0 -106
- package/skills/gm-emit/index.js +0 -90
- package/skills/gm-emit.SKILL.md +0 -70
- package/skills/gm-execute/index.js +0 -91
- package/skills/gm-execute.SKILL.md +0 -88
- package/skills/gm.SKILL.md +0 -63
- package/skills/planning/index.js +0 -107
- package/skills/planning.SKILL.md +0 -118
- package/skills/update-docs/index.js +0 -108
- package/skills/update-docs.SKILL.md +0 -66
- package/test-build.js +0 -29
- package/test-e2e.js +0 -117
- package/test-unified.js +0 -24
- package/test.js +0 -89
- package/update-docs.SKILL.md +0 -66
|
@@ -0,0 +1,694 @@
|
|
|
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
|
+
|
|
55
|
+
function cacheRoot() {
|
|
56
|
+
const home = os.homedir();
|
|
57
|
+
if (process.env.PLUGKIT_CACHE_DIR) return process.env.PLUGKIT_CACHE_DIR;
|
|
58
|
+
if (os.platform() === 'win32') {
|
|
59
|
+
const base = process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
|
|
60
|
+
return path.join(base, 'plugkit', 'bin');
|
|
61
|
+
}
|
|
62
|
+
if (os.platform() === 'darwin') return path.join(home, 'Library', 'Caches', 'plugkit', 'bin');
|
|
63
|
+
const xdg = process.env.XDG_CACHE_HOME || path.join(home, '.cache');
|
|
64
|
+
return path.join(xdg, 'plugkit', 'bin');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function fallbackCacheRoot() {
|
|
68
|
+
return path.join(os.tmpdir(), 'plugkit-cache', 'bin');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function gmToolsDir() {
|
|
72
|
+
const home = process.env.USERPROFILE || process.env.HOME || os.homedir();
|
|
73
|
+
return path.join(home, '.claude', 'gm-tools');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function readVersionFile() {
|
|
77
|
+
const p = path.join(wrapperDir, 'plugkit.version');
|
|
78
|
+
if (!fs.existsSync(p)) throw new Error(`plugkit.version not found at ${p}`);
|
|
79
|
+
return fs.readFileSync(p, 'utf8').trim();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function readShaManifest() {
|
|
83
|
+
const p = path.join(wrapperDir, 'plugkit.sha256');
|
|
84
|
+
if (!fs.existsSync(p)) return null;
|
|
85
|
+
const out = {};
|
|
86
|
+
for (const line of fs.readFileSync(p, 'utf8').split(/\r?\n/)) {
|
|
87
|
+
const m = line.match(/^([0-9a-f]{64})\s+(\S+)\s*$/i);
|
|
88
|
+
if (m) out[m[2]] = m[1].toLowerCase();
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function sha256OfFileSync(filePath) {
|
|
94
|
+
const h = crypto.createHash('sha256');
|
|
95
|
+
const fd = fs.openSync(filePath, 'r');
|
|
96
|
+
try {
|
|
97
|
+
const buf = Buffer.alloc(1024 * 1024);
|
|
98
|
+
for (;;) {
|
|
99
|
+
const n = fs.readSync(fd, buf, 0, buf.length, null);
|
|
100
|
+
if (n <= 0) break;
|
|
101
|
+
h.update(buf.subarray(0, n));
|
|
102
|
+
}
|
|
103
|
+
} finally { try { fs.closeSync(fd); } catch (_) {} }
|
|
104
|
+
return h.digest('hex');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function ensureDir(dir) {
|
|
108
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function pidAlive(pid) {
|
|
112
|
+
try { process.kill(pid, 0); return true; } catch (e) { return e.code === 'EPERM'; }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function acquireLock(lockPath) {
|
|
116
|
+
const start = Date.now();
|
|
117
|
+
for (;;) {
|
|
118
|
+
try {
|
|
119
|
+
const fd = fs.openSync(lockPath, 'wx');
|
|
120
|
+
fs.writeSync(fd, String(process.pid));
|
|
121
|
+
fs.closeSync(fd);
|
|
122
|
+
return true;
|
|
123
|
+
} catch (err) {
|
|
124
|
+
if (err.code !== 'EEXIST') throw err;
|
|
125
|
+
let stale = false;
|
|
126
|
+
try {
|
|
127
|
+
const st = fs.statSync(lockPath);
|
|
128
|
+
if (Date.now() - st.mtimeMs > LOCK_STALE_MS) stale = true;
|
|
129
|
+
const owner = parseInt(fs.readFileSync(lockPath, 'utf8').trim(), 10);
|
|
130
|
+
if (Number.isFinite(owner) && owner !== process.pid && !pidAlive(owner)) stale = true;
|
|
131
|
+
} catch (_) { stale = true; }
|
|
132
|
+
if (stale) {
|
|
133
|
+
try { fs.unlinkSync(lockPath); } catch (_) {}
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (Date.now() - start > ATTEMPT_TIMEOUT_MS) throw new Error(`lock wait timeout: ${lockPath}`);
|
|
137
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 2000);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function releaseLock(lockPath) {
|
|
143
|
+
try { fs.unlinkSync(lockPath); } catch (_) {}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function sha256OfFile(filePath) {
|
|
147
|
+
return new Promise((resolve, reject) => {
|
|
148
|
+
const h = crypto.createHash('sha256');
|
|
149
|
+
const s = fs.createReadStream(filePath);
|
|
150
|
+
s.on('data', c => h.update(c));
|
|
151
|
+
s.on('end', () => resolve(h.digest('hex')));
|
|
152
|
+
s.on('error', reject);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function extractNpmPackageWasm(destPath, version) {
|
|
157
|
+
const tempDir = path.join(path.dirname(destPath), '.npm-extract-' + Date.now());
|
|
158
|
+
try {
|
|
159
|
+
ensureDir(tempDir);
|
|
160
|
+
const startMs = Date.now();
|
|
161
|
+
log(`extracting npm package ${NPM_PACKAGE}@${version} to ${tempDir}`);
|
|
162
|
+
obsEvent('bootstrap', 'npm.extract.start', { package: NPM_PACKAGE, version });
|
|
163
|
+
|
|
164
|
+
const result = spawnSync(
|
|
165
|
+
process.platform === 'win32' ? 'npx.cmd' : 'npx',
|
|
166
|
+
[NPM_PACKAGE + '@' + version, '--prefix', tempDir],
|
|
167
|
+
{
|
|
168
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
169
|
+
timeout: ATTEMPT_TIMEOUT_MS,
|
|
170
|
+
encoding: 'utf8',
|
|
171
|
+
windowsHide: true,
|
|
172
|
+
}
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
if (result.error) throw result.error;
|
|
176
|
+
if (result.status !== 0) {
|
|
177
|
+
throw new Error(`npx extraction failed: ${result.stderr || result.stdout || 'unknown error'}`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const nodeModulesPath = path.join(tempDir, 'node_modules', NPM_PACKAGE, 'plugkit.wasm');
|
|
181
|
+
if (!fs.existsSync(nodeModulesPath)) {
|
|
182
|
+
throw new Error(`plugkit.wasm not found in extracted npm package at ${nodeModulesPath}`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
fs.copyFileSync(nodeModulesPath, destPath);
|
|
186
|
+
log(`extracted ${nodeModulesPath} → ${destPath}`);
|
|
187
|
+
obsEvent('bootstrap', 'npm.extract.end', { dur_ms: Date.now() - startMs, ok: true });
|
|
188
|
+
} finally {
|
|
189
|
+
try { fs.rmSync(tempDir, { recursive: true, force: true, maxRetries: 1, retryDelay: 50 }); } catch (_) {}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function extractNpmPackageWithRetry(destPath, version) {
|
|
194
|
+
let lastErr;
|
|
195
|
+
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
196
|
+
try {
|
|
197
|
+
log(`npm extract attempt ${attempt}/${MAX_ATTEMPTS}: ${NPM_PACKAGE}@${version}`);
|
|
198
|
+
await extractNpmPackageWasm(destPath, version);
|
|
199
|
+
return;
|
|
200
|
+
} catch (err) {
|
|
201
|
+
lastErr = err;
|
|
202
|
+
log(`attempt ${attempt} failed: ${err.message}`);
|
|
203
|
+
obsEvent('bootstrap', 'npm.extract.attempt_failed', { package: NPM_PACKAGE, attempt, max: MAX_ATTEMPTS, err: String(err.message || err) });
|
|
204
|
+
if (attempt < MAX_ATTEMPTS) {
|
|
205
|
+
const wait = BACKOFF_MS[attempt - 1] || 120000;
|
|
206
|
+
log(`backing off ${wait}ms`);
|
|
207
|
+
await new Promise(r => setTimeout(r, wait));
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
throw lastErr;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
function platformKey() {
|
|
216
|
+
const p = os.platform();
|
|
217
|
+
const a = os.arch();
|
|
218
|
+
if (p === 'win32') return a === 'arm64' ? 'win32-arm64' : 'win32-x64';
|
|
219
|
+
if (p === 'darwin') return a === 'arm64' ? 'darwin-arm64' : 'darwin-x64';
|
|
220
|
+
return (a === 'arm64' || a === 'aarch64') ? 'linux-arm64' : 'linux-x64';
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function rtkBinaryName() {
|
|
224
|
+
const key = platformKey();
|
|
225
|
+
return key.startsWith('win32') ? `rtk-${key}.exe` : `rtk-${key}`;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function readRtkVersion() {
|
|
229
|
+
const p = path.join(wrapperDir, 'rtk.version');
|
|
230
|
+
if (!fs.existsSync(p)) return null;
|
|
231
|
+
const v = fs.readFileSync(p, 'utf8').trim();
|
|
232
|
+
return v || null;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function rtkCacheDir(root, plugkitVerDir) {
|
|
236
|
+
const rtkVer = readRtkVersion();
|
|
237
|
+
if (!rtkVer) return plugkitVerDir;
|
|
238
|
+
const dir = path.join(root, `rtk-v${rtkVer}`);
|
|
239
|
+
ensureDir(dir);
|
|
240
|
+
return dir;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function healIfShaMatches(binPath, expectedSha, sentinelPath, partialPath, kind) {
|
|
244
|
+
if (!fs.existsSync(binPath)) return false;
|
|
245
|
+
if (partialPath) { try { if (fs.existsSync(partialPath)) fs.unlinkSync(partialPath); } catch (_) {} }
|
|
246
|
+
if (!expectedSha) return false;
|
|
247
|
+
let got;
|
|
248
|
+
try { got = sha256OfFileSync(binPath); }
|
|
249
|
+
catch (_) { return false; }
|
|
250
|
+
if (got !== expectedSha) {
|
|
251
|
+
try { fs.unlinkSync(binPath); } catch (_) {}
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
try { fs.writeFileSync(sentinelPath, new Date().toISOString()); } catch (_) { return false; }
|
|
255
|
+
obsEvent('bootstrap', 'cache.heal', { path: binPath, kind });
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function daemonVersionSentinel() {
|
|
260
|
+
const root = (() => {
|
|
261
|
+
try { const r = cacheRoot(); ensureDir(r); return r; }
|
|
262
|
+
catch (_) { const r = fallbackCacheRoot(); ensureDir(r); return r; }
|
|
263
|
+
})();
|
|
264
|
+
return path.join(root, '.daemon-version');
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function readDaemonVersion() {
|
|
268
|
+
try { return fs.readFileSync(daemonVersionSentinel(), 'utf8').trim(); }
|
|
269
|
+
catch (_) { return null; }
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function writeDaemonVersion(v) {
|
|
273
|
+
try { fs.writeFileSync(daemonVersionSentinel(), String(v)); } catch (_) {}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function killPid(pid) {
|
|
277
|
+
if (!Number.isFinite(pid) || pid === process.pid || !pidAlive(pid)) return false;
|
|
278
|
+
try { process.kill(pid, 'SIGTERM'); }
|
|
279
|
+
catch (_) { try { process.kill(pid); } catch (_) {} }
|
|
280
|
+
if (os.platform() === 'win32' && pidAlive(pid)) {
|
|
281
|
+
try { spawnSync('taskkill', ['/F', '/PID', String(pid)], { stdio: 'ignore', windowsHide: true, timeout: 3000, killSignal: 'SIGKILL' }); } catch (_) {}
|
|
282
|
+
}
|
|
283
|
+
return true;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function killRunningDaemons(reason) {
|
|
287
|
+
const tmp = os.tmpdir();
|
|
288
|
+
const killedPids = [];
|
|
289
|
+
for (const pidFile of ['glootie-runner.pid', 'plugkit-runner.pid']) {
|
|
290
|
+
const pidPath = path.join(tmp, pidFile);
|
|
291
|
+
if (!fs.existsSync(pidPath)) continue;
|
|
292
|
+
try {
|
|
293
|
+
const pid = parseInt(fs.readFileSync(pidPath, 'utf8').trim(), 10);
|
|
294
|
+
if (killPid(pid)) {
|
|
295
|
+
killedPids.push(pid);
|
|
296
|
+
obsEvent('bootstrap', 'daemon.killed', { pid, pidFile, reason });
|
|
297
|
+
}
|
|
298
|
+
try { fs.unlinkSync(pidPath); } catch (_) {}
|
|
299
|
+
} catch (_) {}
|
|
300
|
+
}
|
|
301
|
+
return killedPids;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function killSpoolWatcherInCwd(reason) {
|
|
305
|
+
try {
|
|
306
|
+
const pidPath = path.join(process.cwd(), '.gm', 'exec-spool', '.watcher.pid');
|
|
307
|
+
if (!fs.existsSync(pidPath)) return null;
|
|
308
|
+
const pid = parseInt(fs.readFileSync(pidPath, 'utf8').trim(), 10);
|
|
309
|
+
if (killPid(pid)) {
|
|
310
|
+
obsEvent('bootstrap', 'watcher.killed', { pid, reason });
|
|
311
|
+
try { fs.unlinkSync(pidPath); } catch (_) {}
|
|
312
|
+
return pid;
|
|
313
|
+
}
|
|
314
|
+
try { fs.unlinkSync(pidPath); } catch (_) {}
|
|
315
|
+
} catch (_) {}
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
function isLockStale(lockPath) {
|
|
321
|
+
try {
|
|
322
|
+
const st = fs.statSync(lockPath);
|
|
323
|
+
if (Date.now() - st.mtimeMs > LOCK_STALE_MS) return true;
|
|
324
|
+
const owner = parseInt(fs.readFileSync(lockPath, 'utf8').trim(), 10);
|
|
325
|
+
if (Number.isFinite(owner) && !pidAlive(owner)) return true;
|
|
326
|
+
} catch (_) { return true; }
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function pruneOldVersions(root, keepVersion, keepRtkVersion) {
|
|
331
|
+
try {
|
|
332
|
+
const entries = fs.readdirSync(root);
|
|
333
|
+
for (const e of entries) {
|
|
334
|
+
const isPlugkit = e.startsWith('v') && !e.startsWith('rtk-');
|
|
335
|
+
const isRtk = e.startsWith('rtk-v');
|
|
336
|
+
if (!isPlugkit && !isRtk) continue;
|
|
337
|
+
if (isPlugkit && e === `v${keepVersion}`) continue;
|
|
338
|
+
if (isRtk && keepRtkVersion && e === `rtk-v${keepRtkVersion}`) continue;
|
|
339
|
+
const dir = path.join(root, e);
|
|
340
|
+
const lock = path.join(dir, '.lock');
|
|
341
|
+
if (fs.existsSync(lock) && !isLockStale(lock)) continue;
|
|
342
|
+
if (fs.existsSync(lock)) { try { fs.unlinkSync(lock); } catch (_) {} }
|
|
343
|
+
try {
|
|
344
|
+
fs.rmSync(dir, { recursive: true, force: true, maxRetries: 1, retryDelay: 50 });
|
|
345
|
+
log(`pruned ${dir}`);
|
|
346
|
+
} catch (err) { log(`prune skip ${dir}: ${err.message}`); }
|
|
347
|
+
}
|
|
348
|
+
} catch (_) {}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function proactiveKillForNewInstall(installedVersion) {
|
|
352
|
+
try {
|
|
353
|
+
const reason = `install:v${installedVersion}`;
|
|
354
|
+
killRunningDaemons(reason);
|
|
355
|
+
killSpoolWatcherInCwd(reason);
|
|
356
|
+
writeDaemonVersion(installedVersion);
|
|
357
|
+
} catch (_) {}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function killStaleDaemonIfVersionChanged() {
|
|
361
|
+
let currentVersion;
|
|
362
|
+
try { currentVersion = readVersionFile(); } catch (_) { return; }
|
|
363
|
+
const cached = resolveCachedBinary({ version: currentVersion });
|
|
364
|
+
if (cached) {
|
|
365
|
+
proactiveKillForNewInstall(currentVersion, cached);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
const recorded = readDaemonVersion();
|
|
369
|
+
if (recorded === currentVersion) return;
|
|
370
|
+
if (recorded) killRunningDaemons(`version_change:${recorded}->${currentVersion}`);
|
|
371
|
+
writeDaemonVersion(currentVersion);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
function resolveCachedRtk() {
|
|
376
|
+
const version = readVersionFile();
|
|
377
|
+
const root = (() => {
|
|
378
|
+
try { const r = cacheRoot(); ensureDir(r); return r; }
|
|
379
|
+
catch (_) { const r = fallbackCacheRoot(); ensureDir(r); return r; }
|
|
380
|
+
})();
|
|
381
|
+
const plugkitVerDir = path.join(root, `v${version}`);
|
|
382
|
+
const cacheDir = rtkCacheDir(root, plugkitVerDir);
|
|
383
|
+
const rtkPath = path.join(cacheDir, rtkBinaryName());
|
|
384
|
+
const rtkOk = path.join(cacheDir, '.rtk-ok');
|
|
385
|
+
if (fs.existsSync(rtkPath) && fs.existsSync(rtkOk)) return rtkPath;
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
async function bootstrapRtk(plugkitVerDir, plugkitVersion, silent, root) {
|
|
390
|
+
const rtkName = rtkBinaryName();
|
|
391
|
+
const cacheDir = rtkCacheDir(root || cacheRoot(), plugkitVerDir);
|
|
392
|
+
const rtkPath = path.join(cacheDir, rtkName);
|
|
393
|
+
const rtkOk = path.join(cacheDir, '.rtk-ok');
|
|
394
|
+
if (fs.existsSync(rtkPath) && fs.existsSync(rtkOk)) {
|
|
395
|
+
if (!silent) log(`rtk cache hit: ${rtkPath}`);
|
|
396
|
+
return rtkPath;
|
|
397
|
+
}
|
|
398
|
+
const rtkSha = readShaManifest();
|
|
399
|
+
// rtk.sha256 may be in a separate file
|
|
400
|
+
const rtkShaPath = path.join(wrapperDir, 'rtk.sha256');
|
|
401
|
+
let expected = null;
|
|
402
|
+
if (fs.existsSync(rtkShaPath)) {
|
|
403
|
+
expected = fs.readFileSync(rtkShaPath, 'utf8').trim();
|
|
404
|
+
}
|
|
405
|
+
const tmp = `${rtkPath}.partial`;
|
|
406
|
+
if (healIfShaMatches(rtkPath, expected, rtkOk, tmp, 'rtk')) {
|
|
407
|
+
if (!silent) log(`rtk cache heal (sha match): ${rtkPath}`);
|
|
408
|
+
return rtkPath;
|
|
409
|
+
}
|
|
410
|
+
const RTKS_RELEASE_REPO = 'AnEntrypoint/plugkit-bin';
|
|
411
|
+
const url = `https://github.com/${RTKS_RELEASE_REPO}/releases/download/v${plugkitVersion}/${rtkName}`;
|
|
412
|
+
const startMs = Date.now();
|
|
413
|
+
let lastErr;
|
|
414
|
+
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
415
|
+
try {
|
|
416
|
+
log(`rtk download attempt ${attempt}/${MAX_ATTEMPTS}: ${url}`);
|
|
417
|
+
const result = spawnSync(
|
|
418
|
+
'curl',
|
|
419
|
+
['-fSL', '--max-time', String(Math.floor(ATTEMPT_TIMEOUT_MS / 1000)), '-o', tmp, url],
|
|
420
|
+
{ stdio: 'pipe', timeout: ATTEMPT_TIMEOUT_MS + 5000, windowsHide: true }
|
|
421
|
+
);
|
|
422
|
+
if (result.error) throw result.error;
|
|
423
|
+
if (result.status !== 0) throw new Error(`curl failed with status ${result.status}`);
|
|
424
|
+
break;
|
|
425
|
+
} catch (err) {
|
|
426
|
+
lastErr = err;
|
|
427
|
+
log(`rtk attempt ${attempt} failed: ${err.message}`);
|
|
428
|
+
obsEvent('bootstrap', 'rtk.download.attempt_failed', { attempt, max: MAX_ATTEMPTS, err: String(err.message || err) });
|
|
429
|
+
if (attempt < MAX_ATTEMPTS) {
|
|
430
|
+
const wait = BACKOFF_MS[attempt - 1] || 120000;
|
|
431
|
+
log(`backing off ${wait}ms`);
|
|
432
|
+
await new Promise(r => setTimeout(r, wait));
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
if (lastErr) throw lastErr;
|
|
437
|
+
if (expected) {
|
|
438
|
+
const got = await sha256OfFile(tmp);
|
|
439
|
+
if (got !== expected) {
|
|
440
|
+
try { fs.unlinkSync(tmp); } catch (_) {}
|
|
441
|
+
throw new Error(`rtk sha256 mismatch: expected ${expected}, got ${got}`);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
try { fs.renameSync(tmp, rtkPath); }
|
|
445
|
+
catch (err) {
|
|
446
|
+
if (err.code === 'EEXIST' || err.code === 'EPERM') {
|
|
447
|
+
try { fs.unlinkSync(rtkPath); } catch (_) {}
|
|
448
|
+
fs.renameSync(tmp, rtkPath);
|
|
449
|
+
} else throw err;
|
|
450
|
+
}
|
|
451
|
+
if (os.platform() !== 'win32') { try { fs.chmodSync(rtkPath, 0o755); } catch (_) {} }
|
|
452
|
+
fs.writeFileSync(rtkOk, new Date().toISOString());
|
|
453
|
+
log(`installed ${rtkPath}`);
|
|
454
|
+
obsEvent('bootstrap', 'install.done', { path: rtkPath, plugkit_version: plugkitVersion, rtk_version: readRtkVersion() || plugkitVersion, kind: 'rtk', dur_ms: Date.now() - startMs });
|
|
455
|
+
return rtkPath;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function spawnDetachedRtkFetch() {
|
|
459
|
+
try {
|
|
460
|
+
const child = spawn(process.execPath, [__filename, '--rtk-only'], {
|
|
461
|
+
detached: true, stdio: 'ignore', windowsHide: true,
|
|
462
|
+
});
|
|
463
|
+
child.unref();
|
|
464
|
+
obsEvent('bootstrap', 'rtk.detached.spawned', { pid: child.pid });
|
|
465
|
+
} catch (err) {
|
|
466
|
+
log(`rtk detach spawn failed: ${err.message}`);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
async function bootstrap(opts) {
|
|
471
|
+
opts = opts || {};
|
|
472
|
+
const version = readVersionFile();
|
|
473
|
+
const shaManifest = readShaManifest();
|
|
474
|
+
const wasmName = 'plugkit.wasm';
|
|
475
|
+
const expectedSha = shaManifest ? shaManifest[wasmName] : null;
|
|
476
|
+
|
|
477
|
+
let root = cacheRoot();
|
|
478
|
+
try { ensureDir(root); }
|
|
479
|
+
catch (_) { root = fallbackCacheRoot(); ensureDir(root); }
|
|
480
|
+
|
|
481
|
+
const verDir = path.join(root, `v${version}`);
|
|
482
|
+
ensureDir(verDir);
|
|
483
|
+
|
|
484
|
+
const finalPath = path.join(verDir, wasmName);
|
|
485
|
+
const okSentinel = path.join(verDir, '.ok');
|
|
486
|
+
const partialPath = `${finalPath}.partial`;
|
|
487
|
+
|
|
488
|
+
if (fs.existsSync(finalPath) && fs.existsSync(okSentinel)) {
|
|
489
|
+
if (expectedSha) {
|
|
490
|
+
const actualSha = sha256OfFileSync(finalPath);
|
|
491
|
+
if (actualSha === expectedSha) {
|
|
492
|
+
obsEvent('bootstrap', 'decision.hit', { reason: 'sha-match', version, path: finalPath });
|
|
493
|
+
copyWasmToGmTools(finalPath, version);
|
|
494
|
+
clearBootstrapError();
|
|
495
|
+
return finalPath;
|
|
496
|
+
}
|
|
497
|
+
log(`decision: fetch reason: cache-hit-sha-mismatch (dir=v${version} expected ${expectedSha.slice(0,12)}... got ${(actualSha||'').slice(0,12)}...)`);
|
|
498
|
+
writeBootstrapError({
|
|
499
|
+
expected_version: version, cached_version: null,
|
|
500
|
+
error_phase: 'cache-hit-sha-mismatch',
|
|
501
|
+
error_message: `cached wasm at ${finalPath} sha=${actualSha} but manifest expects ${expectedSha}`,
|
|
502
|
+
});
|
|
503
|
+
try { fs.unlinkSync(finalPath); } catch (_) {}
|
|
504
|
+
try { fs.unlinkSync(okSentinel); } catch (_) {}
|
|
505
|
+
} else {
|
|
506
|
+
obsEvent('bootstrap', 'decision.hit', { reason: 'sentinel+no-sha-manifest', path: finalPath });
|
|
507
|
+
copyWasmToGmTools(finalPath, version);
|
|
508
|
+
clearBootstrapError();
|
|
509
|
+
return finalPath;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (healIfShaMatches(finalPath, expectedSha, okSentinel, partialPath, 'plugkit-wasm')) {
|
|
514
|
+
obsEvent('bootstrap', 'decision.heal', { reason: 'sha-match', path: finalPath });
|
|
515
|
+
spawnDetachedRtkFetch();
|
|
516
|
+
copyWasmToGmTools(finalPath, version);
|
|
517
|
+
clearBootstrapError();
|
|
518
|
+
return finalPath;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const lockPath = path.join(verDir, '.lock');
|
|
522
|
+
acquireLock(lockPath);
|
|
523
|
+
try {
|
|
524
|
+
if (fs.existsSync(finalPath) && fs.existsSync(okSentinel)) {
|
|
525
|
+
obsEvent('bootstrap', 'decision.hit', { reason: 'lock-race-resolved', path: finalPath });
|
|
526
|
+
copyWasmToGmTools(finalPath, version);
|
|
527
|
+
clearBootstrapError();
|
|
528
|
+
return finalPath;
|
|
529
|
+
}
|
|
530
|
+
if (healIfShaMatches(finalPath, expectedSha, okSentinel, partialPath, 'plugkit-wasm')) {
|
|
531
|
+
obsEvent('bootstrap', 'decision.heal', { reason: 'sha-match-under-lock', path: finalPath });
|
|
532
|
+
spawnDetachedRtkFetch();
|
|
533
|
+
copyWasmToGmTools(finalPath, version);
|
|
534
|
+
clearBootstrapError();
|
|
535
|
+
return finalPath;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (fs.existsSync(partialPath)) {
|
|
539
|
+
try {
|
|
540
|
+
const st = fs.statSync(partialPath);
|
|
541
|
+
if (Date.now() - st.mtimeMs > LOCK_STALE_MS) {
|
|
542
|
+
fs.unlinkSync(partialPath);
|
|
543
|
+
log(`cleared stale partial: ${partialPath}`);
|
|
544
|
+
}
|
|
545
|
+
} catch (_) {}
|
|
546
|
+
}
|
|
547
|
+
try {
|
|
548
|
+
await extractNpmPackageWithRetry(partialPath, version);
|
|
549
|
+
} catch (extractErr) {
|
|
550
|
+
writeBootstrapError({
|
|
551
|
+
expected_version: version, cached_version: null,
|
|
552
|
+
error_phase: 'npm-extract',
|
|
553
|
+
error_message: extractErr && extractErr.message ? extractErr.message : String(extractErr),
|
|
554
|
+
});
|
|
555
|
+
throw extractErr;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
if (expectedSha) {
|
|
559
|
+
const got = await sha256OfFile(partialPath);
|
|
560
|
+
if (got !== expectedSha) {
|
|
561
|
+
try { fs.unlinkSync(partialPath); } catch (_) {}
|
|
562
|
+
writeBootstrapError({
|
|
563
|
+
expected_version: version, cached_version: null,
|
|
564
|
+
error_phase: 'sha256-mismatch',
|
|
565
|
+
error_message: `sha256 mismatch for ${wasmName}: expected ${expectedSha}, got ${got}`,
|
|
566
|
+
});
|
|
567
|
+
throw new Error(`sha256 mismatch for ${wasmName}: expected ${expectedSha}, got ${got}`);
|
|
568
|
+
}
|
|
569
|
+
log('sha256 verified');
|
|
570
|
+
} else {
|
|
571
|
+
log('no sha256 manifest — skipping verify');
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
try { fs.renameSync(partialPath, finalPath); }
|
|
575
|
+
catch (err) {
|
|
576
|
+
if (err.code === 'EEXIST' || err.code === 'EPERM') {
|
|
577
|
+
try { fs.unlinkSync(finalPath); } catch (_) {}
|
|
578
|
+
fs.renameSync(partialPath, finalPath);
|
|
579
|
+
} else throw err;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
fs.writeFileSync(okSentinel, new Date().toISOString());
|
|
583
|
+
log(`decision: fetch reason: install-complete (${finalPath})`);
|
|
584
|
+
obsEvent('bootstrap', 'install.done', { path: finalPath, version, kind: 'plugkit-wasm' });
|
|
585
|
+
proactiveKillForNewInstall(version);
|
|
586
|
+
pruneOldVersions(root, version, readRtkVersion());
|
|
587
|
+
spawnDetachedRtkFetch();
|
|
588
|
+
copyWasmToGmTools(finalPath, version);
|
|
589
|
+
clearBootstrapError();
|
|
590
|
+
return finalPath;
|
|
591
|
+
} finally {
|
|
592
|
+
releaseLock(lockPath);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function copyWasmToGmTools(wasmPath, version) {
|
|
597
|
+
const dst = gmToolsDir();
|
|
598
|
+
fs.mkdirSync(dst, { recursive: true });
|
|
599
|
+
const target = path.join(dst, 'plugkit.wasm');
|
|
600
|
+
|
|
601
|
+
if (fs.existsSync(target)) {
|
|
602
|
+
let needsRefresh = true;
|
|
603
|
+
try {
|
|
604
|
+
const cur = sha256OfFileSync(target);
|
|
605
|
+
const src = sha256OfFileSync(wasmPath);
|
|
606
|
+
if (cur === src) needsRefresh = false;
|
|
607
|
+
} catch (_) {}
|
|
608
|
+
if (!needsRefresh) {
|
|
609
|
+
try { fs.writeFileSync(path.join(dst, 'plugkit.version'), version); } catch (_) {}
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
fs.copyFileSync(wasmPath, target);
|
|
615
|
+
fs.writeFileSync(path.join(dst, 'plugkit.version'), version);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function getWasmPath() {
|
|
619
|
+
const home = process.env.USERPROFILE || process.env.HOME || os.homedir();
|
|
620
|
+
return path.join(home, '.claude', 'gm-tools', 'plugkit.wasm');
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function isReady() {
|
|
624
|
+
const wasm = getWasmPath();
|
|
625
|
+
return fs.existsSync(wasm);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
async function ensureReady() {
|
|
629
|
+
if (isReady()) {
|
|
630
|
+
return { ok: true, wasmPath: getWasmPath(), status: 'already-ready' };
|
|
631
|
+
}
|
|
632
|
+
const wasmPath = await bootstrap();
|
|
633
|
+
return { ok: true, wasmPath, status: 'bootstrapped' };
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
module.exports = {
|
|
637
|
+
bootstrap,
|
|
638
|
+
ensureReady,
|
|
639
|
+
getWasmPath,
|
|
640
|
+
isReady,
|
|
641
|
+
rtkBinaryName,
|
|
642
|
+
cacheRoot,
|
|
643
|
+
obsEvent,
|
|
644
|
+
killRunningDaemons,
|
|
645
|
+
killStaleDaemonIfVersionChanged,
|
|
646
|
+
killSpoolWatcherInCwd,
|
|
647
|
+
proactiveKillForNewInstall,
|
|
648
|
+
resolveCachedRtk,
|
|
649
|
+
bootstrapRtk,
|
|
650
|
+
readDaemonVersion,
|
|
651
|
+
writeDaemonVersion,
|
|
652
|
+
daemonVersionSentinel,
|
|
653
|
+
};
|
|
654
|
+
|
|
655
|
+
if (require.main === module) {
|
|
656
|
+
(async () => {
|
|
657
|
+
try {
|
|
658
|
+
const args = process.argv.slice(2);
|
|
659
|
+
if (args.includes('--rtk-only')) {
|
|
660
|
+
const version = readVersionFile();
|
|
661
|
+
let root = cacheRoot();
|
|
662
|
+
try { ensureDir(root); }
|
|
663
|
+
catch (_) { root = fallbackCacheRoot(); ensureDir(root); }
|
|
664
|
+
const verDir = path.join(root, `v${version}`);
|
|
665
|
+
ensureDir(verDir);
|
|
666
|
+
await bootstrapRtk(verDir, version, true, root);
|
|
667
|
+
process.exit(0);
|
|
668
|
+
} else if (args.includes('--status')) {
|
|
669
|
+
console.log(JSON.stringify({
|
|
670
|
+
ready: isReady(),
|
|
671
|
+
wasmPath: getWasmPath(),
|
|
672
|
+
daemonVersion: readDaemonVersion(),
|
|
673
|
+
cachedRtk: resolveCachedRtk(),
|
|
674
|
+
}));
|
|
675
|
+
process.exit(0);
|
|
676
|
+
} else {
|
|
677
|
+
const result = await ensureReady();
|
|
678
|
+
console.log(JSON.stringify({ bootstrap: result }));
|
|
679
|
+
process.exit(result.ok ? 0 : 1);
|
|
680
|
+
}
|
|
681
|
+
} catch (err) {
|
|
682
|
+
obsEvent('bootstrap', 'fatal', { err: String(err.message || err) });
|
|
683
|
+
try {
|
|
684
|
+
const pinned = (() => { try { return readVersionFile(); } catch (_) { return null; } })();
|
|
685
|
+
writeBootstrapError({
|
|
686
|
+
expected_version: pinned, cached_version: null,
|
|
687
|
+
error_phase: 'fatal', error_message: String(err && err.message || err),
|
|
688
|
+
});
|
|
689
|
+
} catch (_) {}
|
|
690
|
+
console.error('gm-plugkit bootstrap failed:', err.message);
|
|
691
|
+
process.exit(1);
|
|
692
|
+
}
|
|
693
|
+
})();
|
|
694
|
+
}
|