@letrquan/book 0.3.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.
- package/CHANGELOG.md +1024 -0
- package/README.md +6 -3
- package/dist/{additional-roots-H7UNAEHS.js → additional-roots-VAWRQ625.js} +8 -7
- package/dist/{app-GE4OHJOC.js → app-J7BVPSUH.js} +5397 -2953
- package/dist/app-J7BVPSUH.js.map +1 -0
- package/dist/chunk-32WBCUAN.js +660 -0
- package/dist/chunk-32WBCUAN.js.map +1 -0
- package/dist/{chunk-525CAZPY.js → chunk-4DKHLZRI.js} +6 -6
- package/dist/{chunk-4AS7LKKQ.js → chunk-62EEAUID.js} +2 -2
- package/dist/{chunk-AQEQX4FF.js → chunk-AHC7OU75.js} +385 -55
- package/dist/chunk-AHC7OU75.js.map +1 -0
- package/dist/{chunk-EFJ2NH6E.js → chunk-AJZWLKLQ.js} +63 -14
- package/dist/chunk-AJZWLKLQ.js.map +1 -0
- package/dist/chunk-B5FC3MYZ.js +395 -0
- package/dist/chunk-B5FC3MYZ.js.map +1 -0
- package/dist/{chunk-67UZEWWF.js → chunk-DNFQTSMJ.js} +53 -11
- package/dist/chunk-DNFQTSMJ.js.map +1 -0
- package/dist/{chunk-TLX3H2CL.js → chunk-E5WLQSKA.js} +231 -40
- package/dist/chunk-E5WLQSKA.js.map +1 -0
- package/dist/chunk-ECLC4CIJ.js +1181 -0
- package/dist/chunk-ECLC4CIJ.js.map +1 -0
- package/dist/chunk-EEX6QNXV.js +127 -0
- package/dist/chunk-EEX6QNXV.js.map +1 -0
- package/dist/chunk-EFXVSQ5J.js +931 -0
- package/dist/chunk-EFXVSQ5J.js.map +1 -0
- package/dist/{chunk-RFJ6Q6LE.js → chunk-ENLB7NYV.js} +577 -164
- package/dist/chunk-ENLB7NYV.js.map +1 -0
- package/dist/{chunk-IME7R2UI.js → chunk-FPR7SKIE.js} +1268 -883
- package/dist/chunk-FPR7SKIE.js.map +1 -0
- package/dist/{chunk-VPNLCMVR.js → chunk-HFB74CLM.js} +13 -5
- package/dist/chunk-HFB74CLM.js.map +1 -0
- package/dist/{chunk-5RDRAO4B.js → chunk-HOZXKOE3.js} +37 -1
- package/dist/chunk-HOZXKOE3.js.map +1 -0
- package/dist/chunk-IDYWN37T.js +36 -0
- package/dist/chunk-IDYWN37T.js.map +1 -0
- package/dist/{chunk-YC6W4EJH.js → chunk-JGF5HLSI.js} +193 -75
- package/dist/chunk-JGF5HLSI.js.map +1 -0
- package/dist/{chunk-5YHTKWJN.js → chunk-KDNWMKXV.js} +2447 -907
- package/dist/chunk-KDNWMKXV.js.map +1 -0
- package/dist/{chunk-Y2VHN7TC.js → chunk-MEOB2DZA.js} +11 -1
- package/dist/chunk-MEOB2DZA.js.map +1 -0
- package/dist/{chunk-KBQS35OV.js → chunk-MV5PQNIA.js} +2 -2
- package/dist/chunk-N2G4Q7ME.js +2672 -0
- package/dist/chunk-N2G4Q7ME.js.map +1 -0
- package/dist/{chunk-62QL5QUP.js → chunk-OJBMDM5H.js} +17 -1
- package/dist/chunk-OJBMDM5H.js.map +1 -0
- package/dist/{chunk-56SIEMDX.js → chunk-PTDC7ZUP.js} +2 -2
- package/dist/{chunk-X4NJT436.js → chunk-REASC2QG.js} +67 -28
- package/dist/chunk-REASC2QG.js.map +1 -0
- package/dist/chunk-SBTU5BEC.js +114 -0
- package/dist/chunk-SBTU5BEC.js.map +1 -0
- package/dist/{chunk-HQI7NYGX.js → chunk-SWPDKLNM.js} +2 -2
- package/dist/{chunk-W3XFHEF4.js → chunk-UCFJ43OS.js} +140 -28
- package/dist/chunk-UCFJ43OS.js.map +1 -0
- package/dist/{chunk-QYBD22H5.js → chunk-W7SLYS3M.js} +80 -16
- package/dist/chunk-W7SLYS3M.js.map +1 -0
- package/dist/{chunk-SOW4WRQP.js → chunk-WV3O5PGX.js} +3 -3
- package/dist/{chunk-JTPOI3KU.js → chunk-XJRJVXJR.js} +262 -38
- package/dist/chunk-XJRJVXJR.js.map +1 -0
- package/dist/{chunk-224FJZLH.js → chunk-YIL6HTJX.js} +228 -5
- package/dist/chunk-YIL6HTJX.js.map +1 -0
- package/dist/{command-approvals-VBIU6BLM.js → command-approvals-ZRY66LCW.js} +5 -4
- package/dist/context-C3RAP6K5.js +41 -0
- package/dist/{hook-approvals-ZVT4V7IM.js → hook-approvals-DPQD5H65.js} +5 -4
- package/dist/index.js +657 -754
- package/dist/index.js.map +1 -1
- package/dist/{interactive-assets-T4I4SEHU.js → interactive-assets-73AQO5WP.js} +11 -6
- package/dist/{interactive-assets-T4I4SEHU.js.map → interactive-assets-73AQO5WP.js.map} +1 -1
- package/dist/job-runner.js +10 -3
- package/dist/job-runner.js.map +1 -1
- package/dist/job-supervisor.js +13 -4
- package/dist/job-supervisor.js.map +1 -1
- package/dist/loader-DQNPDKOX.js +27 -0
- package/dist/{mcp-approvals-HJ2VDYMC.js → mcp-approvals-27LJYADQ.js} +5 -4
- package/dist/{mcp-config-RNB5LEVE.js → mcp-config-GXSODNCM.js} +3 -2
- package/dist/memory-store-I6Z54VUN.js +51 -0
- package/dist/{model-window-store-MTSQ5JDJ.js → model-window-store-6VKXXTPJ.js} +4 -3
- package/dist/{openai-compatible-2OB54YST.js → openai-compatible-IPUNBJ53.js} +5 -3
- package/dist/{permission-approvals-YLV6UC5W.js → permission-approvals-Q5IMFEZI.js} +5 -4
- package/dist/{permissions-DHOKF3W2.js → permissions-7Y35X6HZ.js} +26 -9
- package/dist/provider-approvals-5AGH3RVT.js +76 -0
- package/dist/{sandbox-WRYTZH3D.js → sandbox-N2RY72GN.js} +6 -5
- package/dist/{scrollback-35ME2GFX.js → scrollback-GD76GNM4.js} +36 -28
- package/dist/scrollback-GD76GNM4.js.map +1 -0
- package/dist/sdk.d.ts +876 -64
- package/dist/sdk.js +57 -37
- package/dist/sdk.js.map +1 -1
- package/dist/settings-loader-KDBH45AQ.js +43 -0
- package/dist/{settings-removed-JWWPT4WA.js → settings-removed-3ATWJEF3.js} +2 -2
- package/dist/shell-selection-Z5WORZU4.js +31 -0
- package/dist/shell-selection-Z5WORZU4.js.map +1 -0
- package/package.json +5 -5
- package/dist/app-GE4OHJOC.js.map +0 -1
- package/dist/chunk-224FJZLH.js.map +0 -1
- package/dist/chunk-5RDRAO4B.js.map +0 -1
- package/dist/chunk-5YHTKWJN.js.map +0 -1
- package/dist/chunk-62QL5QUP.js.map +0 -1
- package/dist/chunk-67UZEWWF.js.map +0 -1
- package/dist/chunk-6OQQOMZR.js +0 -785
- package/dist/chunk-6OQQOMZR.js.map +0 -1
- package/dist/chunk-AQEQX4FF.js.map +0 -1
- package/dist/chunk-C7ADUNPR.js +0 -273
- package/dist/chunk-C7ADUNPR.js.map +0 -1
- package/dist/chunk-EFJ2NH6E.js.map +0 -1
- package/dist/chunk-IME7R2UI.js.map +0 -1
- package/dist/chunk-J62VZYOB.js +0 -1339
- package/dist/chunk-J62VZYOB.js.map +0 -1
- package/dist/chunk-JTPOI3KU.js.map +0 -1
- package/dist/chunk-QYBD22H5.js.map +0 -1
- package/dist/chunk-RFJ6Q6LE.js.map +0 -1
- package/dist/chunk-S4JMID4G.js +0 -259
- package/dist/chunk-S4JMID4G.js.map +0 -1
- package/dist/chunk-TLX3H2CL.js.map +0 -1
- package/dist/chunk-VPNLCMVR.js.map +0 -1
- package/dist/chunk-W3XFHEF4.js.map +0 -1
- package/dist/chunk-X4NJT436.js.map +0 -1
- package/dist/chunk-X66I2NQO.js +0 -137
- package/dist/chunk-X66I2NQO.js.map +0 -1
- package/dist/chunk-Y2VHN7TC.js.map +0 -1
- package/dist/chunk-YC6W4EJH.js.map +0 -1
- package/dist/context-CBOBDY5Q.js +0 -38
- package/dist/loader-S77OVTHH.js +0 -18
- package/dist/memory-store-DZUT5HUH.js +0 -75
- package/dist/scrollback-35ME2GFX.js.map +0 -1
- package/dist/settings-loader-JUNPGAAM.js +0 -35
- package/dist/shell-selection-CDHGJ4GC.js +0 -17
- /package/dist/{additional-roots-H7UNAEHS.js.map → additional-roots-VAWRQ625.js.map} +0 -0
- /package/dist/{chunk-525CAZPY.js.map → chunk-4DKHLZRI.js.map} +0 -0
- /package/dist/{chunk-4AS7LKKQ.js.map → chunk-62EEAUID.js.map} +0 -0
- /package/dist/{chunk-KBQS35OV.js.map → chunk-MV5PQNIA.js.map} +0 -0
- /package/dist/{chunk-56SIEMDX.js.map → chunk-PTDC7ZUP.js.map} +0 -0
- /package/dist/{chunk-HQI7NYGX.js.map → chunk-SWPDKLNM.js.map} +0 -0
- /package/dist/{chunk-SOW4WRQP.js.map → chunk-WV3O5PGX.js.map} +0 -0
- /package/dist/{command-approvals-VBIU6BLM.js.map → command-approvals-ZRY66LCW.js.map} +0 -0
- /package/dist/{context-CBOBDY5Q.js.map → context-C3RAP6K5.js.map} +0 -0
- /package/dist/{hook-approvals-ZVT4V7IM.js.map → hook-approvals-DPQD5H65.js.map} +0 -0
- /package/dist/{loader-S77OVTHH.js.map → loader-DQNPDKOX.js.map} +0 -0
- /package/dist/{mcp-approvals-HJ2VDYMC.js.map → mcp-approvals-27LJYADQ.js.map} +0 -0
- /package/dist/{mcp-config-RNB5LEVE.js.map → mcp-config-GXSODNCM.js.map} +0 -0
- /package/dist/{memory-store-DZUT5HUH.js.map → memory-store-I6Z54VUN.js.map} +0 -0
- /package/dist/{model-window-store-MTSQ5JDJ.js.map → model-window-store-6VKXXTPJ.js.map} +0 -0
- /package/dist/{openai-compatible-2OB54YST.js.map → openai-compatible-IPUNBJ53.js.map} +0 -0
- /package/dist/{permission-approvals-YLV6UC5W.js.map → permission-approvals-Q5IMFEZI.js.map} +0 -0
- /package/dist/{permissions-DHOKF3W2.js.map → permissions-7Y35X6HZ.js.map} +0 -0
- /package/dist/{sandbox-WRYTZH3D.js.map → provider-approvals-5AGH3RVT.js.map} +0 -0
- /package/dist/{settings-loader-JUNPGAAM.js.map → sandbox-N2RY72GN.js.map} +0 -0
- /package/dist/{settings-removed-JWWPT4WA.js.map → settings-loader-KDBH45AQ.js.map} +0 -0
- /package/dist/{shell-selection-CDHGJ4GC.js.map → settings-removed-3ATWJEF3.js.map} +0 -0
|
@@ -1,35 +1,52 @@
|
|
|
1
|
+
import {
|
|
2
|
+
normalizeRemovedSettings
|
|
3
|
+
} from "./chunk-HOZXKOE3.js";
|
|
1
4
|
import {
|
|
2
5
|
collectDeclaredDirectories,
|
|
3
6
|
partitionProjectDirectories
|
|
4
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-4DKHLZRI.js";
|
|
5
8
|
import {
|
|
6
9
|
partitionProjectAllowRules
|
|
7
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-MV5PQNIA.js";
|
|
11
|
+
import {
|
|
12
|
+
createWorkspaceProviderGate,
|
|
13
|
+
droppedProviderOverrides,
|
|
14
|
+
emptyWorkspaceProviderPartition,
|
|
15
|
+
modelSelectionsOf,
|
|
16
|
+
partitionWorkspaceProviders,
|
|
17
|
+
profileOfModelKey,
|
|
18
|
+
providerIdsOf,
|
|
19
|
+
unaffectedModelSelections,
|
|
20
|
+
withheldModelSelections,
|
|
21
|
+
withheldProviderIds,
|
|
22
|
+
withoutWorkspaceProviderOverrides
|
|
23
|
+
} from "./chunk-B5FC3MYZ.js";
|
|
8
24
|
import {
|
|
9
25
|
collectDeclaredHooks,
|
|
10
26
|
partitionProjectHooks
|
|
11
|
-
} from "./chunk-
|
|
27
|
+
} from "./chunk-WV3O5PGX.js";
|
|
12
28
|
import {
|
|
13
29
|
defaultTrustStorePath,
|
|
14
30
|
loadWorkspaceTrust
|
|
15
|
-
} from "./chunk-
|
|
31
|
+
} from "./chunk-HFB74CLM.js";
|
|
16
32
|
import {
|
|
33
|
+
ADVISOR_OFF_VALUE,
|
|
17
34
|
DEFAULT_SETTINGS,
|
|
18
35
|
HOOK_EVENTS,
|
|
19
36
|
SettingsRepository,
|
|
20
37
|
bookSettingsSchema,
|
|
21
38
|
writeFileAtomic
|
|
22
|
-
} from "./chunk-
|
|
39
|
+
} from "./chunk-REASC2QG.js";
|
|
40
|
+
import {
|
|
41
|
+
projectFileWriteRefusal
|
|
42
|
+
} from "./chunk-IDYWN37T.js";
|
|
23
43
|
import {
|
|
24
44
|
resolveBookHome
|
|
25
45
|
} from "./chunk-ZPSZZUWF.js";
|
|
26
|
-
import {
|
|
27
|
-
normalizeRemovedSettings
|
|
28
|
-
} from "./chunk-5RDRAO4B.js";
|
|
29
46
|
|
|
30
47
|
// src/settings-loader.ts
|
|
31
|
-
import { readFileSync, existsSync } from "fs";
|
|
32
|
-
import { join, normalize } from "path";
|
|
48
|
+
import { readFileSync, existsSync, realpathSync } from "fs";
|
|
49
|
+
import { join, normalize, resolve } from "path";
|
|
33
50
|
|
|
34
51
|
// src/env-boolean.ts
|
|
35
52
|
function parseEnvBoolean(raw) {
|
|
@@ -107,7 +124,9 @@ var WORKSPACE_FORBIDDEN_PATHS = [
|
|
|
107
124
|
["permissions", "projectAllowRules"],
|
|
108
125
|
["hooks", "projectEntries"],
|
|
109
126
|
["commands", "projectCommands"],
|
|
110
|
-
["", "projectDirectories"]
|
|
127
|
+
["", "projectDirectories"],
|
|
128
|
+
// The decisions about workspace-declared providers (#418), at the root for the same reason.
|
|
129
|
+
["", "workspaceProviders"]
|
|
111
130
|
];
|
|
112
131
|
function stripPaths(settings, paths) {
|
|
113
132
|
for (const [parent, key] of paths) {
|
|
@@ -128,6 +147,9 @@ var WORKSPACE_SANDBOX_ONLY_TIGHTENING = [
|
|
|
128
147
|
[["sandbox", "autoAllowBashIfSandboxed"], false],
|
|
129
148
|
[["sandbox", "excludedCommands"], null],
|
|
130
149
|
[["sandbox", "filesystem", "allowWrite"], null],
|
|
150
|
+
// `false` (the loader default) is the read-only git dirs; a workspace layer may
|
|
151
|
+
// tighten to it and may never opt itself back out (#373).
|
|
152
|
+
[["sandbox", "filesystem", "allowGitWrites"], false],
|
|
131
153
|
[["sandbox", "network", "allowedDomains"], null]
|
|
132
154
|
];
|
|
133
155
|
function readPath(settings, path) {
|
|
@@ -235,22 +257,68 @@ ${result.error.message}`);
|
|
|
235
257
|
return parsed;
|
|
236
258
|
}
|
|
237
259
|
function settingsLayerPaths(workspace, overridePath, paths = {}) {
|
|
238
|
-
const userPath =
|
|
260
|
+
const userPath = userSettingsPath(paths);
|
|
261
|
+
const projectPath = paths.projectSettingsPath ?? join(workspace, ".book", "settings.json");
|
|
239
262
|
const layers = [
|
|
240
263
|
userPath,
|
|
241
|
-
|
|
264
|
+
...sameFile(projectPath, userPath) ? [] : [projectPath],
|
|
242
265
|
paths.localSettingsPath ?? join(workspace, ".book", "settings.local.json")
|
|
243
266
|
];
|
|
244
267
|
if (overridePath) layers.push(overridePath);
|
|
245
268
|
return layers;
|
|
246
269
|
}
|
|
270
|
+
function userSettingsPath(paths) {
|
|
271
|
+
return paths.userSettingsPath ?? (paths.home ? join(paths.home, ".book", "settings.json") : join(resolveBookHome(), "settings.json"));
|
|
272
|
+
}
|
|
273
|
+
function sameFile(left, right) {
|
|
274
|
+
if (resolve(left) === resolve(right)) return true;
|
|
275
|
+
try {
|
|
276
|
+
return realpathSync.native(left) === realpathSync.native(right);
|
|
277
|
+
} catch {
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
function isUserSettingsFile(path, paths = {}) {
|
|
282
|
+
return sameFile(path, userSettingsPath(paths));
|
|
283
|
+
}
|
|
284
|
+
function emptyWorkspaceProviderGate() {
|
|
285
|
+
return createWorkspaceProviderGate({ partition: emptyWorkspaceProviderPartition() });
|
|
286
|
+
}
|
|
287
|
+
function withoutStaleAdvisorOff(local, project) {
|
|
288
|
+
if (!local || project?.advisorModel !== void 0) return local;
|
|
289
|
+
if (local.advisorModel?.trim().toLowerCase() !== ADVISOR_OFF_VALUE) return local;
|
|
290
|
+
const rest = { ...local };
|
|
291
|
+
delete rest.advisorModel;
|
|
292
|
+
return rest;
|
|
293
|
+
}
|
|
247
294
|
function resolveSettings(workspace, overridePath, paths = {}) {
|
|
295
|
+
return resolveSettingsWithProviderGate(workspace, overridePath, paths).settings;
|
|
296
|
+
}
|
|
297
|
+
function resolveSettingsWithProviderGate(workspace, overridePath, paths = {}) {
|
|
248
298
|
let resolved = structuredClone(DEFAULT_SETTINGS);
|
|
249
|
-
const userPath =
|
|
299
|
+
const userPath = userSettingsPath(paths);
|
|
250
300
|
const user = loadSettingsFile(userPath);
|
|
251
|
-
if (user) resolved = mergeLayer(resolved, user, "trusted");
|
|
252
301
|
const projectPath = paths.projectSettingsPath ?? join(workspace, ".book", "settings.json");
|
|
253
|
-
const project = loadSettingsFile(projectPath);
|
|
302
|
+
const project = sameFile(projectPath, userPath) ? null : loadSettingsFile(projectPath);
|
|
303
|
+
const localPath = paths.localSettingsPath ?? join(workspace, ".book", "settings.local.json");
|
|
304
|
+
const local = withoutStaleAdvisorOff(loadSettingsFile(localPath), project);
|
|
305
|
+
const override = overridePath ? loadSettingsFile(overridePath) : null;
|
|
306
|
+
const trustedProviderIds = providerIdsOf([
|
|
307
|
+
{ providers: user?.provider },
|
|
308
|
+
{ providers: override?.provider }
|
|
309
|
+
]);
|
|
310
|
+
const workspaceProviderLayers = [
|
|
311
|
+
{ path: projectPath, providers: project?.provider },
|
|
312
|
+
{ path: localPath, providers: local?.provider }
|
|
313
|
+
];
|
|
314
|
+
const gateProviders = (layer) => layer.provider ? {
|
|
315
|
+
...layer,
|
|
316
|
+
provider: withoutWorkspaceProviderOverrides(
|
|
317
|
+
layer.provider,
|
|
318
|
+
trustedProviderIds
|
|
319
|
+
)
|
|
320
|
+
} : layer;
|
|
321
|
+
if (user) resolved = mergeLayer(resolved, user, "trusted");
|
|
254
322
|
const declaredProjectAllow = project?.permissions?.allow ?? [];
|
|
255
323
|
const declaredProjectHooks = collectDeclaredHooks(project);
|
|
256
324
|
const declaredProjectDirectories = project?.additionalDirectories ?? [];
|
|
@@ -268,15 +336,10 @@ function resolveSettings(workspace, overridePath, paths = {}) {
|
|
|
268
336
|
if (declaredProjectHooks.length > 0) {
|
|
269
337
|
withheld = { ...withheld, hooks: structuredClone(DEFAULT_SETTINGS.hooks) };
|
|
270
338
|
}
|
|
271
|
-
resolved = mergeLayer(resolved, withheld, "repository");
|
|
272
|
-
}
|
|
273
|
-
const localPath = paths.localSettingsPath ?? join(workspace, ".book", "settings.local.json");
|
|
274
|
-
const local = loadSettingsFile(localPath);
|
|
275
|
-
if (local) resolved = mergeLayer(resolved, local, "local");
|
|
276
|
-
if (overridePath) {
|
|
277
|
-
const override = loadSettingsFile(overridePath);
|
|
278
|
-
if (override) resolved = mergeLayer(resolved, override, "trusted");
|
|
339
|
+
resolved = mergeLayer(resolved, gateProviders(withheld), "repository");
|
|
279
340
|
}
|
|
341
|
+
if (local) resolved = mergeLayer(resolved, gateProviders(local), "local");
|
|
342
|
+
if (override) resolved = mergeLayer(resolved, override, "trusted");
|
|
280
343
|
const trust = loadWorkspaceTrust(
|
|
281
344
|
workspace,
|
|
282
345
|
paths.trustStorePath ?? defaultTrustStorePath(paths.home)
|
|
@@ -295,6 +358,40 @@ function resolveSettings(workspace, overridePath, paths = {}) {
|
|
|
295
358
|
...resolved.projectDirectories,
|
|
296
359
|
...trust.projectDirectories
|
|
297
360
|
};
|
|
361
|
+
resolved.workspaceProviders = {
|
|
362
|
+
...resolved.workspaceProviders,
|
|
363
|
+
...trust.workspaceProviders
|
|
364
|
+
};
|
|
365
|
+
const providerPartition = partitionWorkspaceProviders(
|
|
366
|
+
workspaceProviderLayers,
|
|
367
|
+
trustedProviderIds,
|
|
368
|
+
resolved.workspaceProviders
|
|
369
|
+
);
|
|
370
|
+
for (const { id, declaration } of providerPartition.approved) {
|
|
371
|
+
resolved.provider[id] = declaration;
|
|
372
|
+
}
|
|
373
|
+
const withheldProviders = new Set(withheldProviderIds(providerPartition));
|
|
374
|
+
const selectionLayers = [
|
|
375
|
+
{ path: userPath, trusted: true, selections: modelSelectionsOf(user ?? {}) },
|
|
376
|
+
{ path: projectPath, trusted: false, selections: modelSelectionsOf(project ?? {}) },
|
|
377
|
+
{ path: localPath, trusted: false, selections: modelSelectionsOf(local ?? {}) },
|
|
378
|
+
{ path: overridePath ?? "", trusted: true, selections: modelSelectionsOf(override ?? {}) }
|
|
379
|
+
];
|
|
380
|
+
const withheldSelections = withheldModelSelections(selectionLayers, withheldProviders);
|
|
381
|
+
for (const { key, fallback } of withheldSelections) {
|
|
382
|
+
const profile = profileOfModelKey(key);
|
|
383
|
+
if (profile === void 0) {
|
|
384
|
+
const topLevel = key;
|
|
385
|
+
if (fallback === void 0) delete resolved[topLevel];
|
|
386
|
+
else resolved[topLevel] = fallback;
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
const entry = resolved.agents.profiles[profile];
|
|
390
|
+
if (!entry) continue;
|
|
391
|
+
if (fallback === void 0) delete entry.model;
|
|
392
|
+
else entry.model = fallback;
|
|
393
|
+
if (Object.keys(entry).length === 0) delete resolved.agents.profiles[profile];
|
|
394
|
+
}
|
|
298
395
|
if (declaredProjectAllow.length > 0) {
|
|
299
396
|
const { approved } = partitionProjectAllowRules(
|
|
300
397
|
declaredProjectAllow,
|
|
@@ -332,7 +429,13 @@ function resolveSettings(workspace, overridePath, paths = {}) {
|
|
|
332
429
|
}
|
|
333
430
|
}
|
|
334
431
|
const settings = bookSettingsSchema.parse(resolved);
|
|
335
|
-
|
|
432
|
+
const providerGate = createWorkspaceProviderGate({
|
|
433
|
+
partition: providerPartition,
|
|
434
|
+
droppedOverrides: droppedProviderOverrides(workspaceProviderLayers, trustedProviderIds),
|
|
435
|
+
withheldSelections,
|
|
436
|
+
unaffectedSelections: unaffectedModelSelections(selectionLayers, withheldProviders)
|
|
437
|
+
});
|
|
438
|
+
return { settings, providerGate };
|
|
336
439
|
}
|
|
337
440
|
function applySettingsEnvOverrides(settings, env = process.env) {
|
|
338
441
|
const startupAnimation = parseEnvBoolean(env.BOOK_STARTUP_ANIMATION);
|
|
@@ -351,6 +454,14 @@ function migrateLegacyPermissions(workspace, home, validatedSettings) {
|
|
|
351
454
|
const legacyPath = join(home ? join(home, ".book") : resolveBookHome(), "permissions.json");
|
|
352
455
|
if (!existsSync(legacyPath)) return false;
|
|
353
456
|
const markerPath = join(workspace, ".book", "migrations.json");
|
|
457
|
+
const localPath = join(workspace, ".book", "settings.local.json");
|
|
458
|
+
for (const path of [markerPath, localPath]) {
|
|
459
|
+
const refusal = projectFileWriteRefusal(path);
|
|
460
|
+
if (refusal) {
|
|
461
|
+
console.warn(`\u26A0 Permission rules not migrated: ${path}: ${refusal}.`);
|
|
462
|
+
return false;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
354
465
|
try {
|
|
355
466
|
const marker = JSON.parse(readFileSync(markerPath, "utf-8"));
|
|
356
467
|
if ((marker.legacyPermissions ?? 0) >= LEGACY_PERMISSIONS_MIGRATION_VERSION) return false;
|
|
@@ -365,8 +476,6 @@ function migrateLegacyPermissions(workspace, home, validatedSettings) {
|
|
|
365
476
|
return false;
|
|
366
477
|
}
|
|
367
478
|
if (legacyRules.length === 0) return false;
|
|
368
|
-
const localDir = join(workspace, ".book");
|
|
369
|
-
const localPath = join(localDir, "settings.local.json");
|
|
370
479
|
const result = new SettingsRepository(localPath).update((existing) => {
|
|
371
480
|
const permissions = existing.permissions ?? {};
|
|
372
481
|
for (const key of ["allow", "ask", "deny"]) {
|
|
@@ -400,9 +509,12 @@ export {
|
|
|
400
509
|
mergeSettings,
|
|
401
510
|
loadSettingsFile,
|
|
402
511
|
settingsLayerPaths,
|
|
512
|
+
isUserSettingsFile,
|
|
513
|
+
emptyWorkspaceProviderGate,
|
|
403
514
|
resolveSettings,
|
|
515
|
+
resolveSettingsWithProviderGate,
|
|
404
516
|
applySettingsEnvOverrides,
|
|
405
517
|
startupAnimationEnvNote,
|
|
406
518
|
migrateLegacyPermissions
|
|
407
519
|
};
|
|
408
|
-
//# sourceMappingURL=chunk-
|
|
520
|
+
//# sourceMappingURL=chunk-UCFJ43OS.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/settings-loader.ts","../src/env-boolean.ts"],"sourcesContent":["import { readFileSync, existsSync, realpathSync } from 'fs';\nimport { join, normalize, resolve } from 'path';\nimport {\n ADVISOR_OFF_VALUE,\n bookSettingsSchema,\n DEFAULT_SETTINGS,\n HOOK_EVENTS,\n type BookSettings,\n type HookEntry,\n type HookEvent,\n type ResolvedSettings,\n} from './settings.js';\nimport { SettingsRepository, writeFileAtomic } from './settings-repository.js';\nimport { projectFileWriteRefusal } from './workspace-files.js';\nimport { resolveBookHome } from './book-home.js';\nimport { collectDeclaredDirectories, partitionProjectDirectories } from './additional-roots.js';\nimport { partitionProjectAllowRules } from './permission-approvals.js';\nimport { collectDeclaredHooks, partitionProjectHooks } from './hook-approvals.js';\nimport {\n createWorkspaceProviderGate,\n droppedProviderOverrides,\n emptyWorkspaceProviderPartition,\n modelSelectionsOf,\n partitionWorkspaceProviders,\n profileOfModelKey,\n providerIdsOf,\n unaffectedModelSelections,\n withheldModelSelections,\n withheldProviderIds,\n withoutWorkspaceProviderOverrides,\n type ModelSelectionLayer,\n type ProviderLayer,\n type WorkspaceProviderGate,\n} from './provider-approvals.js';\nimport { defaultTrustStorePath, loadWorkspaceTrust } from './workspace-trust.js';\nimport { normalizeRemovedSettings } from './settings-removed.js';\nimport { parseEnvBoolean } from './env-boolean.js';\n\nconst LEGACY_PERMISSIONS_MIGRATION_VERSION = 1;\n\n/**\n * The sandbox deny lists: paths a command may not write and paths it may not\n * read, plus the domains it may not reach.\n *\n * One list for all three, because they are one rule applied to three keys — and\n * because the rule is no longer \"these keys concatenate\" but \"these keys\n * concatenate *for a workspace layer*\", which both the concatenating set and the\n * deduping one are derived from. A second hand-written copy is how\n * `Notification` went missing from the hooks list in the first place (#295).\n */\nconst SANDBOX_DENY_LIST_PATHS = [\n 'sandbox.filesystem.denyWrite',\n 'sandbox.filesystem.denyRead',\n 'sandbox.network.deniedDomains',\n] as const;\n\n/**\n * Deep-merge two settings objects. For arrays, concatenate (used for\n * permission rules and additionalDirectories). For objects, merge recursively.\n * For scalars, the override wins.\n */\nconst CONCATENATED_ARRAY_PATHS = new Set<string>([\n 'permissions.allow',\n 'permissions.ask',\n 'permissions.deny',\n // Straight from the list the hooks schema is built from: a hand-written copy of\n // it went stale the moment `Notification` was added, and a later layer's\n // notification hooks then replaced the user layer's instead of appending (#295).\n ...HOOK_EVENTS.map((event) => `hooks.${event}`),\n // A later layer adds to the deny lists rather than replacing them, so a\n // workspace layer shipping `denyRead: []` cannot erase the user's entries\n // (#373) — and a later *trusted* layer still can, which is the one way to\n // narrow a list that turned out to be too broad.\n ...SANDBOX_DENY_LIST_PATHS,\n]);\n\n/**\n * The same three paths as a set, for the two questions a merge asks about one of\n * them: whether the layer's entries add to or replace the earlier ones, and\n * whether repeating a path across layers stores it once.\n */\nconst SANDBOX_DENY_LIST_PATH_SET: ReadonlySet<string> = new Set(SANDBOX_DENY_LIST_PATHS);\n\nfunction mergeObject(\n base: Record<string, unknown>,\n override: Record<string, unknown>,\n /** Whether a deny list from this layer adds to the earlier one or replaces it. */\n accumulateDenyLists: boolean,\n prefix = '',\n): Record<string, unknown> {\n const result = structuredClone(base);\n\n for (const [key, value] of Object.entries(override)) {\n // JSON.parse keeps `\"__proto__\"` as an ordinary own key, but `result[key] = …`\n // below would go through the prototype setter: a repository layer could then\n // supply inherited `shell` or `defaultMode` values that sanitizeLayer, which\n // deletes own keys, never sees. No setting has that name, so drop it.\n if (value === undefined || key === '__proto__') continue;\n const path = prefix ? `${prefix}.${key}` : key;\n const existing = result[key];\n\n if (Array.isArray(value)) {\n if (path === 'additionalDirectories') {\n const combined = [...(Array.isArray(existing) ? existing : []), ...value].map((entry) =>\n normalize(String(entry)),\n );\n result[key] = [...new Set(combined)];\n } else if (\n CONCATENATED_ARRAY_PATHS.has(path) &&\n (accumulateDenyLists || !SANDBOX_DENY_LIST_PATH_SET.has(path))\n ) {\n const combined = [...(Array.isArray(existing) ? existing : []), ...value];\n result[key] = SANDBOX_DENY_LIST_PATH_SET.has(path) ? [...new Set(combined)] : combined;\n } else {\n result[key] = structuredClone(value);\n }\n } else if (\n typeof value === 'object' &&\n value !== null &&\n typeof existing === 'object' &&\n existing !== null &&\n !Array.isArray(existing)\n ) {\n result[key] = mergeObject(\n existing as Record<string, unknown>,\n value as Record<string, unknown>,\n accumulateDenyLists,\n path,\n );\n } else {\n result[key] = value;\n }\n }\n\n return result;\n}\n\n/**\n * How much authority a settings layer carries.\n *\n * - `trusted` — the user's own global file, or a path they passed on the CLI.\n * - `local` — `<workspace>/.book/settings.local.json`: gitignored and normally\n * written by the user, or by Book on their behalf.\n * - `repository` — `<workspace>/.book/settings.json`: checked in and controlled\n * by whoever wrote the repository. It may never grant itself trust.\n */\nexport type SettingsLayerTrust = 'trusted' | 'local' | 'repository';\n\n/**\n * Settings paths that a workspace file may not supply for itself. Most record\n * a decision the user made *about* repository-controlled input; honouring one\n * from the workspace would let a clone approve itself. Experimental capability\n * flags are included because merely opening a clone must not opt the user into\n * unstable runtime behavior.\n *\n * The local layer is no safer than the checked-in one here. `.gitignore` does\n * not stop a *tracked* file from reaching a clone, so a repository that\n * force-adds `.book/settings.local.json` ships its own approvals with it. Both\n * workspace layers are therefore stripped. Trust decisions come from the\n * user-global store; experimental flags come from a trusted user-global or\n * explicit settings document (or from a process environment opt-in).\n *\n * An empty `parent` names a key at the document root, which is how the\n * `additionalDirectories` decisions map is expressed: the setting it decides\n * about is itself top-level, so there is no container object to put it in.\n */\nconst WORKSPACE_FORBIDDEN_PATHS: ReadonlyArray<readonly [string, string]> = [\n ['mcp', 'projectServers'],\n ['permissions', 'projectAllowRules'],\n ['hooks', 'projectEntries'],\n ['commands', 'projectCommands'],\n ['', 'projectDirectories'],\n // The decisions about workspace-declared providers (#418), at the root for the same reason.\n ['', 'workspaceProviders'],\n];\n\nfunction stripPaths(\n settings: Partial<BookSettings>,\n paths: ReadonlyArray<readonly [string, string]>,\n): void {\n for (const [parent, key] of paths) {\n if (parent === '') {\n delete (settings as Record<string, unknown>)[key];\n continue;\n }\n const container = (settings as Record<string, unknown>)[parent];\n if (container && typeof container === 'object' && !Array.isArray(container)) {\n delete (container as Record<string, unknown>)[key];\n }\n }\n}\n\n/** One `sandbox.*` key a workspace layer supplied that the loader will not honour. */\nexport interface IgnoredWorkspaceSandboxKey {\n /** Dotted path of the key, e.g. `sandbox.filesystem.allowWrite`. */\n key: string;\n /** The value the layer wrote, so a report can quote what the file actually said. */\n value: unknown;\n}\n\n/**\n * `sandbox.*` keys a workspace layer supplies that only loosen, and therefore\n * never survive to the merge. The second element is the one value the layer may\n * set — the tightening direction — or `null` when any value is dropped.\n *\n * The sandbox is the boundary between a command and the host, so a file inside\n * the workspace must not be able to widen it: a checked-in `settings.json`\n * could switch off the sandbox the user enabled, exclude every command\n * (`excludedCommands: [\"*\"]`), or open a writable root (`allowWrite: [\"/\"]`)\n * — and because those arrays *replaced* the earlier layer's, even the user's own\n * `denyRead` list could be emptied. Turning the sandbox on, adding deny entries\n * and refusing unsandboxed commands stay available to a workspace layer; they\n * only ever narrow it (#373).\n */\nconst WORKSPACE_SANDBOX_ONLY_TIGHTENING: ReadonlyArray<readonly [readonly string[], unknown]> = [\n [['sandbox', 'enabled'], true],\n [['sandbox', 'failIfUnavailable'], true],\n [['sandbox', 'allowUnsandboxedCommands'], false],\n [['sandbox', 'autoAllowBashIfSandboxed'], false],\n [['sandbox', 'excludedCommands'], null],\n [['sandbox', 'filesystem', 'allowWrite'], null],\n // `false` (the loader default) is the read-only git dirs; a workspace layer may\n // tighten to it and may never opt itself back out (#373).\n [['sandbox', 'filesystem', 'allowGitWrites'], false],\n [['sandbox', 'network', 'allowedDomains'], null],\n];\n\n/**\n * The value at a path in a settings document, or `undefined` when a step is\n * missing or is not a plain object.\n *\n * One walker rather than two near-copies: `readContainer` used to be this with an\n * extra condition at the end, and a loop that differs only in its last line is\n * how two helpers drift apart and a delete lands on the wrong object.\n */\nfunction readPath(settings: Record<string, unknown>, path: readonly string[]): unknown {\n let value: unknown = settings;\n for (const key of path) {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) return undefined;\n value = (value as Record<string, unknown>)[key];\n if (value === undefined) return undefined;\n }\n return value;\n}\n\nfunction deletePath(settings: Record<string, unknown>, path: readonly string[]): void {\n const parent = path.slice(0, -1);\n const key = path[path.length - 1];\n const container = parent.length === 0 ? settings : readObjectPath(settings, parent);\n if (container) delete container[key];\n}\n\n/** `readPath` for a path that has to end at an object, which `deletePath` needs. */\nfunction readObjectPath(\n settings: Record<string, unknown>,\n path: readonly string[],\n): Record<string, unknown> | undefined {\n const value = readPath(settings, path);\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : undefined;\n}\n\n/**\n * A workspace layer as the file wrote it: parsed, but not required to be\n * complete. The sandbox rule is about what a document *says*, so the reader\n * takes a partial view — a layer that omits `excludedCommands` entirely is\n * describing itself, not supplying an empty list. Every key is optional here,\n * including the nested ones, which is the whole point of the type.\n */\nexport interface WorkspaceLayerSandboxView {\n sandbox?: Omit<Partial<BookSettings['sandbox']>, 'filesystem' | 'network'> & {\n filesystem?: Partial<BookSettings['sandbox']['filesystem']>;\n network?: Partial<BookSettings['sandbox']['network']>;\n };\n}\n\n/**\n * The `sandbox.*` keys `settings` declares that a workspace layer may not\n * supply, with the values it wrote. Pure: `sanitizeLayer` deletes exactly the\n * keys this reports, and `book doctor` prints the same list, so the enforced\n * rule and the reported one cannot drift apart.\n */\nexport function ignoredWorkspaceSandboxKeys(\n settings: WorkspaceLayerSandboxView,\n): IgnoredWorkspaceSandboxKey[] {\n const record = settings as Record<string, unknown>;\n const ignored: IgnoredWorkspaceSandboxKey[] = [];\n for (const [path, tighteningValue] of WORKSPACE_SANDBOX_ONLY_TIGHTENING) {\n const value = readPath(record, path);\n if (value === undefined) continue;\n if (tighteningValue !== null && value === tighteningValue) continue;\n ignored.push({ key: path.join('.'), value });\n }\n return ignored;\n}\n\n/**\n * How much of an ignored value a report prints.\n *\n * `excludedCommands` and `allowWrite` are globs and directory lists, so a\n * repository can put as much text in one as it likes, and every host that\n * reports an ignored key would then print a screenful of it — pushing the part\n * of the diagnostic that says where the value belongs off the screen. The head\n * is kept because it identifies the value; the length is what follows it.\n */\nconst IGNORED_VALUE_PREVIEW_CHARS = 200;\n\n/** One ignored key as a report line reads it: `sandbox.enabled=false`. */\nexport function formatIgnoredWorkspaceSandboxKey(entry: IgnoredWorkspaceSandboxKey): string {\n const json = JSON.stringify(entry.value) ?? String(entry.value);\n if (json.length <= IGNORED_VALUE_PREVIEW_CHARS) return `${entry.key}=${json}`;\n return `${entry.key}=${json.slice(0, IGNORED_VALUE_PREVIEW_CHARS)}… (truncated, ${\n json.length - IGNORED_VALUE_PREVIEW_CHARS\n } more characters)`;\n}\n\n/**\n * Drop every loosening `sandbox.*` key from a workspace layer. Deleting rather\n * than overwriting is what makes the key harmless: the merge keeps whatever the\n * earlier, more trusted layer said.\n */\nfunction applyIgnoredWorkspaceSandboxKeys(settings: Partial<BookSettings>): void {\n const record = settings as Record<string, unknown>;\n for (const entry of ignoredWorkspaceSandboxKeys(settings)) {\n deletePath(record, entry.key.split('.'));\n }\n}\n\nfunction sanitizeLayer(\n settings: Partial<BookSettings>,\n trust: SettingsLayerTrust,\n): Partial<BookSettings> {\n if (trust === 'trusted') return settings;\n const sanitized = structuredClone(settings);\n // Project/local settings cannot opt a session into the most permissive mode.\n if (sanitized.defaultMode === 'bypassPermissions') delete sanitized.defaultMode;\n // `shell` names the program every Bash command is handed to. A clone that\n // could point it at a binary it ships would run that binary on the first\n // command, so the key is honoured from trusted layers only.\n delete sanitized.shell;\n // Nor may a workspace file widen the sandbox it is supposed to be confined by.\n applyIgnoredWorkspaceSandboxKeys(sanitized);\n stripPaths(sanitized, WORKSPACE_FORBIDDEN_PATHS);\n return sanitized;\n}\n\n/**\n * Merge one layer over the resolved settings.\n *\n * The layer's trust decides two things that are not visible in the layer itself:\n * whether a deny list adds to or replaces the earlier one, and whether turning\n * the sandbox on costs the session its auto-allow. Both are stated here, next to\n * the sanitizing that already reads `trust`, rather than left to each caller.\n */\nfunction mergeLayer(\n resolved: ResolvedSettings,\n layer: Partial<BookSettings>,\n trust: SettingsLayerTrust,\n): ResolvedSettings {\n const candidate = sanitizeLayer(layer, trust);\n // A trusted global safety ceiling cannot be disabled by a lower-trust layer.\n if (\n resolved.disableBypassPermissionsMode === true &&\n candidate.disableBypassPermissionsMode === false\n ) {\n candidate.disableBypassPermissionsMode = true;\n }\n if (trust !== 'trusted' && sandboxTurnedOnByWorkspaceLayer(resolved, candidate)) {\n // Written through the partial view the sanitizer uses: `BookSettings`\n // declares every sandbox key as present, and this layer supplies one.\n const tightened: WorkspaceLayerSandboxView = {\n sandbox: { ...candidate.sandbox, autoAllowBashIfSandboxed: false },\n };\n (candidate as WorkspaceLayerSandboxView).sandbox = tightened.sandbox;\n }\n return mergeSettings(resolved, candidate, trust);\n}\n\n/**\n * Whether this workspace layer is what turns the sandbox on.\n *\n * `enabled: true` is the one `sandbox` key a workspace layer may set, because it\n * only ever applies to sessions that had it off. On its own it narrows nothing a\n * user chose; with `autoAllowBashIfSandboxed` it stops the session from asking\n * before each command, so a checked-in `settings.json` would turn a workspace\n * where the user reads every command into one where nobody is asked at all.\n * Setting it here makes the pair equivalent to the auto-allow alone, which the\n * loader already drops.\n */\nfunction sandboxTurnedOnByWorkspaceLayer(\n resolved: ResolvedSettings,\n candidate: Partial<BookSettings>,\n): boolean {\n return candidate.sandbox?.enabled === true && resolved.sandbox?.enabled !== true;\n}\n\n/** One layer as a caller of {@link workspaceLayerThatEnabledSandbox} has it. */\nexport interface SettingsLayerForSandboxActivation {\n path: string;\n trust: SettingsLayerTrust;\n /** Null when the file is absent, which is not a layer that declared anything. */\n document: Partial<BookSettings> | null;\n}\n\n/**\n * The workspace layer that flipped `sandbox.enabled` from not-true to true, or\n * undefined when none did: the file whose `enabled: true` cost the session its\n * `autoAllowBashIfSandboxed`.\n *\n * Two hosts have to say so — `book doctor`, which lists what each layer changed,\n * and the print/SDK notices, which have no other channel — and both were asking\n * the raw layers \"does this one say `enabled: true`\". That is a different\n * question: once a trusted layer has enabled the sandbox, a repository repeating\n * the key changes nothing and was still reported, every session, as a decision it\n * did not make.\n *\n * The layers go in resolution order *including* the trusted ones, because the\n * trusted baseline is exactly what decides whether a workspace layer is the\n * flipper. The walk is this function's own and reads only `sandbox.enabled`, and\n * it asks the same question through the same {@link sanitizeLayer} the merge\n * applies, so it cannot disagree with\n * {@link sandboxTurnedOnByWorkspaceLayer} about which layer that is.\n */\nexport function workspaceLayerThatEnabledSandbox(\n layers: readonly SettingsLayerForSandboxActivation[],\n): string | undefined {\n let enabled = DEFAULT_SETTINGS.sandbox.enabled;\n for (const layer of layers) {\n if (!layer.document) continue;\n const candidate = sanitizeLayer(layer.document, layer.trust);\n if (layer.trust !== 'trusted' && candidate.sandbox?.enabled === true && enabled !== true) {\n return layer.path;\n }\n if (candidate.sandbox?.enabled !== undefined) enabled = candidate.sandbox.enabled;\n }\n return undefined;\n}\n\n/**\n * Merge a settings layer over resolved settings.\n *\n * @param trust - what the layer is (`trusted` by default: the user-global file\n * or a `--settings` path). Only a workspace layer accumulates the sandbox deny\n * lists; every other array behaviour is the same for all layers.\n */\nexport function mergeSettings(\n base: ResolvedSettings,\n override: Partial<BookSettings>,\n trust: SettingsLayerTrust = 'trusted',\n): ResolvedSettings {\n return mergeObject(\n base as unknown as Record<string, unknown>,\n override as Record<string, unknown>,\n trust !== 'trusted',\n ) as unknown as ResolvedSettings;\n}\n\n/**\n * Load and validate a single settings.json file. Returns null if the file\n * doesn't exist. Throws on parse/validation errors.\n */\nfunction loadSettingsFile(path: string): BookSettings | null {\n if (!existsSync(path)) return null;\n const raw = readFileSync(path, 'utf-8');\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (e) {\n throw new Error(\n `Invalid JSON in settings file: ${path}\\n${e instanceof Error ? e.message : String(e)}`,\n );\n }\n // A setting that was removed must not brick a working install: an obsolete\n // value of a surviving key would otherwise fail the whole document and stop\n // Book from starting, with a schema dump that names no remedy. Removed keys\n // are reported by `book doctor`, which reads the file itself.\n parsed = normalizeRemovedSettings(parsed);\n const result = bookSettingsSchema.safeParse(parsed);\n if (!result.success) throw new Error(`Invalid settings in ${path}:\\n${result.error.message}`);\n // Resolution owns defaults. Returning the source document preserves the distinction\n // between an omitted field and a field explicitly set to an empty collection.\n return parsed as BookSettings;\n}\n\n/**\n * Resolve settings from all scopes: user → project → local.\n * Priority: Local > Project > User.\n *\n * @param workspace - Project root directory\n * @param overridePath - Optional path to an ad-hoc settings file (--settings flag)\n * @returns Fully resolved settings with all defaults filled\n */\nexport interface SettingsResolutionPaths {\n home?: string;\n userSettingsPath?: string;\n projectSettingsPath?: string;\n localSettingsPath?: string;\n trustStorePath?: string;\n}\n\n/**\n * Every settings file this resolution would read, in layer order.\n *\n * Exported so callers that need to inspect the files themselves — `book doctor`\n * reporting removed keys, the credential error checking for a stale `auth`\n * block — resolve the same paths the loader does rather than rebuilding them.\n */\nexport function settingsLayerPaths(\n workspace: string,\n overridePath?: string,\n paths: SettingsResolutionPaths = {},\n): string[] {\n const userPath = userSettingsPath(paths);\n const projectPath = paths.projectSettingsPath ?? join(workspace, '.book', 'settings.json');\n const layers = [\n userPath,\n ...(sameFile(projectPath, userPath) ? [] : [projectPath]),\n paths.localSettingsPath ?? join(workspace, '.book', 'settings.local.json'),\n ];\n if (overridePath) layers.push(overridePath);\n return layers;\n}\n\nfunction userSettingsPath(paths: SettingsResolutionPaths): string {\n return (\n paths.userSettingsPath ??\n (paths.home\n ? join(paths.home, '.book', 'settings.json')\n : join(resolveBookHome(), 'settings.json'))\n );\n}\n\n/**\n * Whether two paths name one file. The lexical check covers a file that does not\n * exist yet; a link to the file, or a workspace reached through a linked home\n * directory, needs the real paths.\n */\nfunction sameFile(left: string, right: string): boolean {\n if (resolve(left) === resolve(right)) return true;\n try {\n return realpathSync.native(left) === realpathSync.native(right);\n } catch {\n return false;\n }\n}\n\n/**\n * Whether `path` is the user settings file.\n *\n * A workspace at `$HOME` (with `BOOK_HOME` unset) has `~/.book/settings.json` as\n * its project settings file too. Read a second time as the repository layer, the\n * user's own file would add every accumulating rule twice, and its hooks, allow\n * rules and directories would be withheld as if a repository had declared them,\n * then registered twice once approved (#429). Every reader of the project layer\n * skips a file this returns true for.\n */\nexport function isUserSettingsFile(path: string, paths: SettingsResolutionPaths = {}): boolean {\n return sameFile(path, userSettingsPath(paths));\n}\n\nexport type { WorkspaceProviderGate };\n\nexport function emptyWorkspaceProviderGate(): WorkspaceProviderGate {\n return createWorkspaceProviderGate({ partition: emptyWorkspaceProviderPartition() });\n}\n\n/**\n * The advisor `off` a local layer wrote, which outranks a lower layer's advisor, is honoured only\n * while a checked-in workspace layer still declares one (`/advisor off` writes it for exactly\n * that case). Once the repository stops declaring an advisor, the `off` is stale and would keep\n * outranking the user's own, so the layer is read without it.\n */\nfunction withoutStaleAdvisorOff(\n local: BookSettings | null,\n project: BookSettings | null,\n): BookSettings | null {\n if (!local || project?.advisorModel !== undefined) return local;\n if (local.advisorModel?.trim().toLowerCase() !== ADVISOR_OFF_VALUE) return local;\n const rest = { ...local };\n delete rest.advisorModel;\n return rest;\n}\n\nexport function resolveSettings(\n workspace: string,\n overridePath?: string,\n paths: SettingsResolutionPaths = {},\n): ResolvedSettings {\n return resolveSettingsWithProviderGate(workspace, overridePath, paths).settings;\n}\n\n/** {@link resolveSettings}, plus what the provider gate withheld and dropped on the way. */\nexport function resolveSettingsWithProviderGate(\n workspace: string,\n overridePath?: string,\n paths: SettingsResolutionPaths = {},\n): { settings: ResolvedSettings; providerGate: WorkspaceProviderGate } {\n let resolved = structuredClone(DEFAULT_SETTINGS);\n\n // Every layer is read before any is merged: whether a workspace layer may speak for a provider\n // depends on which ids the *trusted* layers define, and the `--settings` layer merges last.\n // The read order is unchanged (user, project, local, `--settings`) and nothing merged between\n // two reads can throw, so the first broken file still names the error, as before.\n const userPath = userSettingsPath(paths);\n const user = loadSettingsFile(userPath);\n const projectPath = paths.projectSettingsPath ?? join(workspace, '.book', 'settings.json');\n // Skipped when it is the user file itself, as it is for a workspace at `$HOME` (#429).\n const project = sameFile(projectPath, userPath) ? null : loadSettingsFile(projectPath);\n const localPath = paths.localSettingsPath ?? join(workspace, '.book', 'settings.local.json');\n const local = withoutStaleAdvisorOff(loadSettingsFile(localPath), project);\n const override = overridePath ? loadSettingsFile(overridePath) : null;\n\n const trustedProviderIds = providerIdsOf([\n { providers: user?.provider },\n { providers: override?.provider },\n ]);\n const workspaceProviderLayers: ProviderLayer[] = [\n { path: projectPath, providers: project?.provider },\n { path: localPath, providers: local?.provider },\n ];\n // A workspace layer reaches `provider` only for ids a trusted layer defines, and only with\n // `models` and `type`. The rest is held back for the partition below.\n const gateProviders = (layer: BookSettings): BookSettings =>\n layer.provider\n ? {\n ...layer,\n provider: withoutWorkspaceProviderOverrides(\n layer.provider,\n trustedProviderIds,\n ) as BookSettings['provider'],\n }\n : layer;\n\n // Layer 1: User settings (~/.book/settings.json)\n if (user) resolved = mergeLayer(resolved, user, 'trusted');\n\n // Layer 2: Project settings (<workspace>/.book/settings.json).\n // `allow` rules from the repository layer are held back rather than merged:\n // they only widen authority, and the decisions that release them live in the\n // local layer, which has not been merged yet.\n const declaredProjectAllow = project?.permissions?.allow ?? [];\n // Project-declared hook entries are held back the same way: each one is a\n // shell command the repository would otherwise get Book to run.\n const declaredProjectHooks = collectDeclaredHooks(project);\n // `additionalDirectories` widens the roots the file tools serve, so a checked-in\n // declaration is held back for the same reason a project allow rule is: it\n // grants authority, and the decisions that release it live in the local layer,\n // which has not been merged yet. Released after the trust store loads, by real path.\n const declaredProjectDirectories = project?.additionalDirectories ?? [];\n // Released declarations belong between the user and local layers, matching the\n // relative merge order ungated layers still produce.\n const userHookCounts = Object.fromEntries(\n HOOK_EVENTS.map((event) => [event, resolved.hooks[event].length]),\n ) as Record<HookEvent, number>;\n if (project) {\n let withheld: BookSettings = project;\n if (declaredProjectAllow.length > 0) {\n withheld = { ...withheld, permissions: { ...withheld.permissions, allow: [] } };\n }\n if (declaredProjectDirectories.length > 0) {\n withheld = { ...withheld, additionalDirectories: [] };\n }\n if (declaredProjectHooks.length > 0) {\n withheld = { ...withheld, hooks: structuredClone(DEFAULT_SETTINGS.hooks) };\n }\n resolved = mergeLayer(resolved, gateProviders(withheld), 'repository');\n }\n\n // Layer 3: Local settings (<workspace>/.book/settings.local.json)\n if (local) resolved = mergeLayer(resolved, gateProviders(local), 'local');\n\n // Layer 4 (optional): Ad-hoc override (--settings flag)\n if (override) resolved = mergeLayer(resolved, override, 'trusted');\n\n // Trust decisions come from outside the workspace, so a repository cannot\n // ship its own. Layers the user controls may still carry them — the global\n // file and an explicit `--settings` path are trusted for everything else —\n // but the store has the final say on any key it records.\n const trust = loadWorkspaceTrust(\n workspace,\n paths.trustStorePath ?? defaultTrustStorePath(paths.home),\n );\n resolved.permissions.projectAllowRules = {\n ...resolved.permissions.projectAllowRules,\n ...trust.permissionAllowRules,\n };\n resolved.mcp.projectServers = { ...resolved.mcp.projectServers, ...trust.mcpServers };\n resolved.hooks.projectEntries = { ...resolved.hooks.projectEntries, ...trust.hookEntries };\n resolved.commands.projectCommands = {\n ...resolved.commands.projectCommands,\n ...trust.projectCommands,\n };\n resolved.projectDirectories = {\n ...resolved.projectDirectories,\n ...trust.projectDirectories,\n };\n resolved.workspaceProviders = {\n ...resolved.workspaceProviders,\n ...trust.workspaceProviders,\n };\n\n // Providers only a workspace layer defines: approved ones join `provider`, the rest are absent.\n // Their keys are never resolved here or anywhere downstream of an unapproved entry.\n const providerPartition = partitionWorkspaceProviders(\n workspaceProviderLayers,\n trustedProviderIds,\n resolved.workspaceProviders,\n );\n for (const { id, declaration } of providerPartition.approved) {\n resolved.provider[id] = declaration as BookSettings['provider'][string];\n }\n const withheldProviders = new Set(withheldProviderIds(providerPartition));\n // A `model`, `compactModel`, `advisorModel` or agent-profile `model` a workspace layer set for a\n // withheld provider gives way to what the trusted layers alone say.\n const selectionLayers: ModelSelectionLayer[] = [\n { path: userPath, trusted: true, selections: modelSelectionsOf(user ?? {}) },\n { path: projectPath, trusted: false, selections: modelSelectionsOf(project ?? {}) },\n { path: localPath, trusted: false, selections: modelSelectionsOf(local ?? {}) },\n { path: overridePath ?? '', trusted: true, selections: modelSelectionsOf(override ?? {}) },\n ];\n const withheldSelections = withheldModelSelections(selectionLayers, withheldProviders);\n for (const { key, fallback } of withheldSelections) {\n const profile = profileOfModelKey(key);\n if (profile === undefined) {\n const topLevel = key as 'model' | 'compactModel' | 'advisorModel';\n if (fallback === undefined) delete resolved[topLevel];\n else resolved[topLevel] = fallback;\n continue;\n }\n const entry = resolved.agents.profiles[profile];\n if (!entry) continue;\n if (fallback === undefined) delete entry.model;\n else entry.model = fallback;\n if (Object.keys(entry).length === 0) delete resolved.agents.profiles[profile];\n }\n\n // Every decision source is in place now, so the withheld repository rules can\n // be released — approved ones only.\n if (declaredProjectAllow.length > 0) {\n const { approved } = partitionProjectAllowRules(\n declaredProjectAllow,\n resolved.permissions?.projectAllowRules,\n );\n if (approved.length > 0) {\n resolved.permissions.allow = [...(resolved.permissions.allow ?? []), ...approved];\n }\n }\n\n if (declaredProjectDirectories.length > 0) {\n // Released by real path, not by the text the repository wrote: see `additional-roots.ts`.\n // Approved entries join the resolved list as their declared text, which is what the merge\n // above normalizes and deduplicates; the file tools resolve it again to the same place.\n const { approved } = partitionProjectDirectories(\n collectDeclaredDirectories(workspace, declaredProjectDirectories),\n resolved.projectDirectories,\n );\n if (approved.length > 0) {\n // The approved **real path**, never the declared text. A consumer resolves this list again\n // later with no trust store in hand (see `resolveAdditionalRoots`), so a relative or\n // symlinked spelling would let a relink move a root the user approved as somewhere else.\n // The user saw the real path when the prompt named it; the repository's spelling is what\n // would be dangerous to re-resolve (PR #334 finding 7).\n resolved.additionalDirectories = [\n ...(resolved.additionalDirectories ?? []),\n ...approved.map((directory) => directory.realPath),\n ];\n }\n }\n\n if (declaredProjectHooks.length > 0) {\n const { approved } = partitionProjectHooks(\n declaredProjectHooks,\n resolved.hooks?.projectEntries,\n );\n const approvedByEvent = new Map<HookEvent, HookEntry[]>();\n for (const { event, entry } of approved) {\n const entries = approvedByEvent.get(event) ?? [];\n entries.push(entry);\n approvedByEvent.set(event, entries);\n }\n for (const [event, entries] of approvedByEvent) {\n resolved.hooks[event].splice(userHookCounts[event], 0, ...entries);\n }\n }\n\n const settings = bookSettingsSchema.parse(resolved) as ResolvedSettings;\n const providerGate = createWorkspaceProviderGate({\n partition: providerPartition,\n droppedOverrides: droppedProviderOverrides(workspaceProviderLayers, trustedProviderIds),\n withheldSelections,\n unaffectedSelections: unaffectedModelSelections(selectionLayers, withheldProviders),\n });\n return { settings, providerGate };\n}\n\n/**\n * The environment's say over the merged layers, applied to settings that are\n * about to be used rather than saved.\n *\n * `BOOK_STARTUP_ANIMATION` is here, and not in `loadConfig`, because the layers\n * are only half the answer: a reader that resolved the files and stopped would\n * report the file's value while Book acts on the variable's. So every path that\n * produces the *effective* settings calls this — the startup config, the\n * re-read after a provider is removed, `book config get`/`list`. A path that\n * computes what gets written back to a settings file must not: the variable\n * decides the next launch, and baking it into a file would make a later\n * `unset` look like it had failed.\n *\n * The returned object is a new one; the input is left as the caller resolved it.\n */\nexport function applySettingsEnvOverrides(\n settings: ResolvedSettings,\n env: NodeJS.ProcessEnv = process.env,\n): ResolvedSettings {\n const startupAnimation = parseEnvBoolean(env.BOOK_STARTUP_ANIMATION);\n if (startupAnimation === undefined) return settings;\n return { ...settings, ui: { ...settings.ui, startupAnimation } };\n}\n\n/**\n * What to tell a user whose read or save is being decided by the environment\n * rather than by their settings — the `BOOK_MODEL` warning in one sentence, or\n * nothing when the variable is unset or unreadable.\n */\nexport function startupAnimationEnvNote(env: NodeJS.ProcessEnv = process.env): string | undefined {\n const raw = env.BOOK_STARTUP_ANIMATION;\n if (parseEnvBoolean(raw) === undefined) return undefined;\n return (\n `BOOK_STARTUP_ANIMATION is set to \"${raw}\" — it decides the splash at every ` +\n 'launch, so ui.startupAnimation will not apply while it is set.'\n );\n}\n\nexport { loadSettingsFile };\n\n/**\n * Migrate rules from the legacy ~/.book/permissions.json into the local\n * settings file (<workspace>/.book/settings.local.json). Runs once on first\n * load when the legacy file exists and the local settings don't have rules yet.\n *\n * @param home - User home directory; injectable for isolated migration tests\n * @param validatedSettings - Already-resolved settings from the startup preflight\n * @returns true if migration occurred, false otherwise\n */\nexport function migrateLegacyPermissions(\n workspace: string,\n home?: string,\n validatedSettings?: ResolvedSettings,\n): boolean {\n // Direct callers must observe the same fail-before-storage boundary as the\n // normal startup path. Internal callers pass already-resolved settings to\n // avoid reading the layers twice on the common off path.\n if (!validatedSettings) {\n resolveSettings(workspace, undefined, home ? { home } : undefined);\n }\n\n const legacyPath = join(home ? join(home, '.book') : resolveBookHome(), 'permissions.json');\n if (!existsSync(legacyPath)) return false;\n const markerPath = join(workspace, '.book', 'migrations.json');\n const localPath = join(workspace, '.book', 'settings.local.json');\n // Before the marker is read: it is one of the files this writes.\n for (const path of [markerPath, localPath]) {\n const refusal = projectFileWriteRefusal(path);\n if (refusal) {\n console.warn(`⚠ Permission rules not migrated: ${path}: ${refusal}.`);\n return false;\n }\n }\n try {\n const marker = JSON.parse(readFileSync(markerPath, 'utf-8')) as {\n legacyPermissions?: number;\n };\n if ((marker.legacyPermissions ?? 0) >= LEGACY_PERMISSIONS_MIGRATION_VERSION) return false;\n } catch {\n // Missing or malformed markers are safely rebuilt after a successful migration.\n }\n\n let legacyRules: Array<{ toolName: string; pattern?: string; effect: string }> = [];\n try {\n const raw = readFileSync(legacyPath, 'utf-8');\n const parsed = JSON.parse(raw) as {\n rules: Array<{ toolName: string; pattern?: string; effect: string }>;\n };\n legacyRules = parsed.rules ?? [];\n } catch {\n return false; // corrupt — leave the legacy file alone\n }\n\n if (legacyRules.length === 0) return false;\n\n const result = new SettingsRepository(localPath).update((existing) => {\n const permissions = (existing.permissions ?? {}) as {\n allow?: string[];\n ask?: string[];\n deny?: string[];\n };\n for (const key of ['allow', 'ask', 'deny'] as const) {\n if (!Array.isArray(permissions[key])) permissions[key] = [];\n }\n\n for (const rule of legacyRules) {\n const specifier = rule.pattern ? `${rule.toolName}(${rule.pattern})` : rule.toolName;\n const effect = rule.effect as 'allow' | 'ask' | 'deny';\n if (!permissions[effect]!.includes(specifier)) permissions[effect]!.push(specifier);\n }\n existing.permissions = permissions;\n });\n if (!result.ok) return false;\n\n writeFileAtomic(\n markerPath,\n `${JSON.stringify({ legacyPermissions: LEGACY_PERMISSIONS_MIGRATION_VERSION }, null, 2)}\\n`,\n );\n if (!result.changed) return false;\n\n console.warn(\n `⚠ Migrated ${legacyRules.length} permission rule(s) from ~/.book/permissions.json to ${localPath}. ` +\n 'Delete the legacy file after verifying the migration.',\n );\n return true;\n}\n","/**\n * The one grammar for a boolean environment variable: `1`/`true`/`on`/`yes` and\n * `0`/`false`/`off`/`no`, case-insensitive and trimmed, and undefined for\n * anything else.\n *\n * Tri-state on purpose. A flag that reads \"unset\" as \"off\" cannot tell a caller\n * that wants the default from one that was never told, and every such caller\n * then hard-codes a second grammar beside it. `BOOK_STARTUP_ANIMATION` needs all\n * three: it has to outrank a settings file when set, leave that file alone when\n * not, and say nothing rather than guess when the value is a typo — a wrong guess\n * either delays the first render or hides the input bar a script is waiting for.\n */\nexport function parseEnvBoolean(raw: string | undefined): boolean | undefined {\n switch (raw?.trim().toLowerCase()) {\n case '0':\n case 'false':\n case 'off':\n case 'no':\n return false;\n case '1':\n case 'true':\n case 'on':\n case 'yes':\n return true;\n default:\n return undefined;\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -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
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
148
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
184
|
-
return
|
|
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
|
|
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
|
|
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-
|
|
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"]}
|