@letrquan/book 0.2.0 → 0.4.0-beta.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.
Files changed (177) hide show
  1. package/CHANGELOG.md +3771 -40
  2. package/README.md +135 -1311
  3. package/dist/additional-roots-VAWRQ625.js +32 -0
  4. package/dist/{app-MOXXWEOY.js → app-J7BVPSUH.js} +9648 -5509
  5. package/dist/app-J7BVPSUH.js.map +1 -0
  6. package/dist/chunk-2AAOF5B5.js +28 -0
  7. package/dist/chunk-2AAOF5B5.js.map +1 -0
  8. package/dist/chunk-32WBCUAN.js +660 -0
  9. package/dist/chunk-32WBCUAN.js.map +1 -0
  10. package/dist/chunk-4DKHLZRI.js +120 -0
  11. package/dist/chunk-4DKHLZRI.js.map +1 -0
  12. package/dist/{chunk-CAGOB2N4.js → chunk-62EEAUID.js} +6 -4
  13. package/dist/chunk-62EEAUID.js.map +1 -0
  14. package/dist/chunk-AHC7OU75.js +1124 -0
  15. package/dist/chunk-AHC7OU75.js.map +1 -0
  16. package/dist/{chunk-E75IPST5.js → chunk-AJZWLKLQ.js} +73 -70
  17. package/dist/chunk-AJZWLKLQ.js.map +1 -0
  18. package/dist/chunk-B5FC3MYZ.js +395 -0
  19. package/dist/chunk-B5FC3MYZ.js.map +1 -0
  20. package/dist/{chunk-KWHQ2XXL.js → chunk-DNFQTSMJ.js} +55 -13
  21. package/dist/chunk-DNFQTSMJ.js.map +1 -0
  22. package/dist/{chunk-J7MIZ5CA.js → chunk-E5WLQSKA.js} +719 -89
  23. package/dist/chunk-E5WLQSKA.js.map +1 -0
  24. package/dist/chunk-ECLC4CIJ.js +1181 -0
  25. package/dist/chunk-ECLC4CIJ.js.map +1 -0
  26. package/dist/chunk-EEX6QNXV.js +127 -0
  27. package/dist/chunk-EEX6QNXV.js.map +1 -0
  28. package/dist/chunk-EFXVSQ5J.js +931 -0
  29. package/dist/chunk-EFXVSQ5J.js.map +1 -0
  30. package/dist/{chunk-V4BOYR52.js → chunk-ENLB7NYV.js} +1304 -326
  31. package/dist/chunk-ENLB7NYV.js.map +1 -0
  32. package/dist/chunk-FPR7SKIE.js +6926 -0
  33. package/dist/chunk-FPR7SKIE.js.map +1 -0
  34. package/dist/{chunk-DGMQEI6Y.js → chunk-HFB74CLM.js} +30 -9
  35. package/dist/chunk-HFB74CLM.js.map +1 -0
  36. package/dist/{chunk-5RDRAO4B.js → chunk-HOZXKOE3.js} +37 -1
  37. package/dist/chunk-HOZXKOE3.js.map +1 -0
  38. package/dist/chunk-IDYWN37T.js +36 -0
  39. package/dist/chunk-IDYWN37T.js.map +1 -0
  40. package/dist/chunk-J4F2QLM2.js +273 -0
  41. package/dist/chunk-J4F2QLM2.js.map +1 -0
  42. package/dist/chunk-JGF5HLSI.js +1065 -0
  43. package/dist/chunk-JGF5HLSI.js.map +1 -0
  44. package/dist/{chunk-XIFFTATU.js → chunk-KDNWMKXV.js} +7166 -4237
  45. package/dist/chunk-KDNWMKXV.js.map +1 -0
  46. package/dist/chunk-KXAYUIKP.js +90 -0
  47. package/dist/chunk-KXAYUIKP.js.map +1 -0
  48. package/dist/{chunk-3U5IBM24.js → chunk-MEOB2DZA.js} +12 -2
  49. package/dist/chunk-MEOB2DZA.js.map +1 -0
  50. package/dist/{chunk-WKBO5R4O.js → chunk-MV5PQNIA.js} +2 -2
  51. package/dist/chunk-MV5PQNIA.js.map +1 -0
  52. package/dist/chunk-N2G4Q7ME.js +2672 -0
  53. package/dist/chunk-N2G4Q7ME.js.map +1 -0
  54. package/dist/chunk-OJBMDM5H.js +299 -0
  55. package/dist/chunk-OJBMDM5H.js.map +1 -0
  56. package/dist/{chunk-URLQAXTR.js → chunk-PTDC7ZUP.js} +2 -2
  57. package/dist/chunk-PTDC7ZUP.js.map +1 -0
  58. package/dist/{chunk-574C6MQR.js → chunk-REASC2QG.js} +173 -64
  59. package/dist/chunk-REASC2QG.js.map +1 -0
  60. package/dist/{chunk-7N4J557C.js → chunk-RT5SATF7.js} +1 -1
  61. package/dist/chunk-RT5SATF7.js.map +1 -0
  62. package/dist/chunk-SBTU5BEC.js +114 -0
  63. package/dist/chunk-SBTU5BEC.js.map +1 -0
  64. package/dist/{chunk-5KLVH3PY.js → chunk-SWPDKLNM.js} +5 -3
  65. package/dist/chunk-SWPDKLNM.js.map +1 -0
  66. package/dist/chunk-UCFJ43OS.js +520 -0
  67. package/dist/chunk-UCFJ43OS.js.map +1 -0
  68. package/dist/{chunk-VZQXX3WW.js → chunk-W7SLYS3M.js} +80 -16
  69. package/dist/chunk-W7SLYS3M.js.map +1 -0
  70. package/dist/{chunk-ANSJPYGI.js → chunk-WV3O5PGX.js} +3 -3
  71. package/dist/chunk-WV3O5PGX.js.map +1 -0
  72. package/dist/chunk-XHAO3K7I.js +18 -0
  73. package/dist/chunk-XHAO3K7I.js.map +1 -0
  74. package/dist/chunk-XJRJVXJR.js +1050 -0
  75. package/dist/chunk-XJRJVXJR.js.map +1 -0
  76. package/dist/chunk-YIL6HTJX.js +663 -0
  77. package/dist/chunk-YIL6HTJX.js.map +1 -0
  78. package/dist/{chunk-24AXE6SP.js → chunk-ZPSZZUWF.js} +1 -1
  79. package/dist/chunk-ZPSZZUWF.js.map +1 -0
  80. package/dist/{command-approvals-6Y57VCWZ.js → command-approvals-ZRY66LCW.js} +7 -5
  81. package/dist/context-C3RAP6K5.js +41 -0
  82. package/dist/{hook-approvals-CTUFKEJT.js → hook-approvals-DPQD5H65.js} +6 -5
  83. package/dist/index.js +947 -203
  84. package/dist/index.js.map +1 -1
  85. package/dist/{interactive-assets-CCAAWVKH.js → interactive-assets-73AQO5WP.js} +18 -9
  86. package/dist/interactive-assets-73AQO5WP.js.map +1 -0
  87. package/dist/job-runner.js +166 -111
  88. package/dist/job-runner.js.map +1 -1
  89. package/dist/job-supervisor.d.ts +2 -0
  90. package/dist/job-supervisor.js +121 -0
  91. package/dist/job-supervisor.js.map +1 -0
  92. package/dist/loader-DQNPDKOX.js +27 -0
  93. package/dist/{mcp-approvals-WNPH65BK.js → mcp-approvals-27LJYADQ.js} +6 -5
  94. package/dist/{mcp-config-YIJIES2C.js → mcp-config-GXSODNCM.js} +4 -3
  95. package/dist/memory-store-I6Z54VUN.js +51 -0
  96. package/dist/{model-window-store-YFVNXCIP.js → model-window-store-6VKXXTPJ.js} +9 -4
  97. package/dist/openai-compatible-IPUNBJ53.js +16 -0
  98. package/dist/permission-approvals-Q5IMFEZI.js +15 -0
  99. package/dist/permissions-7Y35X6HZ.js +62 -0
  100. package/dist/provider-approvals-5AGH3RVT.js +76 -0
  101. package/dist/sandbox-N2RY72GN.js +42 -0
  102. package/dist/{scrollback-LUKLIBVI.js → scrollback-GD76GNM4.js} +44 -26
  103. package/dist/scrollback-GD76GNM4.js.map +1 -0
  104. package/dist/sdk.d.ts +2739 -2069
  105. package/dist/sdk.js +77 -42
  106. package/dist/sdk.js.map +1 -1
  107. package/dist/settings-loader-KDBH45AQ.js +43 -0
  108. package/dist/settings-loader-KDBH45AQ.js.map +1 -0
  109. package/dist/{settings-removed-JWWPT4WA.js → settings-removed-3ATWJEF3.js} +2 -2
  110. package/dist/settings-removed-3ATWJEF3.js.map +1 -0
  111. package/dist/shell-selection-Z5WORZU4.js +31 -0
  112. package/dist/shell-selection-Z5WORZU4.js.map +1 -0
  113. package/package.json +25 -24
  114. package/dist/app-MOXXWEOY.js.map +0 -1
  115. package/dist/chunk-24AXE6SP.js.map +0 -1
  116. package/dist/chunk-3U5IBM24.js.map +0 -1
  117. package/dist/chunk-574C6MQR.js.map +0 -1
  118. package/dist/chunk-5GDZ22YP.js +0 -449
  119. package/dist/chunk-5GDZ22YP.js.map +0 -1
  120. package/dist/chunk-5KLVH3PY.js.map +0 -1
  121. package/dist/chunk-5RDRAO4B.js.map +0 -1
  122. package/dist/chunk-7N4J557C.js.map +0 -1
  123. package/dist/chunk-A7JLBK2Y.js +0 -3506
  124. package/dist/chunk-A7JLBK2Y.js.map +0 -1
  125. package/dist/chunk-ANSJPYGI.js.map +0 -1
  126. package/dist/chunk-BTZZUCLI.js +0 -264
  127. package/dist/chunk-BTZZUCLI.js.map +0 -1
  128. package/dist/chunk-C7D4NJ2D.js +0 -217
  129. package/dist/chunk-C7D4NJ2D.js.map +0 -1
  130. package/dist/chunk-CAGOB2N4.js.map +0 -1
  131. package/dist/chunk-DGMQEI6Y.js.map +0 -1
  132. package/dist/chunk-E75IPST5.js.map +0 -1
  133. package/dist/chunk-EYGD6X2T.js +0 -180
  134. package/dist/chunk-EYGD6X2T.js.map +0 -1
  135. package/dist/chunk-GTVB4HEM.js +0 -765
  136. package/dist/chunk-GTVB4HEM.js.map +0 -1
  137. package/dist/chunk-J7MIZ5CA.js.map +0 -1
  138. package/dist/chunk-KWHQ2XXL.js.map +0 -1
  139. package/dist/chunk-L6LK2QIQ.js +0 -657
  140. package/dist/chunk-L6LK2QIQ.js.map +0 -1
  141. package/dist/chunk-MAQXCUR4.js +0 -1007
  142. package/dist/chunk-MAQXCUR4.js.map +0 -1
  143. package/dist/chunk-S4XL7HOM.js +0 -402
  144. package/dist/chunk-S4XL7HOM.js.map +0 -1
  145. package/dist/chunk-URLQAXTR.js.map +0 -1
  146. package/dist/chunk-V4BOYR52.js.map +0 -1
  147. package/dist/chunk-VZQXX3WW.js.map +0 -1
  148. package/dist/chunk-WKBO5R4O.js.map +0 -1
  149. package/dist/chunk-XIFFTATU.js.map +0 -1
  150. package/dist/context-ZC5IFEAX.js +0 -25
  151. package/dist/interactive-assets-CCAAWVKH.js.map +0 -1
  152. package/dist/loader-4IDS2RIE.js +0 -15
  153. package/dist/openai-compatible-RHZUBHV6.js +0 -10
  154. package/dist/permission-approvals-76PVREFD.js +0 -14
  155. package/dist/permissions-CHCKTFQQ.js +0 -28
  156. package/dist/sandbox-CNVAQDA7.js +0 -23
  157. package/dist/scrollback-LUKLIBVI.js.map +0 -1
  158. package/dist/settings-loader-M2QP5TX2.js +0 -21
  159. package/dist/shell-selection-V6QAYXPX.js +0 -17
  160. package/patches/ink+6.8.0.patch +0 -13
  161. package/scripts/apply-ink-patch.mjs +0 -29
  162. package/scripts/ink-patch.mjs +0 -59
  163. package/scripts/verify-ink-patch.mjs +0 -11
  164. /package/dist/{command-approvals-6Y57VCWZ.js.map → additional-roots-VAWRQ625.js.map} +0 -0
  165. /package/dist/{context-ZC5IFEAX.js.map → command-approvals-ZRY66LCW.js.map} +0 -0
  166. /package/dist/{hook-approvals-CTUFKEJT.js.map → context-C3RAP6K5.js.map} +0 -0
  167. /package/dist/{loader-4IDS2RIE.js.map → hook-approvals-DPQD5H65.js.map} +0 -0
  168. /package/dist/{mcp-approvals-WNPH65BK.js.map → loader-DQNPDKOX.js.map} +0 -0
  169. /package/dist/{mcp-config-YIJIES2C.js.map → mcp-approvals-27LJYADQ.js.map} +0 -0
  170. /package/dist/{model-window-store-YFVNXCIP.js.map → mcp-config-GXSODNCM.js.map} +0 -0
  171. /package/dist/{openai-compatible-RHZUBHV6.js.map → memory-store-I6Z54VUN.js.map} +0 -0
  172. /package/dist/{permission-approvals-76PVREFD.js.map → model-window-store-6VKXXTPJ.js.map} +0 -0
  173. /package/dist/{permissions-CHCKTFQQ.js.map → openai-compatible-IPUNBJ53.js.map} +0 -0
  174. /package/dist/{sandbox-CNVAQDA7.js.map → permission-approvals-Q5IMFEZI.js.map} +0 -0
  175. /package/dist/{settings-loader-M2QP5TX2.js.map → permissions-7Y35X6HZ.js.map} +0 -0
  176. /package/dist/{settings-removed-JWWPT4WA.js.map → provider-approvals-5AGH3RVT.js.map} +0 -0
  177. /package/dist/{shell-selection-V6QAYXPX.js.map → sandbox-N2RY72GN.js.map} +0 -0
@@ -1,4 +1,5 @@
1
1
  // src/shell-selection.ts
2
+ import { spawn } from "child_process";
2
3
  import { existsSync as fsExistsSync } from "fs";
3
4
  import { posix, win32 } from "path";
4
5
  var KIND_BY_NAME = {
@@ -18,6 +19,7 @@ var LABELS = {
18
19
  cmd: "cmd.exe",
19
20
  sh: "/bin/sh"
20
21
  };
22
+ var UNKNOWN_BASH_LABEL = "bash (runtime not detected as Git Bash)";
21
23
  function createProbe(platform, env, exists) {
22
24
  const path = platform === "win32" ? win32 : posix;
23
25
  const separator = platform === "win32" ? ";" : ":";
@@ -71,10 +73,9 @@ function createProbe(platform, env, exists) {
71
73
  cmd: () => env.ComSpec || env.COMSPEC || first([path.join(systemRoot, "System32", "cmd.exe")]) || "cmd.exe"
72
74
  };
73
75
  }
74
- function shell(kind, source, file, warning) {
76
+ function shell(kind, source, file) {
75
77
  const resolved = { kind, label: LABELS[kind], source };
76
78
  if (file) resolved.file = file;
77
- if (warning) resolved.warning = warning;
78
79
  return resolved;
79
80
  }
80
81
  function kindFromExecutable(file, platform) {
@@ -115,6 +116,21 @@ function resolveRequested(requested, platform, probe) {
115
116
  }
116
117
  return void 0;
117
118
  }
119
+ var MSYS_RUNTIME_DLL = "msys-2.0.dll";
120
+ var POSIX_SHELL_NAME = /^(?:sh|dash|ash|ksh|mksh|zsh)(?:-?\d+(?:\.\d+)*)?$/;
121
+ function isPosixShellFile(file) {
122
+ const name = win32.basename(file).toLowerCase().replace(/\.exe$/, "");
123
+ return POSIX_SHELL_NAME.test(name);
124
+ }
125
+ function isOnMsysRuntime(resolved, probe) {
126
+ const file = resolved.file;
127
+ if (!file || !receivesCommandOutOfBand(resolved) || WSL_LAUNCHER_DIRECTORY.test(file)) {
128
+ return false;
129
+ }
130
+ const dir = win32.dirname(file);
131
+ if (probe.exists(win32.join(dir, MSYS_RUNTIME_DLL))) return true;
132
+ return win32.basename(dir).toLowerCase() === "bin" && probe.exists(win32.join(dir, "..", "usr", "bin", MSYS_RUNTIME_DLL));
133
+ }
118
134
  function launchedFromPosixShell(env) {
119
135
  if (env.MSYSTEM) return true;
120
136
  const launcher = env.SHELL ?? "";
@@ -138,14 +154,15 @@ function resolveShell(input = {}) {
138
154
  const env = input.env ?? process.env;
139
155
  const probe = createProbe(platform, env, input.existsSync ?? fsExistsSync);
140
156
  const requested = (env.BOOK_SHELL || input.requested || "").trim();
141
- let warning;
142
- if (requested) {
143
- const chosen = resolveRequested(requested, platform, probe);
144
- if (chosen) return chosen;
145
- warning = `Requested shell "${requested}" was not found; Book chose one automatically instead.`;
157
+ const chosen = requested ? resolveRequested(requested, platform, probe) : void 0;
158
+ const resolved = chosen ?? (platform === "win32" ? resolveWindows(env, probe) : shell("sh", "detected"));
159
+ if (requested && !chosen) {
160
+ resolved.warning = `Requested shell "${requested}" was not found; Book chose one automatically instead.`;
146
161
  }
147
- const automatic = platform === "win32" ? resolveWindows(env, probe) : shell("sh", "detected");
148
- return warning ? { ...automatic, warning } : automatic;
162
+ if (platform === "win32" && isOnMsysRuntime(resolved, probe)) resolved.msys = true;
163
+ if (platform === "win32" && resolved.kind === "bash" && !resolved.msys)
164
+ resolved.label = UNKNOWN_BASH_LABEL;
165
+ return resolved;
149
166
  }
150
167
  var POWERSHELL_PRELUDE = "$ProgressPreference = 'SilentlyContinue'; [Console]::OutputEncoding = [System.Text.Encoding]::UTF8; $OutputEncoding = [System.Text.Encoding]::UTF8\n";
151
168
  var POWERSHELL_EPILOGUE = "\nif (-not $?) { if ($LASTEXITCODE) { exit $LASTEXITCODE } else { exit 1 } }";
@@ -159,11 +176,33 @@ function encodePowerShellCommand(command, kind = "pwsh") {
159
176
  function decodePowerShellCommand(encoded) {
160
177
  return Buffer.from(encoded, "base64").toString("utf16le");
161
178
  }
162
- function shellExecution(resolved, command) {
179
+ var WSL_LAUNCHER_DIRECTORY = /[\\/](?:windows[\\/](?:system32|syswow64|sysnative)|microsoft[\\/]windowsapps)[\\/]/i;
180
+ function isGitBash(shell2) {
181
+ return shell2?.kind === "bash" && Boolean(shell2.file) && !WSL_LAUNCHER_DIRECTORY.test(shell2.file ?? "");
182
+ }
183
+ function receivesCommandOutOfBand(shell2) {
184
+ if (isGitBash(shell2)) return true;
185
+ return shell2?.kind === "sh" && !!shell2.file && isPosixShellFile(shell2.file);
186
+ }
187
+ function readsSingleQuoteAsPosix(shell2) {
188
+ if (!receivesCommandOutOfBand(shell2)) return false;
189
+ return shell2?.msys === true && !WSL_LAUNCHER_DIRECTORY.test(shell2.file ?? "");
190
+ }
191
+ var GIT_BASH_COMMAND_VARIABLE = "BOOK_BASH_COMMAND";
192
+ var GIT_BASH_EVAL_SCRIPT = `eval "unset ${GIT_BASH_COMMAND_VARIABLE};\${${GIT_BASH_COMMAND_VARIABLE}:?}"`;
193
+ function shellExecution(resolved, command, platform = process.platform) {
163
194
  switch (resolved.kind) {
164
195
  case "bash":
165
196
  case "sh":
166
- return resolved.file ? { file: resolved.file, args: ["-c", command] } : void 0;
197
+ if (!resolved.file) return void 0;
198
+ if (platform === "win32" && receivesCommandOutOfBand(resolved)) {
199
+ return {
200
+ file: resolved.file,
201
+ args: ["-c", GIT_BASH_EVAL_SCRIPT],
202
+ env: { [GIT_BASH_COMMAND_VARIABLE]: command }
203
+ };
204
+ }
205
+ return { file: resolved.file, args: ["-c", command] };
167
206
  case "pwsh":
168
207
  case "powershell":
169
208
  return resolved.file ? {
@@ -180,14 +219,32 @@ function shellExecution(resolved, command) {
180
219
  return void 0;
181
220
  }
182
221
  }
183
- function describeShell(resolved) {
184
- return resolved.file ? `${resolved.label} (${resolved.file})` : resolved.label;
222
+ function spawnCommandExecution(exec, options) {
223
+ return spawn(exec.file, exec.args, {
224
+ ...options,
225
+ env: exec.env ? { ...options.env ?? process.env, ...exec.env } : options.env,
226
+ shell: false
227
+ });
228
+ }
229
+ function singleQuoteHint(resolved, platform = process.platform) {
230
+ if (platform !== "win32" || readsSingleQuoteAsPosix(resolved)) return "";
231
+ return "Use double quotes, not `'`: under this shell a single quote makes a command compound, so it is asked about or refused.";
232
+ }
233
+ function describeShell(resolved, platform = process.platform) {
234
+ const base = resolved.file ? `${resolved.label} (${resolved.file})` : resolved.label;
235
+ const hint = singleQuoteHint(resolved, platform);
236
+ return hint ? `${base}. ${hint}` : base;
237
+ }
238
+ function shellPromptLine(resolved, platform = process.platform) {
239
+ const hint = singleQuoteHint(resolved, platform);
240
+ const line = shellPromptBase(resolved);
241
+ return hint ? `${line} ${hint}` : line;
185
242
  }
186
- function shellPromptLine(resolved) {
243
+ function shellPromptBase(resolved) {
187
244
  const where = resolved.file ? ` at \`${resolved.file}\`` : "";
188
245
  switch (resolved.kind) {
189
246
  case "bash":
190
- return `- The Bash tool runs Git Bash (POSIX sh)${where}, not cmd.exe or PowerShell. Use Unix shell syntax: \`/dev/null\` not \`NUL\`, forward slashes, \`$VAR\` not \`%VAR%\` or \`$env:VAR\`. Windows drive paths such as \`C:/dir\` and MSYS paths such as \`/c/dir\` both work. The bash sandbox requires bubblewrap and is unavailable on Windows.`;
247
+ return `- The Bash tool runs ${resolved.msys ? "Git Bash" : "a bash that is not confirmed to be Git Bash"} (POSIX sh)${where}, not cmd.exe or PowerShell. Use Unix shell syntax: \`/dev/null\` not \`NUL\`, forward slashes, \`$VAR\` not \`%VAR%\` or \`$env:VAR\`. Windows ` + (resolved.msys ? "drive paths such as `C:/dir` and MSYS paths such as `/c/dir` both work. " : "drive paths such as `C:/dir` work. ") + "The bash sandbox requires bubblewrap and is unavailable on Windows.";
191
248
  case "pwsh":
192
249
  return `- The Bash tool runs PowerShell 7 (pwsh)${where}, not bash or cmd.exe. Use PowerShell syntax: \`&&\` and \`||\` work, \`;\` sequences commands, \`$env:VAR\` reads a variable, and \`Get-ChildItem\`, \`Select-String\`, \`Get-Content\` replace \`ls\`, \`grep\`, \`cat\` unless those tools are on PATH. There are no heredocs: use a single-quoted here-string with the closing \`'@\` at column 0. The bash sandbox is unavailable on Windows.`;
193
250
  case "powershell":
@@ -203,8 +260,15 @@ export {
203
260
  resolveShell,
204
261
  encodePowerShellCommand,
205
262
  decodePowerShellCommand,
263
+ isGitBash,
264
+ receivesCommandOutOfBand,
265
+ readsSingleQuoteAsPosix,
266
+ GIT_BASH_COMMAND_VARIABLE,
267
+ GIT_BASH_EVAL_SCRIPT,
206
268
  shellExecution,
269
+ spawnCommandExecution,
270
+ singleQuoteHint,
207
271
  describeShell,
208
272
  shellPromptLine
209
273
  };
210
- //# sourceMappingURL=chunk-VZQXX3WW.js.map
274
+ //# sourceMappingURL=chunk-W7SLYS3M.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/shell-selection.ts"],"sourcesContent":["/**\n * Which shell the Bash tool spawns.\n *\n * Node's `shell: true` means `/bin/sh` on POSIX and `%ComSpec%` on Windows, so\n * for a long time a tool called `Bash` ran cmd.exe on Windows: one line per\n * command, `%VAR%` quoting, no heredocs, and the shell every model writes\n * worst. Every other native-Windows coding agent resolved this the same way,\n * with PowerShell 7 first and Windows PowerShell 5.1 next; Claude Code keeps\n * Git Bash as its POSIX shell; OpenCode honours the shell Book was launched\n * from before either. This module is that ladder, resolved once per session:\n *\n * 1. `BOOK_SHELL` in the environment, then `shell` in a trusted settings\n * layer - a kind name (`bash`, `pwsh`, `powershell`, `cmd`, `sh`) or an\n * executable path. A request that cannot be found is reported and the\n * automatic ladder continues, rather than failing every command.\n * 2. Git Bash, when Book was launched from one (MSYSTEM or a POSIX $SHELL is\n * set), so the syntax the user sees in their own terminal is the syntax\n * the model writes.\n * 3. PowerShell 7 (`pwsh`), then Windows PowerShell 5.1.\n * 4. Git Bash if it is merely installed.\n * 5. cmd.exe, which still works but is never chosen over a real shell.\n *\n * Off Windows the platform default (`/bin/sh` through `shell: true`) is kept.\n *\n * Pure: every probe (platform, environment, filesystem) is injected, so the\n * ladder is testable on any host. Nothing here is cached at module level;\n * `loadConfig` resolves once and the result rides on `AgentConfig.shell`.\n */\nimport { spawn, type ChildProcess, type SpawnOptions } from 'node:child_process';\nimport { existsSync as fsExistsSync } from 'node:fs';\nimport { posix, win32 } from 'node:path';\nimport type { CommandExecution, ResolvedShell, ShellKind, ShellSource } from './types/runtime.js';\n\nexport interface ShellSelectionInput {\n platform?: NodeJS.Platform;\n env?: NodeJS.ProcessEnv;\n /** `settings.shell`: a shell kind name or an executable path. `BOOK_SHELL` wins over it. */\n requested?: string;\n existsSync?: (path: string) => boolean;\n}\n\nconst KIND_BY_NAME: Record<string, ShellKind> = {\n bash: 'bash',\n 'git-bash': 'bash',\n gitbash: 'bash',\n pwsh: 'pwsh',\n powershell: 'powershell',\n cmd: 'cmd',\n 'cmd.exe': 'cmd',\n sh: 'sh',\n};\n\nconst LABELS: Record<ShellKind, string> = {\n bash: 'Git Bash',\n pwsh: 'PowerShell 7',\n powershell: 'Windows PowerShell 5.1',\n cmd: 'cmd.exe',\n sh: '/bin/sh',\n};\n\n/** The label of a `bash` that was not detected on the MSYS runtime (#566). */\nconst UNKNOWN_BASH_LABEL = 'bash (runtime not detected as Git Bash)';\n\ninterface Probe {\n which: (name: string) => string | undefined;\n exists: (path: string) => boolean;\n pwsh: () => string | undefined;\n powershell: () => string | undefined;\n gitBash: () => string | undefined;\n cmd: () => string | undefined;\n}\n\nfunction createProbe(\n platform: NodeJS.Platform,\n env: NodeJS.ProcessEnv,\n exists: (path: string) => boolean,\n): Probe {\n const path = platform === 'win32' ? win32 : posix;\n const separator = platform === 'win32' ? ';' : ':';\n const pathDirs = (env.PATH ?? env.Path ?? '').split(separator).filter(Boolean);\n const which = (name: string): string | undefined => {\n for (const dir of pathDirs) {\n const candidate = path.join(dir, name);\n if (exists(candidate)) return candidate;\n }\n return undefined;\n };\n const first = (candidates: Array<string | undefined>): string | undefined => {\n for (const candidate of candidates) {\n if (candidate && exists(candidate)) return candidate;\n }\n return undefined;\n };\n const programFiles = env.ProgramFiles || env.PROGRAMFILES || 'C:\\\\Program Files';\n const programFilesX86 = env['ProgramFiles(x86)'] || 'C:\\\\Program Files (x86)';\n const systemRoot = env.SystemRoot || env.windir || 'C:\\\\Windows';\n const localAppData = env.LOCALAPPDATA;\n return {\n which,\n exists,\n pwsh: () =>\n first([\n which('pwsh.exe'),\n path.join(programFiles, 'PowerShell', '7', 'pwsh.exe'),\n localAppData ? path.join(localAppData, 'Microsoft', 'WindowsApps', 'pwsh.exe') : undefined,\n ]),\n // Windows PowerShell lives one directory below System32, where the\n // `system32Executable` helper does not look, so the path is built here.\n powershell: () =>\n first([\n path.join(systemRoot, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe'),\n which('powershell.exe'),\n ]),\n gitBash: () => {\n const git = which('git.exe');\n return first([\n // Git Bash exports EXEPATH as the Git for Windows root.\n env.EXEPATH ? path.join(env.EXEPATH, 'bin', 'bash.exe') : undefined,\n // `Git\\cmd\\git.exe`, `Git\\bin\\git.exe`, and `Git\\mingw64\\bin\\git.exe`\n // all sit two levels below the root that holds `bin\\bash.exe`.\n git ? path.join(path.dirname(path.dirname(git)), 'bin', 'bash.exe') : undefined,\n path.join(programFiles, 'Git', 'bin', 'bash.exe'),\n path.join(programFilesX86, 'Git', 'bin', 'bash.exe'),\n localAppData ? path.join(localAppData, 'Programs', 'Git', 'bin', 'bash.exe') : undefined,\n ]);\n },\n // ComSpec is what Node's `shell: true` spawns verbatim, so it is reported\n // as-is; the System32 path is only a guess for a display name.\n cmd: () =>\n env.ComSpec ||\n env.COMSPEC ||\n first([path.join(systemRoot, 'System32', 'cmd.exe')]) ||\n 'cmd.exe',\n };\n}\n\nfunction shell(kind: ShellKind, source: ShellSource, file?: string): ResolvedShell {\n const resolved: ResolvedShell = { kind, label: LABELS[kind], source };\n if (file) resolved.file = file;\n return resolved;\n}\n\nfunction kindFromExecutable(file: string, platform: NodeJS.Platform): ShellKind {\n const path = platform === 'win32' ? win32 : posix;\n const name = path\n .basename(file)\n .toLowerCase()\n .replace(/\\.exe$/, '');\n if (name === 'pwsh') return 'pwsh';\n if (name === 'powershell') return 'powershell';\n if (name === 'cmd') return 'cmd';\n if (name.includes('bash')) return 'bash';\n return 'sh';\n}\n\nfunction resolveRequested(\n requested: string,\n platform: NodeJS.Platform,\n probe: Probe,\n): ResolvedShell | undefined {\n const byName = KIND_BY_NAME[requested.toLowerCase()];\n if (byName) {\n switch (byName) {\n case 'bash': {\n const file = platform === 'win32' ? probe.gitBash() : probe.which('bash');\n return file ? shell('bash', 'setting', file) : undefined;\n }\n case 'pwsh': {\n const file = probe.pwsh() ?? (platform === 'win32' ? undefined : probe.which('pwsh'));\n return file ? shell('pwsh', 'setting', file) : undefined;\n }\n case 'powershell': {\n const file = probe.powershell();\n return file ? shell('powershell', 'setting', file) : undefined;\n }\n case 'cmd':\n return platform === 'win32' ? shell('cmd', 'setting', probe.cmd()) : undefined;\n case 'sh': {\n if (platform === 'win32') return undefined;\n return shell('sh', 'setting', probe.which('sh') ?? '/bin/sh');\n }\n }\n }\n if (probe.exists(requested)) {\n return shell(kindFromExecutable(requested, platform), 'setting', requested);\n }\n return undefined;\n}\n\n/** The MSYS runtime, which Git for Windows and MSYS2 ship beside every program built on it. */\nconst MSYS_RUNTIME_DLL = 'msys-2.0.dll';\n\n/**\n * POSIX shells, by lowercased name without `.exe`: `sh`, `dash`, `ash`, `ksh`, `mksh` or `zsh`,\n * optionally followed by a version, as installs name their hardlinks (`zsh-5.9`, `zsh5`, `ksh93`).\n * {@link GIT_BASH_EVAL_SCRIPT} is POSIX sh, so only these get the out-of-band delivery (see\n * {@link isPosixShellFile}): `kindFromExecutable` also calls `fish`, `nu`, `busybox` and any other\n * name `sh`, and MSYS2 ships `fish.exe` beside the runtime.\n */\nconst POSIX_SHELL_NAME = /^(?:sh|dash|ash|ksh|mksh|zsh)(?:-?\\d+(?:\\.\\d+)*)?$/;\n\n/**\n * Whether a Windows `sh`-kind `file` has a POSIX shell's name (see {@link POSIX_SHELL_NAME}), and so\n * gets its command out of band (#548). By name alone, with no file probe and wherever it lives:\n * whatever runtime it is on (MSYS, Cygwin, busybox-w32 copied as `sh.exe`) and whether it is the\n * shell itself or a shim, hardlink, copy or app execution alias of one, it fails closed onto the\n * delivery that reaches every runtime byte for byte. A program behind it that cannot see the\n * variable fails loudly on `${BOOK_BASH_COMMAND:?}` rather than run anything.\n */\nfunction isPosixShellFile(file: string): boolean {\n const name = win32\n .basename(file)\n .toLowerCase()\n .replace(/\\.exe$/, '');\n return POSIX_SHELL_NAME.test(name);\n}\n\n/**\n * Whether the Windows `shell` runs on the MSYS runtime (#532, #548): one that gets its command out of\n * band ({@link receivesCommandOutOfBand}: a bash outside the WSL launcher directories, or a POSIX-named\n * sh), whose file is outside the WSL launcher directories (see {@link WSL_LAUNCHER_DIRECTORY}) and\n * has `msys-2.0.dll` beside it, or, for a file in a directory named `bin`, in `..\\usr\\bin`: Git for\n * Windows' `<root>\\bin\\bash.exe` and `<root>\\bin\\sh.exe` are launcher stubs with no DLL of their\n * own that hand their raw command line to `<root>\\usr\\bin`, and no other directory is read as one\n * (a `<root>\\cmd\\bash.exe` is not). This sets `ResolvedShell.msys`, which does not decide delivery: it only lets the\n * permission scanner read `'` as on Linux ({@link readsSingleQuoteAsPosix}), because Windows CI\n * proves the MSYS runtime's delivery. A Cygwin `bash.exe` or `sh.exe` (`cygwin1.dll` beside it), a\n * shim or copy with no DLL beside it (scoop's `shims\\bash.exe`), and busybox-w32 copied as `sh.exe`\n * get the delivery but not the flag.\n */\nfunction isOnMsysRuntime(resolved: ResolvedShell, probe: Probe): boolean {\n const file = resolved.file;\n if (!file || !receivesCommandOutOfBand(resolved) || WSL_LAUNCHER_DIRECTORY.test(file)) {\n return false;\n }\n const dir = win32.dirname(file);\n if (probe.exists(win32.join(dir, MSYS_RUNTIME_DLL))) return true;\n return (\n win32.basename(dir).toLowerCase() === 'bin' &&\n probe.exists(win32.join(dir, '..', 'usr', 'bin', MSYS_RUNTIME_DLL))\n );\n}\n\nfunction launchedFromPosixShell(env: NodeJS.ProcessEnv): boolean {\n if (env.MSYSTEM) return true;\n const launcher = env.SHELL ?? '';\n return /(^|[\\\\/])(ba|z|da)?sh(\\.exe)?$/i.test(launcher);\n}\n\nfunction resolveWindows(env: NodeJS.ProcessEnv, probe: Probe): ResolvedShell {\n if (launchedFromPosixShell(env)) {\n const bash = probe.gitBash();\n if (bash) return shell('bash', 'launching-shell', bash);\n }\n const pwsh = probe.pwsh();\n if (pwsh) return shell('pwsh', 'detected', pwsh);\n const powershell = probe.powershell();\n if (powershell) return shell('powershell', 'detected', powershell);\n const bash = probe.gitBash();\n if (bash) return shell('bash', 'detected', bash);\n return shell('cmd', 'fallback', probe.cmd());\n}\n\n/** Resolve the shell the Bash tool should spawn. See the module comment for the ladder. */\nexport function resolveShell(input: ShellSelectionInput = {}): ResolvedShell {\n const platform = input.platform ?? process.platform;\n const env = input.env ?? process.env;\n const probe = createProbe(platform, env, input.existsSync ?? fsExistsSync);\n const requested = (env.BOOK_SHELL || input.requested || '').trim();\n\n const chosen = requested ? resolveRequested(requested, platform, probe) : undefined;\n // POSIX keeps Node's default: `/bin/sh` through `shell: true`, exactly as before.\n const resolved =\n chosen ?? (platform === 'win32' ? resolveWindows(env, probe) : shell('sh', 'detected'));\n if (requested && !chosen) {\n resolved.warning = `Requested shell \"${requested}\" was not found; Book chose one automatically instead.`;\n }\n // Every route can choose a bash or sh with a file (a path or a `bash` request, the launching\n // shell, the installed fallback), so the MSYS runtime is detected here, once, on the chosen shell,\n // and `shellExecution` and the permission scanner stay pure.\n if (platform === 'win32' && isOnMsysRuntime(resolved, probe)) resolved.msys = true;\n // \"Git Bash\" is a claim about the runtime: a Cygwin bash, a shim or a copy with no DLL beside it\n // is only a bash (#566).\n if (platform === 'win32' && resolved.kind === 'bash' && !resolved.msys)\n resolved.label = UNKNOWN_BASH_LABEL;\n return resolved;\n}\n\n/**\n * Silences progress records (which a redirected stderr would otherwise carry\n * as CLIXML) and sets UTF-8 on both pipes so non-ASCII output survives.\n */\nconst POWERSHELL_PRELUDE =\n \"$ProgressPreference = 'SilentlyContinue'; \" +\n '[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; ' +\n '$OutputEncoding = [System.Text.Encoding]::UTF8\\n';\n\n/**\n * Mirrors bash: the process exits with the last statement's status, using the\n * native command's code when there is one. `$?` refers to the statement\n * before the `if`, i.e. the last line of the user's command.\n */\nconst POWERSHELL_EPILOGUE =\n '\\nif (-not $?) { if ($LASTEXITCODE) { exit $LASTEXITCODE } else { exit 1 } }';\n\n/**\n * Windows PowerShell 5.1 serializes anything it writes to a redirected stderr\n * as CLIXML, so a plain `Get-Item missing` hands the model an XML document\n * instead of \"Cannot find path\". Merging the error stream is not enough on\n * its own: an ErrorRecord that reaches the end of the pipeline is still\n * rendered to the host's error stream. Each record is turned into its message\n * text inside the pipeline instead, and the failure is recorded in a flag\n * because `$?` after the `ForEach-Object` describes the pipeline, not the\n * user's last statement. Verified on this exact script: text errors on\n * stdout, exit codes 0/1/3/7 preserved, table formatting intact.\n */\nfunction wrapForWindowsPowerShell(command: string): string {\n return (\n POWERSHELL_PRELUDE +\n '$global:__bookErr = $false\\n' +\n '& {\\n' +\n command +\n '\\nif (-not $?) { $global:__bookErr = $true }\\n' +\n '} 2>&1 | ForEach-Object { if ($_ -is [System.Management.Automation.ErrorRecord]) { \"$_\" } else { $_ } }\\n' +\n 'if ($global:__bookErr) { if ($LASTEXITCODE) { exit $LASTEXITCODE } else { exit 1 } }'\n );\n}\n\n/**\n * Base64 of UTF-16LE, the encoding `-EncodedCommand` reads. Chosen over\n * `-Command` because Windows PowerShell 5.1 re-parses a `-Command` argument\n * and silently strips embedded double quotes from it; the encoded form is\n * delivered byte for byte. The cost is roughly 2.7x the command length on the\n * command line, which stays inside CreateProcess's 32k limit for anything a\n * model writes as a single tool call.\n */\nexport function encodePowerShellCommand(\n command: string,\n kind: 'pwsh' | 'powershell' = 'pwsh',\n): string {\n const script =\n kind === 'powershell'\n ? wrapForWindowsPowerShell(command)\n : POWERSHELL_PRELUDE + command + POWERSHELL_EPILOGUE;\n return Buffer.from(script, 'utf16le').toString('base64');\n}\n\n/** Decode what `encodePowerShellCommand` produced, for tests and diagnostics. */\nexport function decodePowerShellCommand(encoded: string): string {\n return Buffer.from(encoded, 'base64').toString('utf16le');\n}\n\n/**\n * Directories that hold WSL's `bash.exe` launcher, which `kindFromExecutable` also calls `bash`\n * (any executable name containing \"bash\"): the Windows system directories, and the app execution\n * alias directory (`%LOCALAPPDATA%\\Microsoft\\WindowsApps`). WSL is not the MSYS runtime: it\n * rebuilds argv its own way and never sees a Windows environment variable without WSLENV, so it is\n * not Git Bash. Either slash, any case.\n */\nconst WSL_LAUNCHER_DIRECTORY =\n /[\\\\/](?:windows[\\\\/](?:system32|syswow64|sysnative)|microsoft[\\\\/]windowsapps)[\\\\/]/i;\n\n/**\n * Whether `shell` is Git Bash: `kind: 'bash'` with a `file` (a `bash` without one goes to Node's\n * `shell: true`, which is cmd.exe on Windows), and the file is not WSL's launcher (see\n * {@link WSL_LAUNCHER_DIRECTORY}). By name alone, so Cygwin's `bash.exe`, scoop's `shims\\bash.exe`\n * and a copy count too. Every shell this answers true for is delivered out of band\n * ({@link receivesCommandOutOfBand}), but the permission scanner reads its `'` as on Linux only when\n * resolution found it on the MSYS runtime ({@link readsSingleQuoteAsPosix}, #548).\n */\nexport function isGitBash(shell: Pick<ResolvedShell, 'kind' | 'file'> | undefined): boolean {\n return (\n shell?.kind === 'bash' && Boolean(shell.file) && !WSL_LAUNCHER_DIRECTORY.test(shell.file ?? '')\n );\n}\n\n/**\n * Whether {@link shellExecution} hands `shell` its command out of band on Windows: Git Bash\n * ({@link isGitBash}), or a `kind: 'sh'` shell whose `file` has a POSIX shell's name (#548, see\n * {@link isPosixShellFile}), whatever its runtime and directory. It does not read\n * `ResolvedShell.msys`: delivery fails closed, so a Cygwin sh, or a shim or copy of an MSYS sh with\n * no DLL beside it, is never left on an argv its runtime could rebuild. The one exception is WSL's\n * `bash.exe`, which {@link isGitBash} leaves on argv. Off Windows `shellExecution` does not ask.\n */\nexport function receivesCommandOutOfBand(\n shell: Pick<ResolvedShell, 'kind' | 'file'> | undefined,\n): boolean {\n if (isGitBash(shell)) return true;\n return shell?.kind === 'sh' && !!shell.file && isPosixShellFile(shell.file);\n}\n\n/**\n * Whether the permission scanner reads `'` as on Linux under `shell` on Windows (see\n * `hasCompoundShellSyntax`): a bash or sh that gets its command out of band and that resolution\n * found on the MSYS runtime (`ResolvedShell.msys`, #548), outside the WSL launcher directories. That\n * is Git for Windows' `bin\\bash.exe`, `usr\\bin\\bash.exe` and `sh.exe`, and MSYS2's shells: the\n * runtime whose byte-for-byte delivery Windows CI proves. A Cygwin bash or sh, a shim or copy with no\n * DLL beside it (scoop's `shims\\bash.exe`), and busybox-w32 copied as `sh.exe` get the command out\n * of band too, but still refuse `'` until CI proves their delivery.\n *\n * The {@link receivesCommandOutOfBand} conjunct comes first, so this is never true for a shell that\n * gets the command in argv: the scanner is never laxer than delivery, even for a forged `msys` flag.\n * The converse does not hold, so the scanner must ask this, never `receivesCommandOutOfBand`.\n */\nexport function readsSingleQuoteAsPosix(\n shell: Pick<ResolvedShell, 'kind' | 'file' | 'msys'> | undefined,\n): boolean {\n if (!receivesCommandOutOfBand(shell)) return false;\n return shell?.msys === true && !WSL_LAUNCHER_DIRECTORY.test(shell.file ?? '');\n}\n\n/** The environment variable that carries a Git Bash or POSIX sh command on Windows. */\nexport const GIT_BASH_COMMAND_VARIABLE = 'BOOK_BASH_COMMAND';\n\n/**\n * The fixed `-c` script that runs the command held in `BOOK_BASH_COMMAND`.\n *\n * - It contains a space, so libuv wraps it in `\"…\"` and the MSYS and Cygwin runtimes leave it\n * literal.\n * - It is POSIX sh, so `sh`, `dash`, `ash`, `ksh`, `mksh`, `zsh` and busybox's `sh` run it as Git\n * Bash does (#532, #548), whatever their runtime.\n * - Nothing follows it in argv and it uses no function or shell variable, so `$0`, `$#`, `$1` and\n * `return` behave exactly as under `bash -c <command>`, and so does the exit status.\n * - The `unset` is parsed together with the command's first line and runs before any of it, so\n * neither the command nor its children see the variable; a syntax error there aborts the whole\n * eval, so nothing runs with it set.\n * - `:?` makes a spawn that forgot the variable fail loudly rather than run `eval \"\"` and succeed.\n *\n * Error messages read `bash: eval: line N:` instead of `bash: -c: line N:`, and\n * `BASH_EXECUTION_STRING` holds this script rather than the command.\n */\nexport const GIT_BASH_EVAL_SCRIPT = `eval \"unset ${GIT_BASH_COMMAND_VARIABLE};\\${${GIT_BASH_COMMAND_VARIABLE}:?}\"`;\n\n/**\n * The argv that runs `command` under `shell`, or undefined when the command\n * should go to Node's `shell: true` (cmd.exe and the POSIX default).\n *\n * Git Bash on Windows gets the command out of band, in `BOOK_BASH_COMMAND`\n * with {@link GIT_BASH_EVAL_SCRIPT} as its `-c` script (#523), and so does\n * every POSIX-named sh, whatever its runtime and directory (#532, #548; see\n * {@link receivesCommandOutOfBand}). Passed in argv,\n * the command could reach the shell altered: libuv (`quote_cmd_arg`) leaves an\n * argument with no space, tab or `\"` unquoted on the Windows command line, and\n * the MSYS runtime rebuilds argv from that line itself (`build_argv` in\n * `winsup/cygwin/dcrt0.cc`, which Cygwin's runtime shares, and which a shim\n * reaches by handing its raw command line on). In an unquoted word it strips\n * `'` and `\"` quotes, globs `?*[{` and a leading `~` against the cwd, and\n * replaces a leading `@word` with the contents of that file, so the shell would\n * run a different string from the one the permission scanner read. An\n * environment variable reaches it byte for byte.\n *\n * WSL's `bash.exe` keeps the plain argv: Windows variables do not reach WSL\n * without WSLENV. Non-POSIX shells (`fish`, `nu`, `busybox.exe` under its own\n * name, any other name `kindFromExecutable` calls `sh`) keep it too, because\n * the script is POSIX sh; that is a known gap, not a safe case. Every other\n * shell and platform is unchanged. Spawn the result with\n * {@link spawnCommandExecution}, which applies its `env`.\n */\nexport function shellExecution(\n resolved: ResolvedShell,\n command: string,\n platform: NodeJS.Platform = process.platform,\n): CommandExecution | undefined {\n switch (resolved.kind) {\n case 'bash':\n case 'sh':\n if (!resolved.file) return undefined;\n if (platform === 'win32' && receivesCommandOutOfBand(resolved)) {\n return {\n file: resolved.file,\n args: ['-c', GIT_BASH_EVAL_SCRIPT],\n env: { [GIT_BASH_COMMAND_VARIABLE]: command },\n };\n }\n return { file: resolved.file, args: ['-c', command] };\n case 'pwsh':\n case 'powershell':\n return resolved.file\n ? {\n file: resolved.file,\n args: [\n '-NoLogo',\n '-NoProfile',\n '-NonInteractive',\n '-EncodedCommand',\n encodePowerShellCommand(command, resolved.kind),\n ],\n }\n : undefined;\n case 'cmd':\n // cmd.exe quoting cannot be reproduced from an argv; Node's `shell: true`\n // builds the `/d /s /c \"...\"` line itself.\n return undefined;\n }\n}\n\n/**\n * Spawn a {@link CommandExecution}: `file` with `args` and `shell: false`, with `exec.env` merged\n * last over `options.env` (or the inherited environment). Every site that runs an `exec` goes\n * through here, so the command in `BOOK_BASH_COMMAND` (Git Bash, a POSIX sh) reaches the command's\n * spawn alone and is never dropped by one site that forgot it.\n */\nexport function spawnCommandExecution(\n exec: CommandExecution,\n options: Omit<SpawnOptions, 'shell'>,\n): ChildProcess {\n return spawn(exec.file, exec.args, {\n ...options,\n env: exec.env ? { ...(options.env ?? process.env), ...exec.env } : options.env,\n shell: false,\n });\n}\n\n/**\n * The hint that goes with a shell that refuses `'` (#566): the permission scanner treats every\n * single quote as making a command compound on Windows, unless the shell is on the MSYS runtime\n * ({@link readsSingleQuoteAsPosix}). Empty when `'` is read as on Linux.\n */\nexport function singleQuoteHint(\n resolved: ResolvedShell,\n platform: NodeJS.Platform = process.platform,\n): string {\n if (platform !== 'win32' || readsSingleQuoteAsPosix(resolved)) return '';\n return \"Use double quotes, not `'`: under this shell a single quote makes a command compound, so it is asked about or refused.\";\n}\n\n/** `label (file)` for doctor and the system prompt, plus the quoting hint where `'` is refused. */\nexport function describeShell(\n resolved: ResolvedShell,\n platform: NodeJS.Platform = process.platform,\n): string {\n const base = resolved.file ? `${resolved.label} (${resolved.file})` : resolved.label;\n const hint = singleQuoteHint(resolved, platform);\n return hint ? `${base}. ${hint}` : base;\n}\n\n/**\n * The Harness bullet that tells the model which syntax to write. The shell is\n * fixed for the session, so this belongs in the cached prompt prefix.\n */\nexport function shellPromptLine(\n resolved: ResolvedShell,\n platform: NodeJS.Platform = process.platform,\n): string {\n const hint = singleQuoteHint(resolved, platform);\n const line = shellPromptBase(resolved);\n return hint ? `${line} ${hint}` : line;\n}\n\nfunction shellPromptBase(resolved: ResolvedShell): string {\n const where = resolved.file ? ` at \\`${resolved.file}\\`` : '';\n switch (resolved.kind) {\n case 'bash':\n return (\n `- The Bash tool runs ${resolved.msys ? 'Git Bash' : 'a bash that is not confirmed to be Git Bash'} (POSIX sh)${where}, not cmd.exe or PowerShell. Use Unix shell ` +\n 'syntax: `/dev/null` not `NUL`, forward slashes, `$VAR` not `%VAR%` or `$env:VAR`. Windows ' +\n (resolved.msys\n ? 'drive paths such as `C:/dir` and MSYS paths such as `/c/dir` both work. '\n : 'drive paths such as `C:/dir` work. ') +\n 'The bash sandbox ' +\n 'requires bubblewrap and is unavailable on Windows.'\n );\n case 'pwsh':\n return (\n `- The Bash tool runs PowerShell 7 (pwsh)${where}, not bash or cmd.exe. Use PowerShell ` +\n 'syntax: `&&` and `||` work, `;` sequences commands, `$env:VAR` reads a variable, and ' +\n '`Get-ChildItem`, `Select-String`, `Get-Content` replace `ls`, `grep`, `cat` unless those ' +\n 'tools are on PATH. There are no heredocs: use a single-quoted here-string with the closing ' +\n \"`'@` at column 0. The bash sandbox is unavailable on Windows.\"\n );\n case 'powershell':\n return (\n `- The Bash tool runs Windows PowerShell 5.1${where}, not bash or cmd.exe. ` +\n '`&&` and `||` are NOT available: use `;` to sequence, or `cmd1; if ($?) { cmd2 }`. Avoid ' +\n 'PowerShell 7-only syntax (`??`, `?.`). `$env:VAR` reads a variable; `Get-ChildItem`, ' +\n '`Select-String`, `Get-Content` replace `ls`, `grep`, `cat` unless those tools are on PATH. ' +\n \"There are no heredocs: use a single-quoted here-string with the closing `'@` at column 0. \" +\n 'The bash sandbox is unavailable on Windows.'\n );\n case 'cmd':\n return (\n '- The Bash tool runs cmd.exe because no PowerShell or Git Bash was found (set `shell` in ' +\n '`~/.book/settings.json` or `BOOK_SHELL` to change this). Each command must be a single ' +\n 'line; chain with `&` or `&&`; `%VAR%` reads a variable; `dir`, `findstr`, `type` replace ' +\n '`ls`, `grep`, `cat`; quoting follows cmd.exe rules, not POSIX. The bash sandbox is ' +\n 'unavailable on Windows.'\n );\n case 'sh':\n return (\n `- The Bash tool runs commands through the platform's default shell, \\`/bin/sh\\`${where}. ` +\n 'The bash sandbox requires bubblewrap.'\n );\n }\n}\n"],"mappings":"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","names":["bash","shell"]}
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  updateWorkspaceTrust
3
- } from "./chunk-DGMQEI6Y.js";
3
+ } from "./chunk-HFB74CLM.js";
4
4
  import {
5
5
  HOOK_EVENTS
6
- } from "./chunk-574C6MQR.js";
6
+ } from "./chunk-REASC2QG.js";
7
7
 
8
8
  // src/hook-approvals.ts
9
9
  import { createHash } from "crypto";
@@ -81,4 +81,4 @@ export {
81
81
  describeDeclaredHook,
82
82
  persistProjectHookChoice
83
83
  };
84
- //# sourceMappingURL=chunk-ANSJPYGI.js.map
84
+ //# sourceMappingURL=chunk-WV3O5PGX.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/hook-approvals.ts"],"sourcesContent":["/**\n * Trust decisions for hook entries declared by a repository.\n *\n * A hook entry is a shell command the repository can get Book to execute at any\n * lifecycle event — on every prompt, around every tool call, at session start.\n * Once merged into resolved settings an entry carries no provenance, so nothing\n * downstream can tell a repository's hook from the user's own.\n *\n * Project-declared hooks therefore require a one-time per-workspace decision,\n * keyed by a fingerprint of the full entry (event, matcher, command, env) and\n * recorded in the user-global trust store, outside the workspace, where nothing\n * the repository ships can reach it (see `workspace-trust.ts`). Any change to\n * an entry produces a new fingerprint and reverts it to untrusted.\n *\n * User-global and local-layer hooks need no gate: those layers were written by\n * or for the user.\n */\nimport { createHash } from 'crypto';\nimport { HOOK_EVENTS, type HookEntry, type HookEvent, type ProjectHookChoice } from './settings.js';\nimport { updateWorkspaceTrust } from './workspace-trust.js';\n\n/** Every project-declared hook entry, paired with the event that triggers it. */\nexport interface DeclaredHook {\n event: HookEvent;\n entry: HookEntry;\n}\n\n/** Stable digest of everything the user is asked to trust about a hook. */\nexport function hookFingerprint(event: HookEvent, entry: HookEntry): string {\n const sortedEnv = Object.fromEntries(\n Object.entries(entry.env ?? {}).sort(([a], [b]) => a.localeCompare(b)),\n );\n const canonical = JSON.stringify({\n event,\n matcher: entry.matcher ?? null,\n command: entry.command,\n env: sortedEnv,\n });\n return createHash('sha256').update(canonical).digest('hex').slice(0, 16);\n}\n\n/** Flatten a settings document into its declared hook entries, event paired. */\nexport function collectDeclaredHooks(\n settings:\n | {\n hooks?: Partial<Record<HookEvent, HookEntry[]>> & { projectEntries?: unknown };\n }\n | null\n | undefined,\n): DeclaredHook[] {\n const declared: DeclaredHook[] = [];\n for (const event of HOOK_EVENTS) {\n for (const entry of settings?.hooks?.[event] ?? []) {\n declared.push({ event, entry });\n }\n }\n return declared;\n}\n\nexport type ProjectHookState = ProjectHookChoice | 'unknown';\n\nexport function evaluateProjectHook(\n store: Record<string, ProjectHookChoice> | undefined,\n fingerprint: string,\n): ProjectHookState {\n return store?.[fingerprint] ?? 'unknown';\n}\n\nexport interface ProjectHookPartition {\n /** Approved by the user: these join the resolved hooks. */\n approved: DeclaredHook[];\n /** No decision recorded: withheld until the user makes one. */\n pending: DeclaredHook[];\n /** Explicitly refused: withheld, and not re-offered. */\n rejected: DeclaredHook[];\n}\n\nexport function partitionProjectHooks(\n hooks: readonly DeclaredHook[],\n store: Record<string, ProjectHookChoice> | undefined,\n): ProjectHookPartition {\n const partition: ProjectHookPartition = { approved: [], pending: [], rejected: [] };\n for (const hook of hooks) {\n const state = evaluateProjectHook(store, hookFingerprint(hook.event, hook.entry));\n if (state === 'approved') partition.approved.push(hook);\n else if (state === 'rejected') partition.rejected.push(hook);\n else partition.pending.push(hook);\n }\n return partition;\n}\n\n/**\n * Render repository-authored text for a terminal that is about to ask the user\n * to trust it.\n *\n * A hook command and its environment values are written by the party being\n * gated. Printed raw, a value carrying newlines or ANSI escapes can forge\n * further lines of the report — including an `[x] …` line for a hook that is\n * nothing of the kind. Control characters become visible escapes, and an\n * over-long value is truncated so one entry cannot push the rest of the\n * disclosure off screen.\n */\nexport function renderUntrustedText(value: string, maxLength = 200): string {\n const escaped = value.replace(/[\\u0000-\\u001f\\u007f-\\u009f]/g, (character) => {\n if (character === '\\n') return '\\\\n';\n if (character === '\\r') return '\\\\r';\n if (character === '\\t') return '\\\\t';\n return `\\\\x${character.codePointAt(0)!.toString(16).padStart(2, '0')}`;\n });\n return escaped.length > maxLength ? `${escaped.slice(0, maxLength)}…` : escaped;\n}\n\n/**\n * Everything the fingerprint covers, rendered for a trust decision.\n *\n * The fingerprint digests event, matcher, command *and* env, so a disclosure\n * showing only the command understates what approval grants: `npm test` with\n * `NODE_OPTIONS=--require ./payload.js` reads as harmless and is not. Two\n * entries differing only in matcher or env are also indistinguishable without\n * these lines.\n */\nexport function describeDeclaredHook(hook: DeclaredHook): { headline: string; details: string[] } {\n const details = [`fingerprint: ${hookFingerprint(hook.event, hook.entry)}`];\n if (hook.entry.matcher !== undefined) {\n details.push(`matcher: ${renderUntrustedText(hook.entry.matcher)}`);\n }\n for (const [key, value] of Object.entries(hook.entry.env ?? {}).sort(([a], [b]) =>\n a.localeCompare(b),\n )) {\n details.push(`env: ${renderUntrustedText(key, 64)}=${renderUntrustedText(value)}`);\n }\n return { headline: `${hook.event}: ${renderUntrustedText(hook.entry.command)}`, details };\n}\n\n/**\n * Record an approve/reject decision for one hook, leaving every other recorded\n * decision — in this workspace and in every other — untouched.\n */\nexport function persistProjectHookChoice(\n workspace: string,\n fingerprint: string,\n choice: ProjectHookChoice,\n options: { trustStorePath?: string } = {},\n): { ok: boolean; error?: string } {\n return updateWorkspaceTrust(\n workspace,\n (trust) => {\n trust.hookEntries[fingerprint] = choice;\n },\n options.trustStorePath,\n );\n}\n"],"mappings":";;;;;;;;AAiBA,SAAS,kBAAkB;AAWpB,SAAS,gBAAgB,OAAkB,OAA0B;AAC1E,QAAM,YAAY,OAAO;AAAA,IACvB,OAAO,QAAQ,MAAM,OAAO,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;AAAA,EACvE;AACA,QAAM,YAAY,KAAK,UAAU;AAAA,IAC/B;AAAA,IACA,SAAS,MAAM,WAAW;AAAA,IAC1B,SAAS,MAAM;AAAA,IACf,KAAK;AAAA,EACP,CAAC;AACD,SAAO,WAAW,QAAQ,EAAE,OAAO,SAAS,EAAE,OAAO,KAAK,EAAE,MAAM,GAAG,EAAE;AACzE;AAGO,SAAS,qBACd,UAMgB;AAChB,QAAM,WAA2B,CAAC;AAClC,aAAW,SAAS,aAAa;AAC/B,eAAW,SAAS,UAAU,QAAQ,KAAK,KAAK,CAAC,GAAG;AAClD,eAAS,KAAK,EAAE,OAAO,MAAM,CAAC;AAAA,IAChC;AAAA,EACF;AACA,SAAO;AACT;AAIO,SAAS,oBACd,OACA,aACkB;AAClB,SAAO,QAAQ,WAAW,KAAK;AACjC;AAWO,SAAS,sBACd,OACA,OACsB;AACtB,QAAM,YAAkC,EAAE,UAAU,CAAC,GAAG,SAAS,CAAC,GAAG,UAAU,CAAC,EAAE;AAClF,aAAW,QAAQ,OAAO;AACxB,UAAM,QAAQ,oBAAoB,OAAO,gBAAgB,KAAK,OAAO,KAAK,KAAK,CAAC;AAChF,QAAI,UAAU,WAAY,WAAU,SAAS,KAAK,IAAI;AAAA,aAC7C,UAAU,WAAY,WAAU,SAAS,KAAK,IAAI;AAAA,QACtD,WAAU,QAAQ,KAAK,IAAI;AAAA,EAClC;AACA,SAAO;AACT;AAaO,SAAS,oBAAoB,OAAe,YAAY,KAAa;AAC1E,QAAM,UAAU,MAAM,QAAQ,iCAAiC,CAAC,cAAc;AAC5E,QAAI,cAAc,KAAM,QAAO;AAC/B,QAAI,cAAc,KAAM,QAAO;AAC/B,QAAI,cAAc,IAAM,QAAO;AAC/B,WAAO,MAAM,UAAU,YAAY,CAAC,EAAG,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG,CAAC;AAAA,EACtE,CAAC;AACD,SAAO,QAAQ,SAAS,YAAY,GAAG,QAAQ,MAAM,GAAG,SAAS,CAAC,WAAM;AAC1E;AAWO,SAAS,qBAAqB,MAA6D;AAChG,QAAM,UAAU,CAAC,gBAAgB,gBAAgB,KAAK,OAAO,KAAK,KAAK,CAAC,EAAE;AAC1E,MAAI,KAAK,MAAM,YAAY,QAAW;AACpC,YAAQ,KAAK,gBAAgB,oBAAoB,KAAK,MAAM,OAAO,CAAC,EAAE;AAAA,EACxE;AACA,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,KAAK,MAAM,OAAO,CAAC,CAAC,EAAE;AAAA,IAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,MAC3E,EAAE,cAAc,CAAC;AAAA,EACnB,GAAG;AACD,YAAQ,KAAK,gBAAgB,oBAAoB,KAAK,EAAE,CAAC,IAAI,oBAAoB,KAAK,CAAC,EAAE;AAAA,EAC3F;AACA,SAAO,EAAE,UAAU,GAAG,KAAK,KAAK,KAAK,oBAAoB,KAAK,MAAM,OAAO,CAAC,IAAI,QAAQ;AAC1F;AAMO,SAAS,yBACd,WACA,aACA,QACA,UAAuC,CAAC,GACP;AACjC,SAAO;AAAA,IACL;AAAA,IACA,CAAC,UAAU;AACT,YAAM,YAAY,WAAW,IAAI;AAAA,IACnC;AAAA,IACA,QAAQ;AAAA,EACV;AACF;","names":[]}
@@ -0,0 +1,18 @@
1
+ // src/child-env.ts
2
+ var CHILD_SESSION_DRIVER = "agent";
3
+ var DEFAULTED_NODE_ENV_MARKER = "BOOK_DEFAULTED_NODE_ENV";
4
+ function buildChildEnv(base = process.env, overrides = {}) {
5
+ const env = { ...base, ...overrides };
6
+ delete env[DEFAULTED_NODE_ENV_MARKER];
7
+ env.BOOK_SESSION_DRIVER = CHILD_SESSION_DRIVER;
8
+ const overrideIsAmbient = overrides[DEFAULTED_NODE_ENV_MARKER] !== void 0;
9
+ if (overrideIsAmbient || base[DEFAULTED_NODE_ENV_MARKER] !== void 0 && !overrides.NODE_ENV) {
10
+ delete env.NODE_ENV;
11
+ }
12
+ return env;
13
+ }
14
+
15
+ export {
16
+ buildChildEnv
17
+ };
18
+ //# sourceMappingURL=chunk-XHAO3K7I.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/child-env.ts"],"sourcesContent":["/**\n * The environment every command Book starts is built here.\n *\n * `runtime-env.ts` defaults `NODE_ENV=production` before React loads, because the development\n * renderer is 2-3x slower per pass. That default is a fact about Book's own process: it is why\n * the TUI is fast, and nothing else. Inherited by a child it silently changes what the command\n * does — `npm install` drops devDependencies, a test runner reads a production build flag, a\n * framework refuses to serve a source map — while the user, who set nothing, has no way to see\n * why.\n *\n * So the default is marked (`DEFAULTED_NODE_ENV_MARKER`), and the marker is a full stop: a child\n * never sees it, and a child only loses its NODE_ENV when the marker says Book invented it. A\n * NODE_ENV the user exported before Book started has no marker and always passes through, as\n * does one a caller sets explicitly in `overrides` — a hook's own `env`, an MCP server's `env`,\n * or `ToolContext.env`. The marker is also what covers Book's own detached processes\n * (`job-runner.ts`, `job-supervisor.ts`), which build their environment from `process.env` and\n * would otherwise hand the default on to a persistent job that outlives Book.\n *\n * `ToolContext.env` is `process.env` in the agent, so the default arrives through the override\n * layer as well. The marker is what tells that apart from a request: an override object carrying\n * the marker is a copy of Book's own environment, and its NODE_ENV is the default arriving by a\n * second route. Every other override object is a declaration — even one whose NODE_ENV happens to\n * equal `production`, which is the single value an explicit request and Book's default share, and\n * the one a value-comparison rule would silently delete.\n */\n\n/**\n * Every process Book starts runs under Book, so a `book` among them is driven by Book, not by the\n * user typing (`src/session-driver.ts`): it may keep what it learns, never its delegator's\n * instructions, as a delegated run of any other harness does. Its own `--session-driver` flag\n * still wins.\n */\nexport const CHILD_SESSION_DRIVER = 'agent';\n\n/** Set alongside the default `NODE_ENV`, and stripped from every child. */\nexport const DEFAULTED_NODE_ENV_MARKER = 'BOOK_DEFAULTED_NODE_ENV';\n\n/**\n * Build the environment for one child process.\n *\n * `base` defaults to Book's own environment. The result is a fresh object: a caller that\n * mutates it cannot reach back into `process.env`, which is shared with every other child.\n */\nexport function buildChildEnv(\n base: NodeJS.ProcessEnv = process.env,\n overrides: NodeJS.ProcessEnv = {},\n): NodeJS.ProcessEnv {\n const env: NodeJS.ProcessEnv = { ...base, ...overrides };\n delete env[DEFAULTED_NODE_ENV_MARKER];\n env.BOOK_SESSION_DRIVER = CHILD_SESSION_DRIVER;\n // Only Book's own default is removed, and only when nothing asked for it. A NODE_ENV that\n // arrived some other way is the user's and the command's to keep.\n //\n // An override carrying the marker is a copy of Book's own environment rather than a declaration:\n // `ToolContext.env` is `process.env` in the agent, and a persistent job's spec carries the\n // environment its runner was started with. Both carry the default without having asked for it.\n const overrideIsAmbient = overrides[DEFAULTED_NODE_ENV_MARKER] !== undefined;\n if (overrideIsAmbient || (base[DEFAULTED_NODE_ENV_MARKER] !== undefined && !overrides.NODE_ENV)) {\n delete env.NODE_ENV;\n }\n return env;\n}\n"],"mappings":";AAgCO,IAAM,uBAAuB;AAG7B,IAAM,4BAA4B;AAQlC,SAAS,cACd,OAA0B,QAAQ,KAClC,YAA+B,CAAC,GACb;AACnB,QAAM,MAAyB,EAAE,GAAG,MAAM,GAAG,UAAU;AACvD,SAAO,IAAI,yBAAyB;AACpC,MAAI,sBAAsB;AAO1B,QAAM,oBAAoB,UAAU,yBAAyB,MAAM;AACnE,MAAI,qBAAsB,KAAK,yBAAyB,MAAM,UAAa,CAAC,UAAU,UAAW;AAC/F,WAAO,IAAI;AAAA,EACb;AACA,SAAO;AACT;","names":[]}