gm-skill 2.0.2025 → 2.0.2027
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 +7 -11
- package/bin/bootstrap.js +1 -1
- package/bin/install.js +31 -9
- package/bin/plugkit-slim.wasm.sha256 +1 -1
- package/bin/plugkit.version +1 -1
- package/bin/plugkit.wasm.sha256 +1 -1
- package/gm-plugkit/bootstrap.js +35 -152
- package/gm-plugkit/cli.js +31 -168
- package/gm-plugkit/package.json +3 -5
- package/gm-plugkit/plugkit-slim.wasm.sha256 +1 -1
- package/gm-plugkit/plugkit.version +1 -1
- package/gm.json +2 -2
- package/package.json +1 -3
- package/skills/gm/SKILL.md +4 -4
- package/gm-plugkit/plugkit-wasm-wrapper.js +0 -4939
- package/gm-plugkit/wrapper/cdp-eval.js +0 -123
- package/gm-plugkit/wrapper/fs-atomic.js +0 -26
- package/gm-plugkit/wrapper/kv-store.js +0 -81
- package/gm-plugkit/wrapper/task-manager.js +0 -272
- package/gm-plugkit/wrapper/wasi-shim.js +0 -377
- package/gm-plugkit/wrapper/wasm-bridge.js +0 -71
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import http from 'http';
|
|
3
|
-
|
|
4
|
-
function httpJson(url, timeoutMs) {
|
|
5
|
-
return new Promise((resolve) => {
|
|
6
|
-
const req = http.get(url, { timeout: timeoutMs }, (res) => {
|
|
7
|
-
let body = '';
|
|
8
|
-
res.on('data', (c) => { body += c; });
|
|
9
|
-
res.on('end', () => { try { resolve(JSON.parse(body)); } catch (_) { resolve(null); } });
|
|
10
|
-
});
|
|
11
|
-
req.on('error', () => resolve(null));
|
|
12
|
-
req.on('timeout', () => { req.destroy(); resolve(null); });
|
|
13
|
-
});
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
async function pickPageTarget(port, startUrl, timeoutMs) {
|
|
17
|
-
const deadline = Date.now() + timeoutMs;
|
|
18
|
-
while (Date.now() < deadline) {
|
|
19
|
-
const list = await httpJson(`http://127.0.0.1:${port}/json/list`, 2000);
|
|
20
|
-
if (Array.isArray(list)) {
|
|
21
|
-
const page = list.find((t) => t.type === 'page' && t.webSocketDebuggerUrl);
|
|
22
|
-
if (page) return page;
|
|
23
|
-
}
|
|
24
|
-
if (startUrl) {
|
|
25
|
-
const created = await httpJson(`http://127.0.0.1:${port}/json/new?${encodeURIComponent(startUrl)}`, 3000);
|
|
26
|
-
if (created && created.webSocketDebuggerUrl) return created;
|
|
27
|
-
}
|
|
28
|
-
await new Promise((r) => setTimeout(r, 250));
|
|
29
|
-
}
|
|
30
|
-
return null;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function cdpSession(wsUrl, timeoutMs) {
|
|
34
|
-
return new Promise((resolve, reject) => {
|
|
35
|
-
const ws = new WebSocket(wsUrl);
|
|
36
|
-
let nextId = 1;
|
|
37
|
-
const pending = new Map();
|
|
38
|
-
const timer = setTimeout(() => { try { ws.close(); } catch (_) {} reject(new Error('cdp timeout')); }, timeoutMs);
|
|
39
|
-
ws.addEventListener('open', () => {
|
|
40
|
-
resolve({
|
|
41
|
-
send(method, params) {
|
|
42
|
-
const id = nextId++;
|
|
43
|
-
return new Promise((res, rej) => {
|
|
44
|
-
pending.set(id, { res, rej });
|
|
45
|
-
ws.send(JSON.stringify({ id, method, params: params || {} }));
|
|
46
|
-
});
|
|
47
|
-
},
|
|
48
|
-
close() { clearTimeout(timer); try { ws.close(); } catch (_) {} },
|
|
49
|
-
});
|
|
50
|
-
});
|
|
51
|
-
ws.addEventListener('message', (ev) => {
|
|
52
|
-
let msg;
|
|
53
|
-
try { msg = JSON.parse(ev.data); } catch (_) { return; }
|
|
54
|
-
if (msg.id && pending.has(msg.id)) {
|
|
55
|
-
const { res, rej } = pending.get(msg.id);
|
|
56
|
-
pending.delete(msg.id);
|
|
57
|
-
if (msg.error) rej(new Error(msg.error.message || 'cdp error'));
|
|
58
|
-
else res(msg.result);
|
|
59
|
-
}
|
|
60
|
-
});
|
|
61
|
-
ws.addEventListener('error', () => { clearTimeout(timer); reject(new Error('cdp websocket error')); });
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Direct CDP evaluation, replacing the playwriter relay attach+eval that crashes
|
|
66
|
-
// with UV_HANDLE_CLOSING on Windows. Everything up to obtaining Chrome's CDP
|
|
67
|
-
// endpoint is already done by the wrapper (it launches Chrome with
|
|
68
|
-
// --remote-debugging-port and polls /json/version); this drives that endpoint
|
|
69
|
-
// directly over the DevTools websocket, so the crashing relay process is never
|
|
70
|
-
// spawned. Reads {port, startUrl, scriptFile, resultFile, timeoutMs} from argv[2]
|
|
71
|
-
// as JSON, runs the script via Runtime.evaluate (awaitPromise, returnByValue),
|
|
72
|
-
// and writes the returned value to resultFile -- the same result channel the
|
|
73
|
-
// playwriter path used.
|
|
74
|
-
async function main() {
|
|
75
|
-
const cfg = JSON.parse(process.argv[2]);
|
|
76
|
-
const { port, startUrl, scriptFile, resultFile, timeoutMs } = cfg;
|
|
77
|
-
const script = fs.readFileSync(scriptFile, 'utf-8');
|
|
78
|
-
const target = await pickPageTarget(port, startUrl, Math.min(timeoutMs, 30000));
|
|
79
|
-
if (!target) {
|
|
80
|
-
fs.writeFileSync(resultFile, JSON.stringify({ __cdpError: 'no page target on CDP endpoint' }));
|
|
81
|
-
process.stderr.write('cdp-eval: no page target\n');
|
|
82
|
-
process.exit(1);
|
|
83
|
-
}
|
|
84
|
-
const sess = await cdpSession(target.webSocketDebuggerUrl, timeoutMs);
|
|
85
|
-
try {
|
|
86
|
-
await sess.send('Runtime.enable', {});
|
|
87
|
-
if (startUrl) {
|
|
88
|
-
await sess.send('Page.enable', {});
|
|
89
|
-
await sess.send('Page.navigate', { url: startUrl });
|
|
90
|
-
await new Promise((r) => setTimeout(r, 1200));
|
|
91
|
-
}
|
|
92
|
-
// The wrapper's eval body is a function body ending in `return __RET;`.
|
|
93
|
-
// Wrap it in an async IIFE so Runtime.evaluate resolves the returned value.
|
|
94
|
-
const wrapped = `(async () => { ${script} })()`;
|
|
95
|
-
const res = await sess.send('Runtime.evaluate', {
|
|
96
|
-
expression: wrapped,
|
|
97
|
-
awaitPromise: true,
|
|
98
|
-
returnByValue: true,
|
|
99
|
-
userGesture: true,
|
|
100
|
-
timeout: timeoutMs,
|
|
101
|
-
});
|
|
102
|
-
if (res.exceptionDetails) {
|
|
103
|
-
const msg = res.exceptionDetails.exception && res.exceptionDetails.exception.description
|
|
104
|
-
? res.exceptionDetails.exception.description
|
|
105
|
-
: (res.exceptionDetails.text || 'evaluate exception');
|
|
106
|
-
fs.writeFileSync(resultFile, JSON.stringify({ __cdpError: msg }));
|
|
107
|
-
process.stderr.write(`cdp-eval: exception ${msg}\n`);
|
|
108
|
-
sess.close();
|
|
109
|
-
process.exit(1);
|
|
110
|
-
}
|
|
111
|
-
const value = res.result && ('value' in res.result) ? res.result.value : null;
|
|
112
|
-
fs.writeFileSync(resultFile, JSON.stringify(value === undefined ? null : value));
|
|
113
|
-
sess.close();
|
|
114
|
-
process.exit(0);
|
|
115
|
-
} catch (e) {
|
|
116
|
-
fs.writeFileSync(resultFile, JSON.stringify({ __cdpError: String(e && e.message || e) }));
|
|
117
|
-
process.stderr.write(`cdp-eval: ${e && e.message || e}\n`);
|
|
118
|
-
try { sess.close(); } catch (_) {}
|
|
119
|
-
process.exit(1);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
main();
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
|
|
3
|
-
function atomicWriteRaw(filePath, data) {
|
|
4
|
-
const tmp = filePath + '.tmp.' + process.pid + '.' + Date.now() + '.' + Math.random().toString(36).slice(2, 8);
|
|
5
|
-
fs.writeFileSync(tmp, data);
|
|
6
|
-
try {
|
|
7
|
-
fs.renameSync(tmp, filePath);
|
|
8
|
-
} catch (err) {
|
|
9
|
-
try { fs.unlinkSync(tmp); } catch (_) {}
|
|
10
|
-
throw err;
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function atomicWriteJson(filePath, obj) {
|
|
15
|
-
atomicWriteRaw(filePath, JSON.stringify(obj, null, 2));
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function readJsonFile(fp, fallback) {
|
|
19
|
-
try { return JSON.parse(fs.readFileSync(fp, 'utf-8')); } catch (_) { return fallback; }
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function writeJsonFile(fp, value) {
|
|
23
|
-
try { atomicWriteJson(fp, value); } catch (_) {}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export { atomicWriteRaw, atomicWriteJson, readJsonFile, writeJsonFile };
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
|
|
4
|
-
function safeName(s) { return String(s).replace(/[^A-Za-z0-9._-]/g, '_'); }
|
|
5
|
-
|
|
6
|
-
function projectKvDir(ns) {
|
|
7
|
-
const projectRoot = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
8
|
-
return path.join(projectRoot, '.gm', 'disciplines', safeName(ns));
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function makeLegacyKvDir(kvDir) {
|
|
12
|
-
return function legacyKvDir(ns) {
|
|
13
|
-
return path.join(kvDir, safeName(ns));
|
|
14
|
-
};
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function makeKvFilePath() {
|
|
18
|
-
return function kvFilePath(ns, key, ensureDir) {
|
|
19
|
-
const dir = projectKvDir(ns);
|
|
20
|
-
if (ensureDir) fs.mkdirSync(dir, { recursive: true });
|
|
21
|
-
return path.join(dir, safeName(key) + '.json');
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function makeKvReadResolve(legacyKvDir) {
|
|
26
|
-
const kvFilePath = makeKvFilePath();
|
|
27
|
-
return function kvReadResolve(ns, key) {
|
|
28
|
-
const fp = kvFilePath(ns, key);
|
|
29
|
-
if (fs.existsSync(fp)) return fp;
|
|
30
|
-
const legacy = path.join(legacyKvDir(ns), safeName(key) + '.json');
|
|
31
|
-
if (fs.existsSync(legacy)) return legacy;
|
|
32
|
-
return null;
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function makeKvNamespaceDirs(legacyKvDir) {
|
|
37
|
-
return function kvNamespaceDirs(ns) {
|
|
38
|
-
const out = [];
|
|
39
|
-
const proj = projectKvDir(ns);
|
|
40
|
-
if (fs.existsSync(proj)) out.push(proj);
|
|
41
|
-
const legacy = legacyKvDir(ns);
|
|
42
|
-
if (fs.existsSync(legacy)) out.push(legacy);
|
|
43
|
-
return out;
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function enabledDisciplineNamespaces(baseNs) {
|
|
48
|
-
const projectRoot = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
49
|
-
const set = new Set([baseNs]);
|
|
50
|
-
try {
|
|
51
|
-
const enabledPath = path.join(projectRoot, '.gm', 'disciplines', 'enabled.txt');
|
|
52
|
-
if (fs.existsSync(enabledPath)) {
|
|
53
|
-
const lines = fs.readFileSync(enabledPath, 'utf-8').split(/\r?\n/);
|
|
54
|
-
for (const ln of lines) {
|
|
55
|
-
const name = ln.trim();
|
|
56
|
-
if (name && !name.startsWith('#')) set.add(name);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
} catch (_) {}
|
|
60
|
-
return Array.from(set);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function jaccardOverlap(a, b) {
|
|
64
|
-
if (!a || !b) return 0;
|
|
65
|
-
const tokenize = (s) => new Set(String(s).toLowerCase().split(/[^a-z0-9]+/).filter(t => t.length >= 3));
|
|
66
|
-
const A = tokenize(a), B = tokenize(b);
|
|
67
|
-
if (A.size === 0 || B.size === 0) return 0;
|
|
68
|
-
let inter = 0;
|
|
69
|
-
for (const t of A) if (B.has(t)) inter++;
|
|
70
|
-
return inter / (A.size + B.size - inter);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function makeKvHelpers(kvDir) {
|
|
74
|
-
const legacyKvDir = makeLegacyKvDir(kvDir);
|
|
75
|
-
const kvFilePath = makeKvFilePath();
|
|
76
|
-
const kvReadResolve = makeKvReadResolve(legacyKvDir);
|
|
77
|
-
const kvNamespaceDirs = makeKvNamespaceDirs(legacyKvDir);
|
|
78
|
-
return { safeName, projectKvDir, legacyKvDir, kvFilePath, kvReadResolve, kvNamespaceDirs, enabledDisciplineNamespaces, jaccardOverlap };
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export { safeName, projectKvDir, enabledDisciplineNamespaces, jaccardOverlap, makeKvHelpers };
|
|
@@ -1,272 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
|
|
4
|
-
function makeTaskManager({ spawn, spawnSync, logEvent }) {
|
|
5
|
-
const __tasks = new Map();
|
|
6
|
-
|
|
7
|
-
function tasksDir(cwd) {
|
|
8
|
-
const d = path.join(cwd || process.cwd(), '.gm', 'exec-spool', 'tasks');
|
|
9
|
-
try { fs.mkdirSync(d, { recursive: true }); } catch (_) {}
|
|
10
|
-
return d;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function taskMetaPath(cwd, id) { return path.join(tasksDir(cwd), `${id}.json`); }
|
|
14
|
-
function taskOutPath(cwd, id, which) { return path.join(tasksDir(cwd), `${id}.${which}.log`); }
|
|
15
|
-
|
|
16
|
-
function writeTaskMeta(cwd, id, meta) {
|
|
17
|
-
try { fs.writeFileSync(taskMetaPath(cwd, id), JSON.stringify(meta, null, 2)); } catch (_) {}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function nextTaskId(cwd) {
|
|
21
|
-
const counterPath = path.join(tasksDir(cwd), '.counter');
|
|
22
|
-
let n = 0;
|
|
23
|
-
try { n = parseInt(fs.readFileSync(counterPath, 'utf-8'), 10) || 0; } catch (_) {}
|
|
24
|
-
n += 1;
|
|
25
|
-
try { fs.writeFileSync(counterPath, String(n)); } catch (_) {}
|
|
26
|
-
return `t${n}`;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
let _jsRuntimeCmd = null;
|
|
30
|
-
function resolveJsRuntimeCmd() {
|
|
31
|
-
if (_jsRuntimeCmd) return _jsRuntimeCmd;
|
|
32
|
-
if (!/(^|[\\/])node(\.exe)?$/i.test(String(process.execPath || ''))) {
|
|
33
|
-
_jsRuntimeCmd = process.execPath;
|
|
34
|
-
return _jsRuntimeCmd;
|
|
35
|
-
}
|
|
36
|
-
try {
|
|
37
|
-
const which = process.platform === 'win32' ? 'where' : 'which';
|
|
38
|
-
const out = spawnSync(which, ['bun'], { encoding: 'utf-8', windowsHide: true });
|
|
39
|
-
const first = (out && out.stdout || '').split(/\r?\n/).map((s) => s.trim()).filter(Boolean)[0];
|
|
40
|
-
if (first) { _jsRuntimeCmd = first; return _jsRuntimeCmd; }
|
|
41
|
-
} catch (_) {}
|
|
42
|
-
_jsRuntimeCmd = process.execPath;
|
|
43
|
-
return _jsRuntimeCmd;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function langToCmd(lang, code) {
|
|
47
|
-
if (lang === 'nodejs' || lang === 'js' || lang === 'javascript' || lang === 'node') return { cmd: resolveJsRuntimeCmd(), args: ['-e', code], stdinCode: null };
|
|
48
|
-
if (lang === 'python' || lang === 'py') return { cmd: 'python', args: ['-c', code], stdinCode: null };
|
|
49
|
-
if (lang === 'bash' || lang === 'sh' || lang === 'shell' || lang === 'zsh') return { cmd: 'bash', args: ['-c', code], stdinCode: null };
|
|
50
|
-
if (lang === 'powershell' || lang === 'ps1') return { cmd: 'powershell', args: ['-NoProfile', '-NonInteractive', '-Command', code], stdinCode: null };
|
|
51
|
-
if (lang === 'deno') return { cmd: 'deno', args: ['eval', code], stdinCode: null };
|
|
52
|
-
return null;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const TASK_MAX_TIMEOUT_MS = 10 * 60 * 1000;
|
|
56
|
-
|
|
57
|
-
function spawnTask({ cwd, lang, code, timeoutMs }) {
|
|
58
|
-
const id = nextTaskId(cwd);
|
|
59
|
-
const built = langToCmd(lang, code);
|
|
60
|
-
if (!built) return { ok: false, error: `unsupported lang: ${lang}` };
|
|
61
|
-
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0 || timeoutMs > TASK_MAX_TIMEOUT_MS) {
|
|
62
|
-
timeoutMs = TASK_MAX_TIMEOUT_MS;
|
|
63
|
-
}
|
|
64
|
-
const outLog = taskOutPath(cwd, id, 'stdout');
|
|
65
|
-
const errLog = taskOutPath(cwd, id, 'stderr');
|
|
66
|
-
let outFd = null, errFd = null;
|
|
67
|
-
try { outFd = fs.openSync(outLog, 'a'); } catch (_) {}
|
|
68
|
-
try { errFd = fs.openSync(errLog, 'a'); } catch (_) {}
|
|
69
|
-
const startedMs = Date.now();
|
|
70
|
-
const isPosix = process.platform !== 'win32';
|
|
71
|
-
const child = spawn(built.cmd, built.args, {
|
|
72
|
-
cwd: cwd || process.cwd(),
|
|
73
|
-
detached: isPosix,
|
|
74
|
-
stdio: ['ignore', outFd || 'ignore', errFd || 'ignore'],
|
|
75
|
-
windowsHide: true,
|
|
76
|
-
env: process.env,
|
|
77
|
-
});
|
|
78
|
-
try { if (outFd !== null) fs.closeSync(outFd); } catch (_) {}
|
|
79
|
-
try { if (errFd !== null) fs.closeSync(errFd); } catch (_) {}
|
|
80
|
-
const meta = {
|
|
81
|
-
id,
|
|
82
|
-
pid: child.pid,
|
|
83
|
-
pgid: isPosix ? child.pid : null,
|
|
84
|
-
lang,
|
|
85
|
-
cmd: built.cmd,
|
|
86
|
-
cwd: cwd || process.cwd(),
|
|
87
|
-
started_ms: startedMs,
|
|
88
|
-
timeout_ms: timeoutMs,
|
|
89
|
-
deadline_ms: startedMs + timeoutMs,
|
|
90
|
-
status: 'running',
|
|
91
|
-
exit_code: null,
|
|
92
|
-
stdout_log: outLog,
|
|
93
|
-
stderr_log: errLog,
|
|
94
|
-
};
|
|
95
|
-
__tasks.set(id, { child, meta });
|
|
96
|
-
writeTaskMeta(cwd, id, meta);
|
|
97
|
-
child.on('exit', (code, signal) => {
|
|
98
|
-
meta.status = signal ? 'killed' : (code === 0 ? 'completed' : 'failed');
|
|
99
|
-
meta.exit_code = code;
|
|
100
|
-
meta.signal = signal;
|
|
101
|
-
meta.ended_ms = Date.now();
|
|
102
|
-
writeTaskMeta(meta.cwd, id, meta);
|
|
103
|
-
});
|
|
104
|
-
child.on('error', (err) => {
|
|
105
|
-
meta.status = 'error';
|
|
106
|
-
meta.error = err.message;
|
|
107
|
-
meta.ended_ms = Date.now();
|
|
108
|
-
writeTaskMeta(meta.cwd, id, meta);
|
|
109
|
-
});
|
|
110
|
-
logEvent('plugkit', 'task.spawn', { task_id: id, pid: child.pid, lang, timeout_ms: timeoutMs });
|
|
111
|
-
return { ok: true, task_id: id, pid: child.pid, started_ms: startedMs };
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function stopTaskById(id) {
|
|
115
|
-
const entry = __tasks.get(id);
|
|
116
|
-
if (!entry) {
|
|
117
|
-
return { ok: false, error: 'unknown task_id', task_id: id };
|
|
118
|
-
}
|
|
119
|
-
const { child, meta } = entry;
|
|
120
|
-
if (meta.status !== 'running') return { ok: true, already: meta.status, task_id: id };
|
|
121
|
-
const pid = meta.pid;
|
|
122
|
-
const isPosix = process.platform !== 'win32';
|
|
123
|
-
try {
|
|
124
|
-
if (isPosix && meta.pgid) {
|
|
125
|
-
try { process.kill(-meta.pgid, 'SIGTERM'); } catch (_) {}
|
|
126
|
-
} else {
|
|
127
|
-
try { child.kill('SIGTERM'); } catch (_) {}
|
|
128
|
-
}
|
|
129
|
-
} catch (_) {}
|
|
130
|
-
const graceTimer = setTimeout(() => {
|
|
131
|
-
if (meta.status !== 'running') return;
|
|
132
|
-
if (isPosix && meta.pgid) {
|
|
133
|
-
try { process.kill(-meta.pgid, 'SIGKILL'); } catch (_) {}
|
|
134
|
-
} else if (process.platform === 'win32') {
|
|
135
|
-
try { spawnSync('taskkill', ['/F', '/T', '/PID', String(pid)], { stdio: 'ignore', timeout: 3000 }); } catch (_) {}
|
|
136
|
-
} else {
|
|
137
|
-
try { child.kill('SIGKILL'); } catch (_) {}
|
|
138
|
-
}
|
|
139
|
-
}, 2000);
|
|
140
|
-
graceTimer.unref && graceTimer.unref();
|
|
141
|
-
logEvent('plugkit', 'task.stop', { task_id: id, pid });
|
|
142
|
-
return { ok: true, task_id: id, pid };
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function tailFile(filePath, maxBytes) {
|
|
146
|
-
try {
|
|
147
|
-
const stat = fs.statSync(filePath);
|
|
148
|
-
if (stat.size <= maxBytes) return fs.readFileSync(filePath, 'utf-8');
|
|
149
|
-
const fd = fs.openSync(filePath, 'r');
|
|
150
|
-
try {
|
|
151
|
-
const buf = Buffer.alloc(maxBytes);
|
|
152
|
-
fs.readSync(fd, buf, 0, maxBytes, stat.size - maxBytes);
|
|
153
|
-
return buf.toString('utf-8');
|
|
154
|
-
} finally { try { fs.closeSync(fd); } catch (_) {} }
|
|
155
|
-
} catch (_) { return ''; }
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function listTasks(cwd) {
|
|
159
|
-
const d = tasksDir(cwd);
|
|
160
|
-
const out = [];
|
|
161
|
-
try {
|
|
162
|
-
for (const entry of fs.readdirSync(d)) {
|
|
163
|
-
if (!entry.endsWith('.json') || entry.startsWith('.')) continue;
|
|
164
|
-
try {
|
|
165
|
-
const meta = JSON.parse(fs.readFileSync(path.join(d, entry), 'utf-8'));
|
|
166
|
-
out.push(meta);
|
|
167
|
-
} catch (_) {}
|
|
168
|
-
}
|
|
169
|
-
} catch (_) {}
|
|
170
|
-
return out;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function reapTimedOutTasks() {
|
|
174
|
-
const now = Date.now();
|
|
175
|
-
for (const [id, entry] of __tasks) {
|
|
176
|
-
const m = entry.meta;
|
|
177
|
-
if (m.status === 'running' && m.deadline_ms && now > m.deadline_ms) {
|
|
178
|
-
logEvent('plugkit', 'task.timeout', { task_id: id, pid: m.pid, deadline_ms: m.deadline_ms, now_ms: now });
|
|
179
|
-
stopTaskById(id);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function killAllTasks(reason) {
|
|
185
|
-
let killed = 0;
|
|
186
|
-
for (const [id, entry] of __tasks) {
|
|
187
|
-
if (entry.meta.status === 'running') {
|
|
188
|
-
stopTaskById(id);
|
|
189
|
-
killed += 1;
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
if (killed > 0) logEvent('plugkit', 'task.killAll', { reason, count: killed });
|
|
193
|
-
return killed;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function pidAliveLocal(pid) {
|
|
197
|
-
if (!Number.isFinite(pid) || pid <= 0) return false;
|
|
198
|
-
try { process.kill(pid, 0); return true; } catch (_) { return false; }
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
function sweepOrphanedTaskMetaOnBoot(cwd) {
|
|
202
|
-
let swept = 0;
|
|
203
|
-
try {
|
|
204
|
-
const dir = tasksDir(cwd);
|
|
205
|
-
const now = Date.now();
|
|
206
|
-
for (const name of fs.readdirSync(dir)) {
|
|
207
|
-
if (!/^t\d+\.json$/.test(name)) continue;
|
|
208
|
-
const metaPath = path.join(dir, name);
|
|
209
|
-
let meta = null;
|
|
210
|
-
try { meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8')); } catch (_) { continue; }
|
|
211
|
-
if (!meta || meta.status !== 'running') continue;
|
|
212
|
-
const stale = !pidAliveLocal(meta.pid) || (meta.deadline_ms && now > meta.deadline_ms);
|
|
213
|
-
if (!stale) continue;
|
|
214
|
-
if (pidAliveLocal(meta.pid)) {
|
|
215
|
-
try {
|
|
216
|
-
if (process.platform === 'win32') {
|
|
217
|
-
spawnSync('taskkill', ['/F', '/T', '/PID', String(meta.pid)], { stdio: 'ignore', windowsHide: true, timeout: 3000 });
|
|
218
|
-
} else {
|
|
219
|
-
try { process.kill(-meta.pid, 'SIGKILL'); } catch (_) { try { process.kill(meta.pid, 'SIGKILL'); } catch (_) {} }
|
|
220
|
-
}
|
|
221
|
-
} catch (_) {}
|
|
222
|
-
}
|
|
223
|
-
meta.status = 'reaped-on-boot';
|
|
224
|
-
meta.ended_ms = now;
|
|
225
|
-
try { fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2)); } catch (_) {}
|
|
226
|
-
swept += 1;
|
|
227
|
-
}
|
|
228
|
-
} catch (_) {}
|
|
229
|
-
if (swept > 0) logEvent('plugkit', 'task.bootSweepReaped', { cwd: cwd || process.cwd(), count: swept });
|
|
230
|
-
return swept;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
function hostTaskProc(action, params) {
|
|
234
|
-
switch (action) {
|
|
235
|
-
case 'spawn': return spawnTask(params);
|
|
236
|
-
case 'stop': return stopTaskById(params.id || params.task_id);
|
|
237
|
-
case 'list': return { ok: true, tasks: listTasks(params.cwd) };
|
|
238
|
-
case 'output': return {
|
|
239
|
-
ok: true,
|
|
240
|
-
task_id: params.id || params.task_id,
|
|
241
|
-
stdout: tailFile(taskOutPath(params.cwd, params.id || params.task_id, 'stdout'), params.max_bytes || 65536),
|
|
242
|
-
stderr: tailFile(taskOutPath(params.cwd, params.id || params.task_id, 'stderr'), params.max_bytes || 65536),
|
|
243
|
-
};
|
|
244
|
-
case 'reap': { reapTimedOutTasks(); return { ok: true }; }
|
|
245
|
-
case 'killAll': { const n = killAllTasks(params.reason || 'host_task_proc'); return { ok: true, killed: n }; }
|
|
246
|
-
default: return { ok: false, error: `unknown action: ${action}` };
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
return {
|
|
251
|
-
__tasks,
|
|
252
|
-
tasksDir,
|
|
253
|
-
taskMetaPath,
|
|
254
|
-
taskOutPath,
|
|
255
|
-
writeTaskMeta,
|
|
256
|
-
nextTaskId,
|
|
257
|
-
resolveJsRuntimeCmd,
|
|
258
|
-
langToCmd,
|
|
259
|
-
TASK_MAX_TIMEOUT_MS,
|
|
260
|
-
spawnTask,
|
|
261
|
-
stopTaskById,
|
|
262
|
-
tailFile,
|
|
263
|
-
listTasks,
|
|
264
|
-
reapTimedOutTasks,
|
|
265
|
-
killAllTasks,
|
|
266
|
-
pidAliveLocal,
|
|
267
|
-
sweepOrphanedTaskMetaOnBoot,
|
|
268
|
-
hostTaskProc,
|
|
269
|
-
};
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
export { makeTaskManager };
|