context-mode 2.0.0-next.7 → 2.0.0-next.9

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.
@@ -1,4 +1,5 @@
1
1
  {
2
+ "description": "Context window saver for 10 coding agents. Local and free: sandboxed code execution, FTS5 search, session continuity.",
2
3
  "hooks": "hooks/qwen.json",
3
4
  "mcpServers": {
4
5
  "context-mode": {
@@ -11,5 +12,5 @@
11
12
  }
12
13
  },
13
14
  "name": "context-mode",
14
- "version": "2.0.0-next.7"
15
+ "version": "2.0.0-next.9"
15
16
  }
package/run.mjs CHANGED
@@ -236,7 +236,7 @@ var EDIT_TOOLS = Object.freeze(["Write", "Edit"]);
236
236
  var EDIT_PATH_KEYS = Object.freeze(["file_path", "path"]);
237
237
  var STATS_COLUMNS = Object.freeze(["Scope", "Produced", "Returned", "Saved", "Reduction %"]);
238
238
  var PURGE_SCOPES = Object.freeze(["session", "source", "project"]);
239
- var DOCTOR_CHECK_ORDER = Object.freeze(["D1", "D2", "D3", "D4", "D5", "D5b", "D6", "D7", "D8", "D9", "D10"]);
239
+ var DOCTOR_CHECK_ORDER = Object.freeze(["D1", "D2", "D3", "D4", "D5", "D5b", "D6", "D7", "D8", "D9", "D10", "D11"]);
240
240
  var DOCTOR_LABEL = Object.freeze({
241
241
  D1: "version",
242
242
  D2: "node",
@@ -248,7 +248,8 @@ var DOCTOR_LABEL = Object.freeze({
248
248
  D7: "project and worktree",
249
249
  D8: "context.db",
250
250
  D9: "index",
251
- D10: "enforcement mode"
251
+ D10: "enforcement mode",
252
+ D11: "shell"
252
253
  });
253
254
  var REPORT_TRUNCATED_LINE = `(report truncated at ${REPORT_CAP} B; run ctx_doctor again after the first fix)`;
254
255
  var D2_NODE_FIX = `install Node ${NODE_MIN} or later`;
@@ -271,6 +272,9 @@ var GIT_HEAD_SHA_ARGS = Object.freeze(["rev-parse", "HEAD"]);
271
272
  var UPGRADE_GIT_CHECKOUT_AGENTS = Object.freeze(["pi", "opencode"]);
272
273
  var GIT_NO_PROMPT_ENV = Object.freeze({ GIT_TERMINAL_PROMPT: "0" });
273
274
  var GIT_NO_CRLF_ENV = Object.freeze({ GIT_CONFIG_COUNT: "1", GIT_CONFIG_KEY_0: "core.autocrlf", GIT_CONFIG_VALUE_0: "false" });
275
+ var GIT_BIN = "git";
276
+ var WIN_SHELL_LANGS = Object.freeze(["shell", "bash", "sh"]);
277
+ var GIT_BASH_RELATIVE = Object.freeze([["..", "bin", "bash.exe"], ["..", "..", "bin", "bash.exe"]]);
274
278
  var OPS_SPLIT_FILES = Object.freeze([
275
279
  "doctor-env.ts",
276
280
  "doctor-config.ts",
@@ -287,10 +291,10 @@ var OPS_SPLIT_FILES = Object.freeze([
287
291
  var HOOK_HOST_FILE = Object.freeze({
288
292
  "claude-code": { root: "plugin", file: ["hooks", "claude-code.json"], key: ["hooks"], shape: "nested", timeoutKey: "timeout", matcher: true },
289
293
  codex: { root: "home", file: [".codex", "hooks.json"], key: ["hooks"], shape: "nested", timeoutKey: "timeout", matcher: true },
290
- gemini: { root: "home", file: [".gemini", "settings.json"], key: ["hooks"], shape: "nested", timeoutKey: "timeout", matcher: false },
291
- qwen: { root: "home", file: [".qwen", "settings.json"], key: ["hooks"], shape: "nested", timeoutKey: "timeout", matcher: true },
294
+ gemini: { root: "plugin", file: ["hooks", "hooks.json"], key: ["hooks"], shape: "nested", timeoutKey: "timeout", matcher: false },
295
+ qwen: { root: "plugin", file: ["hooks", "qwen.json"], key: ["hooks"], shape: "nested", timeoutKey: "timeout", matcher: true },
292
296
  "copilot-cli": { root: "home", file: [".copilot", "hooks", "context-mode.json"], key: ["hooks"], shape: "flat", timeoutKey: "timeoutSec", matcher: false },
293
- vscode: { root: "project", file: [".github", "hooks", "context-mode.json"], key: ["hooks"], shape: "flat", timeoutKey: "timeout", matcher: false },
297
+ vscode: { root: "home", file: [".copilot", "hooks", "context-mode-vscode.json"], key: ["hooks"], shape: "flat", timeoutKey: "timeout", matcher: false },
294
298
  kiro: { root: "home", file: [".kiro", "hooks", "context-mode.json"], key: ["hooks"], shape: "kiro", timeoutKey: "timeout", matcher: false },
295
299
  zcode: { root: "home", file: [".zcode", "cli", "config.json"], key: ["hooks", "events"], shape: "zcode", timeoutKey: "timeoutMs", matcher: true },
296
300
  pi: null,
@@ -324,12 +328,16 @@ var V1_SCRIPT_NAMES = Object.freeze([
324
328
  ]);
325
329
  var V1_CODEX_HOOKS_DIR = Object.freeze(["hooks", "codex"]);
326
330
  var V1_CACHE_HEAL_EXTS = Object.freeze([".mjs", ".sh"]);
331
+ var VSCODE_USER_DIR_SEGMENTS = Object.freeze(["Code", "User"]);
332
+ var VSCODE_MCP_FILE = "mcp.json";
327
333
  var KIRO_AGENT_FILE = Object.freeze(["agents", "default.json"]);
328
334
  var KIRO_STEERING_FILE = Object.freeze(["steering", "KIRO.md"]);
329
335
  var OPENCODE_CONFIG_FILES = Object.freeze(["opencode.jsonc", "opencode.json"]);
330
336
  var V1_DATA_ROOT_ENTRIES = Object.freeze(["content", "platform.json"]);
331
337
  var PI_ROLE = "custom";
332
338
  var V1_PATH_SEPS = Object.freeze(["/", "\\"]);
339
+ var HOST_SEP_TOKEN = "${/}";
340
+ var HOOK_SCRIPT_SEPS = Object.freeze([...V1_PATH_SEPS, HOST_SEP_TOKEN]);
333
341
  var V1_REASON = Object.freeze({
334
342
  hook: "old context-mode hook entry: command contains context-mode and an old script name or context-mode hook",
335
343
  cacheHealHook: "old context-mode cache-heal SessionStart hook entry",
@@ -360,7 +368,7 @@ var CODEX_APPROVAL_MODE_FIELD = "default_tools_approval_mode";
360
368
  var CODEX_APPROVAL_APPROVE = "approve";
361
369
  var OWN_MCP_ENTRY = Object.freeze({
362
370
  codex: { file: [".codex", "config.toml"], key: ["mcp_servers", SERVER_KEY], fields: { [CODEX_OMIT_TOOLS_FROM_FIELD]: CODEX_OMIT_DEFERRED, [CODEX_APPROVAL_MODE_FIELD]: CODEX_APPROVAL_APPROVE } },
363
- vscode: { file: [".vscode", "mcp.json"], key: ["servers", SERVER_KEY] },
371
+ vscode: { file: [VSCODE_MCP_FILE], key: ["servers", SERVER_KEY] },
364
372
  kiro: { file: [".kiro", "settings", "mcp.json"], key: ["mcpServers", SERVER_KEY] },
365
373
  zcode: {
366
374
  file: [".zcode", "cli", "config.json"],
@@ -734,8 +742,14 @@ var VSCODE_EVENTS = ["SessionStart", "UserPromptSubmit", "PreToolUse", "PostTool
734
742
  var MCP_PREFIX2 = "mcp_";
735
743
  var MCP_SEP2 = "_";
736
744
  var SESSION_START_NEW = "new";
737
- var SHELL_TOOL_NAME = "run_in_terminal";
738
- var NATIVE_TO_CANONICAL4 = { [SHELL_TOOL_NAME]: "Bash" };
745
+ var SHELL_TOOL_NAMES = Object.freeze(["run_in_terminal", "runTerminalCommand", "bash", "powershell"]);
746
+ var READ_TOOL_NAMES = Object.freeze(["read_file", "view"]);
747
+ var WEBFETCH_TOOL_NAMES = Object.freeze(["fetch_webpage", "web_fetch"]);
748
+ var NATIVE_TO_CANONICAL4 = Object.fromEntries([
749
+ ...SHELL_TOOL_NAMES.map((n) => [n, "Bash"]),
750
+ ...READ_TOOL_NAMES.map((n) => [n, "Read"]),
751
+ ...WEBFETCH_TOOL_NAMES.map((n) => [n, "WebFetch"])
752
+ ]);
739
753
  function canonicalName4(name) {
740
754
  if (name.startsWith(TOOL_NAME_PREFIX6)) return name.slice(TOOL_NAME_PREFIX6.length);
741
755
  if (Object.hasOwn(NATIVE_TO_CANONICAL4, name)) return NATIVE_TO_CANONICAL4[name];
@@ -745,6 +759,26 @@ function canonicalName4(name) {
745
759
  if (sep3 <= 0 || sep3 === rest.length - 1) return name;
746
760
  return "mcp:" + rest.slice(0, sep3) + ":" + rest.slice(sep3 + MCP_SEP2.length);
747
761
  }
762
+ var isNum = (v) => typeof v === "number" && Number.isFinite(v);
763
+ function policyInput(name, canonical, input) {
764
+ if (canonical === "Read" && READ_TOOL_NAMES.includes(name)) {
765
+ const out = { ...input };
766
+ if (name === "read_file") {
767
+ if (typeof input.filePath === "string") out.file_path = input.filePath;
768
+ if (isNum(input.startLine)) out.offset = input.startLine;
769
+ if (isNum(input.startLine) && isNum(input.endLine) && input.endLine >= input.startLine) out.limit = input.endLine - input.startLine + 1;
770
+ } else if (Array.isArray(input.view_range) && isNum(input.view_range[0])) {
771
+ out.offset = input.view_range[0];
772
+ const end = input.view_range[1];
773
+ if (isNum(end) && end >= input.view_range[0]) out.limit = end - input.view_range[0] + 1;
774
+ }
775
+ return out;
776
+ }
777
+ if (canonical === "WebFetch" && name === "fetch_webpage" && Array.isArray(input.urls) && typeof input.urls[0] === "string") {
778
+ return { ...input, url: input.urls[0] };
779
+ }
780
+ return input;
781
+ }
748
782
  function readResult4(resp) {
749
783
  const text = resp === void 0 ? "" : typeof resp === "string" ? resp : JSON.stringify(resp);
750
784
  const isError = typeof resp === "object" && resp !== null && resp.is_error === true;
@@ -792,7 +826,8 @@ function decodeCamel(p, name) {
792
826
  if (event === "UserPromptSubmit" && typeof p.prompt === "string") ev.prompt = p.prompt;
793
827
  if (event === "PreToolUse" || event === "PostToolUse") {
794
828
  const toolName = String(p.toolName ?? "");
795
- ev.tool = { name: toolName, canonicalName: canonicalName4(toolName), input: readArgs2(p.toolArgs) };
829
+ const canonical = canonicalName4(toolName);
830
+ ev.tool = { name: toolName, canonicalName: canonical, input: policyInput(toolName, canonical, readArgs2(p.toolArgs)) };
796
831
  }
797
832
  if (event === "PostToolUse") ev.result = readCamelResult(p.toolResult);
798
833
  return ev;
@@ -814,7 +849,8 @@ function decode6(raw) {
814
849
  if (event === "PreToolUse" || event === "PostToolUse") {
815
850
  const name = String(p.tool_name ?? "");
816
851
  const input = isRecord2(p.tool_input) ? p.tool_input : {};
817
- ev.tool = { name, canonicalName: canonicalName4(name), input };
852
+ const canonical = canonicalName4(name);
853
+ ev.tool = { name, canonicalName: canonical, input: policyInput(name, canonical, input) };
818
854
  }
819
855
  if (event === "PostToolUse") ev.result = readResult4(p.tool_response);
820
856
  return ev;
@@ -838,8 +874,8 @@ var KIRO_EVENTS = {
838
874
  };
839
875
  var PROMPT_ENV = "USER_PROMPT";
840
876
  var MCP_PREFIX3 = "mcp_";
841
- var SHELL_TOOL_NAMES = Object.freeze(["execute_bash", "execute_pwsh", "shell"]);
842
- var NATIVE_TO_CANONICAL5 = Object.fromEntries(SHELL_TOOL_NAMES.map((n) => [n, "Bash"]));
877
+ var SHELL_TOOL_NAMES2 = Object.freeze(["execute_bash", "execute_pwsh", "shell"]);
878
+ var NATIVE_TO_CANONICAL5 = Object.fromEntries(SHELL_TOOL_NAMES2.map((n) => [n, "Bash"]));
843
879
  var MCP_SEP3 = "_";
844
880
  var KIRO_SOURCES = { startup: "startup", resume: "resume", clear: "clear", compact: "compact" };
845
881
  function canonicalName5(name) {
@@ -980,8 +1016,8 @@ var OPENCODE_EVENTS = {
980
1016
  compaction: "PreCompact",
981
1017
  "step-finish": "Usage"
982
1018
  };
983
- var SHELL_TOOL_NAME2 = "bash";
984
- var NATIVE_TO_CANONICAL7 = { webfetch: "WebFetch", [SHELL_TOOL_NAME2]: "Bash" };
1019
+ var SHELL_TOOL_NAME = "bash";
1020
+ var NATIVE_TO_CANONICAL7 = { webfetch: "WebFetch", [SHELL_TOOL_NAME]: "Bash" };
985
1021
  function toCanonicalTool(name) {
986
1022
  if (name.startsWith(OWN_WIRE_PREFIX)) return name.slice(OWN_WIRE_PREFIX.length);
987
1023
  return Object.prototype.hasOwnProperty.call(NATIVE_TO_CANONICAL7, name) ? NATIVE_TO_CANONICAL7[name] : name;
@@ -1130,7 +1166,8 @@ var ADAPTERS = Object.freeze([
1130
1166
  projectSource: "arg:workspaceFolder",
1131
1167
  toolNamePrefix: TOOL_NAME_PREFIX6,
1132
1168
  // PreCompact sets the pending flag (ADR-0009 §6, DEVIATIONS D-M3-A4-1).
1133
- nativeTools: { shell: [SHELL_TOOL_NAME] },
1169
+ nativeTools: { shell: [...SHELL_TOOL_NAMES], read: [...READ_TOOL_NAMES], webfetch: [...WEBFETCH_TOOL_NAMES] },
1170
+ // D-HF-VSCODE-PROJECT-1, D-N7-VSCODE-TOOLS-1
1134
1171
  execReadOnlyHint: false,
1135
1172
  eventMap: { ...CLAUDE_EVENTS, PreCompact: "PreCompact" },
1136
1173
  compactDelivery: "pending-flag",
@@ -1148,7 +1185,7 @@ var ADAPTERS = Object.freeze([
1148
1185
  installMode: "mcp-entry",
1149
1186
  projectSource: "arg:--agent+cwd",
1150
1187
  toolNamePrefix: TOOL_NAME_PREFIX7,
1151
- nativeTools: { shell: [...SHELL_TOOL_NAMES] },
1188
+ nativeTools: { shell: [...SHELL_TOOL_NAMES2] },
1152
1189
  execReadOnlyHint: false,
1153
1190
  // D-HF-KIRO-SHELL-1, D-N7-KIRO-1
1154
1191
  eventMap: { SessionStart: "SessionStart", UserPromptSubmit: "UserPromptSubmit", PreToolUse: "PreToolUse", PostToolUse: "PostToolUse" },
@@ -1205,7 +1242,7 @@ var ADAPTERS = Object.freeze([
1205
1242
  installMode: "in-process",
1206
1243
  projectSource: "spawn-cwd",
1207
1244
  toolNamePrefix: TOOL_NAME_PREFIX10,
1208
- nativeTools: { shell: [SHELL_TOOL_NAME2] },
1245
+ nativeTools: { shell: [SHELL_TOOL_NAME] },
1209
1246
  execReadOnlyHint: false,
1210
1247
  // D-HF-OPENCODE-SHELL-1
1211
1248
  // Usage: step-finish cost row (DEVIATIONS D-M5-arch-2).
@@ -1243,6 +1280,8 @@ var ERROR_TEXT = Object.freeze({
1243
1280
  NO_PROJECT: "Project source <kind> is missing. Fix: run ctx_doctor.",
1244
1281
  UNKNOWN_FIELD: 'Unknown field "<f>". Fix: use "queries".',
1245
1282
  TIMEOUT: "Timed out after <ms> ms. Fix: pass background: true.",
1283
+ NO_GIT_BASH: "Git Bash not found. Fix: install Git for Windows.",
1284
+ // D-VT-WINBASH-1
1246
1285
  NO_RUNTIME: "No <lang> runtime on PATH. Fix: install it or pick another language.",
1247
1286
  FETCH_FAILED: "Fetch failed: <code>. Fix: check the URL or proxy.",
1248
1287
  SSRF: "Blocked private address <host>. Fix: use a public URL.",
@@ -4247,22 +4286,25 @@ function parseShell(cmd) {
4247
4286
  import { join as join2 } from "node:path";
4248
4287
 
4249
4288
  // src/core/project/config-dir.ts
4250
- import { join } from "node:path";
4289
+ import { join, posix, win32 } from "node:path";
4251
4290
  var { mkdirSync: mkdirSync2, realpathSync } = process.getBuiltinModule("node:fs");
4252
- function homeDirOf(env) {
4253
- const home = env.HOME;
4254
- if (home !== void 0 && home !== "") return home;
4255
- const profile = env.USERPROFILE;
4256
- if (profile !== void 0 && profile !== "") return profile;
4257
- return null;
4258
- }
4259
- function homeOf(env) {
4260
- const home = homeDirOf(env);
4291
+ function homeDirOf(env, platform = process.platform) {
4292
+ const set = (v) => v !== void 0 && v !== "";
4293
+ if (platform === "win32") {
4294
+ if (set(env.USERPROFILE)) return env.USERPROFILE;
4295
+ if (set(env.HOMEDRIVE) && set(env.HOMEPATH)) return env.HOMEDRIVE + env.HOMEPATH;
4296
+ return set(env.HOME) ? env.HOME : null;
4297
+ }
4298
+ if (set(env.HOME)) return env.HOME;
4299
+ return set(env.USERPROFILE) ? env.USERPROFILE : null;
4300
+ }
4301
+ function requireHomeDir(env, platform = process.platform) {
4302
+ const home = homeDirOf(env, platform);
4261
4303
  if (home !== null) return home;
4262
- throw new CtxError("NO_HOME", { name: process.platform === "win32" ? "USERPROFILE" : "HOME" });
4304
+ throw new CtxError("NO_HOME", { name: platform === "win32" ? "USERPROFILE" : "HOME" });
4263
4305
  }
4264
4306
  function resolveConfigDir(env) {
4265
- const dir = join(homeOf(env), DATA_ROOT.slice(1));
4307
+ const dir = join(requireHomeDir(env), DATA_ROOT.slice(1));
4266
4308
  mkdirSync2(dir, { recursive: true });
4267
4309
  return realpathSync.native(dir);
4268
4310
  }
@@ -4965,14 +5007,15 @@ function gitDirs(dir) {
4965
5007
  } catch {
4966
5008
  throw new CtxError("NO_GIT_DIR", { path: dir });
4967
5009
  }
5010
+ const start = cur;
4968
5011
  for (; ; ) {
4969
5012
  const gitDir = gitDirAt(join4(cur, GIT_DIR_NAME));
4970
5013
  if (gitDir !== null) {
4971
5014
  const common = firstLine(join4(gitDir, GIT_COMMONDIR_FILE));
4972
- return { commonDir: realpathSync3.native(common === null ? gitDir : resolve2(gitDir, common)), worktree: cur };
5015
+ return { commonDir: realpathSync3.native(common === null ? gitDir : resolve2(gitDir, common)), worktree: cur, start };
4973
5016
  }
4974
5017
  const up = dirname2(cur);
4975
- if (up === cur) return null;
5018
+ if (up === cur) return { commonDir: null, start };
4976
5019
  cur = up;
4977
5020
  }
4978
5021
  }
@@ -5004,7 +5047,7 @@ function firstLine(file) {
5004
5047
  }
5005
5048
  function git(dir, args) {
5006
5049
  const { spawnSync } = process.getBuiltinModule("node:child_process");
5007
- const r = spawnSync("git", args, { cwd: dir, shell: false, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
5050
+ const r = spawnSync("git", args, { cwd: dir, shell: false, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
5008
5051
  if (r.error !== void 0 || r.status !== 0) throw new CtxError("NO_GIT_DIR", { path: dir });
5009
5052
  return r.stdout.split("\n").filter((l) => l !== "");
5010
5053
  }
@@ -5018,16 +5061,24 @@ function resolveProject(agent, src) {
5018
5061
  const dir = dirOf(source, src);
5019
5062
  if (dir === void 0) throw new CtxError("NO_PROJECT", { kind: source });
5020
5063
  const repo = gitDirs(dir);
5021
- const plain = repo === null ? realpathSync3.native(dir) : void 0;
5022
- const commonDir = repo?.commonDir ?? plain;
5023
- const worktree = repo?.worktree ?? plain;
5024
- const projectKey = sha256Hex(commonDir).slice(0, PROJECT_KEY_HEX_LEN);
5025
- const projectDir = join4(resolveConfigDir(process.env), PROJECTS_DIR_NAME, projectKey);
5064
+ const plain = repo.commonDir === null ? repo.start : void 0;
5065
+ const commonDir = repo.commonDir ?? repo.start;
5066
+ const worktree = repo.commonDir === null ? repo.start : repo.worktree;
5067
+ let key;
5068
+ let pdir;
5026
5069
  let root;
5027
5070
  return {
5028
- projectKey,
5029
- projectDir,
5030
- dbPath: join4(projectDir, DB_FILE_NAME),
5071
+ get projectKey() {
5072
+ key ??= sha256Hex(commonDir).slice(0, PROJECT_KEY_HEX_LEN);
5073
+ return key;
5074
+ },
5075
+ get projectDir() {
5076
+ pdir ??= join4(resolveConfigDir(process.env), PROJECTS_DIR_NAME, this.projectKey);
5077
+ return pdir;
5078
+ },
5079
+ get dbPath() {
5080
+ return join4(this.projectDir, DB_FILE_NAME);
5081
+ },
5031
5082
  worktree,
5032
5083
  get containmentRoot() {
5033
5084
  root ??= plain ?? containmentRootOf(dir, commonDir);
@@ -5079,6 +5130,14 @@ function buildChildEnv(parent, extra) {
5079
5130
  // src/core/sandbox/runtimes.ts
5080
5131
  import { accessSync, constants as fsConstants, statSync as statSync3 } from "node:fs";
5081
5132
  import path from "node:path";
5133
+
5134
+ // src/core/sandbox/path-entries.ts
5135
+ function pathEntries(value, platform = process.platform, delimiter = platform === "win32" ? ";" : ":") {
5136
+ const win = platform === "win32";
5137
+ return value.split(delimiter).map((e) => win && e.length >= 2 && e.startsWith('"') && e.endsWith('"') ? e.slice(1, -1) : e).filter(Boolean);
5138
+ }
5139
+
5140
+ // src/core/sandbox/runtimes.ts
5082
5141
  var PATHEXT_SEPARATOR = ";";
5083
5142
  var WIN_STARTABLE_EXT = Object.freeze([".exe", ".com"]);
5084
5143
  function isWin() {
@@ -5088,7 +5147,7 @@ function candidates(lang) {
5088
5147
  const win = isWin();
5089
5148
  switch (lang) {
5090
5149
  case "shell":
5091
- return win ? ["sh.exe", "bash.exe"] : ["sh"];
5150
+ return win ? [] : ["sh"];
5092
5151
  case "powershell":
5093
5152
  return ["pwsh", "pwsh.exe", "powershell.exe"];
5094
5153
  case "javascript":
@@ -5119,7 +5178,8 @@ function isRunnable(full) {
5119
5178
  }
5120
5179
  }
5121
5180
  function findRuntime(lang, env = process.env) {
5122
- const dirs = (env.PATH ?? "").split(path.delimiter).filter((d) => d !== "");
5181
+ if (isWin() && WIN_SHELL_LANGS.includes(lang)) return findGitBash(env);
5182
+ const dirs = pathEntries(env.PATH ?? "", process.platform, path.delimiter);
5123
5183
  const pathExt = isWin() ? (env.PATHEXT ?? PATHEXT_DEFAULT).split(PATHEXT_SEPARATOR).filter((e) => e !== "") : [];
5124
5184
  const names = candidates(lang);
5125
5185
  let shim = null;
@@ -5135,6 +5195,18 @@ function findRuntime(lang, env = process.env) {
5135
5195
  }
5136
5196
  return shim;
5137
5197
  }
5198
+ function findGitBash(env) {
5199
+ const git2 = findRuntime(GIT_BIN, env);
5200
+ if (git2 === null) return null;
5201
+ for (const rel of GIT_BASH_RELATIVE) {
5202
+ const full = path.resolve(path.dirname(git2), ...rel);
5203
+ if (isRunnable(full)) return full;
5204
+ }
5205
+ return null;
5206
+ }
5207
+ function noRuntimeError(lang) {
5208
+ return isWin() && WIN_SHELL_LANGS.includes(lang) ? new CtxError("NO_GIT_BASH") : new CtxError("NO_RUNTIME", { lang });
5209
+ }
5138
5210
 
5139
5211
  // src/core/sandbox/spawn.ts
5140
5212
  import { spawn } from "node:child_process";
@@ -5641,7 +5713,7 @@ async function runWrapperMain(argv, env) {
5641
5713
  if (d.kind === "deny" && userPolicyDeny(d.reason)) return exit(RUN_FAIL_EXIT, "", d.reason + "\n");
5642
5714
  try {
5643
5715
  const runtime = findRuntime(SHELL);
5644
- if (runtime === null) throw new CtxError("NO_RUNTIME", { lang: SHELL });
5716
+ if (runtime === null) throw noRuntimeError(SHELL);
5645
5717
  const project = projectOf(argv, cwd);
5646
5718
  const r = await runProcess([runtime, "-c", command], {
5647
5719
  cwd,