wkappbot-code 0.3.19 → 0.3.20
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 +3 -0
- package/lib/hook-wire.js +4 -2
- package/lib/python-acquire.js +108 -0
- package/lib/python-pick.js +38 -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)), "..");
|
|
@@ -425,6 +426,8 @@ function install(o) {
|
|
|
425
426
|
// is not on PATH is used from its folder, and a missing one is installed with winget (lib/prereq.js)
|
|
426
427
|
st.prereq = ensurePrereqs({ log });
|
|
427
428
|
if (!st.prereq.ok) bad++;
|
|
429
|
+
// 0.3.20 (owner 2026-11-09): a real Python before any step runs python -- the Store alias stub made install exit 49
|
|
430
|
+
st.python = ensurePython({ log });
|
|
428
431
|
if (o.update) log("install --update: refreshing the core, the harness and the public skills");
|
|
429
432
|
const ps = pwshPath();
|
|
430
433
|
log("pwsh: " + (ps || "not found") + " -- the FULL harness (PowerShell 7) runs only on a dev seat (a harness checkout, or --dev-seat);" +
|
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,108 @@
|
|
|
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 } 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;
|
|
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, { shell: platform === "win32" });
|
|
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, EMBED.url], env);
|
|
85
|
+
if (!dl || dl.status !== 0) return report(false, "winget gave no Python and the download of " + EMBED.url + " failed (curl rc " + (dl ? dl.status : "?") + ")", { via: "none" });
|
|
86
|
+
const sum = createHash("sha256").update(read(zip)).digest("hex");
|
|
87
|
+
if (sum !== EMBED.sha256) return report(false, "the embeddable zip sha256 " + sum + " is not the pinned " + EMBED.sha256, { via: "none" });
|
|
88
|
+
const ux = run("tar", ["-xf", zip, "-C", target], env);
|
|
89
|
+
if (!ux || ux.status !== 0) return report(false, "could not unpack " + zip + " (tar rc " + (ux ? ux.status : "?") + ")", { via: "none" });
|
|
90
|
+
dir = target; via = "embed";
|
|
91
|
+
}
|
|
92
|
+
// 3. a python3.exe beside python.exe (winget and the zip ship only python.exe, so `python3` would still hit the
|
|
93
|
+
// WindowsApps stub), PYTHON_CMD for every later step, and the dir ahead of WindowsApps in this process and the user PATH
|
|
94
|
+
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) + ")"); }
|
|
95
|
+
env.PYTHON_CMD = join(dir, "python.exe");
|
|
96
|
+
const k = pathKey(env);
|
|
97
|
+
env[k] = pathAheadOfWindowsApps(env[k], dir);
|
|
98
|
+
if (env !== process.env && o.env === undefined) { const pk = pathKey(process.env); process.env[pk] = pathAheadOfWindowsApps(process.env[pk], dir); }
|
|
99
|
+
if (!setUserPath(dir, run, env)) log("python: WARN could not write the user PATH; add " + dir + " ahead of WindowsApps");
|
|
100
|
+
// 4. re-probe the real exe by path
|
|
101
|
+
const exe = join(dir, "python.exe");
|
|
102
|
+
const v = run(exe, ["-c", "import sys;print(sys.version.split()[0])"], env);
|
|
103
|
+
if (v && v.status === 0 && /^3\./.test(String(v.stdout).trim())) return report(true, String(v.stdout).trim() + " " + exe, { cmd: exe, args: [], via });
|
|
104
|
+
return report(false, exe + " did not run (rc " + (v ? v.status : "?") + ")", { via });
|
|
105
|
+
} catch (e) {
|
|
106
|
+
return report(false, "unexpected: " + (e && e.message || e), { via: "none" });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
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
|
+
const PROBE = ["-c", "import sys;print(sys.version_info[0])"];
|
|
12
|
+
const STORE_RE = /Microsoft Store|WindowsApps|app execution alias/i;
|
|
13
|
+
|
|
14
|
+
const realRun = (cmd, args, env) => spawnSync(cmd, args, { encoding: "utf8", env, timeout: 15000, shell: process.platform === "win32", windowsHide: true });
|
|
15
|
+
|
|
16
|
+
// -> { cmd, args, stubs: [names] } ; cmd "" when no working Python 3 exists.
|
|
17
|
+
export function pickPython(o = {}) {
|
|
18
|
+
const env = o.env || process.env, run = o.run || realRun, stubs = [];
|
|
19
|
+
if (env.PYTHON_CMD) {
|
|
20
|
+
const r = run(env.PYTHON_CMD, PROBE, env);
|
|
21
|
+
if (r && r.status === 0 && String(r.stdout || "").trim() === "3") return { cmd: env.PYTHON_CMD, args: [], stubs };
|
|
22
|
+
}
|
|
23
|
+
for (const [cmd, ...pre] of CANDIDATES) {
|
|
24
|
+
const r = run(cmd, [...pre, ...PROBE], env);
|
|
25
|
+
if (!r) continue;
|
|
26
|
+
const out = String(r.stdout || "") + String(r.stderr || "");
|
|
27
|
+
if (r.status === 0 && String(r.stdout || "").trim() === "3") return { cmd, args: pre, stubs };
|
|
28
|
+
if (r.status === 9009 || r.status === 49 || STORE_RE.test(out)) stubs.push(cmd); // 49 = 9009 % 256 through a shell
|
|
29
|
+
}
|
|
30
|
+
return { cmd: "", args: [], stubs };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// The one WARN line for a Store stub, or "" when none was seen.
|
|
34
|
+
export function stubWarning(stubs) {
|
|
35
|
+
if (!stubs || !stubs.length) return "";
|
|
36
|
+
return "WARN python: " + stubs.join(", ") + " is the Microsoft Store alias stub, not Python -- install Python 3, " +
|
|
37
|
+
"or turn the alias off under Settings > Apps > Advanced app settings > App execution aliases";
|
|
38
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wkappbot-code",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.20",
|
|
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": {
|