@lifeaitools/rdc-skills 0.25.4 → 0.25.9
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/.claude-plugin/plugin.json +1574 -1528
- package/.github/workflows/self-test.yml +34 -34
- package/CHANGELOG.md +326 -313
- package/MANIFEST.md +224 -224
- package/README.md +372 -369
- package/RELEASE.md +26 -2
- package/commands/build.md +181 -181
- package/commands/collab.md +180 -180
- package/commands/deploy.md +148 -148
- package/commands/fixit.md +150 -150
- package/commands/handoff.md +173 -173
- package/commands/overnight.md +220 -220
- package/commands/plan.md +158 -158
- package/commands/preplan.md +131 -131
- package/commands/prototype.md +145 -145
- package/commands/report.md +99 -99
- package/commands/review.md +120 -120
- package/commands/status.md +86 -86
- package/commands/workitems.md +127 -127
- package/git-sha.json +1 -1
- package/guides/agent-bootstrap.md +195 -195
- package/guides/agents/backend.md +102 -102
- package/guides/agents/content.md +94 -94
- package/guides/agents/cs2.md +56 -56
- package/guides/agents/data.md +86 -86
- package/guides/agents/design.md +77 -77
- package/guides/agents/frontend.md +91 -91
- package/guides/agents/infrastructure.md +81 -81
- package/guides/agents/setup.md +272 -272
- package/guides/agents/verify.md +119 -119
- package/guides/agents/viz.md +106 -106
- package/hooks/check-rdc-environment.js +318 -164
- package/hooks/lib/box-lock.js +186 -0
- package/package.json +1 -1
- package/scripts/install-rdc-skills.js +1474 -1400
- package/scripts/local-install-with-stop.sh +41 -0
- package/scripts/probe-box-lock.mjs +179 -0
- package/scripts/probe-installed-hooks.mjs +60 -0
- package/scripts/probe-lock-holders.mjs +36 -0
- package/scripts/self-test.mjs +1459 -1459
- package/scripts/validate-publish-manifests.js +502 -502
- package/skills/build/SKILL.md +578 -578
- package/skills/channel-formatter/SKILL.md +538 -538
- package/skills/collab/SKILL.md +239 -239
- package/skills/convert/SKILL.md +150 -161
- package/skills/deploy/SKILL.md +541 -541
- package/skills/design/SKILL.md +205 -205
- package/skills/env/SKILL.md +139 -139
- package/skills/fixit/SKILL.md +203 -203
- package/skills/handoff/SKILL.md +236 -236
- package/skills/housekeeping/SKILL.md +189 -189
- package/skills/new-model/SKILL.md +14 -3
- package/skills/onramp/SKILL.md +1459 -1459
- package/skills/overnight/SKILL.md +251 -251
- package/skills/plan/SKILL.md +345 -345
- package/skills/preplan/SKILL.md +90 -90
- package/skills/prototype/SKILL.md +150 -150
- package/skills/regen-media/SKILL.md +94 -94
- package/skills/release/SKILL.md +140 -140
- package/skills/report/SKILL.md +100 -100
- package/skills/review/SKILL.md +151 -151
- package/skills/self-test/SKILL.md +108 -108
- package/skills/status/SKILL.md +99 -99
- package/skills/tests/MATRIX.md +55 -55
- package/skills/tests/onramp.test.json +87 -87
- package/skills/tests/rdc-regen-media.test.json +29 -29
- package/skills/watch/SKILL.md +84 -84
- package/skills/workitems/SKILL.md +151 -151
- package/tests/curl-surface.test.mjs +4 -2
- package/tests/help-surface.test.mjs +1 -1
- package/tests/install-rdc-skills.test.mjs +1 -1
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Install rdc-skills globally FROM LOCAL SOURCE, using the stop-first sequence.
|
|
3
|
+
#
|
|
4
|
+
# This is the empirical test of the fix: the same `npm install -g` that produced
|
|
5
|
+
# EBUSY should now succeed because the lock holder is stopped first. It is a LOCAL
|
|
6
|
+
# install (a path, not the registry) — nothing is published.
|
|
7
|
+
#
|
|
8
|
+
# The restart is unconditional: leaving the MCP down after a failed install would
|
|
9
|
+
# be worse than the failure being repaired.
|
|
10
|
+
set -u
|
|
11
|
+
|
|
12
|
+
HOLDER=rdc-skills-mcp
|
|
13
|
+
|
|
14
|
+
echo "== before =="
|
|
15
|
+
pm2 jlist | node -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>{const p=JSON.parse(s).find(x=>x.name===process.argv[1]);console.log(' '+process.argv[1]+': '+(p?p.pm2_env.status:'absent'));})" "$HOLDER"
|
|
16
|
+
npm ls -g '@lifeaitools/rdc-skills' --depth=0 2>/dev/null | tail -1
|
|
17
|
+
|
|
18
|
+
echo "== stop the lock holder =="
|
|
19
|
+
pm2 stop "$HOLDER" >/dev/null 2>&1 && echo " stopped $HOLDER" || echo " could not stop (may be absent)"
|
|
20
|
+
|
|
21
|
+
echo "== npm install -g from local source =="
|
|
22
|
+
set +e
|
|
23
|
+
npm install -g "C:/Dev/rdc-skills" 2>&1 | tail -5
|
|
24
|
+
INSTALL_RC=${PIPESTATUS[0]}
|
|
25
|
+
set -e
|
|
26
|
+
echo " install exit=$INSTALL_RC"
|
|
27
|
+
|
|
28
|
+
echo "== restart (always) =="
|
|
29
|
+
pm2 restart "$HOLDER" >/dev/null 2>&1 && echo " restarted $HOLDER" || echo " restart FAILED"
|
|
30
|
+
|
|
31
|
+
echo "== after =="
|
|
32
|
+
npm ls -g '@lifeaitools/rdc-skills' --depth=0 2>/dev/null | tail -1
|
|
33
|
+
for i in 1 2 3 4 5 6 7 8; do
|
|
34
|
+
code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 3 http://127.0.0.1:3110/health 2>/dev/null)
|
|
35
|
+
[ "$code" = "200" ] && break
|
|
36
|
+
sleep 1
|
|
37
|
+
done
|
|
38
|
+
echo " health 3110 -> HTTP ${code:-none}"
|
|
39
|
+
curl -s --max-time 3 http://127.0.0.1:3110/health 2>/dev/null | head -c 200
|
|
40
|
+
echo
|
|
41
|
+
exit "$INSTALL_RC"
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Prove the box-wide update is SINGLE-FLIGHT under concurrent session starts.
|
|
4
|
+
*
|
|
5
|
+
* The bug this guards: rdc-skills is one box-wide npm package and one PM2 process,
|
|
6
|
+
* but every session/worktree runs the SessionStart hook. Without a lock, N sessions
|
|
7
|
+
* each conclude "unhealthy" and each run `npm install -g` on the same directory.
|
|
8
|
+
*
|
|
9
|
+
* Imports the REAL implementation from hooks/lib/box-lock.js. An earlier version
|
|
10
|
+
* kept its own copy of these functions, which drifted to the old algorithm — so it
|
|
11
|
+
* reported PASS for code that was not shipping. A test that duplicates its subject
|
|
12
|
+
* tests nothing.
|
|
13
|
+
*/
|
|
14
|
+
import { createRequire } from 'node:module';
|
|
15
|
+
import { spawn } from 'node:child_process';
|
|
16
|
+
import fs from 'node:fs';
|
|
17
|
+
import os from 'node:os';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import { fileURLToPath } from 'node:url';
|
|
20
|
+
|
|
21
|
+
const require = createRequire(import.meta.url);
|
|
22
|
+
const HOOK_LIB = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'hooks', 'lib', 'box-lock.js');
|
|
23
|
+
const { acquireBoxLock, releaseBoxLock, LOCK_PATH, LOCK_STALE_MS } = require(HOOK_LIB);
|
|
24
|
+
|
|
25
|
+
const release = () => { try { fs.unlinkSync(LOCK_PATH); } catch { /* ignore */ } };
|
|
26
|
+
|
|
27
|
+
release(); // clean slate
|
|
28
|
+
let fail = 0;
|
|
29
|
+
const check = (name, ok) => { console.log(`${ok ? 'PASS' : 'FAIL'} ${name}`); if (!ok) fail++; };
|
|
30
|
+
|
|
31
|
+
// 1. Happy path. SEQUENTIAL and in-process, so it proves only that a held lock is
|
|
32
|
+
// not handed out twice — it cannot exercise interleaving. Test 7 does that.
|
|
33
|
+
const winners = Array.from({ length: 10 }, () => acquireBoxLock()).filter(Boolean).length;
|
|
34
|
+
check(`sequential starts: exactly 1 updater of 10 (got ${winners})`, winners === 1);
|
|
35
|
+
|
|
36
|
+
// 2. A live holder is respected — a follower must NOT steal the lock.
|
|
37
|
+
check('live lock respected', acquireBoxLock() === false);
|
|
38
|
+
|
|
39
|
+
// 3. Release hands the box to the next session.
|
|
40
|
+
releaseBoxLock();
|
|
41
|
+
check('after release, next session leads', acquireBoxLock() === true);
|
|
42
|
+
releaseBoxLock();
|
|
43
|
+
|
|
44
|
+
// 4. A dead owner must not wedge every future session forever.
|
|
45
|
+
fs.writeFileSync(LOCK_PATH, JSON.stringify({ pid: 999999, ts: new Date().toISOString() }));
|
|
46
|
+
check('dead owner reclaimed', acquireBoxLock() === true);
|
|
47
|
+
releaseBoxLock();
|
|
48
|
+
|
|
49
|
+
// 5. A stale-but-live lock (crashed mid-install, PID reused) is reclaimed by age.
|
|
50
|
+
fs.writeFileSync(LOCK_PATH, JSON.stringify({
|
|
51
|
+
pid: process.pid, ts: new Date(Date.now() - LOCK_STALE_MS - 1000).toISOString(),
|
|
52
|
+
}));
|
|
53
|
+
check('stale lock reclaimed by age', acquireBoxLock() === true);
|
|
54
|
+
releaseBoxLock();
|
|
55
|
+
|
|
56
|
+
// 6. Corrupt bodies must not wedge the box. `pid: 0` is the sharp one:
|
|
57
|
+
// process.kill(0, 0) signals the CURRENT PROCESS GROUP and succeeds, so a naive
|
|
58
|
+
// liveness check reads it as alive forever; paired with an unparseable ts, the
|
|
59
|
+
// age check is NaN and never trips either.
|
|
60
|
+
for (const body of ['not json', '{"pid":0,"ts":"garbage"}', '{"pid":-1,"ts":null}', '{}']) {
|
|
61
|
+
fs.writeFileSync(LOCK_PATH, body);
|
|
62
|
+
check(`corrupt lock reclaimed: ${body.slice(0, 26)}`, acquireBoxLock() === true);
|
|
63
|
+
releaseBoxLock();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 7. REAL concurrency against a STALE lock — the reclaim TOCTOU.
|
|
67
|
+
//
|
|
68
|
+
// The naive reclaim (read -> judge stale -> unlink -> open 'wx') lets two racers
|
|
69
|
+
// both judge the lock stale, and the loser's unlink deletes the WINNER's fresh
|
|
70
|
+
// lock, so both become leader and both run `npm install -g`. Sequential calls in
|
|
71
|
+
// one process can never surface this; only real processes racing can.
|
|
72
|
+
{
|
|
73
|
+
// A leader HOLDS the lock briefly before exiting. Without the hold, a winner
|
|
74
|
+
// exits (releasing on 'exit') before the next racer even calls acquire, so every
|
|
75
|
+
// racer wins in turn and the result looks like a race that never happened.
|
|
76
|
+
const racer = `
|
|
77
|
+
const { acquireBoxLock } = require(${JSON.stringify(HOOK_LIB)});
|
|
78
|
+
const go = Number(process.argv[2]);
|
|
79
|
+
while (Date.now() < go) { /* spin to a shared start instant */ }
|
|
80
|
+
const won = acquireBoxLock();
|
|
81
|
+
process.stdout.write(won ? 'LEADER' : 'follower');
|
|
82
|
+
if (won) { const until = Date.now() + 600; while (Date.now() < until) {} }
|
|
83
|
+
`;
|
|
84
|
+
|
|
85
|
+
// spawn(), NOT spawnSync(): spawnSync BLOCKS until each child exits, so the
|
|
86
|
+
// "racers" ran strictly one after another and never overlapped. That flaw made
|
|
87
|
+
// this test report 12/12 double-leaders against correct code.
|
|
88
|
+
const runRound = () => new Promise((resolve) => {
|
|
89
|
+
fs.writeFileSync(LOCK_PATH, JSON.stringify({ pid: 999999, ts: new Date().toISOString() }));
|
|
90
|
+
const go = Date.now() + 400;
|
|
91
|
+
const out = [];
|
|
92
|
+
let done = 0;
|
|
93
|
+
for (let i = 0; i < 4; i++) {
|
|
94
|
+
const kid = spawn(process.execPath, ['-e', racer, String(go)], { stdio: ['ignore', 'pipe', 'ignore'] });
|
|
95
|
+
let buf = '';
|
|
96
|
+
kid.stdout.on('data', (d) => { buf += d; });
|
|
97
|
+
kid.on('close', () => {
|
|
98
|
+
out.push(buf);
|
|
99
|
+
if (++done === 4) resolve(out.filter((o) => o.includes('LEADER')).length);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const ROUNDS = 12;
|
|
105
|
+
let doubleLeader = 0;
|
|
106
|
+
let noLeader = 0;
|
|
107
|
+
for (let r = 0; r < ROUNDS; r++) {
|
|
108
|
+
const leaders = await runRound();
|
|
109
|
+
if (leaders > 1) doubleLeader++;
|
|
110
|
+
if (leaders === 0) noLeader++;
|
|
111
|
+
release();
|
|
112
|
+
}
|
|
113
|
+
check(`concurrent reclaim: no round elected 2 leaders (${doubleLeader}/${ROUNDS} bad)`, doubleLeader === 0);
|
|
114
|
+
// Every racer bailing would be "safe" but useless — nobody would repair the box.
|
|
115
|
+
check(`concurrent reclaim: a leader was elected each round (${noLeader}/${ROUNDS} empty)`, noLeader === 0);
|
|
116
|
+
}
|
|
117
|
+
release();
|
|
118
|
+
|
|
119
|
+
// 8. NEGATIVE CONTROL — does test 7 actually have teeth?
|
|
120
|
+
//
|
|
121
|
+
// This suite has already produced two false verdicts: a copy of the implementation
|
|
122
|
+
// that drifted (reported PASS for code that was not shipping), and a spawnSync
|
|
123
|
+
// harness whose "racers" never overlapped (reported FAIL for code that was fine).
|
|
124
|
+
// A concurrency test that cannot fail is worse than no test, so run the SAME
|
|
125
|
+
// harness against a deliberately naive lock and require that it catches it.
|
|
126
|
+
{
|
|
127
|
+
const naivePath = path.join(os.tmpdir(), `naive-box-lock-${process.pid}.js`);
|
|
128
|
+
fs.writeFileSync(naivePath, `
|
|
129
|
+
const fs = require('node:fs');
|
|
130
|
+
const LOCK_PATH = ${JSON.stringify(`${LOCK_PATH}.naive`)};
|
|
131
|
+
// The original algorithm: read -> judge stale -> unlink -> open('wx').
|
|
132
|
+
// No rename, no identity check. This is what shipped before the fix.
|
|
133
|
+
function acquireBoxLock() {
|
|
134
|
+
for (let a = 0; a < 2; a++) {
|
|
135
|
+
try { const fd = fs.openSync(LOCK_PATH, 'wx');
|
|
136
|
+
fs.writeSync(fd, JSON.stringify({ pid: process.pid, ts: new Date().toISOString() }));
|
|
137
|
+
fs.closeSync(fd); return true;
|
|
138
|
+
} catch (err) {
|
|
139
|
+
if (err.code !== 'EEXIST') return false;
|
|
140
|
+
try { fs.unlinkSync(LOCK_PATH); } catch { return false; }
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
module.exports = { acquireBoxLock, LOCK_PATH };
|
|
146
|
+
`);
|
|
147
|
+
const naiveLock = `${LOCK_PATH}.naive`;
|
|
148
|
+
const naiveRacer = `
|
|
149
|
+
const { acquireBoxLock } = require(${JSON.stringify(naivePath)});
|
|
150
|
+
const go = Number(process.argv[2]);
|
|
151
|
+
while (Date.now() < go) {}
|
|
152
|
+
const won = acquireBoxLock();
|
|
153
|
+
process.stdout.write(won ? 'LEADER' : 'follower');
|
|
154
|
+
if (won) { const until = Date.now() + 600; while (Date.now() < until) {} }
|
|
155
|
+
`;
|
|
156
|
+
const naiveRound = () => new Promise((resolve) => {
|
|
157
|
+
fs.writeFileSync(naiveLock, JSON.stringify({ pid: 999999, ts: new Date().toISOString() }));
|
|
158
|
+
const go = Date.now() + 400;
|
|
159
|
+
const out = []; let done = 0;
|
|
160
|
+
for (let i = 0; i < 4; i++) {
|
|
161
|
+
const kid = spawn(process.execPath, ['-e', naiveRacer, String(go)], { stdio: ['ignore', 'pipe', 'ignore'] });
|
|
162
|
+
let buf = '';
|
|
163
|
+
kid.stdout.on('data', (d) => { buf += d; });
|
|
164
|
+
kid.on('close', () => { out.push(buf); if (++done === 4) resolve(out.filter((o) => o.includes('LEADER')).length); });
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
let caught = 0;
|
|
168
|
+
const ROUNDS = 12;
|
|
169
|
+
for (let r = 0; r < ROUNDS; r++) {
|
|
170
|
+
if (await naiveRound() > 1) caught++;
|
|
171
|
+
try { fs.unlinkSync(naiveLock); } catch { /* ignore */ }
|
|
172
|
+
}
|
|
173
|
+
check(`negative control: harness DETECTS the naive lock (${caught}/${ROUNDS} rounds caught)`, caught > 0);
|
|
174
|
+
try { fs.unlinkSync(naivePath); } catch { /* ignore */ }
|
|
175
|
+
try { fs.unlinkSync(naiveLock); } catch { /* ignore */ }
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
console.log(fail ? `\n${fail} FAILURE(S)` : '\nall box-lock invariants hold');
|
|
179
|
+
process.exit(fail ? 1 : 0);
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Prove the INSTALLED hook tree can actually load.
|
|
4
|
+
*
|
|
5
|
+
* The source was fine, the npm tarball was fine, and only the copied result was
|
|
6
|
+
* broken: copyHookFiles() was flat, so hooks/lib/ was never created at the
|
|
7
|
+
* destination and require('./lib/box-lock') died MODULE_NOT_FOUND — a SessionStart
|
|
8
|
+
* hook exiting non-zero with EMPTY stdout on every session, before any health check
|
|
9
|
+
* could run, so its own repair path could never recover it.
|
|
10
|
+
*
|
|
11
|
+
* File-presence checks in the repo would not have caught that. This replays the
|
|
12
|
+
* real copy into a scratch dir and LOADS the result.
|
|
13
|
+
*/
|
|
14
|
+
import { createRequire } from 'node:module';
|
|
15
|
+
import { spawnSync } from 'node:child_process';
|
|
16
|
+
import fs from 'node:fs';
|
|
17
|
+
import os from 'node:os';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import { fileURLToPath } from 'node:url';
|
|
20
|
+
|
|
21
|
+
const require = createRequire(import.meta.url);
|
|
22
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
23
|
+
const { copyHookFiles } = require(path.join(ROOT, 'scripts', 'install-rdc-skills.js'));
|
|
24
|
+
|
|
25
|
+
let fail = 0;
|
|
26
|
+
const check = (name, ok, detail = '') => {
|
|
27
|
+
console.log(`${ok ? 'PASS' : 'FAIL'} ${name}${detail ? ` — ${detail}` : ''}`);
|
|
28
|
+
if (!ok) fail++;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const dst = fs.mkdtempSync(path.join(os.tmpdir(), 'rdc-hooks-install-'));
|
|
32
|
+
const count = copyHookFiles(path.join(ROOT, 'hooks'), dst);
|
|
33
|
+
check(`copied hook files (${count})`, count > 0);
|
|
34
|
+
|
|
35
|
+
// The exact loss: a nested dir the flat copy skipped.
|
|
36
|
+
check('hooks/lib/ exists at destination', fs.existsSync(path.join(dst, 'lib')));
|
|
37
|
+
check('hooks/lib/box-lock.js copied', fs.existsSync(path.join(dst, 'lib', 'box-lock.js')));
|
|
38
|
+
check('hooks/lib/run-evidence-gate.mjs copied (.mjs was filtered out too)',
|
|
39
|
+
fs.existsSync(path.join(dst, 'lib', 'run-evidence-gate.mjs')));
|
|
40
|
+
|
|
41
|
+
// Load every top-level hook for real. --check only parses; it does not resolve
|
|
42
|
+
// require(), which is precisely what broke.
|
|
43
|
+
const hooks = fs.readdirSync(dst).filter((f) => f.endsWith('.js'));
|
|
44
|
+
const unresolved = [];
|
|
45
|
+
for (const h of hooks) {
|
|
46
|
+
const entry = path.join(dst, h).replace(/\\/g, '/');
|
|
47
|
+
const res = spawnSync(process.execPath, ['-e', `require(${JSON.stringify(entry)})`], {
|
|
48
|
+
encoding: 'utf8', input: '', timeout: 20000,
|
|
49
|
+
});
|
|
50
|
+
const out = `${res.stdout || ''}${res.stderr || ''}`;
|
|
51
|
+
if (/MODULE_NOT_FOUND|Cannot find module/.test(out)) {
|
|
52
|
+
unresolved.push(`${h}: ${(out.match(/Cannot find module '[^']+'/) || [''])[0]}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
check(`all ${hooks.length} installed hooks resolve their imports`, unresolved.length === 0,
|
|
56
|
+
unresolved.slice(0, 3).join(' | '));
|
|
57
|
+
|
|
58
|
+
fs.rmSync(dst, { recursive: true, force: true });
|
|
59
|
+
console.log(fail ? `\n${fail} FAILURE(S)` : '\ninstalled hook tree loads cleanly');
|
|
60
|
+
process.exit(fail ? 1 : 0);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Read-only probe: which PM2 processes hold the global rdc-skills package dir?
|
|
4
|
+
*
|
|
5
|
+
* Mirrors processesHoldingPackage() in hooks/check-rdc-environment.js so the
|
|
6
|
+
* matcher can be verified WITHOUT running a real repair (which would stop the
|
|
7
|
+
* MCP and reinstall the package).
|
|
8
|
+
*
|
|
9
|
+
* `npm install -g` upgrades by renaming that directory; Windows refuses to rename
|
|
10
|
+
* a live process's cwd, which is the EBUSY that hard-blocked sessions.
|
|
11
|
+
*/
|
|
12
|
+
import { execSync } from 'node:child_process';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
|
|
15
|
+
const shell = (c) => execSync(c, { encoding: 'utf8', timeout: 15000 }).trim();
|
|
16
|
+
const norm = (p) => String(p || '').toLowerCase().replace(/\\/g, '/');
|
|
17
|
+
|
|
18
|
+
const root = shell('npm root -g');
|
|
19
|
+
const pkgDir = norm(path.join(root, '@lifeaitools', 'rdc-skills'));
|
|
20
|
+
|
|
21
|
+
const all = JSON.parse(shell('pm2 jlist'));
|
|
22
|
+
const holders = all.filter((p) => {
|
|
23
|
+
const cwd = norm(p?.pm2_env?.pm_cwd);
|
|
24
|
+
return cwd && (cwd === pkgDir || cwd.startsWith(`${pkgDir}/`));
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
console.log(`package dir : ${pkgDir}`);
|
|
28
|
+
console.log(`pm2 procs : ${all.length}`);
|
|
29
|
+
for (const p of holders) {
|
|
30
|
+
console.log(` HOLDS LOCK: ${p.name} status=${p.pm2_env.status} cwd=${norm(p.pm2_env.pm_cwd)}`);
|
|
31
|
+
}
|
|
32
|
+
console.log(
|
|
33
|
+
holders.length
|
|
34
|
+
? `\nMATCHED ${holders.length} — these are stopped before npm -g, so EBUSY is avoided.`
|
|
35
|
+
: '\nNO MATCH — nothing holds the dir; npm -g would have been safe anyway.',
|
|
36
|
+
);
|