wkappbot-code 0.3.7 → 0.3.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/bin/prepack.js ADDED
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env node
2
+ // prepack -- bakes the minimal web harness INTO the npm package (harness/), so `npm install wkappbot-code` wires the
3
+ // Claude Code hooks from the package alone: no kih or WKAppBot checkout and no reach to the harness web page (a
4
+ // Default-environment cloud VM allows the package registries and little else; spokesman order 2026-11-06).
5
+ // harness/ holds the same four files the web page serves: install.sh, wk-harness-min.tar.gz, its .sha256, version.json.
6
+ // The tarball is the manifest bake (tools/wk-harness-min-copy.sh, scrub gate included) minus .git and bin/linux-x64, the
7
+ // core binary the installer fetches on its own (part: bin); 4 MB instead of 35 MB.
8
+ // Env: WKAPPBOT_CODE_HARNESS_STAGE = an already baked tree to pack (selftest.sh bakes once per run; 50 s per pack saved).
9
+ import { spawnSync } from "node:child_process";
10
+ import { createHash } from "node:crypto";
11
+ import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
12
+ import { tmpdir } from "node:os";
13
+ import { dirname, join, resolve } from "node:path";
14
+ import { fileURLToPath } from "node:url";
15
+
16
+ const PKG_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "..");
17
+ const KIH = resolve(PKG_DIR, "..");
18
+ const OUT = join(PKG_DIR, "harness");
19
+ const PKG = JSON.parse(readFileSync(join(PKG_DIR, "package.json"), "utf8"));
20
+ const die = (m) => { console.error("[wkappbot-code prepack] " + m); process.exit(1); };
21
+ const sh = (p) => process.platform === "win32" ? p.replace(/\\/g, "/") : p;
22
+
23
+ let stage = process.env.WKAPPBOT_CODE_HARNESS_STAGE || "";
24
+ let tmp = "";
25
+ const copy = join(KIH, "tools", "wk-harness-min-copy.sh");
26
+ const install = join(KIH, "webvm", "x64-build", "harness-install.sh");
27
+ // A copy of the package packed OUTSIDE a kih checkout (the self-update selftest's tree) has nothing to bake from: it
28
+ // packs without harness/ and its install falls back to the web harness. The publish packs from the checkout, and
29
+ // selftest x16 refuses a tgz that carries no harness/.
30
+ if (!existsSync(install) || (!stage && !existsSync(copy))) {
31
+ console.error("[wkappbot-code prepack] no kih checkout at " + KIH + ": harness/ not baked (install falls back to the web harness)");
32
+ process.exit(0);
33
+ }
34
+ if (!stage) {
35
+ tmp = mkdtempSync(join(tmpdir(), "wkc-prepack-"));
36
+ stage = join(tmp, "h");
37
+ const r = spawnSync("bash", [sh(copy), sh(KIH), sh(stage), sh(join(KIH, "tools", "wk-harness-min-manifest.txt"))],
38
+ { encoding: "utf8", env: { ...process.env, GIT_CONFIG_GLOBAL: "/dev/null" } });
39
+ if (r.status !== 0) die("harness bake rc " + r.status + "\n" + String(r.stderr).slice(-800));
40
+ }
41
+ if (!existsSync(join(stage, "tools", "wkharness-lite.sh"))) die("the baked tree " + stage + " has no tools/wkharness-lite.sh");
42
+
43
+ rmSync(OUT, { recursive: true, force: true });
44
+ mkdirSync(OUT, { recursive: true });
45
+ const tgz = join(OUT, "wk-harness-min.tar.gz");
46
+ const t = spawnSync("tar", ["--exclude=./.git", "--exclude=./bin/linux-x64", "-czf", sh(tgz), "-C", sh(stage), "."], { encoding: "utf8" });
47
+ if (t.status !== 0) die("tar rc " + t.status + " " + t.stderr);
48
+ const sha = createHash("sha256").update(readFileSync(tgz)).digest("hex");
49
+ writeFileSync(tgz + ".sha256", sha + " wk-harness-min.tar.gz\n");
50
+ const count = (d) => readdirSync(d, { withFileTypes: true }).reduce((n, e) => n + (e.isDirectory() ? (e.name === ".git" ? 0 : count(join(d, e.name))) : 1), 0);
51
+ writeFileSync(join(OUT, "version.json"), JSON.stringify({ version: "npm-" + PKG.version + "-" + sha.slice(0, 12), sha256: sha,
52
+ built: new Date().toISOString().replace(/\.\d+Z$/, "Z"), files: count(stage) }) + "\n");
53
+ copyFileSync(install, join(OUT, "install.sh"));
54
+ if (tmp) rmSync(tmp, { recursive: true, force: true });
55
+ console.error("[wkappbot-code prepack] harness/ baked: " + sha.slice(0, 12) + " (" + readFileSync(tgz).length + " bytes)");
@@ -45,7 +45,11 @@ const PKG_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "..");
45
45
  const PKG = JSON.parse(readFileSync(join(PKG_DIR, "package.json"), "utf8"));
46
46
  const OPENCODE_VERSION = PKG.wkAppbotCode.opencodeVersion;
47
47
  const OWNER_LOGIN = "kiexpert";
48
- const HARNESS_BASE = () => (process.env.WK_HARNESS_BASE || "https://kiexpert.github.io/wkappbot-kih/webvm/x64/harness").replace(/\/+$/, "");
48
+ // The harness this package carries (harness/, baked by bin/prepack.js at pack time) comes first: it needs no checkout
49
+ // and no network (spokesman 2026-11-06, a Default-environment VM). WK_HARNESS_BASE overrides; the web page serves an unbaked tree.
50
+ const BUNDLED_HARNESS = () => join(PKG_DIR, "harness");
51
+ const HARNESS_BASE = () => (process.env.WK_HARNESS_BASE || (existsSync(join(BUNDLED_HARNESS(), "install.sh")) ? pathToFileURL(BUNDLED_HARNESS()).href
52
+ : "https://kiexpert.github.io/wkappbot-kih/webvm/x64/harness")).replace(/\/+$/, "");
49
53
  const HARNESS_DIR = () => process.env.WK_HARNESS_DIR || join(homedir(), ".wk-harness");
50
54
  const HOOKS_DIR = () => join(homedir(), ".wk-harness-hooks"); // tools/wk-global-hooks.sh install default
51
55
  const SETTINGS = () => process.env.WK_SETTINGS || join(homedir(), ".claude", "settings.json");
@@ -185,6 +189,9 @@ function installBin(o) {
185
189
  log("bin: bash " + script + " acquire --dest " + dest);
186
190
  const r = spawnSync("bash", [shPath(script), "acquire", "--dest", shPath(dest)], { encoding: "utf8", env: shEnv() });
187
191
  for (const line of ((r.stdout || "") + (r.stderr || "")).split("\n").filter(Boolean)) log(" " + line);
192
+ // rc 3 = the release host could not be reached (a package-registry-only VM): no core, but NOT a failed install --
193
+ // the hooks and the lite harness need no core and still refuse the risky classes. A sha256 mismatch stays rc 1.
194
+ if (r.status === 3) return { ok: false, degraded: true, dest, note: "core not fetched (release host unreachable); hooks + lite harness run without it -- rerun wkappbot-code install once github.com is reachable" };
188
195
  if (r.status !== 0) return { ok: false, dest, note: "wk-bin-acquire rc " + r.status };
189
196
  // Put `wkappbot` on PATH beside wkappbot-code itself, unless that name is already taken by someone else.
190
197
  let link = "";
@@ -394,7 +401,8 @@ function install(o) {
394
401
  }
395
402
  const dest = binDest(o);
396
403
  st.bin = o.bin ? installBin(o) : { skipped: true, dest };
397
- if (o.bin && !st.bin.ok) { bad++; log("bin: FAILED -- " + st.bin.note); }
404
+ if (o.bin && st.bin.degraded) log("bin: WARN -- " + st.bin.note);
405
+ else if (o.bin && !st.bin.ok) { bad++; log("bin: FAILED -- " + st.bin.note); }
398
406
  st.harness = o.harness ? installHarness(o) : { skipped: true };
399
407
  if (o.harness && !st.harness.ok) { bad++; log("harness: FAILED -- " + st.harness.note); }
400
408
  // The plugin entry names the harness the install just chose (a forced web harness wins over a checkout).
@@ -485,8 +493,14 @@ function uninstall(o) {
485
493
  if (isSymlink(p) || existsSync(p)) { rmSync(p, { force: true }); log("shim: removed " + p); }
486
494
  }
487
495
  // 4. the harness dirs we created, the plugin entry and our config folder
488
- if (st.harness && st.harness.mode === "web" && pr.harnessDirExisted !== true && existsSync(HARNESS_DIR())) { rmSync(HARNESS_DIR(), { recursive: true, force: true }); log("harness: removed " + HARNESS_DIR()); }
489
- if (pr.hooksDirExisted !== true && existsSync(HOOKS_DIR())) { rmSync(HOOKS_DIR(), { recursive: true, force: true }); log("push-gate: removed " + HOOKS_DIR()); }
496
+ // Windows releases a just-closed handle late (EBUSY/EPERM/ENOTEMPTY), so every tree delete retries and, when it
497
+ // still fails, names the path and goes on with the rest instead of aborting the uninstall half way.
498
+ const rmTree = (p, what) => {
499
+ try { rmSync(p, { recursive: true, force: true, maxRetries: 10, retryDelay: 300 }); if (what) log(what + ": removed " + p); }
500
+ catch (e) { rc = 1; log((what || "remove") + ": could not remove " + p + " (" + (e.code || e.message) + ") -- delete it by hand"); }
501
+ };
502
+ if (st.harness && st.harness.mode === "web" && pr.harnessDirExisted !== true && existsSync(HARNESS_DIR())) rmTree(HARNESS_DIR(), "harness");
503
+ if (pr.hooksDirExisted !== true && existsSync(HOOKS_DIR())) rmTree(HOOKS_DIR(), "push-gate");
490
504
  const cfgFile = join(configDir(o), "opencode.json");
491
505
  try {
492
506
  const c = JSON.parse(readFileSync(cfgFile, "utf8"));
@@ -498,8 +512,8 @@ function uninstall(o) {
498
512
  log("config: plugin entry removed from " + cfgFile);
499
513
  }
500
514
  } catch {}
501
- rmSync(STATE_HOME(), { recursive: true, force: true });
502
- rmSync(join(configDir(o), "wkappbot-code"), { recursive: true, force: true });
515
+ rmTree(STATE_HOME());
516
+ rmTree(join(configDir(o), "wkappbot-code"));
503
517
  log("uninstall done. Kept: opencode-ai, the wkappbot core (" + ((st.bin && st.bin.dest) || binDest(o)) + ") and its skills, and this CLI (npm rm -g wkappbot-code)");
504
518
  return rc;
505
519
  }
@@ -539,6 +553,7 @@ function doctor(o) {
539
553
  const dest = o.binDest ? resolve(o.binDest) : (st.bin && st.bin.dest) || binDest(o);
540
554
  let core = "";
541
555
  if (st.bin && st.bin.skipped) add("bin", false, "skipped (--no-bin)", false, true);
556
+ else if (st.bin && st.bin.degraded && !existsSync(dest)) add("bin", false, dest + " absent -- " + st.bin.note, false);
542
557
  else {
543
558
  const r = spawnSync(dest, ["--version"], { encoding: "utf8", timeout: 60000 });
544
559
  core = r.status === 0 ? (r.stdout || "").trim().split("\n")[0] : "";
@@ -100,18 +100,20 @@ default_dest_win() {
100
100
  fi
101
101
  }
102
102
 
103
- # sdk_fetch PLAT DIR -- prints the verified asset path, or returns 1 and prints nothing.
103
+ # sdk_fetch PLAT DIR -- prints the verified asset path; else prints nothing and returns
104
+ # 3 when a download failed (host unreachable, no such asset) or 4 when the asset is UNVERIFIED
105
+ # (no SHA256SUMS line for it, or a sha256 mismatch). Only 3 may degrade an install to hooks-only.
104
106
  sdk_fetch() {
105
107
  command -v wk_stopwatch_lap >/dev/null 2>&1 && wk_stopwatch_lap "sdk_fetch" || true
106
108
  local plat="$1" dir="$2" asset base want got
107
109
  asset="wkappbot-$plat"
108
110
  base="${WK_BIN_BASE:-https://github.com/kiexpert/wkappbot-sdk/releases/latest/download}"
109
- curl -fsSL -m 120 "$base/$asset" -o "$dir/$asset" || return 1
110
- curl -fsSL -m 120 "$base/SHA256SUMS" -o "$dir/SHA256SUMS" || return 1
111
+ curl -fsSL -m "${WK_BIN_TIMEOUT:-120}" "$base/$asset" -o "$dir/$asset" || return 3
112
+ curl -fsSL -m "${WK_BIN_TIMEOUT:-120}" "$base/SHA256SUMS" -o "$dir/SHA256SUMS" || return 3
111
113
  want="$(awk -v f="$asset" '{ n = $2; sub(/^\*/, "", n); if (n == f) { print $1; exit } }' "$dir/SHA256SUMS")"
112
- [ -n "$want" ] || return 1
114
+ [ -n "$want" ] || return 4
113
115
  got="$(sha_of "$dir/$asset" || true)"
114
- [ "$got" = "$want" ] || return 1
116
+ [ "$got" = "$want" ] || return 4
115
117
  echo "$dir/$asset"
116
118
  }
117
119
 
@@ -147,12 +149,20 @@ install_file() {
147
149
 
148
150
  acquire_unix() {
149
151
  command -v wk_stopwatch_lap >/dev/null 2>&1 && wk_stopwatch_lap "acquire_unix" || true
150
- local dest="$1" plat="$2" offline="$3" src label
152
+ local dest="$1" plat="$2" offline="$3" src="" label frc=3
151
153
  [ -n "$dest" ] || dest="$(default_dest_unix)"
152
- if [ "$offline" = 0 ] && src="$(sdk_fetch "$plat" "$WK_TMP")"; then
154
+ if [ "$offline" = 0 ]; then src="$(sdk_fetch "$plat" "$WK_TMP")" && frc=0 || frc=$?; fi
155
+ if [ "$frc" = 0 ]; then
153
156
  label="SDK release wkappbot-$plat"
154
157
  else
155
158
  if [ "$offline" = 0 ]; then note "SDK asset unavailable or unverified, using repo bin/$plat"; fi
159
+ # EXIT 3 = NOTHING TO FETCH FROM, never a security refusal: the release host is unreachable (a
160
+ # package-registry-only VM) and no checkout holds a repo copy. The caller may then install hooks
161
+ # and the lite harness without the core. An UNVERIFIED asset (frc 4) still dies rc 1 below.
162
+ if [ -z "$REPO" ] && [ "$frc" = 3 ]; then
163
+ note "UNREACHABLE: no wkappbot-$plat could be downloaded from ${WK_BIN_BASE:-github.com/kiexpert/wkappbot-sdk releases} and no kih checkout holds bin/$plat; nothing installed"
164
+ exit 3
165
+ fi
156
166
  src="$(repo_file "$plat/wkappbot")"
157
167
  label="repo bin/$plat/wkappbot"
158
168
  fi
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "wkappbot-code",
3
- "version": "0.3.7",
3
+ "version": "0.3.9",
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": {
7
+ "prepack": "node bin/prepack.js",
7
8
  "postinstall": "node bin/postinstall.js"
8
9
  },
9
10
  "bin": {
@@ -17,7 +18,8 @@
17
18
  "plugin/*.js",
18
19
  "opencode.json",
19
20
  "install.sh",
20
- "README.md"
21
+ "README.md",
22
+ "harness/*"
21
23
  ],
22
24
  "wkAppbotCode": {
23
25
  "opencodeVersion": "1.19.5"