wkappbot-code 0.3.10 → 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 +47 -29
- package/bin/wkappbot-code.js +32 -2
- package/lib/config.js +4 -0
- package/lib/setup-run.js +153 -0
- package/lib/wk-bin-acquire.sh +8 -4
- package/package.json +1 -1
package/bin/postinstall.js
CHANGED
|
@@ -2,71 +2,89 @@
|
|
|
2
2
|
// wkappbot-code postinstall -- npm runs this after `npm install wkappbot-code` (local or -g).
|
|
3
3
|
// Owner order 2026-11-06: the Default cloud environment's setup script is `npm install wkappbot-code`, and new cloud
|
|
4
4
|
// ROOTs still started with no hooks, because the package had no install script and npm only copied files.
|
|
5
|
+
// Owner order 2026-11-08: "npm install wkappbot-code should do the initial setup and the tests automatically" -- on
|
|
6
|
+
// Windows, macOS and Linux alike, not only on a cloud VM.
|
|
5
7
|
// CONTRACT: (a) ALWAYS exits 0 and returns within a moment -- the real work runs DETACHED and logs to
|
|
6
|
-
// ~/.wkappbot-code/postinstall.log
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
8
|
+
// ~/.wkappbot-code/postinstall.log; (b) the worker runs `wkappbot-code install` (lays the web harness, wires
|
|
9
|
+
// ~/.claude/settings.json through the lite harness -- PreToolUse, Stop, UserPromptSubmit, SessionStart -- the global git
|
|
10
|
+
// hooks, the wkskill/wk-verify shims and the public skills, keeping every user setting), then `wkappbot-code doctor`,
|
|
11
|
+
// then a short self-test (lib/setup-run.js: core --version, skill read on-load, one hook allow, one hook refuse), and
|
|
12
|
+
// writes ~/.wkappbot-code/postinstall.done {version, installRc, doctor:{pass,warn,fail,skip}, selftest:{n,m}, ...};
|
|
13
|
+
// postinstall.running exists while the worker lives; (c) the first `wkappbot-code` run afterwards prints ONE summary
|
|
14
|
+
// line (lib/setup-run.js printBanner) and opens OpenCode; (d) idempotent: a done file of the same version with
|
|
15
|
+
// installRc 0 skips the rerun. WKAPPBOT_CODE_NO_POSTINSTALL=1 skips everything (the selftest sets it so npm-installs
|
|
16
|
+
// in its cases never touch the machine running it). WKAPPBOT_CODE_DEV_SEAT=1 adds --dev-seat (the full pwsh harness
|
|
17
|
+
// instead of lite); WKAPPBOT_CODE_INSTALL_ARGS adds install flags. A user install takes the lite harness: on a
|
|
18
|
+
// stranger's machine the owner's full fleet harness (busy-fleet, letter gates, emergency-edit) would be the wrong default.
|
|
15
19
|
// Spec: wkappbot skill read agent-harness-landscape-2026-howto
|
|
16
|
-
import { spawn } from "node:child_process";
|
|
17
|
-
import { existsSync, mkdirSync, openSync, readFileSync
|
|
20
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
21
|
+
import { existsSync, mkdirSync, openSync, readFileSync } from "node:fs";
|
|
18
22
|
import { homedir } from "node:os";
|
|
23
|
+
import { appendFileSync } from "node:fs";
|
|
19
24
|
import { dirname, join } from "node:path";
|
|
20
25
|
import { fileURLToPath } from "node:url";
|
|
26
|
+
import { buildDone, files, globalBinDir, markRunning, parseDoctor, platformOf, selfTest, withPathFirst, writeDone } from "../lib/setup-run.js";
|
|
21
27
|
|
|
22
28
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
23
29
|
const CLI = join(HERE, "wkappbot-code.js");
|
|
24
|
-
const
|
|
25
|
-
const LOG =
|
|
26
|
-
const DONE = join(STATE, "postinstall.done");
|
|
30
|
+
const F = files(homedir());
|
|
31
|
+
const STATE = F.dir, LOG = F.log, DONE = F.done;
|
|
27
32
|
|
|
28
33
|
function version() {
|
|
29
34
|
try { return JSON.parse(readFileSync(join(HERE, "..", "package.json"), "utf8")).version || "?"; } catch { return "?"; }
|
|
30
35
|
}
|
|
31
|
-
export function isCloudVm(env = process.env, home = homedir(), platform =
|
|
36
|
+
export function isCloudVm(env = process.env, home = homedir(), platform = platformOf(env)) {
|
|
32
37
|
if (env.CLAUDE_CODE_REMOTE) return true;
|
|
33
38
|
return platform === "linux" && home === "/root" && existsSync(join(home, ".claude"));
|
|
34
39
|
}
|
|
35
|
-
function stamp(msg) { try { mkdirSync(STATE, { recursive: true });
|
|
40
|
+
function stamp(msg) { try { mkdirSync(STATE, { recursive: true }); appendFileSync(LOG, new Date().toISOString() + " " + msg + "\n"); } catch {} }
|
|
36
41
|
|
|
37
|
-
// --run: the detached worker.
|
|
42
|
+
// --run: the detached worker. install, then doctor, then the self-test, then the done file.
|
|
38
43
|
async function runWorker() {
|
|
39
|
-
|
|
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"];
|
|
40
47
|
if (process.env.WKAPPBOT_CODE_DEV_SEAT === "1") args.push("--dev-seat");
|
|
41
48
|
// extra install flags, e.g. "--no-bin --no-skills" (the selftest; a seat without the SDK release assets)
|
|
42
49
|
args.push(...String(process.env.WKAPPBOT_CODE_INSTALL_ARGS || "").split(/\s+/).filter((a) => /^--[a-z-]+$/.test(a)));
|
|
50
|
+
// the children are the worker's own: they must not print the first-run banner (it is for the user's next command)
|
|
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));
|
|
43
53
|
stamp("worker start: node " + args.join(" "));
|
|
44
54
|
const out = openSync(LOG, "a");
|
|
45
|
-
const
|
|
46
|
-
const c = spawn(process.execPath, args, { stdio: ["ignore", out, out], env
|
|
55
|
+
const installRc = await new Promise((res) => {
|
|
56
|
+
const c = spawn(process.execPath, args, { stdio: ["ignore", out, out], env });
|
|
47
57
|
c.on("error", () => res(-1));
|
|
48
58
|
c.on("close", (code) => res(code ?? -1));
|
|
49
59
|
});
|
|
50
|
-
|
|
51
|
-
|
|
60
|
+
stamp("install rc " + installRc + "; doctor");
|
|
61
|
+
const dr = spawnSync(process.execPath, [CLI, "doctor"], { encoding: "utf8", env, timeout: 300000 });
|
|
62
|
+
const dtext = (dr.stdout || "") + (dr.stderr || "");
|
|
63
|
+
try { appendFileSync(LOG, dtext); } catch {}
|
|
64
|
+
const doctor = parseDoctor(dtext);
|
|
65
|
+
stamp("doctor pass " + doctor.pass + " warn " + doctor.warn + " fail " + doctor.fail + " skip " + doctor.skip + "; self-test");
|
|
66
|
+
let st = { n: 0, m: 4, items: [{ ok: false, name: "self-test", detail: "did not run" }] };
|
|
67
|
+
try { st = selfTest({ env, home: homedir() }); } catch (e) { st.items[0].detail = String(e); }
|
|
68
|
+
for (const i of st.items) stamp("selftest " + (i.ok ? "PASS " : "FAIL ") + i.name + (i.ok ? "" : " -- " + i.detail));
|
|
69
|
+
try { writeDone(homedir(), buildDone({ version: version(), installRc, doctor, selftest: st, env: process.env })); } catch (e) { stamp("done file error: " + e); }
|
|
70
|
+
stamp("worker done: install rc " + installRc + ", doctor " + doctor.pass + "/" + (doctor.pass + doctor.warn + doctor.fail) + ", selftest " + st.n + "/" + st.m);
|
|
52
71
|
}
|
|
53
72
|
|
|
54
73
|
function main() {
|
|
55
74
|
if (process.argv[2] === "--run") return runWorker().catch(() => {}).finally(() => process.exit(0));
|
|
56
75
|
if (process.env.WKAPPBOT_CODE_NO_POSTINSTALL === "1") return;
|
|
57
|
-
|
|
58
|
-
console.log("[wkappbot-code] installed. To wire the harness and OpenCode on this machine run: npx wkappbot-code install (then: wkappbot-code doctor)");
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
76
|
+
const cloud = isCloudVm();
|
|
61
77
|
try {
|
|
62
78
|
const d = JSON.parse(readFileSync(DONE, "utf8"));
|
|
63
|
-
if (d.version === version() && d.rc === 0) { console.log("[wkappbot-code]
|
|
79
|
+
if (d.version === version() && (d.installRc ?? d.rc) === 0) { console.log("[wkappbot-code] already set up by " + d.version + " (" + DONE + ")"); return; }
|
|
64
80
|
} catch {}
|
|
65
81
|
mkdirSync(STATE, { recursive: true });
|
|
82
|
+
markRunning(homedir(), version());
|
|
66
83
|
const out = openSync(LOG, "a");
|
|
67
|
-
const c = spawn(process.execPath, [fileURLToPath(import.meta.url), "--run"], { detached: true, stdio: ["ignore", out, out], env: process.env });
|
|
84
|
+
const c = spawn(process.execPath, [fileURLToPath(import.meta.url), "--run"], { detached: true, stdio: ["ignore", out, out], env: process.env, windowsHide: true });
|
|
68
85
|
c.unref();
|
|
69
|
-
console.log("[wkappbot-code] cloud VM detected: wiring the Claude Code harness in the background
|
|
86
|
+
console.log("[wkappbot-code] " + (cloud ? "cloud VM detected: wiring the Claude Code harness in the background" : "setting up in the background (install, doctor, self-test)") +
|
|
87
|
+
" (log: " + LOG + ", result: " + DONE + "); the first `wkappbot-code` run prints the summary");
|
|
70
88
|
}
|
|
71
89
|
|
|
72
90
|
try { main(); } catch (e) { try { stamp("postinstall error: " + e); } catch {} }
|
package/bin/wkappbot-code.js
CHANGED
|
@@ -40,6 +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 { files as setupFiles, launchOpenCode, printBanner } from "../lib/setup-run.js";
|
|
43
44
|
|
|
44
45
|
const PKG_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
45
46
|
const PKG = JSON.parse(readFileSync(join(PKG_DIR, "package.json"), "utf8"));
|
|
@@ -382,7 +383,21 @@ function recordPrior(prev) {
|
|
|
382
383
|
hooksDirExisted: existsSync(HOOKS_DIR()), shimsExisting: shimCandidates().filter((p) => existsSync(p) || isSymlink(p)) };
|
|
383
384
|
}
|
|
384
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
|
+
|
|
385
399
|
function install(o) {
|
|
400
|
+
waitForSetupWorker();
|
|
386
401
|
const prev = readState(o);
|
|
387
402
|
const st = { version: PKG.version, at: new Date().toISOString(), prior: recordPrior(prev) };
|
|
388
403
|
let bad = 0;
|
|
@@ -396,7 +411,9 @@ function install(o) {
|
|
|
396
411
|
if (have === OPENCODE_VERSION) log("opencode " + have + " already installed");
|
|
397
412
|
else {
|
|
398
413
|
log("npm install -g opencode-ai@" + OPENCODE_VERSION + " (was: " + (have || "none") + ")");
|
|
399
|
-
|
|
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); }
|
|
400
417
|
}
|
|
401
418
|
}
|
|
402
419
|
const dest = binDest(o);
|
|
@@ -494,6 +511,15 @@ function uninstall(o) {
|
|
|
494
511
|
for (const p of st.shims || []) {
|
|
495
512
|
if (isSymlink(p) || existsSync(p)) { rmSync(p, { force: true }); log("shim: removed " + p); }
|
|
496
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
|
+
}
|
|
497
523
|
// 4. the harness dirs we created, the plugin entry and our config folder
|
|
498
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.
|
|
499
525
|
const rmTree = (p, what) => {
|
|
@@ -515,7 +541,7 @@ function uninstall(o) {
|
|
|
515
541
|
} catch {}
|
|
516
542
|
rmTree(STATE_HOME());
|
|
517
543
|
rmTree(join(configDir(o), "wkappbot-code"));
|
|
518
|
-
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)");
|
|
519
545
|
return rc;
|
|
520
546
|
}
|
|
521
547
|
|
|
@@ -616,6 +642,10 @@ if (["install", "doctor", "config"].includes(o.cmd) && !o.update) {
|
|
|
616
642
|
const nv = quietCheck(PKG.version, console.log);
|
|
617
643
|
if (nv && (o.autoUpdate || process.env.WKAPPBOT_CODE_AUTO_UPDATE === "1")) upd();
|
|
618
644
|
}
|
|
645
|
+
// The one-line result of the automatic post-install setup (lib/setup-run.js), once; not for the worker's own children.
|
|
646
|
+
if (!["--version", "-v", "version", "uninstall"].includes(o.cmd) && process.env.WKAPPBOT_CODE_SETUP_WORKER !== "1") { try { printBanner(); } catch {} }
|
|
647
|
+
// Bare `wkappbot-code`: open OpenCode (its start screen); no opencode on PATH falls through to the usage below.
|
|
648
|
+
if (process.argv.length <= 2 && o.cmd === "help") { const rc = launchOpenCode(); if (rc !== null) process.exit(rc); }
|
|
619
649
|
switch (o.cmd) {
|
|
620
650
|
case "--version": case "-v": case "version":
|
|
621
651
|
console.log("wkappbot-code " + PKG.version + " (opencode-ai " + OPENCODE_VERSION + ")"); process.exit(0);
|
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
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// wkappbot-code lib/setup-run.js -- what `npm install wkappbot-code` does after the files land, and what the first
|
|
2
|
+
// `wkappbot-code` run says about it (owner order 2026-11-08: "npm install wkappbot-code should do the initial setup and
|
|
3
|
+
// the tests automatically", "typing wkappbot-code should show the OpenCode start screen").
|
|
4
|
+
// worker bin/postinstall.js --run: install, then doctor, then selfTest() below, then ONE postinstall.done
|
|
5
|
+
// JSON {version, installRc, rc, doctor:{pass,warn,fail,skip}, selftest:{n,m}, platform, at, items}.
|
|
6
|
+
// banner bannerFor() / printBanner(): the one line the first run prints ("setup ok, doctor 9/9, selftest 4/4", the
|
|
7
|
+
// failing item with the log path, or "setup still running"), shown ONCE per done file (postinstall.seen).
|
|
8
|
+
// Files in ~/.wkappbot-code: postinstall.log, postinstall.running (worker alive), postinstall.done, postinstall.seen.
|
|
9
|
+
// A user's own settings.json entries are the installer's job to keep (wkappbot-code.js install); this module only reads.
|
|
10
|
+
// Spec: wkappbot skill read agent-harness-landscape-2026-howto
|
|
11
|
+
import { spawnSync } from "node:child_process";
|
|
12
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
13
|
+
import { homedir, tmpdir } from "node:os";
|
|
14
|
+
import { delimiter, dirname, join } from "node:path";
|
|
15
|
+
|
|
16
|
+
const RUNNING_STALE_MS = 30 * 60 * 1000; // a worker that left no done file for 30 minutes is dead, not "still running"
|
|
17
|
+
const IS_WIN = process.platform === "win32";
|
|
18
|
+
|
|
19
|
+
// WKAPPBOT_CODE_FAKE_PLATFORM is the selftest's way to run the non-cloud road "as win32 / darwin" on one machine.
|
|
20
|
+
export const platformOf = (env = process.env) => env.WKAPPBOT_CODE_FAKE_PLATFORM || process.platform;
|
|
21
|
+
export const stateDir = (home = homedir()) => join(home, ".wkappbot-code");
|
|
22
|
+
export const files = (home = homedir()) => {
|
|
23
|
+
const d = stateDir(home);
|
|
24
|
+
return { dir: d, log: join(d, "postinstall.log"), running: join(d, "postinstall.running"), done: join(d, "postinstall.done"), seen: join(d, "postinstall.seen") };
|
|
25
|
+
};
|
|
26
|
+
const readJson = (f) => { try { return JSON.parse(readFileSync(f, "utf8")); } catch { return null; } };
|
|
27
|
+
// Same MSYS rule as wkappbot-code.js shPath: the sh Git for Windows starts reads "D:\a\x" as host D.
|
|
28
|
+
const shPath = (p) => (IS_WIN && /^[A-Za-z]:[\\/]/.test(p) ? "/" + p[0].toLowerCase() + p.slice(2).replace(/\\/g, "/") : p);
|
|
29
|
+
|
|
30
|
+
// ---- doctor ---------------------------------------------------------------------------------------
|
|
31
|
+
// doctor prints one `PASS|WARN|FAIL|SKIP name detail` row per part; SKIP rows are not counted in the n/m.
|
|
32
|
+
export function parseDoctor(text) {
|
|
33
|
+
const c = { pass: 0, warn: 0, fail: 0, skip: 0, failed: [] };
|
|
34
|
+
for (const line of String(text || "").split(/\r?\n/)) {
|
|
35
|
+
const m = /^(PASS|WARN|FAIL|SKIP) (\S+)/.exec(line);
|
|
36
|
+
if (!m) continue;
|
|
37
|
+
c[m[1].toLowerCase()]++;
|
|
38
|
+
if (m[1] === "FAIL") c.failed.push(m[2]);
|
|
39
|
+
}
|
|
40
|
+
return c;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ---- self-test ------------------------------------------------------------------------------------
|
|
44
|
+
function installState(env, home) {
|
|
45
|
+
const cfg = env.OPENCODE_CONFIG_DIR || join(env.XDG_CONFIG_HOME || join(home, ".config"), "opencode");
|
|
46
|
+
return readJson(join(cfg, "wkappbot-code", "install-state.json")) || {};
|
|
47
|
+
}
|
|
48
|
+
const hookPayload = (cmd) => JSON.stringify({ hook_event_name: "PreToolUse", session_id: "wkappbot-code-selftest", cwd: tmpdir(), tool_name: "Bash", tool_input: { command: cmd } });
|
|
49
|
+
|
|
50
|
+
// Four items: the core answers --version, the core prints skill on-load, the lite hook ALLOWS a plain command (rc 0) and
|
|
51
|
+
// REFUSES git --no-verify (rc 2). Returns {n, m, items:[{name, ok, detail}]}.
|
|
52
|
+
export function selfTest({ env = process.env, home = homedir() } = {}) {
|
|
53
|
+
const st = installState(env, home);
|
|
54
|
+
const dest = (st.bin && st.bin.dest) || env.WK_BIN_DEST || join(home, ".local", "share", "wkappbot", IS_WIN ? "wkappbot.exe" : "wkappbot");
|
|
55
|
+
const kih = st.kihDir || env.WK_HARNESS_DIR || join(home, ".wk-harness");
|
|
56
|
+
const lite = join(kih, "tools", "wkharness-lite.sh");
|
|
57
|
+
const items = [];
|
|
58
|
+
const add = (name, ok, detail) => items.push({ name, ok: !!ok, detail: String(detail || "").replace(/\s+/g, " ").slice(0, 200) });
|
|
59
|
+
const scratch = mkdtempSync(join(tmpdir(), "wk-appbot-selftest-"));
|
|
60
|
+
const shell = IS_WIN && /\.(cmd|bat)$/i.test(dest); // node refuses to start a .cmd without a shell
|
|
61
|
+
try {
|
|
62
|
+
const core = (args) => (existsSync(dest) ? spawnSync(dest, args, { encoding: "utf8", timeout: 120000, cwd: scratch, shell, env: { ...env, WKAPPBOT_NO_REMOTE_CHECK: "1" } }) : null);
|
|
63
|
+
let r = core(["--version"]);
|
|
64
|
+
add("core --version", r && r.status === 0 && (r.stdout || "").trim(), r ? "rc " + r.status + " " + (r.stdout || r.stderr) : dest + " absent");
|
|
65
|
+
r = core(["skill", "read", "on-load"]);
|
|
66
|
+
add("skill read on-load", r && /\bID\s*:\s*on-load\b/.test(r.stdout || ""), r ? "rc " + r.status : dest + " absent");
|
|
67
|
+
const hook = (cmd) => (existsSync(lite)
|
|
68
|
+
? spawnSync("sh", [shPath(lite), "pre"], { encoding: "utf8", timeout: 60000, input: hookPayload(cmd), env: { ...env, WKHARNESS_LITE_DIR: shPath(join(scratch, "lite")) } }) : null);
|
|
69
|
+
r = hook("ls /");
|
|
70
|
+
add("hook allow", r && r.status === 0, r ? "rc " + r.status + " " + (r.stderr || "") : lite + " absent");
|
|
71
|
+
r = hook("git commit --no-verify -m selftest");
|
|
72
|
+
add("hook refuse", r && r.status === 2, r ? "rc " + r.status + " " + (r.stderr || "") : lite + " absent");
|
|
73
|
+
} finally { rmSync(scratch, { recursive: true, force: true }); }
|
|
74
|
+
return { n: items.filter((i) => i.ok).length, m: items.length, items };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ---- done / running -------------------------------------------------------------------------------
|
|
78
|
+
export function markRunning(home, version) {
|
|
79
|
+
const f = files(home);
|
|
80
|
+
mkdirSync(f.dir, { recursive: true });
|
|
81
|
+
rmSync(f.done, { force: true }); // the old verdict belongs to the old install
|
|
82
|
+
writeFileSync(f.running, JSON.stringify({ version, at: new Date().toISOString(), pid: process.pid }) + "\n");
|
|
83
|
+
}
|
|
84
|
+
export function writeDone(home, rec) {
|
|
85
|
+
const f = files(home);
|
|
86
|
+
const tmp = f.done + ".tmp";
|
|
87
|
+
writeFileSync(tmp, JSON.stringify(rec) + "\n");
|
|
88
|
+
renameSync(tmp, f.done); // the done file appears whole or not at all (the selftest polls for it)
|
|
89
|
+
rmSync(f.running, { force: true });
|
|
90
|
+
}
|
|
91
|
+
export function buildDone({ version, installRc, doctor, selftest, env = process.env }) {
|
|
92
|
+
return { version, installRc, rc: installRc, doctor: { pass: doctor.pass, warn: doctor.warn, fail: doctor.fail, skip: doctor.skip },
|
|
93
|
+
selftest: { n: selftest.n, m: selftest.m }, platform: platformOf(env), at: new Date().toISOString(),
|
|
94
|
+
items: selftest.items.filter((i) => !i.ok).map((i) => i.name + ": " + i.detail), doctorFailed: doctor.failed };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ---- banner ---------------------------------------------------------------------------------------
|
|
98
|
+
// Pure: the line to print for a state ("" = nothing to say) and whether it counts as seen. `now` is a ms clock.
|
|
99
|
+
export function bannerFor({ done, running, seen, logPath, now = Date.now() }) {
|
|
100
|
+
if (running && !done) {
|
|
101
|
+
const age = now - Date.parse(running.at || 0);
|
|
102
|
+
if (age >= 0 && age < RUNNING_STALE_MS) return { line: "[wkappbot-code] setup still running (log: " + logPath + ") -- opening OpenCode anyway", mark: false };
|
|
103
|
+
return { line: "[wkappbot-code] setup did not finish (worker started " + (running.at || "?") + ", no result); log: " + logPath + " -- rerun: wkappbot-code install", mark: false };
|
|
104
|
+
}
|
|
105
|
+
if (!done || (seen && seen === done.at)) return { line: "", mark: false };
|
|
106
|
+
const d = done.doctor || {}, s = done.selftest || {};
|
|
107
|
+
const total = (d.pass || 0) + (d.warn || 0) + (d.fail || 0);
|
|
108
|
+
const tail = "doctor " + (d.pass || 0) + "/" + total + (d.warn ? " (" + d.warn + " warn)" : "") + ", selftest " + (s.n || 0) + "/" + (s.m || 0);
|
|
109
|
+
const installRc = done.installRc ?? done.rc;
|
|
110
|
+
if (installRc === 0 && !d.fail && s.n === s.m && s.m > 0) return { line: "[wkappbot-code] setup ok, " + tail, mark: true };
|
|
111
|
+
const bad = [];
|
|
112
|
+
if (installRc !== 0) bad.push("install rc " + installRc);
|
|
113
|
+
for (const f of done.doctorFailed || []) bad.push("doctor " + f + " FAIL");
|
|
114
|
+
for (const i of done.items || []) bad.push("selftest " + i);
|
|
115
|
+
if (!bad.length) bad.push(d.fail ? "doctor " + d.fail + " FAIL" : "selftest " + (s.n || 0) + "/" + (s.m || 0));
|
|
116
|
+
return { line: "[wkappbot-code] setup needs attention: " + bad.slice(0, 3).join("; ") + " (" + tail + "); log: " + logPath, mark: true };
|
|
117
|
+
}
|
|
118
|
+
export function printBanner({ home = homedir(), out = console.log, now = Date.now() } = {}) {
|
|
119
|
+
const f = files(home);
|
|
120
|
+
const done = readJson(f.done), running = readJson(f.running);
|
|
121
|
+
let seen = ""; try { seen = readFileSync(f.seen, "utf8").trim(); } catch {}
|
|
122
|
+
const b = bannerFor({ done, running, seen, logPath: f.log, now });
|
|
123
|
+
if (b.line) out(b.line);
|
|
124
|
+
if (b.mark && done) { try { writeFileSync(f.seen, done.at + "\n"); } catch {} }
|
|
125
|
+
return b.line;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ---- bare launch ----------------------------------------------------------------------------------
|
|
129
|
+
// `wkappbot-code` with no arguments opens OpenCode (its start screen). Returns the exit code, or null when opencode
|
|
130
|
+
// cannot be started (not on PATH): the caller then prints the usage as before.
|
|
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)));
|
|
137
|
+
if (r.error) return null;
|
|
138
|
+
return r.status ?? 1;
|
|
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": {
|