wkappbot-code 0.3.29 → 0.3.30
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/bin/wkappbot-code.js +3 -2
- package/lib/rmtree.js +29 -0
- package/package.json +1 -1
package/bin/wkappbot-code.js
CHANGED
|
@@ -41,7 +41,7 @@ import {
|
|
|
41
41
|
import { homedir, tmpdir } from "node:os";
|
|
42
42
|
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
43
43
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
44
|
-
import { quietCheck, runUpdate, updateRow } from "../lib/update.js"; import { rmTreeHard, waitForArchive } from "../lib/rmtree.js"; import { plainFileUrl } from "../lib/fileurl.js"; import { removeStaleWkskill } from "../lib/stale-wkskill.js"; import { checkPath, installBothLinks, removeCreatedLinks } from "../lib/doctor-path.js";
|
|
44
|
+
import { quietCheck, runUpdate, updateRow } from "../lib/update.js"; import { rmTreeHard, waitForArchive, holderReport } from "../lib/rmtree.js"; import { plainFileUrl } from "../lib/fileurl.js"; import { removeStaleWkskill } from "../lib/stale-wkskill.js"; import { checkPath, installBothLinks, removeCreatedLinks } from "../lib/doctor-path.js";
|
|
45
45
|
import { applySets, askEach, buildConfig, configRow, detect, lineDiff, printDetected } from "../lib/config.js";
|
|
46
46
|
import { aiCliRows, installAiClis, removeAiClis } from "../lib/ai-clis.js";
|
|
47
47
|
import { providerCli, providerRow } from "../lib/providers.js";
|
|
@@ -436,6 +436,7 @@ function install(o) {
|
|
|
436
436
|
let bad = 0;
|
|
437
437
|
// 0.3.13 (owner 2026-11-09: Git Bash is a required tool too): bash, sh, curl, tar and git first; on Windows a Git that
|
|
438
438
|
// is not on PATH is used from its folder, and a missing one is installed with winget (lib/prereq.js)
|
|
439
|
+
// 0.3.30 (2026-11-12, spokesman order after gate runs 43246725941 and 43247534046): 0.3.29 left the EPERM on .claude/hooks/wkharness-run.sh in one of two win-x64-nopy runs. The detached shadow run (wkjs_shadow) and the skill-draft child of the hook script were still started inside the harness copy with the script fd open; both now start from cwd / with the fd closed (wk_detach_prep), like the archive child. A failing tree removal on Windows now prints lib/rmtree.js holderReport: every Win32_Process row naming the harness dir plus every shell, node and python process with pid, ppid and command line, so a run that still hits EPERM names the real holder.
|
|
439
440
|
// 0.3.29 (2026-11-12, spokesman order after SDK gate run 43227049219): uninstall waits (bounded 15 s) for a RECORDED archive pid before it removes the harness copy (lib/rmtree.js waitForArchive; the detached archive child starts from cwd / with the script fd closed), so no EPERM on .claude/hooks/wkharness-run.sh; install records st.opencodeBackups and uninstall leaves no opencode.json.bak-<stamp> of its own.
|
|
440
441
|
// 0.3.28 (owner 2026-11-12 via the spokesman): uninstall removes only the settings backup the install recorded (st.settingsBackup); the .cmd twins of the core links are shim candidates; the clean-baseline guard no longer reads --model of non-delegating commands; doctor sets process.exitCode.
|
|
441
442
|
st.prereq = ensurePrereqs({ log });
|
|
@@ -602,7 +603,7 @@ function uninstall(o) {
|
|
|
602
603
|
const rmTree = (p, what) => {
|
|
603
604
|
const r = rmTreeHard(p); if (r.ok) { if (what) log(what + ": removed " + p); return; }
|
|
604
605
|
rc = 1; log((what || "remove") + ": could not remove " + p + " -- blocked by " + r.blockers.slice(0, 5).join("; ") + " -- delete it by hand");
|
|
605
|
-
if (IS_WIN) { const d = spawnSync("tasklist.exe", ["/v"], { encoding: "utf8", timeout: 15000 }); log("remove diagnostics (process list, not file owners): " + String(d.stdout || d.stderr || "").slice(0,
|
|
606
|
+
if (IS_WIN) { const d = spawnSync("tasklist.exe", ["/v"], { encoding: "utf8", timeout: 15000 }); log("remove diagnostics (process list, not file owners): " + String(d.stdout || d.stderr || "").slice(0, 2000)); log("harness holders (rows naming " + p + ", then shell/node/python processes with parent pid and command line):\n" + holderReport(p)); }
|
|
606
607
|
};
|
|
607
608
|
// (B) the detached per-turn archive child may still hold .claude/hooks/wkharness-run.sh (Windows EPERM): wait for its RECORDED pid first.
|
|
608
609
|
if (existsSync(HARNESS_DIR())) { const w = waitForArchive(HARNESS_DIR()); if (w.waited) log("harness: waited " + w.waited + " ms for the archive child" + (w.alive.length ? " (pid " + w.alive.join(",") + " still running)" : "")); }
|
package/lib/rmtree.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// rmTreeHard(p, rm) -> { ok, blockers[] }: a long bounded retry first, then a per-entry pass that names what stays.
|
|
3
3
|
import { existsSync, lstatSync, readdirSync, readFileSync, rmSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
5
6
|
|
|
6
7
|
const sleepMs = (ms) => { try { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); } catch { /* no wait available */ } };
|
|
7
8
|
|
|
@@ -9,6 +10,34 @@ const sleepMs = (ms) => { try { Atomics.wait(new Int32Array(new SharedArrayBuffe
|
|
|
9
10
|
// pid in <harnessDir>/.tmp/hippo-archive/archive-SID.pid and removes it on exit. A Windows uninstall that runs inside that child's
|
|
10
11
|
// lifetime hits EPERM on .claude/hooks/wkharness-run.sh, so uninstall waits (bounded) for every RECORDED pid to leave first.
|
|
11
12
|
// isAlive and sleep are injectable so the selftest fakes a held handle. A pid file whose pid is dead is stale and ignored.
|
|
13
|
+
// holderReport(dir, opts) -> text: who could be holding the harness copy (kih ROOT 3 2026-11-12, spokesman order for 0.3.30). tasklist /v names no file owner, so on
|
|
14
|
+
// Windows this asks Win32_Process for every process whose command line or image path names the dir, plus every shell, node, python and setsid process with its
|
|
15
|
+
// parent pid and command line; the SDK e2e uninstall prints it when the tree removal fails, so a run that hits EPERM names the real holder. exec is injectable.
|
|
16
|
+
export function holderReport(dir, opts = {}) {
|
|
17
|
+
const exec = opts.exec || ((cmd, args) => { const r = spawnSync(cmd, args, { encoding: "utf8", timeout: 20000 }); return String(r.stdout || r.stderr || ""); });
|
|
18
|
+
const isWin = opts.win ?? (process.platform === "win32");
|
|
19
|
+
const needle = String(dir).replace(/\\/g, "/").toLowerCase();
|
|
20
|
+
try {
|
|
21
|
+
if (isWin) {
|
|
22
|
+
const ps = "Get-CimInstance Win32_Process | ForEach-Object { $c = [string]$_.CommandLine; $p = [string]$_.ExecutablePath; \"$($_.ProcessId)`t$($_.ParentProcessId)`t$($_.Name)`t$c`t$p\" }";
|
|
23
|
+
return filterHolderRows(exec("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", ps]), needle);
|
|
24
|
+
}
|
|
25
|
+
return filterHolderRows(exec("ps", ["-eo", "pid=,ppid=,comm=,args="]).split(/\r?\n/).map((l) => l.trim().replace(/\s+/g, "\t")).join("\n"), needle);
|
|
26
|
+
} catch (e) { return "holderReport failed: " + (e && e.message); }
|
|
27
|
+
}
|
|
28
|
+
const HOLDER_NAMES = /^(?:bash|sh|node|python3?|pwsh|powershell|setsid|flock|git|wk[\w.-]*)(?:\.exe)?$/i;
|
|
29
|
+
function filterHolderRows(out, needle) {
|
|
30
|
+
const rows = [];
|
|
31
|
+
for (const line of String(out).split(/\r?\n/)) {
|
|
32
|
+
const f = line.split("\t");
|
|
33
|
+
if (f.length < 3) continue;
|
|
34
|
+
const hay = f.slice(3).join(" ").replace(/\\/g, "/").toLowerCase();
|
|
35
|
+
const named = hay.includes(needle), shellish = HOLDER_NAMES.test(String(f[2]).trim());
|
|
36
|
+
if (named || shellish) rows.push((named ? "HOLDS-DIR " : "shell ") + "pid=" + f[0] + " ppid=" + f[1] + " " + f[2] + " " + f.slice(3).join(" ").slice(0, 300));
|
|
37
|
+
}
|
|
38
|
+
return rows.length ? rows.slice(0, 60).join("\n") : "no process names the harness dir and no shell, node or python process is running";
|
|
39
|
+
}
|
|
40
|
+
|
|
12
41
|
const defaultAlive = (pid) => { try { process.kill(pid, 0); return true; } catch (e) { return !!e && e.code === "EPERM"; } };
|
|
13
42
|
export function waitForArchive(harnessDir, opts = {}) {
|
|
14
43
|
const isAlive = opts.isAlive || defaultAlive, maxMs = opts.maxMs ?? 15000, gap = opts.gap || 250, nap = opts.sleep || sleepMs;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wkappbot-code",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.30",
|
|
4
4
|
"description": "wkappbot-code -- Wisdom Kernel: the pinned OpenCode bridge plus the reflex harness plugin, the wkappbot core, the web harness and the latest public skills in one install",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|