wkappbot-code 0.3.27 → 0.3.29
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 +21 -8
- package/lib/fileurl.js +1 -0
- package/lib/rmtree.js +23 -1
- 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 } 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 } 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";
|
|
@@ -380,15 +380,16 @@ function installPlugin(o, kih, kihSource) {
|
|
|
380
380
|
const side = join(dirname(target), "wkappbot-code.json");
|
|
381
381
|
writeFileSync(side, JSON.stringify(buildConfig({}, it, tmpl), null, 2) + "\n");
|
|
382
382
|
log(target + " is not plain JSON; left untouched, wrote " + side + " -- merge its plugin entry by hand");
|
|
383
|
-
return;
|
|
383
|
+
return null;
|
|
384
384
|
}
|
|
385
385
|
const text = JSON.stringify(buildConfig(user, it, tmpl), null, 2) + "\n";
|
|
386
|
-
let note = "";
|
|
386
|
+
let note = "", backup = null; // backup: the opencode.json copy THIS install takes; install records it, uninstall removes exactly that (0.3.29)
|
|
387
387
|
if (raw && text === raw) note = " (unchanged)";
|
|
388
388
|
else {
|
|
389
389
|
if (raw) {
|
|
390
390
|
const bak = target + ".bak-" + stamp();
|
|
391
391
|
copyFileSync(target, bak);
|
|
392
|
+
backup = bak;
|
|
392
393
|
note = " (merged; backup " + bak + ")";
|
|
393
394
|
}
|
|
394
395
|
for (const l of lineDiff(raw, text)) log("diff " + l);
|
|
@@ -397,14 +398,16 @@ function installPlugin(o, kih, kihSource) {
|
|
|
397
398
|
}
|
|
398
399
|
log("plugin: " + url);
|
|
399
400
|
log("config: " + target + note);
|
|
401
|
+
return backup;
|
|
400
402
|
}
|
|
401
403
|
|
|
402
404
|
// The machine state an uninstall must give back, recorded ONCE (a re-install never overwrites the first record, or
|
|
403
405
|
// it would record our own dispatcher as the "prior" hooksPath). Shim paths are those install may create.
|
|
404
406
|
function shimCandidates() {
|
|
405
|
-
return [join(linkDir(), "wkappbot"), join(linkDir(), "wkappbot-core"), join(HARNESS_LINKS(), "wk-verify"), join(HARNESS_LINKS(), "wk-verify.cmd")]; // wkappbot-core: the harness install.sh links it beside wkappbot (SDK e2e nopwsh u1: local-bin= vs local-bin=wkappbot-core) // Windows (Git Bash) .cmd twins from harness-install.sh: recorded like the rest, so uninstall removes them, never a user file
|
|
407
|
+
return [join(linkDir(), "wkappbot"), join(linkDir(), "wkappbot-core"), join(linkDir(), "wkappbot.cmd"), join(linkDir(), "wkappbot-core.cmd"), join(HARNESS_LINKS(), "wk-verify"), join(HARNESS_LINKS(), "wk-verify.cmd")]; // wkappbot-core: the harness install.sh links it beside wkappbot (SDK e2e nopwsh u1: local-bin= vs local-bin=wkappbot-core) // Windows (Git Bash) .cmd twins from harness-install.sh: recorded like the rest, so uninstall removes them, never a user file
|
|
406
408
|
}
|
|
407
409
|
const PRE_SETTINGS = () => join(STATE_HOME(), "settings.pre-install.json");
|
|
410
|
+
function settingsBackup() { const bak = SETTINGS() + ".bak-" + stamp(); copyFileSync(SETTINGS(), bak); log("settings: backup " + bak); return bak; }
|
|
408
411
|
function recordPrior(prev) {
|
|
409
412
|
if (prev && prev.prior) return prev.prior;
|
|
410
413
|
// The user's settings.json as it was, byte for byte: uninstall gives these bytes back when nothing else changed.
|
|
@@ -433,6 +436,8 @@ function install(o) {
|
|
|
433
436
|
let bad = 0;
|
|
434
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
|
|
435
438
|
// is not on PATH is used from its folder, and a missing one is installed with winget (lib/prereq.js)
|
|
439
|
+
// 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
|
+
// 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.
|
|
436
441
|
st.prereq = ensurePrereqs({ log });
|
|
437
442
|
if (!st.prereq.ok) bad++;
|
|
438
443
|
// 0.3.20 (owner 2026-11-09): a real Python before any step runs python -- the Store alias stub made install exit 49
|
|
@@ -466,6 +471,8 @@ function install(o) {
|
|
|
466
471
|
const kih = (st.harness.mode === "web" && st.harness.ok) ? st.harness.dir : kihDir(o);
|
|
467
472
|
// 0.3.13 (owner 2026-11-09): read the Claude settings first, then point every harness hook at the SAME harness the
|
|
468
473
|
// plugin uses, on both roads (web harness or checkout); the user's own hooks stay; bad JSON is reported, not touched.
|
|
474
|
+
// the one settings.json copy install takes before it edits the file: uninstall removes this recorded copy and no other
|
|
475
|
+
st.settingsBackup = prev.settingsBackup || (o.harness && existsSync(SETTINGS()) ? settingsBackup() : null);
|
|
469
476
|
st.hooks = o.harness ? wireClaudeHooks({ settings: SETTINGS(), kih, pwsh: !!ps, log }) : { skipped: true };
|
|
470
477
|
// the compressed allow list (lib/allow-compact.js): five harness-own prefixes, never the wide rows learned on the owner's seats
|
|
471
478
|
st.allows = (o.harness && o.allows) ? addAllows(SETTINGS(), prev.allows || {}, log) : (prev.allows || { skipped: true });
|
|
@@ -480,7 +487,8 @@ function install(o) {
|
|
|
480
487
|
}
|
|
481
488
|
st.skills = o.skills ? installSkills(o, dest) : { skipped: true };
|
|
482
489
|
if (o.skills && !st.skills.ok) { bad++; log("skills: FAILED -- " + (st.skills.note || "on-load missing from the HQ")); }
|
|
483
|
-
|
|
490
|
+
// 0.3.29: the opencode.json copy a merge takes is recorded (like st.settingsBackup); uninstall removes exactly the recorded copies, never a user's own .bak-*
|
|
491
|
+
{ const ob = installPlugin(o, kih, (st.harness.mode === "web" && st.harness.ok) ? "web harness" : "harness checkout"); st.opencodeBackups = [...(prev.opencodeBackups || []), ...(ob ? [ob] : [])]; }
|
|
484
492
|
st.kihDir = kih;
|
|
485
493
|
// The shims this install created (absent before, present now): uninstall removes exactly these.
|
|
486
494
|
st.shims = [...new Set([...(prev.shims || []), ...shimCandidates().filter((p) => (existsSync(p) || isSymlink(p)) && !st.prior.shimsExisting.includes(p))])];
|
|
@@ -546,7 +554,6 @@ function uninstall(o) {
|
|
|
546
554
|
}
|
|
547
555
|
}
|
|
548
556
|
if (removed || restored) {
|
|
549
|
-
copyFileSync(sp, sp + ".bak-" + stamp());
|
|
550
557
|
if (pr.settingsExisted === false && !Object.keys(d).length) { rmSync(sp); log("settings: removed " + sp + " (it did not exist before install)"); }
|
|
551
558
|
else {
|
|
552
559
|
const same = pre && JSON.stringify(sortKeys(pre)) === JSON.stringify(sortKeys(d));
|
|
@@ -557,6 +564,8 @@ function uninstall(o) {
|
|
|
557
564
|
} else log("settings: no harness hook in " + sp);
|
|
558
565
|
} catch (e) { rc = 1; log("settings: " + sp + " is not plain JSON, left untouched (" + e.message + ")"); }
|
|
559
566
|
}
|
|
567
|
+
// the settings copy install recorded (only that one) goes with the uninstall; a user's own settings.json.bak-* stays
|
|
568
|
+
if (st.settingsBackup && existsSync(st.settingsBackup)) { rmSync(st.settingsBackup, { force: true }); log("settings: removed the backup install recorded, " + st.settingsBackup); }
|
|
560
569
|
// 3. the shims install created
|
|
561
570
|
for (const p of st.shims || []) {
|
|
562
571
|
if (isSymlink(p) || existsSync(p)) { rmSync(p, { force: true }); log("shim: removed " + p); }
|
|
@@ -595,6 +604,8 @@ function uninstall(o) {
|
|
|
595
604
|
rc = 1; log((what || "remove") + ": could not remove " + p + " -- blocked by " + r.blockers.slice(0, 5).join("; ") + " -- delete it by hand");
|
|
596
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, 4000)); }
|
|
597
606
|
};
|
|
607
|
+
// (B) the detached per-turn archive child may still hold .claude/hooks/wkharness-run.sh (Windows EPERM): wait for its RECORDED pid first.
|
|
608
|
+
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)" : "")); }
|
|
598
609
|
if (st.harness && st.harness.mode === "web" && pr.harnessDirExisted !== true && existsSync(HARNESS_DIR())) rmTree(HARNESS_DIR(), "harness");
|
|
599
610
|
if (pr.hooksDirExisted !== true && existsSync(HOOKS_DIR())) rmTree(HOOKS_DIR(), "push-gate");
|
|
600
611
|
const cfgFile = join(configDir(o), "opencode.json");
|
|
@@ -602,12 +613,14 @@ function uninstall(o) {
|
|
|
602
613
|
const c = JSON.parse(readFileSync(cfgFile, "utf8"));
|
|
603
614
|
const keep = (c.plugin || []).filter((p) => !String(Array.isArray(p) ? p[0] : p).includes("wisdom-kernel"));
|
|
604
615
|
if (keep.length !== (c.plugin || []).length) {
|
|
605
|
-
|
|
616
|
+
// 0.3.29 (spokesman ruling 2026-11-12): no backup of OUR OWN edit -- it left an opencode.json.bak-<stamp> behind. Only our entry goes.
|
|
606
617
|
if (keep.length) c.plugin = keep; else delete c.plugin;
|
|
607
618
|
writeFileSync(cfgFile, JSON.stringify(c, null, 2) + "\n");
|
|
608
619
|
log("config: plugin entry removed from " + cfgFile);
|
|
609
620
|
}
|
|
610
621
|
} catch {}
|
|
622
|
+
// the opencode.json copies install recorded (and only those) go with the uninstall; a user's own opencode.json.bak-* stays
|
|
623
|
+
for (const b of (st.opencodeBackups || [])) { try { if (existsSync(b)) { rmSync(b, { force: true }); log("config: removed the backup install recorded, " + b); } } catch (e) { log("config: could not remove " + b + " (" + e.message + ")"); } }
|
|
611
624
|
rmTree(STATE_HOME());
|
|
612
625
|
rmTree(join(configDir(o), "wkappbot-code"));
|
|
613
626
|
log("uninstall done." + (linkRemoved ? " Removed the link " + linkRemoved + "." : "") + " Kept: opencode-ai, the wkappbot core (" + core + ") and its skills, and this CLI (npm rm -g wkappbot-code)");
|
|
@@ -730,7 +743,7 @@ switch (o.cmd) {
|
|
|
730
743
|
console.log("wkappbot-code " + PKG.version + " (opencode-ai " + OPENCODE_VERSION + ")"); process.exit(0);
|
|
731
744
|
case "install": process.exit(install(o));
|
|
732
745
|
case "uninstall": process.exit(uninstall(o));
|
|
733
|
-
case "doctor": process.
|
|
746
|
+
case "doctor": process.exitCode = doctor(o); break;
|
|
734
747
|
case "update": process.exit(upd());
|
|
735
748
|
case "config": installPlugin(o, kihDir(o), kihCheckout(o) ? "harness checkout" : "web harness"); process.exit(0);
|
|
736
749
|
default:
|
package/lib/fileurl.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// fileurl.js -- a file:// base that curl can open on every platform.
|
|
2
|
+
// VERIFIED 2026-11-12 (kih ROOT 3): selftest-rmtree.mjs case r4 (plainFileUrl decodes %7E to ~, an https base is untouched) PASS in RESULT 8/8.
|
|
2
3
|
// pathToFileURL percent-encodes "~" ("RUNNER~1" -> "RUNNER%7E1") and Windows curl does not decode a file:// path, so a
|
|
3
4
|
// Windows profile with a short name, a space or a non-ASCII letter failed to read the harness. plainFileUrl hands curl the plain path.
|
|
4
5
|
import { fileURLToPath } from "node:url";
|
package/lib/rmtree.js
CHANGED
|
@@ -1,10 +1,32 @@
|
|
|
1
1
|
// rmtree.js -- delete a tree on a platform that releases a just-closed handle late (Windows: EBUSY/EPERM/ENOTEMPTY).
|
|
2
2
|
// rmTreeHard(p, rm) -> { ok, blockers[] }: a long bounded retry first, then a per-entry pass that names what stays.
|
|
3
|
-
import { existsSync, lstatSync, readdirSync, rmSync } from "node:fs";
|
|
3
|
+
import { existsSync, lstatSync, readdirSync, readFileSync, rmSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
|
|
6
6
|
const sleepMs = (ms) => { try { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); } catch { /* no wait available */ } };
|
|
7
7
|
|
|
8
|
+
// waitForArchive(harnessDir, opts) -> { waited, alive[] }: the detached per-turn archive child (tools/wk-hippo-archive.sh) records its
|
|
9
|
+
// pid in <harnessDir>/.tmp/hippo-archive/archive-SID.pid and removes it on exit. A Windows uninstall that runs inside that child's
|
|
10
|
+
// lifetime hits EPERM on .claude/hooks/wkharness-run.sh, so uninstall waits (bounded) for every RECORDED pid to leave first.
|
|
11
|
+
// isAlive and sleep are injectable so the selftest fakes a held handle. A pid file whose pid is dead is stale and ignored.
|
|
12
|
+
const defaultAlive = (pid) => { try { process.kill(pid, 0); return true; } catch (e) { return !!e && e.code === "EPERM"; } };
|
|
13
|
+
export function waitForArchive(harnessDir, opts = {}) {
|
|
14
|
+
const isAlive = opts.isAlive || defaultAlive, maxMs = opts.maxMs ?? 15000, gap = opts.gap || 250, nap = opts.sleep || sleepMs;
|
|
15
|
+
const dir = join(harnessDir, ".tmp", "hippo-archive");
|
|
16
|
+
const pids = () => {
|
|
17
|
+
let names = []; try { names = readdirSync(dir).filter((n) => /\.pid$/.test(n)); } catch { return []; }
|
|
18
|
+
const out = [];
|
|
19
|
+
for (const n of names) {
|
|
20
|
+
let pid = 0; try { pid = parseInt(String(readFileSync(join(dir, n), "utf8")).trim(), 10); } catch { /* unreadable */ }
|
|
21
|
+
if (pid > 0 && isAlive(pid)) out.push(pid);
|
|
22
|
+
}
|
|
23
|
+
return out;
|
|
24
|
+
};
|
|
25
|
+
let alive = pids(), waited = 0;
|
|
26
|
+
while (alive.length && waited < maxMs) { nap(gap); waited += gap; alive = pids(); }
|
|
27
|
+
return { waited, alive };
|
|
28
|
+
}
|
|
29
|
+
|
|
8
30
|
// rm is injectable so the selftest can fake a held handle (default: fs.rmSync with Node's own retry)
|
|
9
31
|
export function rmTreeHard(p, rm = (q, o) => rmSync(q, o), opts = {}) {
|
|
10
32
|
const tries = opts.tries || 6, gap = opts.gap || 1000;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wkappbot-code",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.29",
|
|
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": {
|