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.
- package/bin/postinstall.js +7 -4
- package/bin/wkappbot-code.js +28 -3
- package/lib/config.js +4 -0
- package/lib/setup-run.js +22 -4
- package/lib/wk-bin-acquire.sh +8 -4
- package/package.json +1 -1
package/bin/postinstall.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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) => {
|
package/bin/wkappbot-code.js
CHANGED
|
@@ -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
|
-
|
|
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 (" +
|
|
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
|
|
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
|
-
|
|
132
|
-
|
|
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
|
+
}
|
package/lib/wk-bin-acquire.sh
CHANGED
|
@@ -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
|
-
|
|
145
|
-
run
|
|
146
|
-
run
|
|
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.
|
|
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": {
|