wkappbot-code 0.3.30 → 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.
@@ -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, 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";
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,8 @@ 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.
439
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.
440
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.
441
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.
@@ -603,10 +605,11 @@ function uninstall(o) {
603
605
  const rmTree = (p, what) => {
604
606
  const r = rmTreeHard(p); if (r.ok) { if (what) log(what + ": removed " + p); return; }
605
607
  rc = 1; log((what || "remove") + ": could not remove " + p + " -- blocked by " + r.blockers.slice(0, 5).join("; ") + " -- delete it by hand");
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)); }
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(" ;; ")); }
607
609
  };
608
610
  // (B) the detached per-turn archive child may still hold .claude/hooks/wkharness-run.sh (Windows EPERM): wait for its RECORDED pid first.
609
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)" : "")); }
610
613
  if (st.harness && st.harness.mode === "web" && pr.harnessDirExisted !== true && existsSync(HARNESS_DIR())) rmTree(HARNESS_DIR(), "harness");
611
614
  if (pr.hooksDirExisted !== true && existsSync(HOOKS_DIR())) rmTree(HOOKS_DIR(), "push-gate");
612
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 bak = target + ".bak-" + stamp(); copyFileSync(target, bak); log("backup " + bak); }
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wkappbot-code",
3
- "version": "0.3.30",
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": {