wkappbot-code 0.3.11 → 0.3.12

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.
@@ -5,7 +5,7 @@
5
5
  // Owner order 2026-11-08: "npm install wkappbot-code should do the initial setup and the tests automatically" -- on
6
6
  // Windows, macOS and Linux alike, not only on a cloud VM.
7
7
  // CONTRACT: (a) ALWAYS exits 0 and returns within a moment -- the real work runs DETACHED and logs to
8
- // ~/.wkappbot-code/postinstall.log; (b) the worker runs `wkappbot-code install --no-npm` (lays the web harness, wires
8
+ // ~/.wkappbot-code/postinstall.log; (b) the worker runs `wkappbot-code install` (lays the web harness, wires
9
9
  // ~/.claude/settings.json through the lite harness -- PreToolUse, Stop, UserPromptSubmit, SessionStart -- the global git
10
10
  // hooks, the wkskill/wk-verify shims and the public skills, keeping every user setting), then `wkappbot-code doctor`,
11
11
  // then a short self-test (lib/setup-run.js: core --version, skill read on-load, one hook allow, one hook refuse), and
@@ -23,7 +23,7 @@ import { homedir } from "node:os";
23
23
  import { appendFileSync } from "node:fs";
24
24
  import { dirname, join } from "node:path";
25
25
  import { fileURLToPath } from "node:url";
26
- import { buildDone, files, markRunning, parseDoctor, platformOf, selfTest, writeDone } from "../lib/setup-run.js";
26
+ import { buildDone, files, globalBinDir, markRunning, parseDoctor, platformOf, selfTest, withPathFirst, writeDone } from "../lib/setup-run.js";
27
27
 
28
28
  const HERE = dirname(fileURLToPath(import.meta.url));
29
29
  const CLI = join(HERE, "wkappbot-code.js");
@@ -41,12 +41,15 @@ function stamp(msg) { try { mkdirSync(STATE, { recursive: true }); appendFileSyn
41
41
 
42
42
  // --run: the detached worker. install, then doctor, then the self-test, then the done file.
43
43
  async function runWorker() {
44
- const args = [CLI, "install", "--no-npm"];
44
+ // 0.3.12: no --no-npm -- the worker installs the pinned OpenCode (npm -g, so `opencode` lands on PATH beside wkappbot-code);
45
+ // the selftests keep their machine untouched by adding --no-npm through WKAPPBOT_CODE_INSTALL_ARGS.
46
+ const args = [CLI, "install"];
45
47
  if (process.env.WKAPPBOT_CODE_DEV_SEAT === "1") args.push("--dev-seat");
46
48
  // extra install flags, e.g. "--no-bin --no-skills" (the selftest; a seat without the SDK release assets)
47
49
  args.push(...String(process.env.WKAPPBOT_CODE_INSTALL_ARGS || "").split(/\s+/).filter((a) => /^--[a-z-]+$/.test(a)));
48
50
  // the children are the worker's own: they must not print the first-run banner (it is for the user's next command)
49
- const env = { ...process.env, WKAPPBOT_CODE_NO_UPDATE_CHECK: "1", WKAPPBOT_CODE_SETUP_WORKER: "1" };
51
+ // the global npm bin dir holds `opencode` right after the install, whether or not the user PATH names it yet
52
+ const env = withPathFirst({ ...process.env, WKAPPBOT_CODE_NO_UPDATE_CHECK: "1", WKAPPBOT_CODE_SETUP_WORKER: "1" }, globalBinDir(process.env));
50
53
  stamp("worker start: node " + args.join(" "));
51
54
  const out = openSync(LOG, "a");
52
55
  const installRc = await new Promise((res) => {
@@ -40,7 +40,7 @@ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
40
40
  import { fileURLToPath, pathToFileURL } from "node:url";
41
41
  import { quietCheck, runUpdate, updateRow } from "../lib/update.js"; import { rmTreeHard } from "../lib/rmtree.js"; import { plainFileUrl } from "../lib/fileurl.js";
42
42
  import { applySets, askEach, buildConfig, configRow, detect, lineDiff, printDetected } from "../lib/config.js";
43
- import { launchOpenCode, printBanner } from "../lib/setup-run.js";
43
+ import { files as setupFiles, launchOpenCode, printBanner } from "../lib/setup-run.js";
44
44
 
45
45
  const PKG_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "..");
46
46
  const PKG = JSON.parse(readFileSync(join(PKG_DIR, "package.json"), "utf8"));
@@ -383,7 +383,21 @@ function recordPrior(prev) {
383
383
  hooksDirExisted: existsSync(HOOKS_DIR()), shimsExisting: shimCandidates().filter((p) => existsSync(p) || isSymlink(p)) };
384
384
  }
385
385
 
386
+ // A user's own `wkappbot-code install` right after `npm i -g` must not race the background setup worker (both wrote the
387
+ // same core file; SDK run 42504778443, install rc 1): wait for a live worker's postinstall.running to go, at most 10 min.
388
+ function waitForSetupWorker() {
389
+ if (process.env.WKAPPBOT_CODE_SETUP_WORKER === "1") return; // the worker's own install
390
+ const f = setupFiles(homedir());
391
+ const alive = () => { try { const r = JSON.parse(readFileSync(f.running, "utf8")); const age = Date.now() - Date.parse(r.at || 0); return age >= 0 && age < 30 * 60 * 1000; } catch { return false; } };
392
+ if (!alive()) return;
393
+ log("waiting for the background setup to finish (log: " + f.log + ")");
394
+ const nap = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
395
+ for (let i = 0; i < 600 && alive(); i++) nap(1000);
396
+ log(alive() ? "background setup still running after 10 min; installing anyway" : "background setup finished; continuing");
397
+ }
398
+
386
399
  function install(o) {
400
+ waitForSetupWorker();
387
401
  const prev = readState(o);
388
402
  const st = { version: PKG.version, at: new Date().toISOString(), prior: recordPrior(prev) };
389
403
  let bad = 0;
@@ -397,7 +411,9 @@ function install(o) {
397
411
  if (have === OPENCODE_VERSION) log("opencode " + have + " already installed");
398
412
  else {
399
413
  log("npm install -g opencode-ai@" + OPENCODE_VERSION + " (was: " + (have || "none") + ")");
400
- execFileSync("npm", ["install", "-g", "opencode-ai@" + OPENCODE_VERSION, "--no-audit", "--no-fund"], { shell: process.platform === "win32", stdio: "inherit" });
414
+ // 0.3.12: a failed OpenCode install (offline, no npm rights) must not abort the harness, plugin and config parts below.
415
+ try { execFileSync("npm", ["install", "-g", "opencode-ai@" + OPENCODE_VERSION, "--no-audit", "--no-fund"], { shell: process.platform === "win32", stdio: "inherit" }); }
416
+ catch (e) { bad++; log("opencode: npm install FAILED (" + String(e.message).split("\n")[0] + ") -- run: npm install -g opencode-ai@" + OPENCODE_VERSION); }
401
417
  }
402
418
  }
403
419
  const dest = binDest(o);
@@ -495,6 +511,15 @@ function uninstall(o) {
495
511
  for (const p of st.shims || []) {
496
512
  if (isSymlink(p) || existsSync(p)) { rmSync(p, { force: true }); log("shim: removed " + p); }
497
513
  }
514
+ // 3b. the ~/.local/bin/wkappbot link: removed ONLY when it points at the core this install placed (the core file stays)
515
+ const core = (st.bin && st.bin.dest) || binDest(o);
516
+ let linkRemoved = "";
517
+ for (const p of new Set(shimCandidates().concat([join(homedir(), ".local", "bin", "wkappbot")]))) {
518
+ if (!isSymlink(p) || (st.shims || []).includes(p)) continue;
519
+ let tgt = ""; try { tgt = resolve(dirname(p), readlinkSync(p)); } catch {}
520
+ if (tgt && real(tgt) === real(core)) { rmSync(p, { force: true }); linkRemoved = p; log("shim: removed " + p + " (a link to the core this install placed, " + core + ")"); }
521
+ else log("shim: kept " + p + " (not a link to " + core + ")");
522
+ }
498
523
  // 4. the harness dirs we created, the plugin entry and our config folder
499
524
  // Windows frees a closed handle late (ENOTEMPTY): lib/rmtree.js retries ~6 s, then a per-entry pass names the blocker; a failure goes on.
500
525
  const rmTree = (p, what) => {
@@ -516,7 +541,7 @@ function uninstall(o) {
516
541
  } catch {}
517
542
  rmTree(STATE_HOME());
518
543
  rmTree(join(configDir(o), "wkappbot-code"));
519
- 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)");
544
+ 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)");
520
545
  return rc;
521
546
  }
522
547
 
package/lib/config.js CHANGED
@@ -16,6 +16,8 @@ export const PROVIDERS = [
16
16
  ["GROQ_API_KEY", "groq", "groq/llama-3.3-70b-versatile"],
17
17
  ["OPENROUTER_API_KEY", "openrouter", "openrouter/anthropic/claude-sonnet-4.5"],
18
18
  ];
19
+ // The free OpenCode tier: no API key, no login (opencode.ai Zen free models). Used only when the user set nothing.
20
+ export const FREE_MODEL = "opencode/big-pickle";
19
21
  const defaultModel = (prov) => { const d = PROVIDERS.find((p) => p[1] === prov); return d ? d[2] : prov + "/default"; };
20
22
 
21
23
  // Provider ids of an existing `opencode auth login` (key names only).
@@ -34,6 +36,8 @@ export function detect({ env = process.env, user = {}, pluginUrl = "", kih = "",
34
36
  else {
35
37
  const a = authProviders(env)[0];
36
38
  if (a) it.model = { value: defaultModel(a), source: "opencode auth login (" + a + ")" };
39
+ // 0.3.12: nothing set anywhere -> the free OpenCode tier (no key, no login), so a fresh install opens a working start screen
40
+ else it.model = { value: FREE_MODEL, source: "OpenCode free default, no key needed" };
37
41
  }
38
42
  }
39
43
  if (it.model) it.provider = { value: it.model.value.split("/")[0], source: it.model.source };
package/lib/setup-run.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // wkappbot-code lib/setup-run.js -- what `npm install wkappbot-code` does after the files land, and what the first
2
2
  // `wkappbot-code` run says about it (owner order 2026-11-08: "npm install wkappbot-code should do the initial setup and
3
3
  // the tests automatically", "typing wkappbot-code should show the OpenCode start screen").
4
- // worker bin/postinstall.js --run: install --no-npm, then doctor, then selfTest() below, then ONE postinstall.done
4
+ // worker bin/postinstall.js --run: install, then doctor, then selfTest() below, then ONE postinstall.done
5
5
  // JSON {version, installRc, rc, doctor:{pass,warn,fail,skip}, selftest:{n,m}, platform, at, items}.
6
6
  // banner bannerFor() / printBanner(): the one line the first run prints ("setup ok, doctor 9/9, selftest 4/4", the
7
7
  // failing item with the log path, or "setup still running"), shown ONCE per done file (postinstall.seen).
@@ -11,7 +11,7 @@
11
11
  import { spawnSync } from "node:child_process";
12
12
  import { existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
13
13
  import { homedir, tmpdir } from "node:os";
14
- import { join } from "node:path";
14
+ import { delimiter, dirname, join } from "node:path";
15
15
 
16
16
  const RUNNING_STALE_MS = 30 * 60 * 1000; // a worker that left no done file for 30 minutes is dead, not "still running"
17
17
  const IS_WIN = process.platform === "win32";
@@ -128,8 +128,26 @@ export function printBanner({ home = homedir(), out = console.log, now = Date.no
128
128
  // ---- bare launch ----------------------------------------------------------------------------------
129
129
  // `wkappbot-code` with no arguments opens OpenCode (its start screen). Returns the exit code, or null when opencode
130
130
  // cannot be started (not on PATH): the caller then prints the usage as before.
131
- export function launchOpenCode(env = process.env) {
132
- const r = spawnSync("opencode", [], { stdio: "inherit", shell: IS_WIN, env });
131
+ // 0.3.12: `opencode` is looked up on PATH first, then beside this CLI (argv1 = the npm bin dir where `npm i -g opencode-ai`
132
+ // linked it), so a global prefix that is not on the user PATH still opens it.
133
+ export function launchOpenCode(env = process.env, argv1 = process.argv[1] || "") {
134
+ const run = (cmd, e) => spawnSync(cmd, [], { stdio: "inherit", shell: IS_WIN, env: e });
135
+ let r = run("opencode", env);
136
+ if (r.error && argv1) r = run("opencode", withPathFirst({ ...env }, dirname(argv1)));
133
137
  if (r.error) return null;
134
138
  return r.status ?? 1;
135
139
  }
140
+
141
+ // The npm global bin dir (where `npm i -g` links binaries): prefix/bin, or the prefix itself on Windows. "" when unknown.
142
+ export function globalBinDir(env = process.env) {
143
+ const r = spawnSync("npm", ["prefix", "-g"], { encoding: "utf8", env, shell: IS_WIN, timeout: 30000 });
144
+ const p = r.status === 0 ? String(r.stdout || "").trim().split(/\r?\n/).pop() : "";
145
+ return p ? (IS_WIN ? p : join(p, "bin")) : "";
146
+ }
147
+ // env with dir first on PATH (the key is spelled Path on Windows).
148
+ export function withPathFirst(env, dir) {
149
+ if (!dir) return env;
150
+ const k = Object.keys(env).find((x) => x.toLowerCase() === "path") || "PATH";
151
+ env[k] = dir + delimiter + (env[k] || "");
152
+ return env;
153
+ }
@@ -130,7 +130,9 @@ repo_file() {
130
130
  echo "$REPO/bin/$rel"
131
131
  }
132
132
 
133
- # install_file SRC DEST LABEL -- copy via .new then mv; "up to date" when DEST already matches.
133
+ # install_file SRC DEST LABEL -- copy via a per-process .new.PID then mv; "up to date" when DEST already matches.
134
+ # The temp name is unique per process: two installs at once (npm's background setup and a user's own
135
+ # `wkappbot-code install`, SDK run 42504778443) both wrote DEST.new and one failed with cp: File exists.
134
136
  install_file() {
135
137
  command -v wk_stopwatch_lap >/dev/null 2>&1 && wk_stopwatch_lap "install_file" || true
136
138
  local src="$1" dest="$2" label="$3" sum verb="installed"
@@ -141,9 +143,11 @@ install_file() {
141
143
  fi
142
144
  if [ "$WK_DRY" = 1 ]; then verb="would install"; fi
143
145
  run mkdir -p "$(dirname "$dest")"
144
- run cp "$src" "$dest.new"
145
- run chmod +x "$dest.new"
146
- run mv -f "$dest.new" "$dest"
146
+ local tmp="$dest.new.$$"
147
+ run rm -f "$tmp"
148
+ run cp "$src" "$tmp"
149
+ run chmod +x "$tmp"
150
+ run mv -f "$tmp" "$dest"
147
151
  echo "$verb: $dest (source: $label) sha256=$sum"
148
152
  }
149
153
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wkappbot-code",
3
- "version": "0.3.11",
3
+ "version": "0.3.12",
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": {