gm-plugkit 2.0.2063 → 2.0.2065

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.
Files changed (2) hide show
  1. package/bootstrap-shared.js +288 -0
  2. package/package.json +2 -1
@@ -0,0 +1,288 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+ const { spawnSync } = require('child_process');
7
+ const { pidAlive, sha256OfFileSync } = require('./gm-process');
8
+
9
+ // Functions shared byte-for-byte between gm-plugkit/bootstrap.js and
10
+ // bin/bootstrap.js -- the two installer entry points diverge in install
11
+ // STRATEGY (npm-install vs npx-extract, slim/fat artifact selection,
12
+ // pinned-reexec) but the cache/lock/prune/kill/wiring mechanics underneath
13
+ // were identical copy-paste, drifting apart in small accidental ways release
14
+ // over release. Centralized here so a fix lands once for both callers.
15
+
16
+ const LOCK_STALE_MS = 30 * 60 * 1000;
17
+ const ATTEMPT_TIMEOUT_MS = 10 * 60 * 1000;
18
+
19
+ function obsEvent(subsystem, event, fields) {
20
+ if (process.env.GM_LOG_DISABLE) return;
21
+ try {
22
+ const root = process.env.GM_LOG_DIR || path.join(os.homedir(), '.claude', 'gm-log');
23
+ const day = new Date().toISOString().slice(0, 10);
24
+ const dir = path.join(root, day);
25
+ fs.mkdirSync(dir, { recursive: true });
26
+ const line = JSON.stringify({
27
+ ts: new Date().toISOString(),
28
+ sub: subsystem,
29
+ event,
30
+ pid: process.pid,
31
+ 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 cacheRoot() {
39
+ const home = os.homedir();
40
+ if (process.env.PLUGKIT_CACHE_DIR) return process.env.PLUGKIT_CACHE_DIR;
41
+ if (os.platform() === 'win32') {
42
+ const base = process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
43
+ return path.join(base, 'plugkit', 'bin');
44
+ }
45
+ if (os.platform() === 'darwin') return path.join(home, 'Library', 'Caches', 'plugkit', 'bin');
46
+ const xdg = process.env.XDG_CACHE_HOME || path.join(home, '.cache');
47
+ return path.join(xdg, 'plugkit', 'bin');
48
+ }
49
+
50
+ function fallbackCacheRoot() {
51
+ return path.join(os.tmpdir(), 'plugkit-cache', 'bin');
52
+ }
53
+
54
+ function gmToolsDir() {
55
+ const home = process.env.USERPROFILE || process.env.HOME || os.homedir();
56
+ const primary = path.join(home, '.gm-tools');
57
+ const fallback = path.join(home, '.claude', 'gm-tools');
58
+ if (fs.existsSync(primary)) return primary;
59
+ if (fs.existsSync(fallback)) return fallback;
60
+ return primary;
61
+ }
62
+
63
+ function ensureDir(dir) {
64
+ fs.mkdirSync(dir, { recursive: true });
65
+ }
66
+
67
+ function acquireLock(lockPath) {
68
+ const start = Date.now();
69
+ for (;;) {
70
+ try {
71
+ const fd = fs.openSync(lockPath, 'wx');
72
+ fs.writeSync(fd, String(process.pid));
73
+ fs.closeSync(fd);
74
+ return true;
75
+ } catch (err) {
76
+ if (err.code !== 'EEXIST') throw err;
77
+ let stale = false;
78
+ try {
79
+ const st = fs.statSync(lockPath);
80
+ if (Date.now() - st.mtimeMs > LOCK_STALE_MS) stale = true;
81
+ const owner = parseInt(fs.readFileSync(lockPath, 'utf8').trim(), 10);
82
+ if (Number.isFinite(owner) && owner !== process.pid && !pidAlive(owner)) stale = true;
83
+ } catch (_) { stale = true; }
84
+ if (stale) {
85
+ try { fs.unlinkSync(lockPath); } catch (_) {}
86
+ continue;
87
+ }
88
+ if (Date.now() - start > ATTEMPT_TIMEOUT_MS) throw new Error(`lock wait timeout: ${lockPath}`);
89
+ try { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 2000); }
90
+ catch (e) { obsEvent('bootstrap', 'acquire-lock.atomics-wait-failed', { error: e.message, lockPath }); }
91
+ }
92
+ }
93
+ }
94
+
95
+ function releaseLock(lockPath) {
96
+ // best-effort, missing file is fine
97
+ try { fs.unlinkSync(lockPath); } catch (_) {}
98
+ }
99
+
100
+ function isLockStale(lockPath) {
101
+ try {
102
+ const st = fs.statSync(lockPath);
103
+ if (Date.now() - st.mtimeMs > LOCK_STALE_MS) return true;
104
+ const owner = parseInt(fs.readFileSync(lockPath, 'utf8').trim(), 10);
105
+ if (Number.isFinite(owner) && !pidAlive(owner)) return true;
106
+ } catch (_) { return true; }
107
+ return false;
108
+ }
109
+
110
+ function pruneOldVersions(root, keepVersion) {
111
+ try {
112
+ const entries = fs.readdirSync(root);
113
+ for (const e of entries) {
114
+ if (!e.startsWith('v')) continue;
115
+ if (e === `v${keepVersion}`) continue;
116
+ const dir = path.join(root, e);
117
+ const lock = path.join(dir, '.lock');
118
+ if (fs.existsSync(lock) && !isLockStale(lock)) continue;
119
+ if (fs.existsSync(lock)) { try { fs.unlinkSync(lock); } catch (_) {} }
120
+ try {
121
+ fs.rmSync(dir, { recursive: true, force: true, maxRetries: 1, retryDelay: 50 });
122
+ } catch (_) { /* prune skip, non-fatal */ }
123
+ }
124
+ } catch (_) {}
125
+ }
126
+
127
+ function healIfShaMatches(binPath, expectedSha, sentinelPath, partialPath, kind) {
128
+ if (!fs.existsSync(binPath)) return false;
129
+ if (partialPath) { try { if (fs.existsSync(partialPath)) fs.unlinkSync(partialPath); } catch (_) {} }
130
+ if (!expectedSha) return false;
131
+ let got;
132
+ try { got = sha256OfFileSync(binPath); }
133
+ catch (_) { return false; }
134
+ if (got !== expectedSha) {
135
+ try { fs.unlinkSync(binPath); } catch (_) {}
136
+ return false;
137
+ }
138
+ try { fs.writeFileSync(sentinelPath, new Date().toISOString()); } catch (_) { return false; }
139
+ obsEvent('bootstrap', 'cache.heal', { path: binPath, kind });
140
+ return true;
141
+ }
142
+
143
+ function daemonVersionSentinel() {
144
+ const root = (() => {
145
+ try { const r = cacheRoot(); ensureDir(r); return r; }
146
+ catch (_) { const r = fallbackCacheRoot(); ensureDir(r); return r; }
147
+ })();
148
+ return path.join(root, '.daemon-version');
149
+ }
150
+
151
+ function readDaemonVersion() {
152
+ try { return fs.readFileSync(daemonVersionSentinel(), 'utf8').trim(); }
153
+ catch (_) { return null; }
154
+ }
155
+
156
+ function writeDaemonVersion(v) {
157
+ try { fs.writeFileSync(daemonVersionSentinel(), String(v)); } catch (_) {}
158
+ }
159
+
160
+ function pidCommandLineForKillGuard(pid) {
161
+ try {
162
+ if (process.platform === 'win32') {
163
+ const r = spawnSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', `(Get-CimInstance Win32_Process -Filter "ProcessId=${Number(pid)}").CommandLine`], { encoding: 'utf8', windowsHide: true, timeout: 5000 });
164
+ return String((r && r.stdout) || '');
165
+ }
166
+ const r = spawnSync('ps', ['-p', String(pid), '-o', 'args='], { encoding: 'utf8', timeout: 5000 });
167
+ return String((r && r.stdout) || '');
168
+ } catch (_) { return ''; }
169
+ }
170
+
171
+ function pidIsPlugkitProcess(pid) {
172
+ return /agentplug-runner(\.exe)?/i.test(pidCommandLineForKillGuard(pid));
173
+ }
174
+
175
+ function writeKillAttribution(targetSpoolDir, info) {
176
+ try {
177
+ fs.mkdirSync(targetSpoolDir, { recursive: true });
178
+ fs.writeFileSync(path.join(targetSpoolDir, '.kill-attribution.json'), JSON.stringify({ killer_pid: process.pid, killer_cwd: process.cwd(), killer_script: __filename, ts: Date.now(), ...info }, null, 2));
179
+ } catch (_) {}
180
+ }
181
+
182
+ function killPid(pid) {
183
+ if (!Number.isFinite(pid) || pid === process.pid || !pidAlive(pid)) return false;
184
+ try { process.kill(pid, 'SIGTERM'); }
185
+ catch (_) { try { process.kill(pid); } catch (_) {} }
186
+ if (os.platform() === 'win32' && pidAlive(pid)) {
187
+ try { spawnSync('taskkill', ['/F', '/PID', String(pid)], { stdio: 'ignore', windowsHide: true, timeout: 3000, killSignal: 'SIGKILL' }); } catch (_) {}
188
+ }
189
+ return true;
190
+ }
191
+
192
+ function killSpoolWatcherInCwd(reason) {
193
+ try {
194
+ const pidPath = path.join(process.cwd(), '.gm', 'exec-spool', '.watcher.pid');
195
+ if (!fs.existsSync(pidPath)) return null;
196
+ const pid = parseInt(fs.readFileSync(pidPath, 'utf8').trim(), 10);
197
+ if (pidAlive(pid) && !pidIsPlugkitProcess(pid)) {
198
+ obsEvent('bootstrap', 'watcher.kill-skipped-pid-reused', { pid, reason });
199
+ try { fs.unlinkSync(pidPath); } catch (_) {}
200
+ return null;
201
+ }
202
+ writeKillAttribution(path.join(process.cwd(), '.gm', 'exec-spool'), { reason, target_pid: pid, via: 'killSpoolWatcherInCwd' });
203
+ if (killPid(pid)) {
204
+ obsEvent('bootstrap', 'watcher.killed', { pid, reason });
205
+ try { fs.unlinkSync(pidPath); } catch (_) {}
206
+ return pid;
207
+ }
208
+ try { fs.unlinkSync(pidPath); } catch (_) {}
209
+ } catch (_) {}
210
+ return null;
211
+ }
212
+
213
+ function proactiveKillForNewInstall(installedVersion) {
214
+ try {
215
+ const reason = `install:v${installedVersion}`;
216
+ killSpoolWatcherInCwd(reason);
217
+ writeDaemonVersion(installedVersion);
218
+ } catch (_) {}
219
+ }
220
+
221
+ function ensureNextStepWiring(cwd) {
222
+ const changes = [];
223
+ const gmDir = path.join(cwd, '.gm');
224
+ try { fs.mkdirSync(gmDir, { recursive: true }); }
225
+ catch (e) { obsEvent('bootstrap', 'next-step.wiring.target-failed', { target: gmDir, error: e.message }); }
226
+
227
+ const nextStepPath = path.join(gmDir, 'next-step.md');
228
+ const nextStepBody = '# Next step\n\nDispatch the `instruction` verb to plugkit by writing `.gm/exec-spool/in/instruction/<N>.txt` (any unique N) with body `{}` (or `{"prompt":"<user request>"}` on the first dispatch of the turn). Read the response from `.gm/exec-spool/out/<N>.json` and follow the imperative in the `instruction` field.\n\nThis file is auto-rewritten by plugkit on every instruction dispatch.\n';
229
+ try {
230
+ if (!fs.existsSync(nextStepPath)) {
231
+ fs.writeFileSync(nextStepPath, nextStepBody);
232
+ changes.push('seeded .gm/next-step.md');
233
+ }
234
+ } catch (e) { obsEvent('bootstrap', 'next-step.wiring.target-failed', { target: nextStepPath, error: e.message }); }
235
+
236
+ const claudeMdPath = path.join(cwd, 'CLAUDE.md');
237
+ try {
238
+ if (!fs.existsSync(claudeMdPath)) {
239
+ fs.writeFileSync(claudeMdPath, '@AGENTS.md\n');
240
+ changes.push('created CLAUDE.md');
241
+ } else {
242
+ const cur = fs.readFileSync(claudeMdPath, 'utf8');
243
+ const hasLine = cur.split(/\r?\n/).some(l => l.trim() === '@AGENTS.md');
244
+ if (!hasLine) {
245
+ fs.writeFileSync(claudeMdPath, '@AGENTS.md\n' + cur);
246
+ changes.push('prepended @AGENTS.md to CLAUDE.md');
247
+ }
248
+ }
249
+ } catch (e) { obsEvent('bootstrap', 'next-step.wiring.target-failed', { target: claudeMdPath, error: e.message }); }
250
+
251
+ const agentsMdPath = path.join(cwd, 'AGENTS.md');
252
+ try {
253
+ if (fs.existsSync(agentsMdPath)) {
254
+ const cur = fs.readFileSync(agentsMdPath, 'utf8');
255
+ const hasLine = cur.split(/\r?\n/).some(l => l.trim() === '@.gm/next-step.md');
256
+ if (!hasLine) {
257
+ const sep = cur.endsWith('\n') ? '' : '\n';
258
+ fs.writeFileSync(agentsMdPath, cur + sep + '\n@.gm/next-step.md\n');
259
+ changes.push('appended @.gm/next-step.md to AGENTS.md');
260
+ }
261
+ }
262
+ } catch (e) { obsEvent('bootstrap', 'next-step.wiring.target-failed', { target: agentsMdPath, error: e.message }); }
263
+
264
+ return changes;
265
+ }
266
+
267
+ module.exports = {
268
+ obsEvent,
269
+ cacheRoot,
270
+ fallbackCacheRoot,
271
+ gmToolsDir,
272
+ ensureDir,
273
+ acquireLock,
274
+ releaseLock,
275
+ isLockStale,
276
+ pruneOldVersions,
277
+ healIfShaMatches,
278
+ daemonVersionSentinel,
279
+ readDaemonVersion,
280
+ writeDaemonVersion,
281
+ pidCommandLineForKillGuard,
282
+ pidIsPlugkitProcess,
283
+ writeKillAttribution,
284
+ killPid,
285
+ killSpoolWatcherInCwd,
286
+ proactiveKillForNewInstall,
287
+ ensureNextStepWiring,
288
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm-plugkit",
3
- "version": "2.0.2063",
3
+ "version": "2.0.2065",
4
4
  "description": "Bootstrap and daemon-spawn tool for gm plugkit binary. Downloads the correct platform wasm, verifies SHA256, and launches agentplug-runner (the native wasm host) as the spool watcher daemon.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -10,6 +10,7 @@
10
10
  "cli.js",
11
11
  "index.js",
12
12
  "bootstrap.js",
13
+ "bootstrap-shared.js",
13
14
  "gm-log.js",
14
15
  "gm-process.js",
15
16
  "plugkit.version",