@volter/supercode-health 0.1.11 → 0.1.12
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/lib/drift.mjs +35 -10
- package/package.json +1 -1
package/lib/drift.mjs
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// A process runs a file of a supercode install (the native, `…/@volter/supercode-cli-<os>-<cpu>/bin/supercode`, or
|
|
6
6
|
// node running a file of an `@volter/supercode*` package); when that file was written after the process started (its
|
|
7
7
|
// ctime: an extractor may set a file's mtime, never its ctime), the release on disk is not the one it runs.
|
|
8
|
-
import { statSync } from 'node:fs';
|
|
8
|
+
import { realpathSync, statSync } from 'node:fs';
|
|
9
9
|
import { run } from './run.mjs';
|
|
10
10
|
|
|
11
11
|
/** A supercode install's file: the native, or a script of an @volter/supercode* package. */
|
|
@@ -15,8 +15,26 @@ const INSTALL_FILE = /^\/\S*\/node_modules\/@volter\/supercode(?:-[a-z0-9-]+)?\/
|
|
|
15
15
|
* one, never a path in its arguments (a Claude session's `--settings` names supercode's hooks). */
|
|
16
16
|
function ran(command) {
|
|
17
17
|
const words = command.split(/\s+/u);
|
|
18
|
-
const
|
|
19
|
-
|
|
18
|
+
const node = /(^|\/)node$/u.test(words[0] ?? '');
|
|
19
|
+
let program = node ? words.slice(1).find((word) => !word.startsWith('-')) : words[0];
|
|
20
|
+
if (!program?.startsWith('/')) return null;
|
|
21
|
+
// by its real path: a link (the machine's stable program, a bin link) names an install's file under another name
|
|
22
|
+
try { program = realpathSync(program); } catch { /* gone: matched as named */ }
|
|
23
|
+
return INSTALL_FILE.test(program) ? { file: program, native: !node } : null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The inode each native process runs (its `txt` mapping, lsof's first), by pid; empty where lsof cannot say. An
|
|
27
|
+
* install that replaced the file gave its path a new inode, while the process still runs the old one. */
|
|
28
|
+
async function runningInodes(pids) {
|
|
29
|
+
const inodes = new Map();
|
|
30
|
+
if (!pids.length) return inodes;
|
|
31
|
+
const answer = await run('lsof', ['-n', '-P', '-a', '-d', 'txt', '-F', 'pi', '-p', pids.join(',')], { timeoutMs: 10_000 }).catch(() => null);
|
|
32
|
+
let pid = null;
|
|
33
|
+
for (const line of String(answer?.stdout ?? '').split('\n')) {
|
|
34
|
+
if (line.startsWith('p')) pid = Number(line.slice(1));
|
|
35
|
+
else if (line.startsWith('i') && pid && !inodes.has(pid)) inodes.set(pid, Number(line.slice(1)));
|
|
36
|
+
}
|
|
37
|
+
return inodes;
|
|
20
38
|
}
|
|
21
39
|
|
|
22
40
|
/** `ps`'s start time (`lstart`, as `Tue Oct 6 12:09:42 2026`) in ms, or NaN. */
|
|
@@ -29,18 +47,25 @@ const startedMs = (lstart) => Date.parse(String(lstart).trim().replace(/\s+/g, '
|
|
|
29
47
|
*/
|
|
30
48
|
export async function staleSupercode({ rows = null } = {}) {
|
|
31
49
|
const lines = rows ?? String((await run('ps', ['-A', '-ww', '-o', 'pid=,lstart=,command='], { timeoutMs: 10_000 })).stdout ?? '').split('\n');
|
|
32
|
-
const
|
|
50
|
+
const found = [];
|
|
33
51
|
for (const line of lines) {
|
|
34
52
|
const match = /^\s*(\d+)\s+(\w{3}\s+\w{3}\s+\d+\s+[\d:]+\s+\d{4})\s+(.*)$/u.exec(line);
|
|
35
53
|
if (!match) continue;
|
|
36
54
|
const [, pid, lstart, command] = match;
|
|
37
|
-
const
|
|
38
|
-
if (
|
|
55
|
+
const program = ran(command);
|
|
56
|
+
if (program) found.push({ pid: Number(pid), lstart, command, ...program });
|
|
57
|
+
}
|
|
58
|
+
// a native is read by the inode it runs, where lsof says it: the strongest reading (a ctime also moves on a chmod)
|
|
59
|
+
const inodes = rows ? new Map() : await runningInodes(found.filter((row) => row.native).map((row) => row.pid));
|
|
60
|
+
const stale = [];
|
|
61
|
+
for (const { pid, lstart, command, file } of found) {
|
|
39
62
|
const started = startedMs(lstart);
|
|
40
|
-
let
|
|
41
|
-
try {
|
|
42
|
-
|
|
43
|
-
if (
|
|
63
|
+
let stat;
|
|
64
|
+
try { stat = statSync(file); } catch { continue; } // a file gone is a removed install, said elsewhere
|
|
65
|
+
const installed = stat.ctimeMs;
|
|
66
|
+
if (inodes.has(pid)) {
|
|
67
|
+
if (inodes.get(pid) === stat.ino) continue;
|
|
68
|
+
} else if (Number.isNaN(started) || installed <= started + 1000) continue; // ps says a start to the second
|
|
44
69
|
stale.push({ pid: Number(pid), command: command.slice(0, 200), file, startedAt: new Date(started).toISOString(), installedAt: new Date(installed).toISOString() });
|
|
45
70
|
}
|
|
46
71
|
return stale.sort((a, b) => b.installedAt.localeCompare(a.installedAt));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-health",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.12",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Optional machine pack: a cheap, deterministic probe of a machine's vitals that mails the machine's maintainer when a threshold is crossed. Each feature sits behind its own permission barrier.",
|
|
6
6
|
"license": "MIT",
|