wkappbot-code 0.3.17 → 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 +7 -4
- package/lib/ai-clis.js +18 -7
- package/lib/hook-wire.js +4 -2
- package/lib/prereq.js +5 -2
- package/lib/python-acquire.js +108 -0
- package/lib/python-pick.js +38 -0
- package/lib/setup-run.js +15 -3
- 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)), "..");
|
|
@@ -200,8 +201,8 @@ function installBin(o) {
|
|
|
200
201
|
log("bin: bash " + script + " acquire --dest " + target);
|
|
201
202
|
const r = spawnSync("bash", [shPath(script), "acquire", "--dest", shPath(target)], { encoding: "utf8", env: shEnv() });
|
|
202
203
|
for (const line of ((r.stdout || "") + (r.stderr || "")).split("\n").filter(Boolean)) log(" " + line);
|
|
203
|
-
// rc 3 = the release host could not be reached (a package-registry-only VM): no core
|
|
204
|
-
//
|
|
204
|
+
// rc 3 = the release host could not be reached (a package-registry-only VM): no core. The hooks and the lite harness
|
|
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 };
|
|
207
208
|
// Put `wkappbot` on PATH beside wkappbot-code itself, unless that name is already taken by someone else.
|
|
@@ -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);" +
|
|
@@ -441,7 +444,7 @@ function install(o) {
|
|
|
441
444
|
}
|
|
442
445
|
}
|
|
443
446
|
// 0.3.13 (owner 2026-11-08): the famous AI CLIs come along, so a user who knows nothing about LLMs still has them.
|
|
444
|
-
st.aiClis = (o.npm && o.aiClis) ? installAiClis(log, process.env, undefined, (prev.aiClis && prev.aiClis.installed) || [])
|
|
447
|
+
st.aiClis = (o.npm && o.aiClis) ? installAiClis(log, process.env, undefined, (prev.aiClis && prev.aiClis.installed) || [], (prev.aiClis && prev.aiClis.prefix) || "")
|
|
445
448
|
: ((prev.aiClis && (prev.aiClis.installed || []).length) ? prev.aiClis : { skipped: true }); // a skip still owns what an earlier install added
|
|
446
449
|
const dest = binDest(o);
|
|
447
450
|
st.bin = o.bin ? installBin(o) : { skipped: true, dest };
|
|
@@ -569,7 +572,7 @@ function uninstall(o) {
|
|
|
569
572
|
else log("shim: kept " + cmdShim + " (it no longer calls " + core + ")");
|
|
570
573
|
}
|
|
571
574
|
// 3d. the AI CLIs this install added (lib/ai-clis.js), never one the user had before
|
|
572
|
-
removeAiClis((st.aiClis && st.aiClis.installed) || [], log);
|
|
575
|
+
removeAiClis((st.aiClis && st.aiClis.installed) || [], log, process.env, undefined, (st.aiClis && st.aiClis.prefix) || "");
|
|
573
576
|
// 4. the harness dirs we created, the plugin entry and our config folder
|
|
574
577
|
// 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.
|
|
575
578
|
const rmTree = (p, what) => {
|
package/lib/ai-clis.js
CHANGED
|
@@ -23,6 +23,14 @@ export function cliVersion(name, env = process.env) {
|
|
|
23
23
|
return m ? m[0] : line.slice(0, 40);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
// The npm global prefix an install uses RIGHT NOW (npm_config_prefix included: npm exports it to a postinstall worker, so a
|
|
27
|
+
// worker lays the CLIs into the prefix of the `npm install -g wkappbot-code` that started it). Recorded in the state so an
|
|
28
|
+
// uninstall run from another shell takes the CLIs back from THAT prefix, not from whatever its own default prefix is.
|
|
29
|
+
export function npmPrefix(env = process.env) {
|
|
30
|
+
const r = spawnSync("npm", ["prefix", "-g"], { encoding: "utf8", timeout: 30000, shell: IS_WIN, env });
|
|
31
|
+
return r.status === 0 ? String(r.stdout || "").trim().split("\n").pop().trim() : "";
|
|
32
|
+
}
|
|
33
|
+
|
|
26
34
|
function npmGlobal(pkgs, env) {
|
|
27
35
|
const r = spawnSync("npm", ["install", "-g", ...pkgs, "--no-audit", "--no-fund"], { stdio: "inherit", shell: IS_WIN, env });
|
|
28
36
|
return r.status === 0;
|
|
@@ -30,13 +38,14 @@ function npmGlobal(pkgs, env) {
|
|
|
30
38
|
|
|
31
39
|
// Installs every CLI of AI_CLIS that is not on PATH yet. Returns { installed: [...], present: [...], failed: [...] }.
|
|
32
40
|
// prevInstalled: the names an earlier install added; a rerun keeps owning them, so uninstall still takes them back.
|
|
33
|
-
export function installAiClis(log, env = process.env, list = AI_CLIS, prevInstalled = []) {
|
|
41
|
+
export function installAiClis(log, env = process.env, list = AI_CLIS, prevInstalled = [], prevPrefix = "") {
|
|
34
42
|
const present = [], missing = [];
|
|
35
43
|
for (const c of list) (cliVersion(c.name, env) ? present : missing).push(c);
|
|
36
44
|
if (present.length) log("ai-clis: already installed: " + present.map((c) => c.name).join(", "));
|
|
37
|
-
const res = { installed: present.map((c) => c.name).filter((n) => prevInstalled.includes(n)), present: present.map((c) => c.name), failed: [] };
|
|
45
|
+
const res = { installed: present.map((c) => c.name).filter((n) => prevInstalled.includes(n)), present: present.map((c) => c.name), failed: [], prefix: prevPrefix };
|
|
38
46
|
if (!missing.length) return res;
|
|
39
|
-
|
|
47
|
+
res.prefix = npmPrefix(env) || prevPrefix;
|
|
48
|
+
log("ai-clis: npm install -g " + missing.map((c) => c.pkg).join(" ") + (res.prefix ? " (prefix " + res.prefix + ")" : ""));
|
|
40
49
|
if (npmGlobal(missing.map((c) => c.pkg), env)) res.installed.push(...missing.map((c) => c.name));
|
|
41
50
|
else {
|
|
42
51
|
log("ai-clis: the one-pass install failed, retrying each CLI alone");
|
|
@@ -49,12 +58,14 @@ export function installAiClis(log, env = process.env, list = AI_CLIS, prevInstal
|
|
|
49
58
|
// uninstall takes back what install added (SDK os-qa 2026-11-09, run 42552244938: the npm prefix bin kept claude, codex,
|
|
50
59
|
// gemini and qwen after uninstall on every leg). Only the names install recorded go; a CLI the user had stays. A failed
|
|
51
60
|
// npm uninstall is a WARN with the command to finish it by hand.
|
|
52
|
-
|
|
61
|
+
// prefix: the npm global prefix install recorded (SDK os-qa u1-uninstall, 2026-11-09: the worker laid the CLIs into the
|
|
62
|
+
// prefix of the npm that started it, and an uninstall from a shell with another default prefix left them behind).
|
|
63
|
+
export function removeAiClis(names, log, env = process.env, list = AI_CLIS, prefix = "") {
|
|
53
64
|
const pkgs = list.filter((c) => (names || []).includes(c.name)).map((c) => c.pkg);
|
|
54
65
|
if (!pkgs.length) return true;
|
|
55
|
-
log("ai-clis: npm uninstall -g " + pkgs.join(" "));
|
|
56
|
-
const r = spawnSync("npm", ["uninstall", "-g", ...pkgs, "--no-audit", "--no-fund"], { stdio: "inherit", shell: IS_WIN, env });
|
|
57
|
-
if (r.status !== 0) log("ai-clis: WARN npm uninstall rc " + r.status + " -- run: npm uninstall -g " + pkgs.join(" "));
|
|
66
|
+
log("ai-clis: npm uninstall -g " + pkgs.join(" ") + (prefix ? " (prefix " + prefix + ")" : ""));
|
|
67
|
+
const r = spawnSync("npm", ["uninstall", "-g", ...pkgs, ...(prefix ? ["--prefix", prefix] : []), "--no-audit", "--no-fund"], { stdio: "inherit", shell: IS_WIN, env });
|
|
68
|
+
if (r.status !== 0) log("ai-clis: WARN npm uninstall rc " + r.status + " -- run: npm uninstall -g " + pkgs.join(" ") + (prefix ? " --prefix " + prefix : ""));
|
|
58
69
|
return r.status === 0;
|
|
59
70
|
}
|
|
60
71
|
|
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
|
}
|
package/lib/prereq.js
CHANGED
|
@@ -24,7 +24,7 @@ const realRun = (cmd, args, env) => spawnSync(cmd, args, { stdio: "inherit", env
|
|
|
24
24
|
export function ensurePrereqs(o = {}) {
|
|
25
25
|
const platform = o.platform || process.platform, env = o.env || process.env, log = o.log || (() => {});
|
|
26
26
|
const which = o.which || realWhich, exists = o.exists || existsSync, run = o.run || realRun;
|
|
27
|
-
const probe = () => { const found = {}, missing = []; for (const t of TOOLS) { const p = which(t, env); if (p) found[t] = p; else missing.push(t); } return { found, missing }; };
|
|
27
|
+
let gitDir = ""; const inGit = (t) => { if (!gitDir) return ""; for (const sub of ["bin", join("usr", "bin"), "cmd", join("ucrt64", "bin"), join("mingw64", "bin"), join("clangarm64", "bin")]) { const f = join(gitDir, sub, t + ".exe"); if (exists(f)) return f; } return ""; }; const probe = () => { const found = {}, missing = []; for (const t of TOOLS) { const p = which(t, env) || inGit(t); if (p) found[t] = p; else missing.push(t); } return { found, missing }; };
|
|
28
28
|
let p = probe(), via = "path";
|
|
29
29
|
if (!p.missing.length) return { ok: true, missing: [], found: p.found, via, note: "" };
|
|
30
30
|
if (platform !== "win32") {
|
|
@@ -36,17 +36,20 @@ export function ensurePrereqs(o = {}) {
|
|
|
36
36
|
for (const d of GIT_DIRS(env)) {
|
|
37
37
|
if (!exists(join(d, "bin", "bash.exe"))) continue;
|
|
38
38
|
const key = Object.keys(env).find((k) => k.toUpperCase() === "PATH") || "PATH";
|
|
39
|
-
|
|
39
|
+
const dirs = [join(d, "bin"), join(d, "usr", "bin"), join(d, "cmd"), ...[join(d, "ucrt64", "bin"), join(d, "mingw64", "bin"), join(d, "clangarm64", "bin")].filter((x) => exists(x))];
|
|
40
|
+
env[key] = dirs.join(delimiter) + delimiter + (env[key] || "");
|
|
40
41
|
return d;
|
|
41
42
|
}
|
|
42
43
|
return "";
|
|
43
44
|
};
|
|
44
45
|
let dir = addGit();
|
|
46
|
+
gitDir = dir;
|
|
45
47
|
if (dir) { p = probe(); via = "git-dir"; log("prereq: Git Bash found at " + dir + " (not on PATH) -- used for this install; add " + join(dir, "bin") + " to your PATH"); }
|
|
46
48
|
if (p.missing.length && !o.noInstall && which("winget", env)) {
|
|
47
49
|
log("prereq: Git for Windows (Git Bash) is missing -- installing it with winget");
|
|
48
50
|
const rc = run("winget", ["install", "--id", "Git.Git", "-e", "--source", "winget", "--silent", "--accept-package-agreements", "--accept-source-agreements"], env);
|
|
49
51
|
dir = addGit();
|
|
52
|
+
gitDir = dir;
|
|
50
53
|
if (dir) { p = probe(); via = "winget"; log("prereq: winget installed Git for Windows at " + dir + " (rc " + rc + ")"); }
|
|
51
54
|
else log("prereq: winget rc " + rc + " but no Git Bash was found afterwards");
|
|
52
55
|
}
|
|
@@ -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/lib/setup-run.js
CHANGED
|
@@ -133,13 +133,25 @@ export function printBanner({ home = homedir(), out = console.log, now = Date.no
|
|
|
133
133
|
// cannot be started (not on PATH): the caller then prints the usage as before.
|
|
134
134
|
// 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`
|
|
135
135
|
// linked it), so a global prefix that is not on the user PATH still opens it.
|
|
136
|
+
// 0.3.19: the binary is resolved BY FILE before spawning. On Windows a shell spawn of a missing name returns status 1
|
|
137
|
+
// with no r.error ("'opencode' is not recognized"), so the old error-only fallback never reached the sibling .cmd shim.
|
|
136
138
|
export function launchOpenCode(env = process.env, argv1 = process.argv[1] || "") {
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
139
|
+
const bin = findOpenCode(env, argv1 ? dirname(argv1) : "");
|
|
140
|
+
if (!bin) return null;
|
|
141
|
+
const shell = IS_WIN && /\.(?:cmd|bat)$/i.test(bin);
|
|
142
|
+
const r = spawnSync(shell ? '"' + bin + '"' : bin, [], { stdio: "inherit", shell, env: withPathFirst({ ...env }, dirname(bin)) });
|
|
140
143
|
if (r.error) return null;
|
|
141
144
|
return r.status ?? 1;
|
|
142
145
|
}
|
|
146
|
+
// The opencode file on PATH, else beside this CLI (the npm bin dir); "" when there is none anywhere.
|
|
147
|
+
export function findOpenCode(env = process.env, besideDir = "") {
|
|
148
|
+
const names = IS_WIN ? ["opencode.cmd", "opencode.exe", "opencode"] : ["opencode"];
|
|
149
|
+
const k = Object.keys(env).find((x) => x.toLowerCase() === "path") || "PATH";
|
|
150
|
+
const dirs = String(env[k] || "").split(delimiter).filter(Boolean);
|
|
151
|
+
if (besideDir) dirs.push(besideDir);
|
|
152
|
+
for (const d of dirs) for (const n of names) { const p = join(d, n); if (existsSync(p)) return p; }
|
|
153
|
+
return "";
|
|
154
|
+
}
|
|
143
155
|
|
|
144
156
|
// The npm global bin dir (where `npm i -g` links binaries): prefix/bin, or the prefix itself on Windows. "" when unknown.
|
|
145
157
|
export function globalBinDir(env = process.env) {
|
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": {
|