@mxalbert/context-mode 2.0.0
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/.claude-plugin/marketplace.json +33 -0
- package/.claude-plugin/plugin.json +31 -0
- package/.codex-plugin/hooks.json +65 -0
- package/.codex-plugin/mcp.json +13 -0
- package/.codex-plugin/plugin.json +31 -0
- package/.openclaw-plugin/index.ts +12 -0
- package/.openclaw-plugin/openclaw.plugin.json +38 -0
- package/.openclaw-plugin/package.json +28 -0
- package/LICENSE +94 -0
- package/README.md +1619 -0
- package/bin/statusline.mjs +378 -0
- package/build/adapters/antigravity/index.d.ts +51 -0
- package/build/adapters/antigravity/index.js +169 -0
- package/build/adapters/antigravity-cli/index.d.ts +51 -0
- package/build/adapters/antigravity-cli/index.js +342 -0
- package/build/adapters/base.d.ts +92 -0
- package/build/adapters/base.js +135 -0
- package/build/adapters/claude-code/config.d.ts +8 -0
- package/build/adapters/claude-code/config.js +8 -0
- package/build/adapters/claude-code/hooks.d.ts +106 -0
- package/build/adapters/claude-code/hooks.js +183 -0
- package/build/adapters/claude-code/index.d.ts +80 -0
- package/build/adapters/claude-code/index.js +569 -0
- package/build/adapters/claude-code-base.d.ts +49 -0
- package/build/adapters/claude-code-base.js +113 -0
- package/build/adapters/client-map.d.ts +10 -0
- package/build/adapters/client-map.js +42 -0
- package/build/adapters/codex/config.d.ts +8 -0
- package/build/adapters/codex/config.js +8 -0
- package/build/adapters/codex/hooks.d.ts +55 -0
- package/build/adapters/codex/hooks.js +64 -0
- package/build/adapters/codex/index.d.ts +87 -0
- package/build/adapters/codex/index.js +995 -0
- package/build/adapters/codex/paths.d.ts +1 -0
- package/build/adapters/codex/paths.js +12 -0
- package/build/adapters/codex/usage.d.ts +107 -0
- package/build/adapters/codex/usage.js +227 -0
- package/build/adapters/copilot-base.d.ts +94 -0
- package/build/adapters/copilot-base.js +297 -0
- package/build/adapters/copilot-cli/hooks.d.ts +33 -0
- package/build/adapters/copilot-cli/hooks.js +64 -0
- package/build/adapters/copilot-cli/index.d.ts +48 -0
- package/build/adapters/copilot-cli/index.js +341 -0
- package/build/adapters/cursor/config.d.ts +4 -0
- package/build/adapters/cursor/config.js +4 -0
- package/build/adapters/cursor/hooks.d.ts +55 -0
- package/build/adapters/cursor/hooks.js +82 -0
- package/build/adapters/cursor/index.d.ts +64 -0
- package/build/adapters/cursor/index.js +517 -0
- package/build/adapters/detect.d.ts +136 -0
- package/build/adapters/detect.js +623 -0
- package/build/adapters/gemini-cli/config.d.ts +8 -0
- package/build/adapters/gemini-cli/config.js +8 -0
- package/build/adapters/gemini-cli/hooks.d.ts +83 -0
- package/build/adapters/gemini-cli/hooks.js +115 -0
- package/build/adapters/gemini-cli/index.d.ts +80 -0
- package/build/adapters/gemini-cli/index.js +483 -0
- package/build/adapters/jetbrains-copilot/config.d.ts +8 -0
- package/build/adapters/jetbrains-copilot/config.js +8 -0
- package/build/adapters/jetbrains-copilot/hooks.d.ts +59 -0
- package/build/adapters/jetbrains-copilot/hooks.js +87 -0
- package/build/adapters/jetbrains-copilot/index.d.ts +31 -0
- package/build/adapters/jetbrains-copilot/index.js +131 -0
- package/build/adapters/kimi/config.d.ts +8 -0
- package/build/adapters/kimi/config.js +8 -0
- package/build/adapters/kimi/hooks.d.ts +28 -0
- package/build/adapters/kimi/hooks.js +34 -0
- package/build/adapters/kimi/index.d.ts +66 -0
- package/build/adapters/kimi/index.js +537 -0
- package/build/adapters/kimi/paths.d.ts +21 -0
- package/build/adapters/kimi/paths.js +52 -0
- package/build/adapters/kimi/usage.d.ts +82 -0
- package/build/adapters/kimi/usage.js +217 -0
- package/build/adapters/kiro/hooks.d.ts +61 -0
- package/build/adapters/kiro/hooks.js +94 -0
- package/build/adapters/kiro/index.d.ts +61 -0
- package/build/adapters/kiro/index.js +301 -0
- package/build/adapters/omp/index.d.ts +65 -0
- package/build/adapters/omp/index.js +182 -0
- package/build/adapters/omp/plugin.d.ts +81 -0
- package/build/adapters/omp/plugin.js +331 -0
- package/build/adapters/omp/usage.d.ts +49 -0
- package/build/adapters/omp/usage.js +110 -0
- package/build/adapters/openclaw/config.d.ts +8 -0
- package/build/adapters/openclaw/config.js +8 -0
- package/build/adapters/openclaw/hooks.d.ts +50 -0
- package/build/adapters/openclaw/hooks.js +61 -0
- package/build/adapters/openclaw/index.d.ts +74 -0
- package/build/adapters/openclaw/index.js +439 -0
- package/build/adapters/openclaw/mcp-tools.d.ts +54 -0
- package/build/adapters/openclaw/mcp-tools.js +207 -0
- package/build/adapters/openclaw/plugin.d.ts +141 -0
- package/build/adapters/openclaw/plugin.js +818 -0
- package/build/adapters/openclaw/session-db.d.ts +55 -0
- package/build/adapters/openclaw/session-db.js +88 -0
- package/build/adapters/openclaw/usage.d.ts +34 -0
- package/build/adapters/openclaw/usage.js +52 -0
- package/build/adapters/openclaw/workspace-router.d.ts +29 -0
- package/build/adapters/openclaw/workspace-router.js +64 -0
- package/build/adapters/opencode/config.d.ts +8 -0
- package/build/adapters/opencode/config.js +8 -0
- package/build/adapters/opencode/hooks.d.ts +38 -0
- package/build/adapters/opencode/hooks.js +50 -0
- package/build/adapters/opencode/index.d.ts +144 -0
- package/build/adapters/opencode/index.js +638 -0
- package/build/adapters/opencode/plugin.d.ts +249 -0
- package/build/adapters/opencode/plugin.js +1536 -0
- package/build/adapters/opencode/v2.d.ts +117 -0
- package/build/adapters/opencode/v2.js +224 -0
- package/build/adapters/opencode/zod3tov4.d.ts +1 -0
- package/build/adapters/opencode/zod3tov4.js +132 -0
- package/build/adapters/pi/extension.d.ts +81 -0
- package/build/adapters/pi/extension.js +861 -0
- package/build/adapters/pi/index.d.ts +57 -0
- package/build/adapters/pi/index.js +173 -0
- package/build/adapters/pi/mcp-bridge.d.ts +268 -0
- package/build/adapters/pi/mcp-bridge.js +894 -0
- package/build/adapters/qwen-code/hooks.d.ts +26 -0
- package/build/adapters/qwen-code/hooks.js +29 -0
- package/build/adapters/qwen-code/index.d.ts +40 -0
- package/build/adapters/qwen-code/index.js +361 -0
- package/build/adapters/qwen-code/usage.d.ts +90 -0
- package/build/adapters/qwen-code/usage.js +222 -0
- package/build/adapters/types.d.ts +360 -0
- package/build/adapters/types.js +118 -0
- package/build/adapters/vscode-copilot/config.d.ts +8 -0
- package/build/adapters/vscode-copilot/config.js +8 -0
- package/build/adapters/vscode-copilot/hooks.d.ts +71 -0
- package/build/adapters/vscode-copilot/hooks.js +98 -0
- package/build/adapters/vscode-copilot/index.d.ts +32 -0
- package/build/adapters/vscode-copilot/index.js +222 -0
- package/build/adapters/zed/index.d.ts +40 -0
- package/build/adapters/zed/index.js +163 -0
- package/build/cli.d.ts +34 -0
- package/build/cli.js +1828 -0
- package/build/db-base.d.ts +250 -0
- package/build/db-base.js +756 -0
- package/build/executor.d.ts +72 -0
- package/build/executor.js +678 -0
- package/build/exit-classify.d.ts +19 -0
- package/build/exit-classify.js +12 -0
- package/build/fetch/blocks.d.ts +145 -0
- package/build/fetch/blocks.js +244 -0
- package/build/fetch/extract.d.ts +68 -0
- package/build/fetch/extract.js +123 -0
- package/build/fetch/page-store.d.ts +66 -0
- package/build/fetch/page-store.js +174 -0
- package/build/fetch-cache.d.ts +13 -0
- package/build/fetch-cache.js +15 -0
- package/build/lifecycle.d.ts +124 -0
- package/build/lifecycle.js +278 -0
- package/build/package-identity.d.ts +26 -0
- package/build/package-identity.js +28 -0
- package/build/runPool.d.ts +36 -0
- package/build/runPool.js +51 -0
- package/build/runtime.d.ts +71 -0
- package/build/runtime.js +659 -0
- package/build/search/auto-memory.d.ts +47 -0
- package/build/search/auto-memory.js +170 -0
- package/build/search/ctx-search-schema.d.ts +90 -0
- package/build/search/ctx-search-schema.js +135 -0
- package/build/search/flood-guard.d.ts +57 -0
- package/build/search/flood-guard.js +80 -0
- package/build/search/unified.d.ts +56 -0
- package/build/search/unified.js +104 -0
- package/build/security.d.ts +206 -0
- package/build/security.js +780 -0
- package/build/server.d.ts +240 -0
- package/build/server.js +5034 -0
- package/build/session/analytics.d.ts +773 -0
- package/build/session/analytics.js +2420 -0
- package/build/session/db.d.ts +468 -0
- package/build/session/db.js +1330 -0
- package/build/session/error-classifier.d.ts +87 -0
- package/build/session/error-classifier.js +303 -0
- package/build/session/event-emit.d.ts +48 -0
- package/build/session/event-emit.js +101 -0
- package/build/session/extract.d.ts +333 -0
- package/build/session/extract.js +2688 -0
- package/build/session/model-prices.json +429 -0
- package/build/session/persist-tool-calls.d.ts +54 -0
- package/build/session/persist-tool-calls.js +105 -0
- package/build/session/pricing.d.ts +64 -0
- package/build/session/pricing.js +151 -0
- package/build/session/project-attribution.d.ts +73 -0
- package/build/session/project-attribution.js +245 -0
- package/build/session/purge.d.ts +138 -0
- package/build/session/purge.js +240 -0
- package/build/session/retrieval-marker.d.ts +39 -0
- package/build/session/retrieval-marker.js +65 -0
- package/build/session/snapshot.d.ts +46 -0
- package/build/session/snapshot.js +532 -0
- package/build/store-directory.d.ts +56 -0
- package/build/store-directory.js +254 -0
- package/build/store.d.ts +147 -0
- package/build/store.js +1714 -0
- package/build/truncate.d.ts +57 -0
- package/build/truncate.js +142 -0
- package/build/types.d.ts +117 -0
- package/build/types.js +20 -0
- package/build/util/claude-config.d.ts +32 -0
- package/build/util/claude-config.js +84 -0
- package/build/util/hook-config.d.ts +27 -0
- package/build/util/hook-config.js +76 -0
- package/build/util/jsonc.d.ts +14 -0
- package/build/util/jsonc.js +104 -0
- package/build/util/plugin-cache-integrity.d.ts +51 -0
- package/build/util/plugin-cache-integrity.js +146 -0
- package/build/util/project-dir.d.ts +149 -0
- package/build/util/project-dir.js +340 -0
- package/build/util/sibling-mcp.d.ts +79 -0
- package/build/util/sibling-mcp.js +181 -0
- package/cli.bundle.mjs +1495 -0
- package/configs/antigravity/GEMINI.md +68 -0
- package/configs/antigravity/mcp_config.json +7 -0
- package/configs/antigravity-cli/hooks/hooks.json +37 -0
- package/configs/antigravity-cli/hooks.json +37 -0
- package/configs/antigravity-cli/mcp_config.json +10 -0
- package/configs/antigravity-cli/plugin.json +14 -0
- package/configs/antigravity-cli/rules/context-mode.md +77 -0
- package/configs/antigravity-cli/skills/context-mode/SKILL.md +77 -0
- package/configs/claude-code/CLAUDE.md +91 -0
- package/configs/codex/AGENTS.md +99 -0
- package/configs/codex/config.toml +8 -0
- package/configs/codex/hooks.json +47 -0
- package/configs/copilot-cli/.github/plugin/plugin.json +23 -0
- package/configs/copilot-cli/.mcp.json +12 -0
- package/configs/copilot-cli/README.md +47 -0
- package/configs/copilot-cli/hooks.json +41 -0
- package/configs/copilot-cli/skills/context-mode/SKILL.md +38 -0
- package/configs/cursor/context-mode.mdc +76 -0
- package/configs/cursor/hooks.json +21 -0
- package/configs/cursor/mcp.json +7 -0
- package/configs/gemini-cli/GEMINI.md +88 -0
- package/configs/gemini-cli/mcp.json +7 -0
- package/configs/gemini-cli/settings.json +60 -0
- package/configs/jetbrains-copilot/copilot-instructions.md +80 -0
- package/configs/jetbrains-copilot/hooks.json +16 -0
- package/configs/jetbrains-copilot/mcp.json +7 -0
- package/configs/kilo/AGENTS.md +84 -0
- package/configs/kilo/kilo.json +6 -0
- package/configs/kimi/hooks.json +54 -0
- package/configs/kiro/KIRO.md +84 -0
- package/configs/kiro/agent.json +18 -0
- package/configs/kiro/mcp.json +7 -0
- package/configs/omp/SYSTEM.md +85 -0
- package/configs/omp/mcp.json +7 -0
- package/configs/openclaw/AGENTS.md +87 -0
- package/configs/openclaw/openclaw.json +13 -0
- package/configs/opencode/AGENTS.md +84 -0
- package/configs/opencode/opencode.json +6 -0
- package/configs/pi/AGENTS.md +3 -0
- package/configs/qwen-code/QWEN.md +91 -0
- package/configs/vscode-copilot/copilot-instructions.md +80 -0
- package/configs/vscode-copilot/hooks.json +16 -0
- package/configs/vscode-copilot/mcp.json +7 -0
- package/configs/zed/AGENTS.md +68 -0
- package/hooks/antigravity-cli/payload.mjs +98 -0
- package/hooks/antigravity-cli/posttooluse.mjs +138 -0
- package/hooks/antigravity-cli/pretooluse.mjs +78 -0
- package/hooks/antigravity-cli/stop.mjs +58 -0
- package/hooks/auto-injection.mjs +102 -0
- package/hooks/cache-heal-utils.mjs +379 -0
- package/hooks/codex/platform.mjs +1 -0
- package/hooks/codex/posttooluse.mjs +66 -0
- package/hooks/codex/precompact.mjs +70 -0
- package/hooks/codex/pretooluse.mjs +42 -0
- package/hooks/codex/sessionstart.mjs +121 -0
- package/hooks/codex/stop.mjs +138 -0
- package/hooks/codex/userpromptsubmit.mjs +75 -0
- package/hooks/copilot-cli/posttooluse.mjs +79 -0
- package/hooks/copilot-cli/precompact.mjs +66 -0
- package/hooks/copilot-cli/pretooluse.mjs +41 -0
- package/hooks/copilot-cli/sessionstart.mjs +121 -0
- package/hooks/copilot-cli/stop.mjs +59 -0
- package/hooks/copilot-cli/userpromptsubmit.mjs +77 -0
- package/hooks/core/codex-caps.mjs +112 -0
- package/hooks/core/formatters.mjs +356 -0
- package/hooks/core/mcp-ready.mjs +109 -0
- package/hooks/core/platform-detect.mjs +49 -0
- package/hooks/core/routing.mjs +1050 -0
- package/hooks/core/stdin.mjs +90 -0
- package/hooks/core/tool-naming.mjs +57 -0
- package/hooks/cursor/afteragentresponse.mjs +74 -0
- package/hooks/cursor/hooks.json +31 -0
- package/hooks/cursor/posttooluse.mjs +72 -0
- package/hooks/cursor/pretooluse.mjs +26 -0
- package/hooks/cursor/sessionstart.mjs +97 -0
- package/hooks/cursor/stop.mjs +48 -0
- package/hooks/ensure-deps.mjs +254 -0
- package/hooks/formatters/claude-code.mjs +113 -0
- package/hooks/formatters/cursor.mjs +37 -0
- package/hooks/formatters/gemini-cli.mjs +55 -0
- package/hooks/formatters/vscode-copilot.mjs +55 -0
- package/hooks/gemini-cli/aftermodel.mjs +70 -0
- package/hooks/gemini-cli/aftertool.mjs +61 -0
- package/hooks/gemini-cli/beforeagent.mjs +99 -0
- package/hooks/gemini-cli/beforetool.mjs +27 -0
- package/hooks/gemini-cli/precompress.mjs +55 -0
- package/hooks/gemini-cli/sessionstart.mjs +137 -0
- package/hooks/heal-partial-install.mjs +712 -0
- package/hooks/hooks.json +143 -0
- package/hooks/jetbrains-copilot/posttooluse.mjs +61 -0
- package/hooks/jetbrains-copilot/precompact.mjs +55 -0
- package/hooks/jetbrains-copilot/pretooluse.mjs +27 -0
- package/hooks/jetbrains-copilot/sessionstart.mjs +124 -0
- package/hooks/kimi/platform.mjs +1 -0
- package/hooks/kimi/posttooluse.mjs +72 -0
- package/hooks/kimi/precompact.mjs +80 -0
- package/hooks/kimi/pretooluse.mjs +42 -0
- package/hooks/kimi/sessionend.mjs +61 -0
- package/hooks/kimi/sessionstart.mjs +113 -0
- package/hooks/kimi/stop.mjs +132 -0
- package/hooks/kimi/userpromptsubmit.mjs +90 -0
- package/hooks/kiro/agentspawn.mjs +97 -0
- package/hooks/kiro/posttooluse.mjs +51 -0
- package/hooks/kiro/pretooluse.mjs +64 -0
- package/hooks/kiro/userpromptsubmit.mjs +88 -0
- package/hooks/normalize-hooks.mjs +323 -0
- package/hooks/platform-bridge.mjs +340 -0
- package/hooks/posttooluse.mjs +225 -0
- package/hooks/precompact.mjs +100 -0
- package/hooks/pretooluse.mjs +227 -0
- package/hooks/qwen-code/platform.mjs +1 -0
- package/hooks/qwen-code/stop.mjs +168 -0
- package/hooks/routing-block.mjs +105 -0
- package/hooks/run-hook.mjs +95 -0
- package/hooks/security.bundle.mjs +2 -0
- package/hooks/session-attribution.bundle.mjs +1 -0
- package/hooks/session-db.bundle.mjs +145 -0
- package/hooks/session-directive.mjs +520 -0
- package/hooks/session-extract.bundle.mjs +3 -0
- package/hooks/session-helpers.mjs +428 -0
- package/hooks/session-loaders.mjs +448 -0
- package/hooks/session-snapshot.bundle.mjs +32 -0
- package/hooks/sessionstart.mjs +464 -0
- package/hooks/stop.mjs +82 -0
- package/hooks/suppress-stderr.mjs +25 -0
- package/hooks/userpromptsubmit.mjs +108 -0
- package/hooks/vscode-copilot/posttooluse.mjs +61 -0
- package/hooks/vscode-copilot/precompact.mjs +55 -0
- package/hooks/vscode-copilot/pretooluse.mjs +27 -0
- package/hooks/vscode-copilot/sessionstart.mjs +129 -0
- package/openclaw.plugin.json +38 -0
- package/package.json +132 -0
- package/scripts/heal-better-sqlite3.mjs +430 -0
- package/scripts/heal-installed-plugins.mjs +635 -0
- package/scripts/plugin-cache-integrity.mjs +248 -0
- package/scripts/postinstall.mjs +397 -0
- package/server.bundle.mjs +1421 -0
- package/server.js +5 -0
- package/skills/.ignore +7 -0
- package/skills/context-mode/SKILL.md +300 -0
- package/skills/context-mode/references/anti-patterns.md +283 -0
- package/skills/context-mode/references/patterns-javascript.md +298 -0
- package/skills/context-mode/references/patterns-python.md +304 -0
- package/skills/context-mode/references/patterns-shell.md +277 -0
- package/skills/ctx-doctor/SKILL.md +22 -0
- package/skills/ctx-index/SKILL.md +46 -0
- package/skills/ctx-insight/SKILL.md +24 -0
- package/skills/ctx-purge/SKILL.md +49 -0
- package/skills/ctx-search/SKILL.md +35 -0
- package/skills/ctx-stats/SKILL.md +26 -0
- package/skills/ctx-upgrade/SKILL.md +31 -0
- package/start.mjs +616 -0
package/build/runtime.js
ADDED
|
@@ -0,0 +1,659 @@
|
|
|
1
|
+
import { execFileSync, execSync } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { JS_RUNTIMES } from "./adapters/types.js";
|
|
4
|
+
/**
|
|
5
|
+
* Allowlist for SHELL env override. Only POSIX shells + Windows shells permit
|
|
6
|
+
* arbitrary command interpretation; anything else (e.g., /usr/bin/python set
|
|
7
|
+
* as SHELL) would let an attacker redirect the executor to a non-shell binary.
|
|
8
|
+
*
|
|
9
|
+
* basename split handles BOTH `/` and `\` separators so a Windows-style path
|
|
10
|
+
* (`C:\Program Files\PowerShell\7\pwsh.exe`) classifies correctly even when
|
|
11
|
+
* the runtime is on POSIX (where node:path.basename only splits on `/`).
|
|
12
|
+
*
|
|
13
|
+
* Match is case-insensitive; `.exe` extension tolerated for Windows binaries.
|
|
14
|
+
*/
|
|
15
|
+
const ALLOWED_SHELL_BASENAMES = /^(bash|sh|zsh|dash|pwsh|powershell|cmd)(\.exe)?$/i;
|
|
16
|
+
const BUN_BASENAME = /^bun(\.exe)?$/i;
|
|
17
|
+
function runtimeBasename(runtimePath) {
|
|
18
|
+
const segments = runtimePath.split(/[\\/]/);
|
|
19
|
+
return segments[segments.length - 1] ?? runtimePath;
|
|
20
|
+
}
|
|
21
|
+
export function isAllowlistedShell(shellPath) {
|
|
22
|
+
// Cross-OS basename: split on either separator, take the last segment.
|
|
23
|
+
return ALLOWED_SHELL_BASENAMES.test(runtimeBasename(shellPath));
|
|
24
|
+
}
|
|
25
|
+
function isWindowsWslBash(shellPath) {
|
|
26
|
+
const lower = shellPath.toLowerCase().replace(/\//g, "\\");
|
|
27
|
+
return /\\windows\\(?:system32|sysnative)\\bash\.exe$/.test(lower) ||
|
|
28
|
+
/\\microsoft\\windowsapps\\bash\.exe$/.test(lower);
|
|
29
|
+
}
|
|
30
|
+
function isWindowsSystemCmd(shellPath) {
|
|
31
|
+
const lower = shellPath.toLowerCase().replace(/\//g, "\\");
|
|
32
|
+
return /\\windows\\(?:system32|sysnative)\\cmd\.exe$/.test(lower);
|
|
33
|
+
}
|
|
34
|
+
const isWindows = process.platform === "win32";
|
|
35
|
+
function commandExists(cmd) {
|
|
36
|
+
try {
|
|
37
|
+
const check = isWindows ? `where ${cmd}` : `command -v ${cmd}`;
|
|
38
|
+
execSync(check, { stdio: "pipe" });
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Stricter probe than commandExists() — also verifies the resolved binary
|
|
47
|
+
* actually runs. On Windows, `where python3` matches the Microsoft Store
|
|
48
|
+
* App Execution Alias stub at C:\Users\<u>\AppData\Local\Microsoft\WindowsApps\
|
|
49
|
+
* even when no real Python is installed; the stub exits non-zero (9009) and
|
|
50
|
+
* pops the Store. Filter those entries out and require `<cmd> --version` to
|
|
51
|
+
* exit 0 before declaring the runtime available (#455).
|
|
52
|
+
*/
|
|
53
|
+
function runnableExists(cmd) {
|
|
54
|
+
if (isWindows) {
|
|
55
|
+
// Reject if every `where` hit lives under Microsoft\WindowsApps (Store stubs).
|
|
56
|
+
try {
|
|
57
|
+
const out = execSync(`where ${cmd}`, { encoding: "utf-8", stdio: "pipe" });
|
|
58
|
+
const hits = out.trim().split(/\r?\n/).map(p => p.trim()).filter(Boolean);
|
|
59
|
+
if (hits.length === 0)
|
|
60
|
+
return false;
|
|
61
|
+
const realHits = hits.filter(p => !/\\Microsoft\\WindowsApps\\/i.test(p));
|
|
62
|
+
if (realHits.length === 0)
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
else if (!commandExists(cmd)) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
// Probe with --version. On Windows, allow 5s for cold-start (MS Store stub
|
|
73
|
+
// fallthrough can be slow). On POSIX, 1500ms is plenty for a real binary
|
|
74
|
+
// and keeps cold detection of python3 → python → py under ~5s total (#454).
|
|
75
|
+
try {
|
|
76
|
+
// DEP0190 fix: avoid args array with shell:true on Windows.
|
|
77
|
+
// Use execSync with a command string when shell is required;
|
|
78
|
+
// keep execFileSync (no shell) on POSIX.
|
|
79
|
+
if (isWindows) {
|
|
80
|
+
execSync(`"${cmd}" --version`, { stdio: "pipe", timeout: 5000 });
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
execFileSync(cmd, ["--version"], { stdio: "pipe", timeout: 1500 });
|
|
84
|
+
}
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function bunExists() {
|
|
92
|
+
if (commandExists("bun"))
|
|
93
|
+
return true;
|
|
94
|
+
for (const p of bunFallbackPaths()) {
|
|
95
|
+
if (existsSync(p))
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
function bunCommand() {
|
|
101
|
+
// Prefer absolute .exe paths so spawn() can run with shell:false on Windows.
|
|
102
|
+
// `where bun` may resolve to a `bun.cmd` npm shim (#506) which CreateProcess
|
|
103
|
+
// cannot execute directly — return the real .exe wherever we can find one.
|
|
104
|
+
for (const p of bunFallbackPaths()) {
|
|
105
|
+
if (existsSync(p))
|
|
106
|
+
return p;
|
|
107
|
+
}
|
|
108
|
+
// Bare name only if PATH resolution confirms it. On Windows this is
|
|
109
|
+
// typically a .cmd shim — the executor's needsShell list (which now
|
|
110
|
+
// includes "bun" — see #506) ensures shell:true so cmd.exe can resolve it.
|
|
111
|
+
if (commandExists("bun"))
|
|
112
|
+
return "bun";
|
|
113
|
+
// Synthetic last-resort path for diagnostics/error messages.
|
|
114
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
115
|
+
return isWindows ? `${home}\\.bun\\bin\\bun.exe` : `${home}/.bun/bin/bun`;
|
|
116
|
+
}
|
|
117
|
+
/** Fallback paths where Bun may be installed but not on PATH. */
|
|
118
|
+
function bunFallbackPaths() {
|
|
119
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
120
|
+
if (isWindows) {
|
|
121
|
+
const localAppData = process.env.LOCALAPPDATA ?? "";
|
|
122
|
+
const appData = process.env.APPDATA ?? "";
|
|
123
|
+
return [
|
|
124
|
+
// Native bun installer locations (irm bun.sh/install.ps1).
|
|
125
|
+
...(home ? [`${home}\\.bun\\bin\\bun.exe`] : []),
|
|
126
|
+
...(localAppData ? [`${localAppData}\\bun\\bin\\bun.exe`] : []),
|
|
127
|
+
// npm i -g bun installs bun.exe under the npm prefix (typically
|
|
128
|
+
// %APPDATA%\npm\node_modules\bun\bin\bun.exe). Without this, npm
|
|
129
|
+
// installs were "found" via bun.cmd shim on PATH and the bare "bun"
|
|
130
|
+
// string was returned — spawn() then ENOENT'd because CreateProcess
|
|
131
|
+
// can't execute .cmd files (#506).
|
|
132
|
+
...(appData ? [`${appData}\\npm\\node_modules\\bun\\bin\\bun.exe`] : []),
|
|
133
|
+
];
|
|
134
|
+
}
|
|
135
|
+
return home ? [`${home}/.bun/bin/bun`] : [];
|
|
136
|
+
}
|
|
137
|
+
/** Well-known Git-for-Windows bash.exe locations (MSYS bash that performs
|
|
138
|
+
* Windows→POSIX path conversion for native git — #826). */
|
|
139
|
+
const KNOWN_GIT_BASH_PATHS = [
|
|
140
|
+
"C:\\Program Files\\Git\\usr\\bin\\bash.exe",
|
|
141
|
+
"C:\\Program Files (x86)\\Git\\usr\\bin\\bash.exe",
|
|
142
|
+
];
|
|
143
|
+
/**
|
|
144
|
+
* On Windows, resolve the first non-WSL bash that is actually available.
|
|
145
|
+
*
|
|
146
|
+
* Availability is gated by `where bash` (#796): bash must be discoverable on
|
|
147
|
+
* PATH for us to claim it. WSL bash (C:\Windows\System32\bash.exe) cannot
|
|
148
|
+
* handle Windows paths, so we skip it and prefer Git Bash / MSYS2 bash.
|
|
149
|
+
*
|
|
150
|
+
* Routing the gate through `where bash` — rather than probing the known Git
|
|
151
|
+
* Bash paths with existsSync first — is deliberate: when bash is genuinely
|
|
152
|
+
* unavailable, the caller must fall through to pwsh (PR intent). Probing the
|
|
153
|
+
* filesystem first re-detected a real Git Bash on the runner even though the
|
|
154
|
+
* scenario was "bash unavailable", so pwsh was never reached.
|
|
155
|
+
*
|
|
156
|
+
* #826 is preserved: when `where bash` surfaces a Git Bash candidate we
|
|
157
|
+
* canonicalize it to the absolute Git\usr\bin\bash.exe path (so native git
|
|
158
|
+
* keeps MSYS path conversion) by preferring a matching known path that exists.
|
|
159
|
+
*/
|
|
160
|
+
function resolveWindowsBash() {
|
|
161
|
+
let candidates;
|
|
162
|
+
try {
|
|
163
|
+
const result = execSync("where bash", { encoding: "utf-8", stdio: "pipe" });
|
|
164
|
+
candidates = result.trim().split(/\r?\n/).map(p => p.trim()).filter(Boolean);
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
// bash not on PATH → genuinely unavailable. Fall through to pwsh/etc.
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
for (const p of candidates) {
|
|
171
|
+
const lower = p.toLowerCase();
|
|
172
|
+
if (lower.includes("system32") || lower.includes("windowsapps"))
|
|
173
|
+
continue;
|
|
174
|
+
// Prefer the canonical Git\usr\bin\bash.exe so native git retains MSYS
|
|
175
|
+
// path conversion. `where bash` on a Git-for-Windows install may surface
|
|
176
|
+
// the Git\cmd\bash shim or usr\bin path; upgrade to a known absolute path
|
|
177
|
+
// when one exists on disk.
|
|
178
|
+
for (const known of KNOWN_GIT_BASH_PATHS) {
|
|
179
|
+
if (existsSync(known))
|
|
180
|
+
return known;
|
|
181
|
+
}
|
|
182
|
+
return p;
|
|
183
|
+
}
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
function resolveWindowsShell(windowsBash = resolveWindowsBash()) {
|
|
187
|
+
// Prefer Git Bash (#826) so native git keeps its MSYS path conversion.
|
|
188
|
+
// The caller passes the already-resolved windowsBash to avoid probing the
|
|
189
|
+
// filesystem twice (it also feeds the cmd.exe shellOverride guard above).
|
|
190
|
+
// Fall back through POSIX sh, then PowerShell Core (pwsh) for proper UTF-8
|
|
191
|
+
// handling, then Windows PowerShell, then cmd.exe as the last resort.
|
|
192
|
+
return windowsBash
|
|
193
|
+
?? (commandExists("sh")
|
|
194
|
+
? "sh"
|
|
195
|
+
: commandExists("pwsh")
|
|
196
|
+
? "pwsh"
|
|
197
|
+
: commandExists("powershell")
|
|
198
|
+
? "powershell"
|
|
199
|
+
: "cmd.exe");
|
|
200
|
+
}
|
|
201
|
+
function getVersion(cmd, args = ["--version"]) {
|
|
202
|
+
try {
|
|
203
|
+
// DEP0190 fix: avoid args array with shell:true on Windows.
|
|
204
|
+
if (process.platform === "win32") {
|
|
205
|
+
// Hardening (PR #537 review): quote any cmd.exe metacharacter, not just
|
|
206
|
+
// whitespace. Current arg sources are internally controlled, but cheap
|
|
207
|
+
// defense-in-depth for future call sites.
|
|
208
|
+
const cmdStr = [cmd, ...args]
|
|
209
|
+
.map(a => /[\s"&|<>^()%!]/.test(a) ? JSON.stringify(a) : a)
|
|
210
|
+
.join(" ");
|
|
211
|
+
return execSync(cmdStr, {
|
|
212
|
+
encoding: "utf-8",
|
|
213
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
214
|
+
timeout: 5000,
|
|
215
|
+
})
|
|
216
|
+
.trim()
|
|
217
|
+
.split(/\r?\n/)[0];
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
return execFileSync(cmd, args, {
|
|
221
|
+
encoding: "utf-8",
|
|
222
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
223
|
+
timeout: 5000,
|
|
224
|
+
})
|
|
225
|
+
.trim()
|
|
226
|
+
.split(/\r?\n/)[0];
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
return "unknown";
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Resolve the JavaScript runtime used by PolyglotExecutor.
|
|
235
|
+
*
|
|
236
|
+
* PR #190 (f69b0d2) made `process.execPath` the default so snap-Node
|
|
237
|
+
* envs would not re-invoke the snap wrapper via PATH. That assumed
|
|
238
|
+
* `process.execPath` always points at a JS runtime — true on Node,
|
|
239
|
+
* tsx, and snap-Node, but FALSE when context-mode runs in-process
|
|
240
|
+
* inside a bun-compiled self-contained binary (OpenCode, Kilo, …).
|
|
241
|
+
* In those hosts, `process.execPath` resolves to `opencode.exe` /
|
|
242
|
+
* `opencode` (NOT node), and spawning that with a `.js` argument
|
|
243
|
+
* triggers the yargs "Failed to change directory" error (#731).
|
|
244
|
+
*
|
|
245
|
+
* Fix: gate `process.execPath` on the existing `JS_RUNTIMES`
|
|
246
|
+
* allowlist (single source of truth — same set used by
|
|
247
|
+
* `buildNodeCommand()` in src/adapters/types.ts since PR #708). When
|
|
248
|
+
* the execPath basename is not a known JS runtime, fall back to a
|
|
249
|
+
* PATH-resolved `node`. If neither is reachable, return `null` and
|
|
250
|
+
* let ctx_doctor surface an actionable error.
|
|
251
|
+
*
|
|
252
|
+
* The cross-OS guard is the allowlist itself — NOT a `win32` check.
|
|
253
|
+
* OpenCode ships self-contained binaries on macOS and Linux too,
|
|
254
|
+
* and the bug reproduces identically there.
|
|
255
|
+
*/
|
|
256
|
+
export function resolveJavascriptRuntime(bun, deps = {}) {
|
|
257
|
+
if (bun)
|
|
258
|
+
return bun;
|
|
259
|
+
const execPath = deps.execPath ?? process.execPath;
|
|
260
|
+
const cmdExists = deps.commandExists ?? commandExists;
|
|
261
|
+
// Cross-OS basename: split on either separator, strip optional `.exe`.
|
|
262
|
+
const base = execPath
|
|
263
|
+
.split(/[\\/]/)
|
|
264
|
+
.pop()
|
|
265
|
+
.replace(/\.exe$/i, "");
|
|
266
|
+
if (JS_RUNTIMES.has(base)) {
|
|
267
|
+
// Real JS runtime (node, bun, deno) — preserves #190 snap-Node fix
|
|
268
|
+
// because the snap wrapper's binary is literally named `node`.
|
|
269
|
+
//
|
|
270
|
+
// Issue #800 — liveness guard: on Homebrew, process.execPath points into
|
|
271
|
+
// the versioned Cellar (/opt/homebrew/Cellar/node/26.0.0/bin/node).
|
|
272
|
+
// `brew upgrade` + `brew cleanup` deletes the old Cellar, so the path
|
|
273
|
+
// dangles for the life of the already-running MCP server. If the path
|
|
274
|
+
// doesn't exist on disk, skip it and fall through to PATH node.
|
|
275
|
+
if (existsSync(execPath)) {
|
|
276
|
+
return execPath;
|
|
277
|
+
}
|
|
278
|
+
// Stale execPath (deleted Cellar, corrupted install, uninstall while
|
|
279
|
+
// process alive). Fall through to PATH resolution below.
|
|
280
|
+
}
|
|
281
|
+
// Host binary (opencode/kilo/etc.) — fall back to node on PATH.
|
|
282
|
+
if (cmdExists("node"))
|
|
283
|
+
return "node";
|
|
284
|
+
// No usable runtime — doctor + summary must handle null gracefully.
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
export function detectRuntimes() {
|
|
288
|
+
const hasBun = bunExists();
|
|
289
|
+
const bun = hasBun ? bunCommand() : null;
|
|
290
|
+
// Honor SHELL env var when it points at a real binary AND the basename is
|
|
291
|
+
// an allowlisted shell. Lets users with non-standard setups (custom bash,
|
|
292
|
+
// msys2, pwsh) pin context-mode to their preferred shell.
|
|
293
|
+
//
|
|
294
|
+
// Allowlist (PR #401 ops review): basename must match
|
|
295
|
+
// /^(bash|sh|zsh|dash|pwsh|powershell|cmd)(\.exe)?$/. Without this guard, an attacker
|
|
296
|
+
// who controls SHELL (e.g., supply-chain compromise of a profile script)
|
|
297
|
+
// could redirect the executor to /usr/bin/python or any arbitrary binary.
|
|
298
|
+
const userShell = process.env.SHELL;
|
|
299
|
+
const isWin = process.platform === "win32";
|
|
300
|
+
const windowsBash = isWin ? resolveWindowsBash() : null;
|
|
301
|
+
const shellOverride = userShell &&
|
|
302
|
+
existsSync(userShell) &&
|
|
303
|
+
isAllowlistedShell(userShell) &&
|
|
304
|
+
!(isWin && isWindowsWslBash(userShell)) &&
|
|
305
|
+
// Windows OpenSSH can inject the system cmd.exe as ambient SHELL. When
|
|
306
|
+
// Git Bash is installed, treating that as an explicit override breaks the
|
|
307
|
+
// POSIX shell executor path restored by #36/#384/#791.
|
|
308
|
+
!(isWin && windowsBash && isWindowsSystemCmd(userShell))
|
|
309
|
+
? userShell
|
|
310
|
+
: null;
|
|
311
|
+
return {
|
|
312
|
+
javascript: resolveJavascriptRuntime(bun),
|
|
313
|
+
typescript: bun
|
|
314
|
+
? bun
|
|
315
|
+
: commandExists("tsx")
|
|
316
|
+
? "tsx"
|
|
317
|
+
: commandExists("ts-node")
|
|
318
|
+
? "ts-node"
|
|
319
|
+
: null,
|
|
320
|
+
python: runnableExists("python3")
|
|
321
|
+
? "python3"
|
|
322
|
+
: runnableExists("python")
|
|
323
|
+
? "python"
|
|
324
|
+
: runnableExists("py")
|
|
325
|
+
? "py"
|
|
326
|
+
: null,
|
|
327
|
+
shell: shellOverride ?? (isWin
|
|
328
|
+
? resolveWindowsShell(windowsBash)
|
|
329
|
+
: commandExists("bash") ? "bash" : "sh"),
|
|
330
|
+
ruby: commandExists("ruby") ? "ruby" : null,
|
|
331
|
+
go: commandExists("go") ? "go" : null,
|
|
332
|
+
rust: commandExists("rustc") ? "rustc" : null,
|
|
333
|
+
php: commandExists("php") ? "php" : null,
|
|
334
|
+
perl: commandExists("perl") ? "perl" : null,
|
|
335
|
+
r: commandExists("Rscript")
|
|
336
|
+
? "Rscript"
|
|
337
|
+
: commandExists("r")
|
|
338
|
+
? "r"
|
|
339
|
+
: null,
|
|
340
|
+
elixir: commandExists("elixir") ? "elixir" : null,
|
|
341
|
+
csharp: commandExists("dotnet-script") ? "dotnet-script" : null,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
export function hasBunRuntime() {
|
|
345
|
+
return bunExists();
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Cached result of {@link resolveHookRuntime}. Populated on first call so the
|
|
349
|
+
* relatively expensive `bun --version` probe runs at most once per process.
|
|
350
|
+
* Reset via {@link resetHookRuntimeCache} (test-only).
|
|
351
|
+
*/
|
|
352
|
+
let _hookRuntimeCache = null;
|
|
353
|
+
/**
|
|
354
|
+
* Reset the hook-runtime resolution cache. Test-only — production code
|
|
355
|
+
* should never call this. Vitest mocks `node:child_process`/`node:fs`
|
|
356
|
+
* per-test, so the per-process cache from a previous test would otherwise
|
|
357
|
+
* mask the mock and yield the host's real bun/node detection result.
|
|
358
|
+
*/
|
|
359
|
+
export function resetHookRuntimeCache() {
|
|
360
|
+
_hookRuntimeCache = null;
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Parse a `bun --version` stdout string and return true when the version is
|
|
364
|
+
* ≥1.0.0. Anything that doesn't match `MAJOR.MINOR.PATCH` (with optional
|
|
365
|
+
* pre-release suffix) returns false — we refuse to trust runtimes whose
|
|
366
|
+
* version we can't read because the failure mode is silent miscompare
|
|
367
|
+
* (e.g. a banner line getting interpreted as "0.0.0").
|
|
368
|
+
*/
|
|
369
|
+
function bunVersionAtLeast1(versionOutput) {
|
|
370
|
+
const trimmed = versionOutput.trim();
|
|
371
|
+
const m = /^(\d+)\.(\d+)\.(\d+)/.exec(trimmed);
|
|
372
|
+
if (!m)
|
|
373
|
+
return false;
|
|
374
|
+
const major = Number(m[1]);
|
|
375
|
+
return Number.isFinite(major) && major >= 1;
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Resolve the JS runtime to use for spawning hook scripts (issue #738).
|
|
379
|
+
*
|
|
380
|
+
* Returns Bun when:
|
|
381
|
+
* - a bun binary is located via {@link bunCommand} (already handles the
|
|
382
|
+
* Windows .cmd shim trap from #506 + absolute path fallbacks), AND
|
|
383
|
+
* - `bun --version` exits 0 within the probe timeout, AND
|
|
384
|
+
* - the reported semver major is ≥1.
|
|
385
|
+
*
|
|
386
|
+
* Returns Node (`process.execPath`) on every other path — missing bun,
|
|
387
|
+
* version probe failure, version <1, malformed version banner. Silent
|
|
388
|
+
* fallback: never throws, never logs to stderr (a noisy log would clutter
|
|
389
|
+
* the same MCP boot output that #719 tightened up).
|
|
390
|
+
*
|
|
391
|
+
* Result is cached at module load so the cost is amortised across every
|
|
392
|
+
* hook command emission for the lifetime of the process. The cache also
|
|
393
|
+
* keeps the behaviour deterministic — if the user `brew uninstall bun`
|
|
394
|
+
* mid-session, the cached resolution stays valid for that session and the
|
|
395
|
+
* next MCP boot re-detects.
|
|
396
|
+
*
|
|
397
|
+
* Why bun ≥1.0 instead of "any bun":
|
|
398
|
+
* - Bun 0.x had multiple ESM/module-resolution regressions that broke
|
|
399
|
+
* dynamic `import()` inside hooks (and our hooks do ~7 dynamic imports
|
|
400
|
+
* in `pretooluse.mjs`).
|
|
401
|
+
* - 1.0 ships stable npm-compat that our better-sqlite3-adjacent code
|
|
402
|
+
* relies on indirectly (hooks share `ensure-deps.mjs` which is
|
|
403
|
+
* bun-safe past 1.0 but not 0.x).
|
|
404
|
+
*
|
|
405
|
+
* NOT used by:
|
|
406
|
+
* - `buildNodeCommand` — kept on `process.execPath` for openclaw doctor /
|
|
407
|
+
* upgrade hints which must invoke the better-sqlite3-loading CLI on
|
|
408
|
+
* Node (#543: bun cannot dlopen better-sqlite3's prebuilt .node).
|
|
409
|
+
* - `ensure-deps.mjs` — separate path, must stay on Node for the same
|
|
410
|
+
* reason.
|
|
411
|
+
* - `ctx_upgrade` — separate path, must stay on Node for the same reason.
|
|
412
|
+
*/
|
|
413
|
+
/**
|
|
414
|
+
* Liveness-guarded Node path for the hook-runtime fallback (issue #841).
|
|
415
|
+
*
|
|
416
|
+
* `process.execPath` is pinned into every baked hook command because PATH
|
|
417
|
+
* resolution is unreliable for hooks (#190 snap-Node re-invokes the wrapper;
|
|
418
|
+
* #369 Windows Git Bash / MSYS can't resolve a bare `node`). But under a
|
|
419
|
+
* version manager (mise / asdf / nvm) execPath is a *version-pinned* absolute
|
|
420
|
+
* path — e.g. `~/.local/share/mise/installs/node/20.1.0/bin/node`. A routine
|
|
421
|
+
* `mise upgrade node` installs the next patch and DELETES the 20.1.0 dir, so
|
|
422
|
+
* the cached path dangles and every hook spawn fails with ENOENT — silently
|
|
423
|
+
* killing context-mode for that user.
|
|
424
|
+
*
|
|
425
|
+
* Same liveness-guard shape as the #800/#803 fix in
|
|
426
|
+
* {@link resolveJavascriptRuntime}: use the pinned execPath IFF it still
|
|
427
|
+
* exists on disk (preserving the #190/#369 reasons it was pinned), otherwise
|
|
428
|
+
* re-resolve a working `node` from PATH. The version manager's shim dir is on
|
|
429
|
+
* PATH and always points at the current patch, so bare `node` heals the host
|
|
430
|
+
* without a re-install. Falls back to the (stale) execPath only when no PATH
|
|
431
|
+
* node is reachable either — a strictly-better last resort than a dangling
|
|
432
|
+
* versioned path, and the doctor/upgrade flows surface the actionable error.
|
|
433
|
+
*/
|
|
434
|
+
function liveNodeRuntime() {
|
|
435
|
+
if (existsSync(process.execPath)) {
|
|
436
|
+
return { path: process.execPath, isBun: false };
|
|
437
|
+
}
|
|
438
|
+
if (commandExists("node")) {
|
|
439
|
+
return { path: "node", isBun: false };
|
|
440
|
+
}
|
|
441
|
+
return { path: process.execPath, isBun: false };
|
|
442
|
+
}
|
|
443
|
+
export function resolveHookRuntime() {
|
|
444
|
+
if (_hookRuntimeCache)
|
|
445
|
+
return _hookRuntimeCache;
|
|
446
|
+
const nodeFallback = liveNodeRuntime();
|
|
447
|
+
try {
|
|
448
|
+
if (!bunExists()) {
|
|
449
|
+
_hookRuntimeCache = nodeFallback;
|
|
450
|
+
return _hookRuntimeCache;
|
|
451
|
+
}
|
|
452
|
+
const bun = bunCommand();
|
|
453
|
+
// Re-use the same probe shape as getVersion (POSIX execFile, Windows
|
|
454
|
+
// execSync quoted string for DEP0190 compliance).
|
|
455
|
+
let versionOutput;
|
|
456
|
+
try {
|
|
457
|
+
if (process.platform === "win32") {
|
|
458
|
+
const out = execSync(`"${bun}" --version`, {
|
|
459
|
+
encoding: "utf-8",
|
|
460
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
461
|
+
timeout: 5000,
|
|
462
|
+
});
|
|
463
|
+
versionOutput = String(out);
|
|
464
|
+
}
|
|
465
|
+
else {
|
|
466
|
+
const out = execFileSync(bun, ["--version"], {
|
|
467
|
+
encoding: "utf-8",
|
|
468
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
469
|
+
timeout: 5000,
|
|
470
|
+
});
|
|
471
|
+
versionOutput = String(out);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
catch {
|
|
475
|
+
_hookRuntimeCache = nodeFallback;
|
|
476
|
+
return _hookRuntimeCache;
|
|
477
|
+
}
|
|
478
|
+
if (!bunVersionAtLeast1(versionOutput)) {
|
|
479
|
+
_hookRuntimeCache = nodeFallback;
|
|
480
|
+
return _hookRuntimeCache;
|
|
481
|
+
}
|
|
482
|
+
_hookRuntimeCache = { path: bun, isBun: true };
|
|
483
|
+
return _hookRuntimeCache;
|
|
484
|
+
}
|
|
485
|
+
catch {
|
|
486
|
+
_hookRuntimeCache = nodeFallback;
|
|
487
|
+
return _hookRuntimeCache;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
export function getRuntimeSummary(runtimes) {
|
|
491
|
+
const lines = [];
|
|
492
|
+
const bunPreferred = runtimes.javascript?.endsWith("bun") ?? false;
|
|
493
|
+
if (runtimes.javascript) {
|
|
494
|
+
lines.push(` JavaScript: ${runtimes.javascript} (${getVersion(runtimes.javascript)})${bunPreferred ? " ⚡" : ""}`);
|
|
495
|
+
}
|
|
496
|
+
else {
|
|
497
|
+
// #731: host binary (opencode/kilo) AND no PATH-resolvable node.
|
|
498
|
+
// Surface actionable guidance instead of rendering literal `null`.
|
|
499
|
+
lines.push(` JavaScript: not available (install node or bun — host process is not a JS runtime)`);
|
|
500
|
+
}
|
|
501
|
+
if (runtimes.typescript) {
|
|
502
|
+
lines.push(` TypeScript: ${runtimes.typescript} (${getVersion(runtimes.typescript)})`);
|
|
503
|
+
}
|
|
504
|
+
else {
|
|
505
|
+
lines.push(` TypeScript: not available (install bun, tsx, or ts-node)`);
|
|
506
|
+
}
|
|
507
|
+
if (runtimes.python) {
|
|
508
|
+
lines.push(` Python: ${runtimes.python} (${getVersion(runtimes.python)})`);
|
|
509
|
+
}
|
|
510
|
+
else {
|
|
511
|
+
lines.push(` Python: not available`);
|
|
512
|
+
}
|
|
513
|
+
lines.push(` Shell: ${runtimes.shell} (${getVersion(runtimes.shell)})`);
|
|
514
|
+
// Optional runtimes — only show if available
|
|
515
|
+
if (runtimes.ruby)
|
|
516
|
+
lines.push(` Ruby: ${runtimes.ruby} (${getVersion(runtimes.ruby)})`);
|
|
517
|
+
if (runtimes.go)
|
|
518
|
+
lines.push(` Go: ${runtimes.go} (${getVersion(runtimes.go, ["version"])})`);
|
|
519
|
+
if (runtimes.rust)
|
|
520
|
+
lines.push(` Rust: ${runtimes.rust} (${getVersion(runtimes.rust)})`);
|
|
521
|
+
if (runtimes.php)
|
|
522
|
+
lines.push(` PHP: ${runtimes.php} (${getVersion(runtimes.php)})`);
|
|
523
|
+
if (runtimes.perl)
|
|
524
|
+
lines.push(` Perl: ${runtimes.perl} (${getVersion(runtimes.perl)})`);
|
|
525
|
+
if (runtimes.r)
|
|
526
|
+
lines.push(` R: ${runtimes.r} (${getVersion(runtimes.r)})`);
|
|
527
|
+
if (runtimes.elixir)
|
|
528
|
+
lines.push(` Elixir: ${runtimes.elixir} (${getVersion(runtimes.elixir)})`);
|
|
529
|
+
if (runtimes.csharp)
|
|
530
|
+
lines.push(` C#: ${runtimes.csharp} (${getVersion(runtimes.csharp)})`);
|
|
531
|
+
if (!bunPreferred) {
|
|
532
|
+
lines.push("");
|
|
533
|
+
lines.push(" Tip: Install Bun for 3-5x faster JS/TS execution → https://bun.sh");
|
|
534
|
+
}
|
|
535
|
+
return lines.join("\n");
|
|
536
|
+
}
|
|
537
|
+
export function getAvailableLanguages(runtimes) {
|
|
538
|
+
const langs = ["javascript", "shell"];
|
|
539
|
+
if (runtimes.typescript)
|
|
540
|
+
langs.push("typescript");
|
|
541
|
+
if (runtimes.python)
|
|
542
|
+
langs.push("python");
|
|
543
|
+
if (runtimes.ruby)
|
|
544
|
+
langs.push("ruby");
|
|
545
|
+
if (runtimes.go)
|
|
546
|
+
langs.push("go");
|
|
547
|
+
if (runtimes.rust)
|
|
548
|
+
langs.push("rust");
|
|
549
|
+
if (runtimes.php)
|
|
550
|
+
langs.push("php");
|
|
551
|
+
if (runtimes.perl)
|
|
552
|
+
langs.push("perl");
|
|
553
|
+
if (runtimes.r)
|
|
554
|
+
langs.push("r");
|
|
555
|
+
if (runtimes.elixir)
|
|
556
|
+
langs.push("elixir");
|
|
557
|
+
if (runtimes.csharp)
|
|
558
|
+
langs.push("csharp");
|
|
559
|
+
return langs;
|
|
560
|
+
}
|
|
561
|
+
export function buildCommand(runtimes, language, filePath) {
|
|
562
|
+
switch (language) {
|
|
563
|
+
case "javascript":
|
|
564
|
+
if (!runtimes.javascript) {
|
|
565
|
+
// #731: in-process plugin host (opencode/kilo binary) AND no
|
|
566
|
+
// PATH-resolvable node. Refuse early with an actionable error
|
|
567
|
+
// instead of spawning the host binary (the original bug shape).
|
|
568
|
+
throw new Error("No JavaScript runtime available. Install Node.js or Bun on PATH (the host process is not itself a JS runtime).");
|
|
569
|
+
}
|
|
570
|
+
return BUN_BASENAME.test(runtimeBasename(runtimes.javascript))
|
|
571
|
+
? [runtimes.javascript, "run", filePath]
|
|
572
|
+
: [runtimes.javascript, filePath];
|
|
573
|
+
case "typescript":
|
|
574
|
+
if (!runtimes.typescript) {
|
|
575
|
+
throw new Error("No TypeScript runtime available. Install one of: bun (recommended), tsx (npm i -g tsx), or ts-node.");
|
|
576
|
+
}
|
|
577
|
+
if (BUN_BASENAME.test(runtimeBasename(runtimes.typescript)))
|
|
578
|
+
return [runtimes.typescript, "run", filePath];
|
|
579
|
+
if (runtimes.typescript === "tsx")
|
|
580
|
+
return ["tsx", filePath];
|
|
581
|
+
return ["ts-node", filePath];
|
|
582
|
+
case "python":
|
|
583
|
+
if (!runtimes.python) {
|
|
584
|
+
throw new Error("No Python runtime available. Install python3 or python.");
|
|
585
|
+
}
|
|
586
|
+
return [runtimes.python, filePath];
|
|
587
|
+
case "shell": {
|
|
588
|
+
// Re-evaluate platform per call so detection-time and command-build-time
|
|
589
|
+
// can be tested independently (and to allow tests to stub process.platform).
|
|
590
|
+
const winNow = process.platform === "win32";
|
|
591
|
+
if (winNow) {
|
|
592
|
+
const shellName = runtimes.shell.toLowerCase();
|
|
593
|
+
if (shellName.includes("bash") || shellName.endsWith("/sh") || shellName.endsWith("\\sh.exe")) {
|
|
594
|
+
// bash -c "source 'path'" — avoids MSYS2 path mangling on non-C:
|
|
595
|
+
// drives. When bash.exe receives a script as a direct argument,
|
|
596
|
+
// MSYS rewrites D:\tmp\script → D:\c\tmp\script and execution
|
|
597
|
+
// breaks. The -c flag prevents MSYS from touching the file arg.
|
|
598
|
+
// Single-quote escape: ' → '\''
|
|
599
|
+
const escaped = filePath.replace(/'/g, "'\\''");
|
|
600
|
+
return [runtimes.shell, "-c", `source '${escaped}'`];
|
|
601
|
+
}
|
|
602
|
+
if (shellName.includes("powershell") || shellName.includes("pwsh")) {
|
|
603
|
+
// Windows PowerShell defaults to Restricted when no execution policy
|
|
604
|
+
// is configured. Use process-scoped Bypass so generated temp scripts
|
|
605
|
+
// run without changing machine/user policy.
|
|
606
|
+
return [runtimes.shell, "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", filePath];
|
|
607
|
+
}
|
|
608
|
+
const shellBase = shellName.split(/[\\/]/).pop() ?? shellName;
|
|
609
|
+
if (shellBase === "cmd" || shellBase === "cmd.exe") {
|
|
610
|
+
return [runtimes.shell, "/d", "/s", "/c", filePath];
|
|
611
|
+
}
|
|
612
|
+
// Other Windows shells: direct file.
|
|
613
|
+
}
|
|
614
|
+
return [runtimes.shell, filePath];
|
|
615
|
+
}
|
|
616
|
+
case "ruby":
|
|
617
|
+
if (!runtimes.ruby) {
|
|
618
|
+
throw new Error("Ruby not available. Install ruby.");
|
|
619
|
+
}
|
|
620
|
+
return [runtimes.ruby, filePath];
|
|
621
|
+
case "go":
|
|
622
|
+
if (!runtimes.go) {
|
|
623
|
+
throw new Error("Go not available. Install go.");
|
|
624
|
+
}
|
|
625
|
+
return ["go", "run", filePath];
|
|
626
|
+
case "rust": {
|
|
627
|
+
if (!runtimes.rust) {
|
|
628
|
+
throw new Error("Rust not available. Install rustc via https://rustup.rs");
|
|
629
|
+
}
|
|
630
|
+
// Rust needs compile + run — handled specially in executor
|
|
631
|
+
return ["__rust_compile_run__", filePath];
|
|
632
|
+
}
|
|
633
|
+
case "php":
|
|
634
|
+
if (!runtimes.php) {
|
|
635
|
+
throw new Error("PHP not available. Install php.");
|
|
636
|
+
}
|
|
637
|
+
return ["php", filePath];
|
|
638
|
+
case "perl":
|
|
639
|
+
if (!runtimes.perl) {
|
|
640
|
+
throw new Error("Perl not available. Install perl.");
|
|
641
|
+
}
|
|
642
|
+
return ["perl", filePath];
|
|
643
|
+
case "r":
|
|
644
|
+
if (!runtimes.r) {
|
|
645
|
+
throw new Error("R not available. Install R / Rscript.");
|
|
646
|
+
}
|
|
647
|
+
return [runtimes.r, filePath];
|
|
648
|
+
case "elixir":
|
|
649
|
+
if (!runtimes.elixir) {
|
|
650
|
+
throw new Error("Elixir not available. Install elixir.");
|
|
651
|
+
}
|
|
652
|
+
return ["elixir", filePath];
|
|
653
|
+
case "csharp":
|
|
654
|
+
if (!runtimes.csharp) {
|
|
655
|
+
throw new Error("C# not available. Install dotnet-script via `dotnet tool install -g dotnet-script`.");
|
|
656
|
+
}
|
|
657
|
+
return [runtimes.csharp, filePath];
|
|
658
|
+
}
|
|
659
|
+
}
|