bx-mac 1.0.2 → 1.2.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/README.md +11 -3
- package/dist/bx.js +141 -36
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -44,6 +44,7 @@ bx ~/work/my-project ~/work/shared-lib
|
|
|
44
44
|
- **No protection against root/sudo** — the sandbox applies to the user-level process
|
|
45
45
|
- **macOS only** — relies on `sandbox-exec` (Apple-specific)
|
|
46
46
|
- **Not dynamic** — the sandbox profile is a snapshot of `$HOME` at launch time; directories or files created later are **not** automatically blocked
|
|
47
|
+
- **File names visible** — blocked files cannot be read or written, but their names still appear in directory listings (a kernel-level `readdir` constraint, same as `chmod 000`)
|
|
47
48
|
- **Not a vault** — `sandbox-exec` is undocumented; treat this as a safety net, not a guarantee
|
|
48
49
|
|
|
49
50
|
## 📥 Install
|
|
@@ -122,6 +123,9 @@ bx --dry ~/work/my-project
|
|
|
122
123
|
|
|
123
124
|
# 🔍 See the generated sandbox profile
|
|
124
125
|
bx --verbose ~/work/my-project
|
|
126
|
+
|
|
127
|
+
# 🔄 Use an isolated app profile
|
|
128
|
+
bx --vscode-user code ~/work/my-project
|
|
125
129
|
```
|
|
126
130
|
|
|
127
131
|
## ⚙️ Options
|
|
@@ -131,7 +135,7 @@ bx --verbose ~/work/my-project
|
|
|
131
135
|
| `--dry` | Show a tree of all protected, read-only, and accessible paths — don't launch anything |
|
|
132
136
|
| `--verbose` | Print the generated sandbox profile plus launch details (binary, arguments, cwd, focus command) |
|
|
133
137
|
| `--background` | Run the app detached in the background (like `nohup &`), output goes to `/tmp/bx-<pid>.log` |
|
|
134
|
-
| `--
|
|
138
|
+
| `--vscode-user [path]` | Use an isolated app profile (default: `~/.vscode-sandbox`, or specify a custom path) |
|
|
135
139
|
|
|
136
140
|
On normal runs, bx also prints a short policy summary (number of workdirs, blocked directories, hidden paths, and read-only directories).
|
|
137
141
|
|
|
@@ -166,8 +170,9 @@ path = "/usr/local/bin/code"
|
|
|
166
170
|
| `fallback` | Absolute fallback path if `mdfind` discovery fails |
|
|
167
171
|
| `args` | Extra arguments always passed to the app |
|
|
168
172
|
| `passPaths` | Paths passed as app launch args (`true`/`false`/`N`/`["~/p1", "~/p2"]`) |
|
|
169
|
-
| `paths` | Default working directories when none are given on the CLI (supports `~/` paths) |
|
|
173
|
+
| `paths` | Default working directories when none are given on the CLI (supports `~/` paths and `*` globs) |
|
|
170
174
|
| `background` | Run the app detached in the background by default (`true`/`false`) |
|
|
175
|
+
| `profile` | Use an isolated app profile (`true` = `~/.vscode-sandbox`, `"path"` = custom path) |
|
|
171
176
|
|
|
172
177
|
**Resolution order:** `path` → `mdfind` by `bundle` + `binary` → `fallback`
|
|
173
178
|
|
|
@@ -342,7 +347,7 @@ Or preconfigure them in `~/.bxconfig.toml`:
|
|
|
342
347
|
paths = ["~/work/project-a", "~/work/project-b"]
|
|
343
348
|
```
|
|
344
349
|
|
|
345
|
-
For VSCode specifically, `--
|
|
350
|
+
For VSCode specifically, `--vscode-user` forces a separate Electron process via an isolated `--user-data-dir`, but this means separate extensions and settings. You can specify a custom path (`--vscode-user ~/my-profile`) or use the default (`--vscode-user` alone uses `~/.vscode-sandbox`). This can also be configured per app in `~/.bxconfig.toml` via the `profile` field.
|
|
346
351
|
|
|
347
352
|
## 💡 Tips
|
|
348
353
|
|
|
@@ -369,12 +374,15 @@ Some AI coding tools ship with their own sandboxing. bx complements these by pro
|
|
|
369
374
|
- [Claude Code](https://code.claude.com/docs/en/sandboxing) — built-in sandbox for file and command restrictions
|
|
370
375
|
- [Gemini CLI](https://geminicli.com/docs/cli/sandbox/) — sandbox mode for file system access control
|
|
371
376
|
- [OpenAI Codex](https://developers.openai.com/codex/concepts/sandboxing) — containerized sandboxing for code execution
|
|
377
|
+
- [VS Code Copilot](https://code.visualstudio.com/docs/copilot/agents/agent-tools#_sandbox-agent-commands) — agent sandbox mode (preview) that restricts write access to the working directory and blocks network access for terminal commands (`chat.agent.sandbox` setting)
|
|
372
378
|
|
|
373
379
|
These are great when available, but they only protect within their own tool. bx wraps the entire process — so even if a tool's built-in sandbox is misconfigured, disabled, or absent, your files stay protected.
|
|
374
380
|
|
|
375
381
|
## 🔗 Alternatives
|
|
376
382
|
|
|
377
383
|
- [Agent Safehouse](https://agent-safehouse.dev/) — macOS kernel-level sandboxing for LLM coding agents via `sandbox-exec`. Deny-first model that blocks write access outside the project directory.
|
|
384
|
+
- **Docker / VMs** — for stronger isolation, run AI tools in a virtualized environment (containers, VMs). Full process and network isolation at the cost of setup overhead.
|
|
385
|
+
- **Web sandboxes** — browser-based approaches for running AI agents. See Simon Willison's [Living dangerously with Claude](https://simonwillison.net/2025/Oct/22/living-dangerously-with-claude/) for an overview.
|
|
378
386
|
|
|
379
387
|
## 💛 Sponsor
|
|
380
388
|
|
package/dist/bx.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { accessSync, constants, cpSync, existsSync, globSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { accessSync, closeSync, constants, cpSync, existsSync, globSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { basename, dirname, join, resolve } from "node:path";
|
|
4
|
-
import { execFileSync, spawn } from "node:child_process";
|
|
4
|
+
import { execFileSync, execSync, spawn } from "node:child_process";
|
|
5
5
|
import { createInterface } from "node:readline";
|
|
6
6
|
import process$1 from "node:process";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
@@ -811,12 +811,15 @@ function parsePassPaths(val) {
|
|
|
811
811
|
if (typeof val === "number" && Number.isInteger(val) && val > 0) return val;
|
|
812
812
|
if (Array.isArray(val)) {
|
|
813
813
|
const paths = val.filter((a) => typeof a === "string");
|
|
814
|
+
if (val.length !== paths.length) console.error(fmt.warn("non-string items in array config were ignored"));
|
|
814
815
|
if (paths.length > 0) return paths;
|
|
816
|
+
return false;
|
|
815
817
|
}
|
|
816
818
|
}
|
|
817
819
|
function parseStringArray(val) {
|
|
818
820
|
if (Array.isArray(val)) {
|
|
819
821
|
const items = val.filter((a) => typeof a === "string");
|
|
822
|
+
if (val.length !== items.length) console.error(fmt.warn("non-string items in array config were ignored"));
|
|
820
823
|
if (items.length > 0) return items;
|
|
821
824
|
}
|
|
822
825
|
}
|
|
@@ -830,7 +833,8 @@ function parseAppDef(def) {
|
|
|
830
833
|
args: parseStringArray(def.args),
|
|
831
834
|
passPaths: parsePassPaths(def.passPaths ?? def.passWorkPaths ?? def.passWorkdirs),
|
|
832
835
|
paths: parseStringArray(def.paths ?? def.workdirs),
|
|
833
|
-
background: typeof def.background === "boolean" ? def.background : void 0
|
|
836
|
+
background: typeof def.background === "boolean" ? def.background : void 0,
|
|
837
|
+
profile: typeof def.profile === "boolean" || typeof def.profile === "string" ? def.profile : void 0
|
|
834
838
|
};
|
|
835
839
|
}
|
|
836
840
|
/**
|
|
@@ -855,7 +859,8 @@ function loadConfig(home) {
|
|
|
855
859
|
"passWorkdirs",
|
|
856
860
|
"paths",
|
|
857
861
|
"workdirs",
|
|
858
|
-
"background"
|
|
862
|
+
"background",
|
|
863
|
+
"profile"
|
|
859
864
|
]);
|
|
860
865
|
for (const [key, val] of Object.entries(doc)) {
|
|
861
866
|
if (key === "apps") continue;
|
|
@@ -1082,9 +1087,12 @@ function collectBlockedDirs(parentDir, home, scriptDir, allowedDirs) {
|
|
|
1082
1087
|
} catch {
|
|
1083
1088
|
continue;
|
|
1084
1089
|
}
|
|
1085
|
-
if (!isDir) continue;
|
|
1086
1090
|
if (parentDir === home && name === "Library") continue;
|
|
1087
1091
|
if (scriptDir.startsWith(fullPath + "/") || scriptDir === fullPath) continue;
|
|
1092
|
+
if (!isDir) {
|
|
1093
|
+
blocked.push(fullPath);
|
|
1094
|
+
continue;
|
|
1095
|
+
}
|
|
1088
1096
|
const status = isAllowedOrAncestor(fullPath, allowedDirs);
|
|
1089
1097
|
if (status === "allowed") continue;
|
|
1090
1098
|
if (status === "ancestor") {
|
|
@@ -1120,7 +1128,7 @@ function collectIgnoredPaths(home, workDirs) {
|
|
|
1120
1128
|
return ignored;
|
|
1121
1129
|
}
|
|
1122
1130
|
function sbplEscape(path) {
|
|
1123
|
-
return path.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
|
|
1131
|
+
return path.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
|
|
1124
1132
|
}
|
|
1125
1133
|
function sbplSubpath(path) {
|
|
1126
1134
|
return ` (subpath "${sbplEscape(path)}")`;
|
|
@@ -1155,7 +1163,7 @@ function collectSystemDenyPaths(home) {
|
|
|
1155
1163
|
return paths;
|
|
1156
1164
|
}
|
|
1157
1165
|
function generateProfile(workDirs, blockedDirs, ignoredPaths, readOnlyDirs = [], home = "") {
|
|
1158
|
-
const blockedRules = sbplDenyBlock("Blocked
|
|
1166
|
+
const blockedRules = sbplDenyBlock("Blocked paths (auto-generated from $HOME contents)", "file*", blockedDirs.map(sbplPathRule));
|
|
1159
1167
|
const ignoredRules = sbplDenyBlock("Hidden paths from .bxignore", "file*", ignoredPaths.map(sbplPathRule));
|
|
1160
1168
|
const readOnlyRules = sbplDenyBlock("Read-only directories", "file-write*", readOnlyDirs.map(sbplSubpath));
|
|
1161
1169
|
const systemRules = home ? sbplDenyBlock("System-level restrictions", "file*", collectSystemDenyPaths(home).map(sbplSubpath)) : "";
|
|
@@ -1236,7 +1244,7 @@ async function checkAppAlreadyRunning(mode, apps) {
|
|
|
1236
1244
|
}
|
|
1237
1245
|
console.error(`\n${fmt.warn(`"${appName}" is already running`)}`);
|
|
1238
1246
|
console.error(fmt.detail("the workspace will open in the EXISTING instance — sandbox will NOT apply"));
|
|
1239
|
-
if (mode === "code") console.error(fmt.detail("quit the app first, or use --
|
|
1247
|
+
if (mode === "code") console.error(fmt.detail("quit the app first, or use --vscode-user for an isolated instance"));
|
|
1240
1248
|
else console.error(fmt.detail("quit the app first to ensure sandbox protection"));
|
|
1241
1249
|
const rl = createInterface({
|
|
1242
1250
|
input: process$1.stdin,
|
|
@@ -1280,12 +1288,12 @@ function parseArgs(validModes) {
|
|
|
1280
1288
|
const rawArgs = process$1.argv.slice(2);
|
|
1281
1289
|
const verbose = rawArgs.includes("--verbose");
|
|
1282
1290
|
const dry = rawArgs.includes("--dry");
|
|
1283
|
-
const
|
|
1291
|
+
const vscodeUser = parseVscodeUserFlag(rawArgs);
|
|
1284
1292
|
const background = rawArgs.includes("--background");
|
|
1285
|
-
const positional = rawArgs
|
|
1293
|
+
const positional = collectPositional(rawArgs);
|
|
1286
1294
|
const doubleDashIdx = rawArgs.indexOf("--");
|
|
1287
1295
|
const appArgs = doubleDashIdx >= 0 ? rawArgs.slice(doubleDashIdx + 1) : [];
|
|
1288
|
-
const beforeDash = doubleDashIdx >= 0 ? rawArgs.slice(0, doubleDashIdx)
|
|
1296
|
+
const beforeDash = doubleDashIdx >= 0 ? collectPositional(rawArgs.slice(0, doubleDashIdx)) : positional;
|
|
1289
1297
|
let mode = "code";
|
|
1290
1298
|
let workArgs;
|
|
1291
1299
|
let implicitWorkdirs = false;
|
|
@@ -1307,12 +1315,42 @@ function parseArgs(validModes) {
|
|
|
1307
1315
|
workArgs,
|
|
1308
1316
|
verbose,
|
|
1309
1317
|
dry,
|
|
1310
|
-
|
|
1318
|
+
vscodeUser,
|
|
1311
1319
|
background,
|
|
1312
1320
|
appArgs,
|
|
1313
1321
|
implicit: implicitWorkdirs
|
|
1314
1322
|
};
|
|
1315
1323
|
}
|
|
1324
|
+
function looksLikePath(val) {
|
|
1325
|
+
return val.includes("/") || val.startsWith("~") || val.startsWith(".");
|
|
1326
|
+
}
|
|
1327
|
+
/** Filter positional args, skipping the path value after --vscode-user */
|
|
1328
|
+
function collectPositional(args) {
|
|
1329
|
+
const result = [];
|
|
1330
|
+
for (let i = 0; i < args.length; i++) {
|
|
1331
|
+
if (args[i] === "--vscode-user" || args[i] === "--vscode-user-data") {
|
|
1332
|
+
const next = args[i + 1];
|
|
1333
|
+
if (next && looksLikePath(next)) i++;
|
|
1334
|
+
continue;
|
|
1335
|
+
}
|
|
1336
|
+
if (args[i].startsWith("--")) continue;
|
|
1337
|
+
result.push(args[i]);
|
|
1338
|
+
}
|
|
1339
|
+
return result;
|
|
1340
|
+
}
|
|
1341
|
+
function parseVscodeUserFlag(args) {
|
|
1342
|
+
for (let i = 0; i < args.length; i++) {
|
|
1343
|
+
if (args[i] === "--vscode-user" || args[i] === "--vscode-user-data") {
|
|
1344
|
+
const next = args[i + 1];
|
|
1345
|
+
if (next && looksLikePath(next)) return next;
|
|
1346
|
+
return true;
|
|
1347
|
+
}
|
|
1348
|
+
if (args[i] === "--vscode-user=false" || args[i] === "--vscode-user-data=false") return false;
|
|
1349
|
+
const eqMatch = args[i].match(/^--vscode-user=(.+)$/);
|
|
1350
|
+
if (eqMatch) return eqMatch[1];
|
|
1351
|
+
}
|
|
1352
|
+
return false;
|
|
1353
|
+
}
|
|
1316
1354
|
//#endregion
|
|
1317
1355
|
//#region src/modes.ts
|
|
1318
1356
|
function isBuiltinMode(mode) {
|
|
@@ -1342,15 +1380,30 @@ function hasAppSandboxEntitlement(entitlements) {
|
|
|
1342
1380
|
if (new RegExp(`<key>\\s*${SANDBOX_KEY.replace(/\./g, "\\.")}\\s*</key>\\s*<false\\s*/>`, "i").test(entitlements)) return false;
|
|
1343
1381
|
return new RegExp(`${SANDBOX_KEY.replace(/\./g, "\\.")}\\s*[=:]\\s*(1|true)`, "i").test(entitlements);
|
|
1344
1382
|
}
|
|
1345
|
-
function
|
|
1346
|
-
|
|
1383
|
+
function resolveProfileDir(home, profile) {
|
|
1384
|
+
if (typeof profile === "string") return resolve(profile.replace(/^~\//, home + "/"));
|
|
1385
|
+
return join(home, ".vscode-sandbox");
|
|
1386
|
+
}
|
|
1387
|
+
async function setupVSCodeProfile(home, profile) {
|
|
1388
|
+
const dataDir = resolveProfileDir(home, profile);
|
|
1347
1389
|
const globalExt = join(home, ".vscode", "extensions");
|
|
1348
1390
|
const localExt = join(dataDir, "extensions");
|
|
1349
1391
|
mkdirSync(dataDir, { recursive: true });
|
|
1350
1392
|
if (!existsSync(localExt) && existsSync(globalExt)) {
|
|
1351
|
-
|
|
1352
|
-
|
|
1393
|
+
const rl = createInterface({
|
|
1394
|
+
input: process$1.stdin,
|
|
1395
|
+
output: process$1.stderr
|
|
1396
|
+
});
|
|
1397
|
+
const answer = await new Promise((res) => {
|
|
1398
|
+
rl.question(`${fmt.info(`copy extensions to ${dataDir}?`)} [Y/n] `, res);
|
|
1399
|
+
});
|
|
1400
|
+
rl.close();
|
|
1401
|
+
if (!answer || answer.match(/^y(es)?$/i)) {
|
|
1402
|
+
console.error(fmt.detail("copying extensions from global install..."));
|
|
1403
|
+
cpSync(globalExt, localExt, { recursive: true });
|
|
1404
|
+
}
|
|
1353
1405
|
}
|
|
1406
|
+
console.error(fmt.detail(`profile: ${dataDir}`));
|
|
1354
1407
|
}
|
|
1355
1408
|
function buildCommand(mode, workDirs, home, profileSandbox, appArgs, apps) {
|
|
1356
1409
|
if (isBuiltinMode(mode)) return buildBuiltinCommand(mode, appArgs);
|
|
@@ -1358,10 +1411,20 @@ function buildCommand(mode, workDirs, home, profileSandbox, appArgs, apps) {
|
|
|
1358
1411
|
}
|
|
1359
1412
|
function buildBuiltinCommand(mode, appArgs) {
|
|
1360
1413
|
switch (mode) {
|
|
1361
|
-
case "term":
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1414
|
+
case "term": {
|
|
1415
|
+
const shell = process$1.env.SHELL ?? "/bin/zsh";
|
|
1416
|
+
if (!existsSync(shell)) {
|
|
1417
|
+
console.error(fmt.warn(`shell not found: ${shell}, falling back to /bin/zsh`));
|
|
1418
|
+
return {
|
|
1419
|
+
bin: "/bin/zsh",
|
|
1420
|
+
args: ["-l"]
|
|
1421
|
+
};
|
|
1422
|
+
}
|
|
1423
|
+
return {
|
|
1424
|
+
bin: shell,
|
|
1425
|
+
args: ["-l"]
|
|
1426
|
+
};
|
|
1427
|
+
}
|
|
1365
1428
|
case "claude": return {
|
|
1366
1429
|
bin: "claude",
|
|
1367
1430
|
args: []
|
|
@@ -1387,8 +1450,8 @@ function buildAppCommand(mode, workDirs, home, profileSandbox, appArgs, apps) {
|
|
|
1387
1450
|
}
|
|
1388
1451
|
const bin = executableFromBundle(resolvedPath, app);
|
|
1389
1452
|
const args = [];
|
|
1390
|
-
if (
|
|
1391
|
-
const dataDir =
|
|
1453
|
+
if (profileSandbox) {
|
|
1454
|
+
const dataDir = resolveProfileDir(home, profileSandbox);
|
|
1392
1455
|
args.push("--user-data-dir", join(dataDir, "data"));
|
|
1393
1456
|
args.push("--extensions-dir", join(dataDir, "extensions"));
|
|
1394
1457
|
}
|
|
@@ -1454,6 +1517,26 @@ function bringAppToFront(mode, apps) {
|
|
|
1454
1517
|
}, 250);
|
|
1455
1518
|
}
|
|
1456
1519
|
//#endregion
|
|
1520
|
+
//#region src/paths.ts
|
|
1521
|
+
/** Expand glob patterns (e.g. ~/work/*) in path lists. Only directories are matched. */
|
|
1522
|
+
function expandGlobs(paths, home) {
|
|
1523
|
+
const result = [];
|
|
1524
|
+
for (const p of paths) {
|
|
1525
|
+
const resolved = p.replace(/^~(\/|$)/, home + "/");
|
|
1526
|
+
if (!resolved.includes("*")) {
|
|
1527
|
+
result.push(resolved);
|
|
1528
|
+
continue;
|
|
1529
|
+
}
|
|
1530
|
+
const dir = dirname(resolved);
|
|
1531
|
+
const pattern = basename(resolved);
|
|
1532
|
+
const regex = new RegExp("^" + pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*") + "$");
|
|
1533
|
+
try {
|
|
1534
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) if (entry.isDirectory() && regex.test(entry.name)) result.push(join(dir, entry.name));
|
|
1535
|
+
} catch {}
|
|
1536
|
+
}
|
|
1537
|
+
return result;
|
|
1538
|
+
}
|
|
1539
|
+
//#endregion
|
|
1457
1540
|
//#region src/help.ts
|
|
1458
1541
|
function printHelp(version) {
|
|
1459
1542
|
const HOME = process.env.HOME;
|
|
@@ -1485,9 +1568,10 @@ Options:
|
|
|
1485
1568
|
--dry show what will be protected, don't launch
|
|
1486
1569
|
--verbose print the generated sandbox profile
|
|
1487
1570
|
--background run in background, log output to /tmp/bx-<pid>.log
|
|
1488
|
-
--
|
|
1571
|
+
--vscode-user [path] use an isolated app profile (default: ~/.vscode-sandbox)
|
|
1489
1572
|
-v, --version show version
|
|
1490
1573
|
-h, --help show this help
|
|
1574
|
+
--docs open documentation in browser
|
|
1491
1575
|
|
|
1492
1576
|
Configuration:
|
|
1493
1577
|
~/.bxconfig.toml app definitions (TOML):
|
|
@@ -1498,6 +1582,8 @@ Configuration:
|
|
|
1498
1582
|
path = "..." explicit executable path
|
|
1499
1583
|
args = ["..."] extra arguments
|
|
1500
1584
|
passPaths = true|false|N|[...] paths passed as launch args
|
|
1585
|
+
profile = true|"path" use an isolated app profile
|
|
1586
|
+
paths = ["~/work/*"] default workdirs (globs supported)
|
|
1501
1587
|
background = true run in background by default
|
|
1502
1588
|
built-in apps (code, xcode) can be overridden
|
|
1503
1589
|
~/.bxignore sandbox rules (one per line):
|
|
@@ -1524,7 +1610,7 @@ function kindIcon(kind) {
|
|
|
1524
1610
|
default: return `${RED}✖${RESET}`;
|
|
1525
1611
|
}
|
|
1526
1612
|
}
|
|
1527
|
-
function insertPath(root,
|
|
1613
|
+
function insertPath(root, absPath, kind, isDir) {
|
|
1528
1614
|
const parts = absPath.split("/").filter(Boolean);
|
|
1529
1615
|
let node = root;
|
|
1530
1616
|
for (const part of parts) {
|
|
@@ -1539,7 +1625,6 @@ function insertPath(root, homeParts, absPath, kind, isDir) {
|
|
|
1539
1625
|
* Intermediate directories on the home path are kept as navigation context. */
|
|
1540
1626
|
function pruneTree(node, currentParts, homeParts, depth) {
|
|
1541
1627
|
if (node.kind) return true;
|
|
1542
|
-
depth < homeParts.length && (currentParts[depth], homeParts[depth]);
|
|
1543
1628
|
for (const [name, child] of [...node.children]) if (!pruneTree(child, [...currentParts, name], homeParts, depth + 1)) node.children.delete(name);
|
|
1544
1629
|
return node.children.size > 0;
|
|
1545
1630
|
}
|
|
@@ -1547,7 +1632,7 @@ function isDirectory(path) {
|
|
|
1547
1632
|
try {
|
|
1548
1633
|
return statSync(path).isDirectory();
|
|
1549
1634
|
} catch {
|
|
1550
|
-
return
|
|
1635
|
+
return true;
|
|
1551
1636
|
}
|
|
1552
1637
|
}
|
|
1553
1638
|
function printNode(node, prefix) {
|
|
@@ -1567,11 +1652,11 @@ function printNode(node, prefix) {
|
|
|
1567
1652
|
function printDryRunTree({ home, blockedDirs, ignoredPaths, readOnlyDirs, workDirs, systemDenyPaths = [] }) {
|
|
1568
1653
|
const root = { children: /* @__PURE__ */ new Map() };
|
|
1569
1654
|
const homeParts = home.split("/").filter(Boolean);
|
|
1570
|
-
for (const dir of blockedDirs) insertPath(root,
|
|
1571
|
-
for (const path of ignoredPaths) insertPath(root,
|
|
1572
|
-
for (const dir of readOnlyDirs) insertPath(root,
|
|
1573
|
-
for (const dir of workDirs) insertPath(root,
|
|
1574
|
-
for (const dir of systemDenyPaths) insertPath(root,
|
|
1655
|
+
for (const dir of blockedDirs) insertPath(root, dir, "blocked", isDirectory(dir));
|
|
1656
|
+
for (const path of ignoredPaths) insertPath(root, path, "ignored", isDirectory(path));
|
|
1657
|
+
for (const dir of readOnlyDirs) insertPath(root, dir, "read-only", true);
|
|
1658
|
+
for (const dir of workDirs) insertPath(root, dir, "workdir", true);
|
|
1659
|
+
for (const dir of systemDenyPaths) insertPath(root, dir, "blocked", true);
|
|
1575
1660
|
pruneTree(root, [], homeParts, 0);
|
|
1576
1661
|
console.log(`\n${CYAN}/${RESET}`);
|
|
1577
1662
|
printNode(root, "");
|
|
@@ -1579,7 +1664,7 @@ function printDryRunTree({ home, blockedDirs, ignoredPaths, readOnlyDirs, workDi
|
|
|
1579
1664
|
}
|
|
1580
1665
|
//#endregion
|
|
1581
1666
|
//#region src/index.ts
|
|
1582
|
-
const VERSION = "1.0
|
|
1667
|
+
const VERSION = "1.2.0";
|
|
1583
1668
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
1584
1669
|
if (process$1.argv.includes("--version") || process$1.argv.includes("-v")) {
|
|
1585
1670
|
console.log(`bx ${VERSION}`);
|
|
@@ -1589,6 +1674,10 @@ if (process$1.argv.includes("--help") || process$1.argv.includes("-h")) {
|
|
|
1589
1674
|
printHelp(VERSION);
|
|
1590
1675
|
process$1.exit(0);
|
|
1591
1676
|
}
|
|
1677
|
+
if (process$1.argv.includes("--docs")) {
|
|
1678
|
+
execSync("open https://github.com/holtwick/bx-mac");
|
|
1679
|
+
process$1.exit(0);
|
|
1680
|
+
}
|
|
1592
1681
|
if (!process$1.env.HOME) {
|
|
1593
1682
|
console.error(`\n${fmt.error("$HOME environment variable is not set")}\n`);
|
|
1594
1683
|
process$1.exit(1);
|
|
@@ -1598,9 +1687,13 @@ async function main() {
|
|
|
1598
1687
|
checkOwnSandbox();
|
|
1599
1688
|
checkExternalSandbox();
|
|
1600
1689
|
const apps = getAvailableApps(loadConfig(HOME));
|
|
1601
|
-
const { mode, workArgs, verbose, dry,
|
|
1690
|
+
const { mode, workArgs, verbose, dry, vscodeUser: vscodeUserFlag, background: backgroundFlag, appArgs, implicit } = parseArgs(getValidModes(apps));
|
|
1602
1691
|
const app = apps[mode];
|
|
1603
|
-
const workDirs = (implicit && app?.paths?.length ? app.paths : workArgs).map((a) => realpathSync(resolve(a
|
|
1692
|
+
const workDirs = expandGlobs(implicit && app?.paths?.length ? app.paths : workArgs, HOME).map((a) => realpathSync(resolve(a)));
|
|
1693
|
+
if (workDirs.length === 0) {
|
|
1694
|
+
console.error(`\n${fmt.error("no matching working directories found")}\n`);
|
|
1695
|
+
process$1.exit(1);
|
|
1696
|
+
}
|
|
1604
1697
|
if (implicit && !app?.paths?.length) {
|
|
1605
1698
|
if (workDirs.some((d) => d === HOME)) {
|
|
1606
1699
|
console.error(`\n${fmt.error("no working directory specified and current directory is $HOME")}\n`);
|
|
@@ -1608,6 +1701,8 @@ async function main() {
|
|
|
1608
1701
|
console.error(fmt.detail(`Config: set default paths in ~/.bxconfig.toml:\n`));
|
|
1609
1702
|
console.error(fmt.detail(`[${mode}]`));
|
|
1610
1703
|
console.error(fmt.detail(`paths = ["~/work/my-project"]\n`));
|
|
1704
|
+
console.error(fmt.detail(`Run bx --help for more info.`));
|
|
1705
|
+
console.error(fmt.detail(`Docs: https://github.com/holtwick/bx-mac\n`));
|
|
1611
1706
|
process$1.exit(1);
|
|
1612
1707
|
}
|
|
1613
1708
|
if (!dry) await confirmLaunch(workDirs[0], mode);
|
|
@@ -1615,7 +1710,8 @@ async function main() {
|
|
|
1615
1710
|
if (!dry) checkVSCodeTerminal();
|
|
1616
1711
|
checkWorkDirs(workDirs, HOME);
|
|
1617
1712
|
await checkAppAlreadyRunning(mode, apps);
|
|
1618
|
-
|
|
1713
|
+
const profileSandbox = vscodeUserFlag !== false ? vscodeUserFlag : app?.profile ?? false;
|
|
1714
|
+
if (profileSandbox) await setupVSCodeProfile(HOME, profileSandbox);
|
|
1619
1715
|
const { allowed, readOnly } = parseHomeConfig(HOME, workDirs);
|
|
1620
1716
|
const blockedDirs = collectBlockedDirs(HOME, HOME, __dirname, new Set([...allowed, ...readOnly]));
|
|
1621
1717
|
const ignoredPaths = collectIgnoredPaths(HOME, workDirs);
|
|
@@ -1678,7 +1774,12 @@ async function main() {
|
|
|
1678
1774
|
CODEBOX_SANDBOX: "1"
|
|
1679
1775
|
}
|
|
1680
1776
|
});
|
|
1777
|
+
child.on("error", (err) => {
|
|
1778
|
+
console.error(fmt.error(`failed to start sandbox: ${err.message}`));
|
|
1779
|
+
process$1.exit(1);
|
|
1780
|
+
});
|
|
1681
1781
|
child.unref();
|
|
1782
|
+
closeSync(logFd);
|
|
1682
1783
|
bringAppToFront(mode, apps);
|
|
1683
1784
|
console.error(fmt.info(`running in background (pid ${child.pid})`));
|
|
1684
1785
|
console.error(fmt.detail(`log: ${logPath}`));
|
|
@@ -1702,7 +1803,6 @@ async function main() {
|
|
|
1702
1803
|
CODEBOX_SANDBOX: "1"
|
|
1703
1804
|
}
|
|
1704
1805
|
});
|
|
1705
|
-
bringAppToFront(mode, apps);
|
|
1706
1806
|
const cleanup = () => {
|
|
1707
1807
|
try {
|
|
1708
1808
|
rmSync(tmpDir, {
|
|
@@ -1712,6 +1812,11 @@ async function main() {
|
|
|
1712
1812
|
} catch {}
|
|
1713
1813
|
};
|
|
1714
1814
|
process$1.on("exit", cleanup);
|
|
1815
|
+
child.on("error", (err) => {
|
|
1816
|
+
console.error(fmt.error(`failed to start sandbox: ${err.message}`));
|
|
1817
|
+
process$1.exit(1);
|
|
1818
|
+
});
|
|
1819
|
+
bringAppToFront(mode, apps);
|
|
1715
1820
|
child.on("close", (code) => {
|
|
1716
1821
|
process$1.exit(code ?? 0);
|
|
1717
1822
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bx-mac",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Sandbox any macOS app — only your project directory stays accessible",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
],
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@types/node": "^25.5.0",
|
|
51
|
-
"rolldown": "^1.0.0-rc.
|
|
51
|
+
"rolldown": "^1.0.0-rc.13",
|
|
52
52
|
"smol-toml": "^1.6.1",
|
|
53
53
|
"vitest": "^4.1.2"
|
|
54
54
|
},
|