wkappbot-code 0.3.19 → 0.3.22
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 +17 -0
- package/lib/hook-wire.js +4 -2
- package/lib/python-acquire.js +114 -0
- package/lib/python-pick.js +45 -0
- package/package.json +1 -1
package/bin/wkappbot-code.js
CHANGED
|
@@ -47,6 +47,7 @@ import { hooksRow, wireClaudeHooks } from "../lib/hook-wire.js";
|
|
|
47
47
|
import { installStarterMd, removeStarterMd } from "../lib/starter-md.js";
|
|
48
48
|
import { addAllows, removeAllows } from "../lib/allow-compact.js";
|
|
49
49
|
import { ensurePrereqs } from "../lib/prereq.js";
|
|
50
|
+
import { ensurePython } from "../lib/python-acquire.js";
|
|
50
51
|
import { files as setupFiles, launchOpenCode, printBanner } from "../lib/setup-run.js";
|
|
51
52
|
|
|
52
53
|
const PKG_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
@@ -204,6 +205,13 @@ function installBin(o) {
|
|
|
204
205
|
// need no core and still refuse the risky classes, but the install reports the missing core as a FAILED part (0.3.13).
|
|
205
206
|
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" };
|
|
206
207
|
if (r.status !== 0) return { ok: false, dest, note: "wk-bin-acquire rc " + r.status };
|
|
208
|
+
// Core recognizes this marker beside the actual executable; no Core rebuild needed.
|
|
209
|
+
if (IS_WIN) {
|
|
210
|
+
const core = join(target, "wkappbot-core.exe");
|
|
211
|
+
if (!existsSync(core)) return { ok: false, dest, note: "Windows core missing after acquire: " + core };
|
|
212
|
+
try { writeFileSync(join(target, ".wkappbot-code-install"), "wkappbot-code npm install\n"); }
|
|
213
|
+
catch (e) { return { ok: false, dest, note: "npm license marker: " + e.message }; }
|
|
214
|
+
}
|
|
207
215
|
// Put `wkappbot` on PATH beside wkappbot-code itself, unless that name is already taken by someone else.
|
|
208
216
|
let link = "";
|
|
209
217
|
const ld = linkDir();
|
|
@@ -425,6 +433,8 @@ function install(o) {
|
|
|
425
433
|
// is not on PATH is used from its folder, and a missing one is installed with winget (lib/prereq.js)
|
|
426
434
|
st.prereq = ensurePrereqs({ log });
|
|
427
435
|
if (!st.prereq.ok) bad++;
|
|
436
|
+
// 0.3.20 (owner 2026-11-09): a real Python before any step runs python -- the Store alias stub made install exit 49
|
|
437
|
+
st.python = ensurePython({ log });
|
|
428
438
|
if (o.update) log("install --update: refreshing the core, the harness and the public skills");
|
|
429
439
|
const ps = pwshPath();
|
|
430
440
|
log("pwsh: " + (ps || "not found") + " -- the FULL harness (PowerShell 7) runs only on a dev seat (a harness checkout, or --dev-seat);" +
|
|
@@ -568,6 +578,12 @@ function uninstall(o) {
|
|
|
568
578
|
if (body.includes(core)) { rmSync(cmdShim, { force: true }); linkRemoved = cmdShim; log("shim: removed " + cmdShim + " (it called the core this install placed)"); }
|
|
569
579
|
else log("shim: kept " + cmdShim + " (it no longer calls " + core + ")");
|
|
570
580
|
}
|
|
581
|
+
// Undo only our npm license marker; do not alter a marker owned by another installer.
|
|
582
|
+
if (IS_WIN && st.bin?.ok) {
|
|
583
|
+
const marker = join(dirname(core), ".wkappbot-code-install");
|
|
584
|
+
try { if (existsSync(marker) && readFileSync(marker, "utf8") === "wkappbot-code npm install\n") { rmSync(marker); log("bin: removed npm license marker " + marker); } }
|
|
585
|
+
catch (e) { rc = 1; log("bin: could not remove npm license marker (" + e.message + ")"); }
|
|
586
|
+
}
|
|
571
587
|
// 3d. the AI CLIs this install added (lib/ai-clis.js), never one the user had before
|
|
572
588
|
removeAiClis((st.aiClis && st.aiClis.installed) || [], log, process.env, undefined, (st.aiClis && st.aiClis.prefix) || "");
|
|
573
589
|
// 4. the harness dirs we created, the plugin entry and our config folder
|
|
@@ -575,6 +591,7 @@ function uninstall(o) {
|
|
|
575
591
|
const rmTree = (p, what) => {
|
|
576
592
|
const r = rmTreeHard(p); if (r.ok) { if (what) log(what + ": removed " + p); return; }
|
|
577
593
|
rc = 1; log((what || "remove") + ": could not remove " + p + " -- blocked by " + r.blockers.slice(0, 5).join("; ") + " -- delete it by hand");
|
|
594
|
+
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)); }
|
|
578
595
|
};
|
|
579
596
|
if (st.harness && st.harness.mode === "web" && pr.harnessDirExisted !== true && existsSync(HARNESS_DIR())) rmTree(HARNESS_DIR(), "harness");
|
|
580
597
|
if (pr.hooksDirExisted !== true && existsSync(HOOKS_DIR())) rmTree(HOOKS_DIR(), "push-gate");
|
package/lib/hook-wire.js
CHANGED
|
@@ -13,6 +13,7 @@ import { spawnSync } from "node:child_process";
|
|
|
13
13
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
14
14
|
import { dirname, join, resolve } from "node:path";
|
|
15
15
|
import { fileURLToPath } from "node:url";
|
|
16
|
+
import { pickPython, stubWarning } from "./python-pick.js";
|
|
16
17
|
|
|
17
18
|
// The same harness-hook pattern as tools/wkharness-lite.sh --wire HARNESS_HOOK. ANCHORED (2026-11-09 review, proven on
|
|
18
19
|
// fixtures): the loose form also took a user's own pwsh lint.ps1, bash ~/bin/session-start.sh and my-wkharness-notes.sh
|
|
@@ -147,8 +148,9 @@ export function ensureRules(kih, log = () => {}) {
|
|
|
147
148
|
if (existsSync(want)) return "present";
|
|
148
149
|
const shipped = join(kih, "tools", "wkharness-lite-rules.tsv");
|
|
149
150
|
if (existsSync(shipped)) { mkdirSync(dirname(want), { recursive: true }); writeFileSync(want, readFileSync(shipped)); return "copied"; }
|
|
150
|
-
const py =
|
|
151
|
-
if (py.
|
|
151
|
+
const py = pickPython();
|
|
152
|
+
if (py.stubs.length) log("hooks: " + stubWarning(py.stubs));
|
|
153
|
+
if (py.cmd) {
|
|
152
154
|
const r = spawnSync("sh", [join(kih, "tools", "wkharness-lite.sh").replace(/\\/g, "/"), "--wire"], { encoding: "utf8" });
|
|
153
155
|
if (r.status === 0 && existsSync(want)) return "built";
|
|
154
156
|
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// wkappbot-code lib/python-acquire.js -- a REAL Python 3 on Windows before any step calls python (owner 2026-11-09:
|
|
2
|
+
// "the harness install fails with code 49 when the Store alias is there -- detect the fake python, then force-install").
|
|
3
|
+
// 49 is 9009 % 256: the Microsoft Store alias stub (WindowsApps\python.exe) seen through Git Bash or node.
|
|
4
|
+
// ensurePython: probe (lib/python-pick.js, which RUNS each candidate); if no real Python and win32:
|
|
5
|
+
// 1. winget install --id Python.Python.3.12 --scope user --silent (accept agreements)
|
|
6
|
+
// 2. else the python.org embeddable zip, pinned 3.12.7 and sha256-checked, unpacked into ~/.wk-harness/python
|
|
7
|
+
// 3. the real python dir goes AHEAD of %LOCALAPPDATA%\Microsoft\WindowsApps in the user PATH and in this process
|
|
8
|
+
// 4. re-probe, then ONE line: "OK python <version> <path>" or "FAIL python: <reason>". It never throws.
|
|
9
|
+
// Off Windows it only probes and reports (the package manager owns python there; lib/prereq.js names it).
|
|
10
|
+
// Spec: wkappbot skill read harness-installer-owns-all-setup
|
|
11
|
+
import { spawnSync } from "node:child_process";
|
|
12
|
+
import { createHash } from "node:crypto";
|
|
13
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync } from "node:fs";
|
|
14
|
+
import { homedir } from "node:os";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { pickPython, stubWarning } from "./python-pick.js";
|
|
17
|
+
|
|
18
|
+
export const EMBED = {
|
|
19
|
+
version: "3.12.7",
|
|
20
|
+
url: "https://www.python.org/ftp/python/3.12.7/python-3.12.7-embed-amd64.zip",
|
|
21
|
+
// measured 2026-11-09 from the python.org download over TLS (the release page lists no sha256 for this file)
|
|
22
|
+
sha256: "0d57bb6cb078b74d23dbfe91f77d6780d45bed328911609f1f7ee2ba1606bf44",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const realRun = (cmd, args, env, extra = {}) => spawnSync(cmd, args, { encoding: "utf8", env, timeout: 600000, windowsHide: true, ...extra });
|
|
26
|
+
const pathKey = (env) => Object.keys(env).find((k) => k.toUpperCase() === "PATH") || "PATH";
|
|
27
|
+
|
|
28
|
+
// dir first on a Windows PATH string (";" separated on every host, so the selftest runs anywhere), but always ahead
|
|
29
|
+
// of any WindowsApps entry
|
|
30
|
+
export function pathAheadOfWindowsApps(pathStr, dir) {
|
|
31
|
+
const parts = String(pathStr || "").split(";").filter((p) => p && p.toLowerCase() !== dir.toLowerCase());
|
|
32
|
+
const i = parts.findIndex((p) => /\\Microsoft\\WindowsApps\\?$/i.test(p));
|
|
33
|
+
if (i < 0) return [dir, ...parts].join(";");
|
|
34
|
+
parts.splice(i, 0, dir);
|
|
35
|
+
return parts.join(";");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// The folder winget's per-user Python lands in (LOCALAPPDATA\Programs\Python\Python3xx), newest first.
|
|
39
|
+
function wingetPythonDir(env, exists, list) {
|
|
40
|
+
const base = join(env.LOCALAPPDATA || join(homedir(), "AppData", "Local"), "Programs", "Python");
|
|
41
|
+
let names = [];
|
|
42
|
+
try { names = list(base).filter((n) => /^Python3\d+$/i.test(n)).sort().reverse(); } catch { return ""; }
|
|
43
|
+
for (const n of names) if (exists(join(base, n, "python.exe"))) return join(base, n);
|
|
44
|
+
return "";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function setUserPath(dir, run, env) {
|
|
48
|
+
// the user PATH (registry), not the merged one; same ordering rule as tools/wk-fresh-windows-tools.ps1
|
|
49
|
+
const ps = (cmd, e) => run("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", cmd], e);
|
|
50
|
+
const cur = ps("[Environment]::GetEnvironmentVariable('Path','User')", env);
|
|
51
|
+
if (!cur || cur.status !== 0) return false;
|
|
52
|
+
const next = pathAheadOfWindowsApps(String(cur.stdout || "").trim(), dir);
|
|
53
|
+
// the value travels in an env var, so no quoting of the PATH text is ever needed
|
|
54
|
+
const w = ps("[Environment]::SetEnvironmentVariable('Path',$env:WK_NEW_USER_PATH,'User')", { ...env, WK_NEW_USER_PATH: next });
|
|
55
|
+
return w && w.status === 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function ensurePython(o = {}) {
|
|
59
|
+
const env = o.env || process.env, log = o.log || (() => {}), run = o.run || realRun;
|
|
60
|
+
const platform = o.platform || process.platform, exists = o.exists || existsSync, list = o.list || readdirSync;
|
|
61
|
+
const home = o.home || homedir(), read = o.read || readFileSync, EMB = o.embed || EMBED;
|
|
62
|
+
const report = (ok, msg, extra = {}) => { log((ok ? "OK python " : "FAIL python: ") + msg); return { ok, note: msg, ...extra }; };
|
|
63
|
+
try {
|
|
64
|
+
let p = pickPython({ env, run: o.probeRun });
|
|
65
|
+
const warn = stubWarning(p.stubs);
|
|
66
|
+
if (p.cmd) {
|
|
67
|
+
const v = run(p.cmd, [...p.args, "-c", "import sys;print(sys.version.split()[0]);print(sys.executable)"], env); // no shell: see python-pick.js
|
|
68
|
+
const [ver, exe] = String((v && v.stdout) || "").trim().split(/\r?\n/);
|
|
69
|
+
return report(true, (ver || "3") + " " + (exe || p.cmd), { cmd: p.cmd, args: p.args, via: "present" });
|
|
70
|
+
}
|
|
71
|
+
if (warn) log(warn);
|
|
72
|
+
if (platform !== "win32") return report(false, "no python3 -- install it with the package manager (apt/dnf/brew install python3)", { via: "none" });
|
|
73
|
+
if (o.noInstall) return report(false, (warn ? "only the Microsoft Store alias stub" : "no Python 3") + " -- run: wkappbot-code install", { via: "none" });
|
|
74
|
+
let dir = "", via = "";
|
|
75
|
+
// 1. winget, per user
|
|
76
|
+
const wg = run("winget", ["install", "--id", "Python.Python.3.12", "-e", "--scope", "user", "--silent", "--accept-package-agreements", "--accept-source-agreements"], env, { shell: true });
|
|
77
|
+
log("python: winget install Python.Python.3.12 rc " + (wg ? wg.status : "?"));
|
|
78
|
+
dir = wingetPythonDir(env, exists, list);
|
|
79
|
+
if (dir) via = "winget";
|
|
80
|
+
// 2. the pinned embeddable zip
|
|
81
|
+
if (!dir) {
|
|
82
|
+
const target = join(home, ".wk-harness", "python"), zip = join(home, ".wk-harness", "python-embed.zip");
|
|
83
|
+
mkdirSync(target, { recursive: true });
|
|
84
|
+
const dl = run("curl", ["-fsSL", "--retry", "6", "--retry-delay", "5", "-m", "300", "-o", zip, EMB.url], env);
|
|
85
|
+
if (!dl || dl.status !== 0) return report(false, "winget gave no Python and the download of " + EMB.url + " failed (curl rc " + (dl ? dl.status : "?") + ")", { via: "none" });
|
|
86
|
+
const sum = createHash("sha256").update(read(zip)).digest("hex");
|
|
87
|
+
if (sum !== EMB.sha256) return report(false, "the embeddable zip sha256 " + sum + " is not the pinned " + EMB.sha256, { via: "none" });
|
|
88
|
+
// Windows: the bsdtar in System32 reads a zip; the GNU tar of Git Bash, first on PATH there, cannot (rc 128, 0.3.20)
|
|
89
|
+
const tarExe = platform === "win32" ? join(env.SystemRoot || env.windir || "C:\\Windows", "System32", "tar.exe") : "tar";
|
|
90
|
+
let ux = run(tarExe, ["-xf", zip, "-C", target], env);
|
|
91
|
+
if ((!ux || ux.status !== 0) && platform === "win32")
|
|
92
|
+
ux = run("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", "Expand-Archive -Force -LiteralPath $env:WK_ZIP -DestinationPath $env:WK_DEST"], { ...env, WK_ZIP: zip, WK_DEST: target });
|
|
93
|
+
if (!ux || ux.status !== 0) return report(false, "could not unpack " + zip + " (" + tarExe + " rc " + (ux ? ux.status : "?") + ")", { via: "none" });
|
|
94
|
+
// the zip's pythonXY._pth keeps the script folder off sys.path, so harness scripts could not import their siblings
|
|
95
|
+
try { for (const n of list(target)) if (/^python\d*\._pth$/i.test(n)) (o.unlink || unlinkSync)(join(target, n)); } catch {}
|
|
96
|
+
dir = target; via = "embed";
|
|
97
|
+
}
|
|
98
|
+
// 3. a python3.exe beside python.exe (winget and the zip ship only python.exe, so `python3` would still hit the
|
|
99
|
+
// WindowsApps stub), PYTHON_CMD for every later step, and the dir ahead of WindowsApps in this process and the user PATH
|
|
100
|
+
try { if (!exists(join(dir, "python3.exe"))) (o.copy || copyFileSync)(join(dir, "python.exe"), join(dir, "python3.exe")); } catch (e) { log("python: WARN no python3.exe beside python.exe (" + (e && e.message || e) + ")"); }
|
|
101
|
+
env.PYTHON_CMD = join(dir, "python.exe");
|
|
102
|
+
const k = pathKey(env);
|
|
103
|
+
env[k] = pathAheadOfWindowsApps(env[k], dir);
|
|
104
|
+
if (env !== process.env && o.env === undefined) { const pk = pathKey(process.env); process.env[pk] = pathAheadOfWindowsApps(process.env[pk], dir); }
|
|
105
|
+
if (!setUserPath(dir, run, env)) log("python: WARN could not write the user PATH; add " + dir + " ahead of WindowsApps");
|
|
106
|
+
// 4. re-probe the real exe by path
|
|
107
|
+
const exe = join(dir, "python.exe");
|
|
108
|
+
const v = run(exe, ["-c", "import sys;print(sys.version.split()[0])"], env);
|
|
109
|
+
if (v && v.status === 0 && /^3\./.test(String(v.stdout).trim())) return report(true, String(v.stdout).trim() + " " + exe, { cmd: exe, args: [], via });
|
|
110
|
+
return report(false, exe + " did not run (rc " + (v ? v.status : "?") + ")", { via });
|
|
111
|
+
} catch (e) {
|
|
112
|
+
return report(false, "unexpected: " + (e && e.message || e), { via: "none" });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// wkappbot-code lib/python-pick.js -- the ONE way the package chooses a Python 3 (owner order 2026-11-09).
|
|
2
|
+
// A name on PATH is not a Python: on Windows %LOCALAPPDATA%\Microsoft\WindowsApps\python.exe and python3.exe are
|
|
3
|
+
// Store app-execution-alias stubs. `command -v python3` finds them, yet running one prints "Python was not found
|
|
4
|
+
// ... Microsoft Store" and exits 9009. So every candidate is RUN: python3, then python, then py -3, each with
|
|
5
|
+
// -c "import sys;print(sys.version_info[0])", and accepted only on rc 0 with the output 3. A WindowsApps path, rc 9009
|
|
6
|
+
// or the Store text marks a stub; pickPython reports it so the caller prints ONE warning (stubWarning).
|
|
7
|
+
// Twin for sh: tools/wk-python-pick.sh (wk_python). Spec: wkappbot skill read harness-installer-owns-all-setup
|
|
8
|
+
import { spawnSync } from "node:child_process";
|
|
9
|
+
|
|
10
|
+
export const CANDIDATES = [["python3"], ["python"], ["py", "-3"]];
|
|
11
|
+
// Windows asks python and py -3 first: python.org and tool-cache installs ship python.exe with no python3.exe, so
|
|
12
|
+
// python3 there is usually the WindowsApps stub (spokesman order 2026-11-10).
|
|
13
|
+
export const candidatesFor = (platform) => (platform === "win32" ? [["python"], ["py", "-3"], ["python3"]] : CANDIDATES);
|
|
14
|
+
const PROBE = ["-c", "import sys;print(sys.version_info[0])"];
|
|
15
|
+
const STORE_RE = /Microsoft Store|WindowsApps|app execution alias/i;
|
|
16
|
+
|
|
17
|
+
// NEVER a shell: on win32 shell:true handed the probe to cmd.exe unquoted, so the ";" and "(" of -c broke it and every
|
|
18
|
+
// REAL Python failed (0.3.20, SDK os-qa 42730232937 win-x64). Node finds python.exe and py.exe on PATH by itself, and
|
|
19
|
+
// a WindowsApps stub still runs and exits 9009.
|
|
20
|
+
export const spawnOpts = (env, timeout = 15000) => ({ encoding: "utf8", env, timeout, shell: false, windowsHide: true });
|
|
21
|
+
const realRun = (cmd, args, env) => spawnSync(cmd, args, spawnOpts(env));
|
|
22
|
+
|
|
23
|
+
// -> { cmd, args, stubs: [names] } ; cmd "" when no working Python 3 exists.
|
|
24
|
+
export function pickPython(o = {}) {
|
|
25
|
+
const env = o.env || process.env, run = o.run || realRun, stubs = [];
|
|
26
|
+
if (env.PYTHON_CMD) {
|
|
27
|
+
const r = run(env.PYTHON_CMD, PROBE, env);
|
|
28
|
+
if (r && r.status === 0 && String(r.stdout || "").trim() === "3") return { cmd: env.PYTHON_CMD, args: [], stubs };
|
|
29
|
+
}
|
|
30
|
+
for (const [cmd, ...pre] of candidatesFor(o.platform || process.platform)) {
|
|
31
|
+
const r = run(cmd, [...pre, ...PROBE], env);
|
|
32
|
+
if (!r) continue;
|
|
33
|
+
const out = String(r.stdout || "") + String(r.stderr || "");
|
|
34
|
+
if (r.status === 0 && String(r.stdout || "").trim() === "3") return { cmd, args: pre, stubs };
|
|
35
|
+
if (r.status === 9009 || r.status === 49 || STORE_RE.test(out)) stubs.push(cmd); // 49 = 9009 % 256 through a shell
|
|
36
|
+
}
|
|
37
|
+
return { cmd: "", args: [], stubs };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// The one WARN line for a Store stub, or "" when none was seen.
|
|
41
|
+
export function stubWarning(stubs) {
|
|
42
|
+
if (!stubs || !stubs.length) return "";
|
|
43
|
+
return "WARN python: " + stubs.join(", ") + " is the Microsoft Store alias stub, not Python -- install Python 3, " +
|
|
44
|
+
"or turn the alias off under Settings > Apps > Advanced app settings > App execution aliases";
|
|
45
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wkappbot-code",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.22",
|
|
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": {
|