gm-plugkit 2.0.1441 → 2.0.1443
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 +21 -2
- package/lang-host-runner.js +51 -0
- package/package.json +3 -1
- package/supervisor.js +236 -0
package/bootstrap.js
CHANGED
|
@@ -879,13 +879,32 @@ function probeUnsupervisedWatcher(spoolDir) {
|
|
|
879
879
|
} catch (_) {}
|
|
880
880
|
}
|
|
881
881
|
|
|
882
|
+
function resolveNodeRuntime() {
|
|
883
|
+
const isNodeExe = (p) => /(^|[\\/])node(\.exe)?$/i.test(String(p || ''));
|
|
884
|
+
const candidates = [];
|
|
885
|
+
if (isNodeExe(process.env.GM_NODE_PATH)) candidates.push(process.env.GM_NODE_PATH);
|
|
886
|
+
if (isNodeExe(process.execPath)) candidates.push(process.execPath);
|
|
887
|
+
try {
|
|
888
|
+
const which = process.platform === 'win32' ? 'where' : 'which';
|
|
889
|
+
const out = require('child_process').spawnSync(which, ['node'], { encoding: 'utf8', windowsHide: true });
|
|
890
|
+
if (out && out.stdout) {
|
|
891
|
+
const first = out.stdout.split(/\r?\n/).map((s) => s.trim()).filter(Boolean)[0];
|
|
892
|
+
if (first) candidates.push(first);
|
|
893
|
+
}
|
|
894
|
+
} catch (_) {}
|
|
895
|
+
for (const c of candidates) {
|
|
896
|
+
try { const r = require('child_process').spawnSync(c, ['--version'], { stdio: 'ignore', windowsHide: true }); if (r && r.status === 0) return c; } catch (_) {}
|
|
897
|
+
}
|
|
898
|
+
return process.execPath;
|
|
899
|
+
}
|
|
900
|
+
|
|
882
901
|
function startSpoolDaemon() {
|
|
883
902
|
try {
|
|
884
903
|
const wrapper = path.join(gmToolsDir(), 'plugkit-wasm-wrapper.js');
|
|
885
904
|
if (!fs.existsSync(wrapper)) {
|
|
886
905
|
return { ok: false, error: `wrapper not at ${wrapper} — ensureReady() must run first` };
|
|
887
906
|
}
|
|
888
|
-
const runtime =
|
|
907
|
+
const runtime = resolveNodeRuntime();
|
|
889
908
|
const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
890
909
|
const spoolDir = path.join(projectDir, '.gm', 'exec-spool');
|
|
891
910
|
fs.mkdirSync(spoolDir, { recursive: true });
|
|
@@ -919,7 +938,7 @@ function startSpoolDaemon() {
|
|
|
919
938
|
|
|
920
939
|
const logFd = fs.openSync(logPath, 'a');
|
|
921
940
|
try { fs.writeSync(logFd, `\n--- supervisor spawn ${new Date().toISOString()} parent=${process.pid} ---\n`); } catch (_) {}
|
|
922
|
-
const child = require('child_process').spawn(
|
|
941
|
+
const child = require('child_process').spawn(runtime, [supervisor], {
|
|
923
942
|
detached: true,
|
|
924
943
|
stdio: ['ignore', logFd, logFd],
|
|
925
944
|
windowsHide: true,
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Legacy fallback. The canonical surface for lang/*.js plugins is the wasm
|
|
3
|
+
// `lang` verb in rs-plugkit, dispatched via .gm/exec-spool/in/lang/<N>.txt.
|
|
4
|
+
// This standalone runner is kept for direct CLI debug + pre-cascade situations.
|
|
5
|
+
'use strict';
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
async function main() {
|
|
10
|
+
const [, , projectDir, command, codeB64] = process.argv;
|
|
11
|
+
if (!projectDir || !command || codeB64 === undefined) {
|
|
12
|
+
console.log(JSON.stringify({ ok: false, error: 'usage: lang-host-runner <projectDir> <command> <code-base64>' }));
|
|
13
|
+
process.exit(2);
|
|
14
|
+
}
|
|
15
|
+
const code = Buffer.from(codeB64, 'base64').toString('utf8');
|
|
16
|
+
const langDir = path.join(projectDir, 'lang');
|
|
17
|
+
if (!fs.existsSync(langDir)) {
|
|
18
|
+
console.log(JSON.stringify({ ok: false, error: 'no-lang-dir', langDir }));
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const files = fs.readdirSync(langDir).filter(f => f.endsWith('.js') && f !== 'loader.js');
|
|
22
|
+
const plugins = files.reduce((acc, f) => {
|
|
23
|
+
try {
|
|
24
|
+
const p = require(path.join(langDir, f));
|
|
25
|
+
if (p && typeof p.id === 'string' && p.exec && p.exec.match instanceof RegExp && typeof p.exec.run === 'function') {
|
|
26
|
+
acc.push(p);
|
|
27
|
+
}
|
|
28
|
+
} catch (_) {}
|
|
29
|
+
return acc;
|
|
30
|
+
}, []);
|
|
31
|
+
const plugin = plugins.find(p => p.exec.match.test(command));
|
|
32
|
+
if (!plugin) {
|
|
33
|
+
console.log(JSON.stringify({ ok: false, error: 'no-plugin-matched', command, available: plugins.map(p => p.id) }));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const t0 = Date.now();
|
|
37
|
+
const timer = setTimeout(() => {
|
|
38
|
+
console.log(JSON.stringify({ ok: false, error: 'timeout', plugin_id: plugin.id, ms: Date.now() - t0 }));
|
|
39
|
+
process.exit(0);
|
|
40
|
+
}, 30000);
|
|
41
|
+
try {
|
|
42
|
+
const out = await plugin.exec.run(code, projectDir);
|
|
43
|
+
clearTimeout(timer);
|
|
44
|
+
console.log(JSON.stringify({ ok: true, plugin_id: plugin.id, output: String(out), ms: Date.now() - t0 }));
|
|
45
|
+
} catch (e) {
|
|
46
|
+
clearTimeout(timer);
|
|
47
|
+
console.log(JSON.stringify({ ok: false, error: String(e && e.message || e), plugin_id: plugin.id, ms: Date.now() - t0 }));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
main();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gm-plugkit",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.1443",
|
|
4
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
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
"cli.js",
|
|
12
12
|
"index.js",
|
|
13
13
|
"bootstrap.js",
|
|
14
|
+
"supervisor.js",
|
|
15
|
+
"lang-host-runner.js",
|
|
14
16
|
"plugkit-wasm-wrapper.js",
|
|
15
17
|
"plugkit.version",
|
|
16
18
|
"plugkit.sha256",
|
package/supervisor.js
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
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 { spawn, spawnSync } = require('child_process');
|
|
8
|
+
|
|
9
|
+
const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
10
|
+
const spoolDir = path.join(projectDir, '.gm', 'exec-spool');
|
|
11
|
+
fs.mkdirSync(spoolDir, { recursive: true });
|
|
12
|
+
|
|
13
|
+
const STATUS_PATH = path.join(spoolDir, '.status.json');
|
|
14
|
+
const SHUTDOWN_REASON_PATH = path.join(spoolDir, '.shutdown-reason.json');
|
|
15
|
+
const SUPERVISOR_PATH = path.join(spoolDir, '.supervisor.json');
|
|
16
|
+
const LOG_PATH = path.join(spoolDir, '.watcher.log');
|
|
17
|
+
const GM_LOG_ROOT = process.env.GM_LOG_DIR || path.join(os.homedir(), '.claude', 'gm-log');
|
|
18
|
+
|
|
19
|
+
const POLL_INTERVAL_MS = 10_000;
|
|
20
|
+
const STATUS_STALE_MS = 30_000;
|
|
21
|
+
const MAX_RESTART_BURST = 5;
|
|
22
|
+
const RESTART_WINDOW_MS = 60_000;
|
|
23
|
+
|
|
24
|
+
function logEvent(event, fields) {
|
|
25
|
+
try {
|
|
26
|
+
const day = new Date().toISOString().slice(0, 10);
|
|
27
|
+
const dir = path.join(GM_LOG_ROOT, day);
|
|
28
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
29
|
+
const line = JSON.stringify({
|
|
30
|
+
ts: new Date().toISOString(),
|
|
31
|
+
sub: 'plugkit',
|
|
32
|
+
event,
|
|
33
|
+
pid: process.pid,
|
|
34
|
+
sess: process.env.CLAUDE_SESSION_ID || '',
|
|
35
|
+
cwd: process.cwd(),
|
|
36
|
+
role: 'supervisor',
|
|
37
|
+
...fields,
|
|
38
|
+
}) + '\n';
|
|
39
|
+
fs.appendFileSync(path.join(dir, 'plugkit.jsonl'), line);
|
|
40
|
+
} catch (_) {}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function writeSupervisorStatus(state, extra) {
|
|
44
|
+
try {
|
|
45
|
+
fs.writeFileSync(SUPERVISOR_PATH, JSON.stringify({
|
|
46
|
+
pid: process.pid,
|
|
47
|
+
ts: Date.now(),
|
|
48
|
+
state,
|
|
49
|
+
...(extra || {}),
|
|
50
|
+
}));
|
|
51
|
+
} catch (_) {}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function pidAlive(pid) {
|
|
55
|
+
if (!Number.isFinite(pid) || pid <= 0) return false;
|
|
56
|
+
try { process.kill(pid, 0); return true; } catch (_) { return false; }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function readStatus() {
|
|
60
|
+
try { return JSON.parse(fs.readFileSync(STATUS_PATH, 'utf-8')); } catch (_) { return null; }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function readShutdownReason() {
|
|
64
|
+
try { return JSON.parse(fs.readFileSync(SHUTDOWN_REASON_PATH, 'utf-8')); } catch (_) { return null; }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let lastSpawnedAt = 0;
|
|
68
|
+
let restartTimestamps = [];
|
|
69
|
+
let currentChildPid = null;
|
|
70
|
+
let currentBootReason = 'initial';
|
|
71
|
+
|
|
72
|
+
function spawnWatcher(bootReason) {
|
|
73
|
+
lastSpawnedAt = Date.now();
|
|
74
|
+
restartTimestamps.push(Date.now());
|
|
75
|
+
restartTimestamps = restartTimestamps.filter(t => Date.now() - t < RESTART_WINDOW_MS);
|
|
76
|
+
if (restartTimestamps.length > MAX_RESTART_BURST) {
|
|
77
|
+
logEvent('supervisor.giving-up', {
|
|
78
|
+
reason: 'restart-burst-exceeded',
|
|
79
|
+
restarts_in_window: restartTimestamps.length,
|
|
80
|
+
window_ms: RESTART_WINDOW_MS,
|
|
81
|
+
max: MAX_RESTART_BURST,
|
|
82
|
+
severity: 'critical',
|
|
83
|
+
});
|
|
84
|
+
writeSupervisorStatus('giving-up', { reason: 'restart-burst-exceeded' });
|
|
85
|
+
process.exit(2);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const primaryWrapper = path.join(os.homedir(), '.gm-tools', 'plugkit-wasm-wrapper.js');
|
|
89
|
+
const fallbackWrapper = path.join(os.homedir(), '.claude', 'gm-tools', 'plugkit-wasm-wrapper.js');
|
|
90
|
+
const wrapper = fs.existsSync(primaryWrapper) ? primaryWrapper : fallbackWrapper;
|
|
91
|
+
if (!fs.existsSync(wrapper)) {
|
|
92
|
+
logEvent('supervisor.wrapper-missing', { wrapper, severity: 'critical' });
|
|
93
|
+
writeSupervisorStatus('error', { error: 'wrapper-missing' });
|
|
94
|
+
process.exit(3);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const isNodeExe = (p) => /(^|[\\/])node(\.exe)?$/i.test(String(p || ''));
|
|
98
|
+
const resolveNode = () => {
|
|
99
|
+
const candidates = [];
|
|
100
|
+
if (isNodeExe(process.env.PLUGKIT_RUNTIME)) candidates.push(process.env.PLUGKIT_RUNTIME);
|
|
101
|
+
if (isNodeExe(process.execPath)) candidates.push(process.execPath);
|
|
102
|
+
if (process.env.GM_NODE_PATH) candidates.push(process.env.GM_NODE_PATH);
|
|
103
|
+
try {
|
|
104
|
+
const which = process.platform === 'win32' ? 'where' : 'which';
|
|
105
|
+
const out = spawnSync(which, ['node'], { encoding: 'utf8', windowsHide: true });
|
|
106
|
+
if (out && out.stdout) {
|
|
107
|
+
const first = out.stdout.split(/\r?\n/).map((s) => s.trim()).filter(Boolean)[0];
|
|
108
|
+
if (first) candidates.push(first);
|
|
109
|
+
}
|
|
110
|
+
} catch (_) {}
|
|
111
|
+
for (const c of candidates) {
|
|
112
|
+
try { const r = spawnSync(c, ['--version'], { stdio: 'ignore', windowsHide: true }); if (r && r.status === 0) return c; } catch (_) {}
|
|
113
|
+
}
|
|
114
|
+
return process.execPath;
|
|
115
|
+
};
|
|
116
|
+
let cmd = resolveNode();
|
|
117
|
+
let args = [wrapper, 'spool'];
|
|
118
|
+
|
|
119
|
+
let logFd = null;
|
|
120
|
+
try { logFd = fs.openSync(LOG_PATH, 'a'); } catch (_) {}
|
|
121
|
+
try {
|
|
122
|
+
if (logFd !== null) fs.writeSync(logFd, `\n--- watcher spawn ${new Date().toISOString()} supervisor=${process.pid} reason=${bootReason} ---\n`);
|
|
123
|
+
} catch (_) {}
|
|
124
|
+
|
|
125
|
+
const child = spawn(cmd, args, {
|
|
126
|
+
detached: false,
|
|
127
|
+
stdio: ['ignore', logFd || 'ignore', logFd || 'ignore'],
|
|
128
|
+
windowsHide: true,
|
|
129
|
+
env: {
|
|
130
|
+
...process.env,
|
|
131
|
+
CLAUDE_PROJECT_DIR: projectDir,
|
|
132
|
+
PLUGKIT_BOOT_REASON: bootReason,
|
|
133
|
+
PLUGKIT_SUPERVISOR_PID: String(process.pid),
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
try { if (logFd !== null) fs.closeSync(logFd); } catch (_) {}
|
|
138
|
+
currentChildPid = child.pid;
|
|
139
|
+
currentBootReason = bootReason;
|
|
140
|
+
writeSupervisorStatus('watching', { watcher_pid: child.pid, boot_reason: bootReason });
|
|
141
|
+
logEvent('supervisor.spawned-watcher', { watcher_pid: child.pid, boot_reason: bootReason, runtime: cmd });
|
|
142
|
+
|
|
143
|
+
child.on('exit', (code, signal) => {
|
|
144
|
+
const shutdownReason = readShutdownReason();
|
|
145
|
+
const reason = shutdownReason && shutdownReason.reason;
|
|
146
|
+
const idleClean = reason === 'idle';
|
|
147
|
+
const plannedReasons = new Set(['idle', 'sigterm', 'version-change', 'wrapper-change', 'peer-stale-takeover', 'external-planned']);
|
|
148
|
+
const isPlanned = plannedReasons.has(reason);
|
|
149
|
+
const eventName = idleClean
|
|
150
|
+
? 'supervisor.watcher-exited-idle'
|
|
151
|
+
: reason === 'version-change'
|
|
152
|
+
? 'supervisor.watcher-exited-for-update'
|
|
153
|
+
: 'supervisor.watcher-exited-unexpectedly';
|
|
154
|
+
logEvent(eventName, {
|
|
155
|
+
watcher_pid: currentChildPid,
|
|
156
|
+
exit_code: code,
|
|
157
|
+
signal,
|
|
158
|
+
shutdown_reason: reason || null,
|
|
159
|
+
had_shutdown_reason_file: shutdownReason !== null,
|
|
160
|
+
severity: isPlanned ? 'info' : 'critical',
|
|
161
|
+
uptime_ms: Date.now() - lastSpawnedAt,
|
|
162
|
+
...(shutdownReason || {}),
|
|
163
|
+
});
|
|
164
|
+
if (idleClean) {
|
|
165
|
+
writeSupervisorStatus('exited-idle', { watcher_pid: currentChildPid });
|
|
166
|
+
try { fs.unlinkSync(SUPERVISOR_PATH); } catch (_) {}
|
|
167
|
+
process.exit(0);
|
|
168
|
+
}
|
|
169
|
+
const respawnReason = reason === 'version-change' ? 'planned-restart-version-change' : 'unplanned-restart-after-exit';
|
|
170
|
+
writeSupervisorStatus('restarting', {
|
|
171
|
+
prior_watcher_pid: currentChildPid,
|
|
172
|
+
prior_exit_code: code,
|
|
173
|
+
prior_signal: signal,
|
|
174
|
+
prior_shutdown_reason: reason || null,
|
|
175
|
+
respawn_reason: respawnReason,
|
|
176
|
+
});
|
|
177
|
+
setTimeout(() => spawnWatcher(respawnReason), 1500);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
child.on('error', (err) => {
|
|
181
|
+
logEvent('supervisor.spawn-error', { error: err.message, severity: 'critical' });
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function checkWatcherHealth() {
|
|
186
|
+
if (!currentChildPid) return;
|
|
187
|
+
if (!pidAlive(currentChildPid)) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const status = readStatus();
|
|
191
|
+
if (!status) {
|
|
192
|
+
logEvent('supervisor.status-missing', {
|
|
193
|
+
watcher_pid: currentChildPid,
|
|
194
|
+
severity: 'warn',
|
|
195
|
+
});
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const age = Date.now() - (status.ts || 0);
|
|
199
|
+
if (age > STATUS_STALE_MS) {
|
|
200
|
+
logEvent('supervisor.heartbeat-stale', {
|
|
201
|
+
watcher_pid: currentChildPid,
|
|
202
|
+
status_pid: status.pid,
|
|
203
|
+
status_age_ms: age,
|
|
204
|
+
stale_limit_ms: STATUS_STALE_MS,
|
|
205
|
+
severity: 'critical',
|
|
206
|
+
});
|
|
207
|
+
try { process.kill(currentChildPid, 'SIGTERM'); } catch (_) {}
|
|
208
|
+
if (process.platform === 'win32') {
|
|
209
|
+
try { spawnSync('taskkill', ['/F', '/T', '/PID', String(currentChildPid)], { stdio: 'ignore', windowsHide: true, timeout: 3000 }); } catch (_) {}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
process.on('SIGINT', () => {
|
|
215
|
+
logEvent('supervisor.shutdown', { reason: 'sigint' });
|
|
216
|
+
writeSupervisorStatus('shutdown', { reason: 'sigint' });
|
|
217
|
+
if (currentChildPid && pidAlive(currentChildPid)) {
|
|
218
|
+
try { process.kill(currentChildPid, 'SIGTERM'); } catch (_) {}
|
|
219
|
+
}
|
|
220
|
+
process.exit(0);
|
|
221
|
+
});
|
|
222
|
+
process.on('SIGTERM', () => {
|
|
223
|
+
logEvent('supervisor.shutdown', { reason: 'sigterm' });
|
|
224
|
+
writeSupervisorStatus('shutdown', { reason: 'sigterm' });
|
|
225
|
+
if (currentChildPid && pidAlive(currentChildPid)) {
|
|
226
|
+
try { process.kill(currentChildPid, 'SIGTERM'); } catch (_) {}
|
|
227
|
+
}
|
|
228
|
+
process.exit(0);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
writeSupervisorStatus('starting', {});
|
|
232
|
+
logEvent('supervisor.starting', { spool_dir: spoolDir });
|
|
233
|
+
try { fs.unlinkSync(path.join(spoolDir, '.pre-supervised-watcher.json')); } catch (_) {}
|
|
234
|
+
spawnWatcher('initial');
|
|
235
|
+
setInterval(checkWatcherHealth, POLL_INTERVAL_MS);
|
|
236
|
+
setInterval(() => writeSupervisorStatus('watching', { watcher_pid: currentChildPid, boot_reason: currentBootReason }), 10_000);
|