wkappbot-code 0.3.29 → 0.3.32
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 +6 -2
- package/lib/holders.js +32 -0
- package/lib/providers.js +4 -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 { waitForHolders } from "../lib/holders.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,9 @@ 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.32 (2026-11-12, kih ROOT 3): the uninstall also waits for every process whose command line names the harness dir (lib/holders.js) and prints the holders as one line the SDK e2e cause filter keeps.
|
|
440
|
+
// 0.3.31 (2026-11-12, spokesman order): provider add on a clean machine creates the OpenCode config folder (lib/providers.js mkdirSync) instead of dying with ENOENT, and the opencode.json backup of provider add and remove goes under <config dir>/wkappbot-code/backups, which uninstall removes with the rest of its state, so no opencode.json.bak-<stamp> stays in the user config folder.
|
|
441
|
+
// 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
442
|
// 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
443
|
// 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
444
|
st.prereq = ensurePrereqs({ log });
|
|
@@ -602,10 +605,11 @@ function uninstall(o) {
|
|
|
602
605
|
const rmTree = (p, what) => {
|
|
603
606
|
const r = rmTreeHard(p); if (r.ok) { if (what) log(what + ": removed " + p); return; }
|
|
604
607
|
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,
|
|
608
|
+
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 blocked by (rows naming " + p + " first, then other shell/node/python processes with parent pid and command line; one line so the SDK e2e cause filter keeps it) " + holderReport(p).split("\n").join(" ;; ")); }
|
|
606
609
|
};
|
|
607
610
|
// (B) the detached per-turn archive child may still hold .claude/hooks/wkharness-run.sh (Windows EPERM): wait for its RECORDED pid first.
|
|
608
611
|
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)" : "")); }
|
|
612
|
+
if (st.harness && st.harness.mode === "web" && pr.harnessDirExisted !== true && existsSync(HARNESS_DIR())) { const h = waitForHolders(HARNESS_DIR()); if (h.waited) log("harness: waited " + h.waited + " ms for process(es) naming the harness dir" + (h.alive.length ? " (pid " + h.alive.join(",") + " still running)" : "")); }
|
|
609
613
|
if (st.harness && st.harness.mode === "web" && pr.harnessDirExisted !== true && existsSync(HARNESS_DIR())) rmTree(HARNESS_DIR(), "harness");
|
|
610
614
|
if (pr.hooksDirExisted !== true && existsSync(HOOKS_DIR())) rmTree(HOOKS_DIR(), "push-gate");
|
|
611
615
|
const cfgFile = join(configDir(o), "opencode.json");
|
package/lib/holders.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// holders.js -- wait for the processes that still hold the harness copy before an uninstall removes it (kih ROOT 3 2026-11-12, 0.3.32).
|
|
2
|
+
// The 0.3.30 holder report named a live bash.exe at the failing Windows uninstall (SDK gate runs 43264303070 and 43269021533). lib/rmtree.js waitForArchive only waits
|
|
3
|
+
// for the RECORDED archive pid, so any other detached hook child (the shadow run, the skill-draft child, a flock wrapper) still held .claude/hooks/wkharness-run.sh.
|
|
4
|
+
// waitForHolders waits (bounded) for every process whose command line names the harness dir, never the uninstall's own pid or parent pid. exec and sleep are injectable.
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
|
+
|
|
7
|
+
const sleepMs = (ms) => { try { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); } catch { /* no wait available */ } };
|
|
8
|
+
const procExec = (cmd, args) => { const r = spawnSync(cmd, args, { encoding: "utf8", timeout: 20000 }); return String(r.stdout || r.stderr || ""); };
|
|
9
|
+
|
|
10
|
+
export function listHolders(dir, opts = {}) {
|
|
11
|
+
const exec = opts.exec || procExec, isWin = opts.win ?? (process.platform === "win32");
|
|
12
|
+
const needle = String(dir).replace(/\\/g, "/").toLowerCase(), own = new Set([process.pid, process.ppid]);
|
|
13
|
+
let table = "";
|
|
14
|
+
try {
|
|
15
|
+
table = isWin ? exec("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", "Get-CimInstance Win32_Process | ForEach-Object { \"$($_.ProcessId)`t$($_.ParentProcessId)`t$($_.Name)`t$([string]$_.CommandLine)\" }"])
|
|
16
|
+
: exec("ps", ["-eo", "pid=,ppid=,comm=,args="]).split(/\r?\n/).map((l) => l.trim().replace(/\s+/g, "\t")).join("\n");
|
|
17
|
+
} catch { return []; }
|
|
18
|
+
const pids = [];
|
|
19
|
+
for (const line of String(table).split(/\r?\n/)) {
|
|
20
|
+
const f = line.split("\t"), pid = parseInt(f[0], 10);
|
|
21
|
+
if (f.length < 4 || !(pid > 0) || own.has(pid)) continue;
|
|
22
|
+
if (f.slice(3).join(" ").replace(/\\/g, "/").toLowerCase().includes(needle)) pids.push(pid);
|
|
23
|
+
}
|
|
24
|
+
return pids;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function waitForHolders(dir, opts = {}) {
|
|
28
|
+
const maxMs = opts.maxMs ?? 20000, gap = opts.gap || 500, nap = opts.sleep || sleepMs;
|
|
29
|
+
let alive = listHolders(dir, opts), waited = 0;
|
|
30
|
+
while (alive.length && waited < maxMs) { nap(gap); waited += gap; alive = listHolders(dir, opts); }
|
|
31
|
+
return { waited, alive };
|
|
32
|
+
}
|
package/lib/providers.js
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
// ("{env:VAR}"); a key or a URL typed where a name belongs is refused and never echoed, and no env var value is read
|
|
4
4
|
// except by the doctor row, which only checks that a variable is set and prints its name.
|
|
5
5
|
// Pure build/merge/remove functions first, then the CLI body. Spec: wkappbot skill read agent-harness-landscape-2026
|
|
6
|
-
import { copyFileSync, existsSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { dirname, join } from "node:path";
|
|
7
8
|
|
|
8
9
|
export const NPM = "@ai-sdk/openai-compatible";
|
|
9
10
|
export const ENV_NAME = /^[A-Z_][A-Z0-9_]*$/;
|
|
@@ -118,7 +119,8 @@ export function providerCli(argv, { target, log = console.log, err = console.err
|
|
|
118
119
|
next = m.cfg;
|
|
119
120
|
}
|
|
120
121
|
const text = JSON.stringify(next, null, 2) + "\n";
|
|
121
|
-
if (raw) { const
|
|
122
|
+
if (raw) { const bdir = join(dirname(target), "wkappbot-code", "backups"); mkdirSync(bdir, { recursive: true }); const bak = join(bdir, "opencode.json.bak-" + stamp()); copyFileSync(target, bak); log("backup " + bak); } // under the state folder uninstall removes, never beside the user file (kih ROOT 3 2026-11-12)
|
|
123
|
+
mkdirSync(dirname(target), { recursive: true }); // a clean machine has no OpenCode config folder yet (kih ROOT 3 2026-11-12)
|
|
122
124
|
writeFileSync(target + ".tmp", text);
|
|
123
125
|
renameSync(target + ".tmp", target);
|
|
124
126
|
log("provider " + o.id + (o.sub === "add" ? " added to " : " removed from ") + target);
|
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.32",
|
|
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": {
|