bx-mac 0.6.0 → 0.7.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 +4 -0
- package/dist/bx.js +87 -10
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -87,6 +87,9 @@ bx claude ~/work/my-project
|
|
|
87
87
|
# ⚡ Run a script in a sandbox
|
|
88
88
|
bx exec ~/work/my-project -- python train.py
|
|
89
89
|
|
|
90
|
+
# 🔍 Preview what will be protected (no launch)
|
|
91
|
+
bx --dry ~/work/my-project
|
|
92
|
+
|
|
90
93
|
# 🔍 See the generated sandbox profile
|
|
91
94
|
bx --verbose ~/work/my-project
|
|
92
95
|
```
|
|
@@ -95,6 +98,7 @@ bx --verbose ~/work/my-project
|
|
|
95
98
|
|
|
96
99
|
| Option | Description |
|
|
97
100
|
|---|---|
|
|
101
|
+
| `--dry` | Show a tree of all protected, read-only, and accessible paths — don't launch anything |
|
|
98
102
|
| `--verbose` | Print the generated sandbox profile to stderr |
|
|
99
103
|
| `--profile-sandbox` | Use an isolated VSCode profile (separate extensions/settings, `code` mode only) |
|
|
100
104
|
|
package/dist/bx.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
const __VERSION__ = "0.
|
|
2
|
+
const __VERSION__ = "0.7.0";
|
|
3
3
|
import { accessSync, constants, cpSync, existsSync, globSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { dirname, join, resolve } from "node:path";
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
6
|
+
import { createInterface } from "node:readline";
|
|
6
7
|
import { fileURLToPath } from "node:url";
|
|
7
8
|
import process from "node:process";
|
|
8
9
|
//#region src/guards.ts
|
|
@@ -76,6 +77,7 @@ const MODES = [
|
|
|
76
77
|
function parseArgs() {
|
|
77
78
|
const rawArgs = process.argv.slice(2);
|
|
78
79
|
const verbose = rawArgs.includes("--verbose");
|
|
80
|
+
const dry = rawArgs.includes("--dry");
|
|
79
81
|
const profileSandbox = rawArgs.includes("--profile-sandbox");
|
|
80
82
|
const positional = rawArgs.filter((a) => !a.startsWith("--"));
|
|
81
83
|
const doubleDashIdx = rawArgs.indexOf("--");
|
|
@@ -83,11 +85,14 @@ function parseArgs() {
|
|
|
83
85
|
const beforeDash = doubleDashIdx >= 0 ? rawArgs.slice(0, doubleDashIdx).filter((a) => !a.startsWith("--")) : positional;
|
|
84
86
|
let mode = "code";
|
|
85
87
|
let workArgs;
|
|
88
|
+
let explicit = false;
|
|
86
89
|
if (beforeDash.length > 0 && MODES.includes(beforeDash[0])) {
|
|
87
90
|
mode = beforeDash[0];
|
|
88
91
|
workArgs = beforeDash.slice(1);
|
|
92
|
+
explicit = true;
|
|
89
93
|
} else workArgs = beforeDash;
|
|
90
94
|
if (workArgs.length === 0) workArgs = ["."];
|
|
95
|
+
else explicit = true;
|
|
91
96
|
if (mode === "exec" && execCmd.length === 0) {
|
|
92
97
|
console.error("sandbox: exec mode requires a command after \"--\"");
|
|
93
98
|
console.error("usage: bx exec [workdir...] -- command [args...]");
|
|
@@ -97,8 +102,10 @@ function parseArgs() {
|
|
|
97
102
|
mode,
|
|
98
103
|
workArgs,
|
|
99
104
|
verbose,
|
|
105
|
+
dry,
|
|
100
106
|
profileSandbox,
|
|
101
|
-
execCmd
|
|
107
|
+
execCmd,
|
|
108
|
+
implicit: !explicit
|
|
102
109
|
};
|
|
103
110
|
}
|
|
104
111
|
//#endregion
|
|
@@ -202,7 +209,13 @@ function collectBlockedDirs(parentDir, home, scriptDir, allowedDirs) {
|
|
|
202
209
|
for (const name of readdirSync(parentDir)) {
|
|
203
210
|
if (name.startsWith(".")) continue;
|
|
204
211
|
const fullPath = join(parentDir, name);
|
|
205
|
-
|
|
212
|
+
let isDir;
|
|
213
|
+
try {
|
|
214
|
+
isDir = statSync(fullPath).isDirectory();
|
|
215
|
+
} catch {
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
if (!isDir) continue;
|
|
206
219
|
if (parentDir === home && name === "Library") continue;
|
|
207
220
|
if (scriptDir.startsWith(fullPath + "/") || scriptDir === fullPath) continue;
|
|
208
221
|
if (allowedDirs.has(fullPath)) continue;
|
|
@@ -234,7 +247,11 @@ function collectIgnoredPaths(home, workDirs) {
|
|
|
234
247
|
function generateProfile(workDirs, blockedDirs, ignoredPaths, readOnlyDirs = []) {
|
|
235
248
|
const denyRules = blockedDirs.map((dir) => ` (subpath "${dir}")`).join("\n");
|
|
236
249
|
const ignoredRules = ignoredPaths.length > 0 ? `\n; Hidden paths from .bxignore\n(deny file*\n${ignoredPaths.map((p) => {
|
|
237
|
-
|
|
250
|
+
let isDir = false;
|
|
251
|
+
try {
|
|
252
|
+
isDir = existsSync(p) && statSync(p).isDirectory();
|
|
253
|
+
} catch {}
|
|
254
|
+
return isDir ? ` (subpath "${p}")` : ` (literal "${p}")`;
|
|
238
255
|
}).join("\n")}\n)\n` : "";
|
|
239
256
|
const readOnlyRules = readOnlyDirs.length > 0 ? `\n; Read-only directories\n(deny file-write*\n${readOnlyDirs.map((dir) => ` (subpath "${dir}")`).join("\n")}\n)\n` : "";
|
|
240
257
|
return `; Auto-generated sandbox profile
|
|
@@ -317,6 +334,7 @@ Usage:
|
|
|
317
334
|
bx exec [workdir...] -- command [args...] arbitrary command
|
|
318
335
|
|
|
319
336
|
Options:
|
|
337
|
+
--dry show what will be protected, don't launch
|
|
320
338
|
--verbose print the generated sandbox profile
|
|
321
339
|
--profile-sandbox use an isolated VSCode profile (code mode only)
|
|
322
340
|
-v, --version show version
|
|
@@ -332,12 +350,25 @@ Configuration:
|
|
|
332
350
|
https://github.com/holtwick/bx-mac`);
|
|
333
351
|
process.exit(0);
|
|
334
352
|
}
|
|
335
|
-
|
|
336
|
-
checkVSCodeTerminal();
|
|
337
|
-
checkExternalSandbox();
|
|
338
|
-
const { mode, workArgs, verbose, profileSandbox, execCmd } = parseArgs();
|
|
353
|
+
const { mode, workArgs, verbose, dry, profileSandbox, execCmd, implicit } = parseArgs();
|
|
339
354
|
const HOME = process.env.HOME;
|
|
340
355
|
const WORK_DIRS = workArgs.map((a) => resolve(a));
|
|
356
|
+
if (implicit && !dry) {
|
|
357
|
+
const rl = createInterface({
|
|
358
|
+
input: process.stdin,
|
|
359
|
+
output: process.stderr
|
|
360
|
+
});
|
|
361
|
+
const answer = await new Promise((res) => {
|
|
362
|
+
rl.question(`sandbox: open ${WORK_DIRS[0]} in VSCode? [Y/n] `, res);
|
|
363
|
+
});
|
|
364
|
+
rl.close();
|
|
365
|
+
if (answer && !answer.match(/^y(es)?$/i)) process.exit(0);
|
|
366
|
+
}
|
|
367
|
+
if (!dry) {
|
|
368
|
+
checkOwnSandbox();
|
|
369
|
+
checkVSCodeTerminal();
|
|
370
|
+
checkExternalSandbox();
|
|
371
|
+
}
|
|
341
372
|
checkWorkDirs(WORK_DIRS, HOME);
|
|
342
373
|
if (mode === "code" && profileSandbox) setupVSCodeProfile(HOME);
|
|
343
374
|
const { allowed, readOnly } = parseHomeConfig(HOME, WORK_DIRS);
|
|
@@ -347,8 +378,6 @@ const extraIgnored = ignoredPaths.length - PROTECTED_DOTDIRS.length;
|
|
|
347
378
|
if (extraIgnored > 0) console.error(`sandbox: .bxignore hides ${extraIgnored} extra path(s)`);
|
|
348
379
|
if (readOnly.size > 0) console.error(`sandbox: ${readOnly.size} read-only director${readOnly.size === 1 ? "y" : "ies"}`);
|
|
349
380
|
const profile = generateProfile(WORK_DIRS, blockedDirs, ignoredPaths, [...readOnly]);
|
|
350
|
-
const profilePath = join("/tmp", `bx-${process.pid}.sb`);
|
|
351
|
-
writeFileSync(profilePath, profile);
|
|
352
381
|
const dirLabel = WORK_DIRS.length === 1 ? WORK_DIRS[0] : `${WORK_DIRS.length} directories`;
|
|
353
382
|
console.error(`sandbox: ${mode} mode, working directory: ${dirLabel}`);
|
|
354
383
|
if (verbose) {
|
|
@@ -356,6 +385,54 @@ if (verbose) {
|
|
|
356
385
|
console.error(profile);
|
|
357
386
|
console.error("--- End of profile ---\n");
|
|
358
387
|
}
|
|
388
|
+
if (dry) {
|
|
389
|
+
const R = "\x1B[31m", G = "\x1B[32m", Y = "\x1B[33m", C = "\x1B[36m", D = "\x1B[2m", X = "\x1B[0m";
|
|
390
|
+
const icon = (k) => k === "read-only" ? `${Y}◉${X}` : k === "workdir" ? `${G}✔${X}` : `${R}✖${X}`;
|
|
391
|
+
const tag = (k) => `${D}${k}${X}`;
|
|
392
|
+
const root = { children: /* @__PURE__ */ new Map() };
|
|
393
|
+
function addEntry(absPath, kind, isDir) {
|
|
394
|
+
const parts = (absPath.startsWith(HOME + "/") ? absPath.slice(HOME.length + 1) : absPath).split("/");
|
|
395
|
+
let node = root;
|
|
396
|
+
for (const part of parts) {
|
|
397
|
+
if (!node.children.has(part)) node.children.set(part, { children: /* @__PURE__ */ new Map() });
|
|
398
|
+
node = node.children.get(part);
|
|
399
|
+
}
|
|
400
|
+
node.kind = kind;
|
|
401
|
+
node.isDir = isDir;
|
|
402
|
+
}
|
|
403
|
+
for (const d of blockedDirs) addEntry(d, "blocked", true);
|
|
404
|
+
for (const p of ignoredPaths) {
|
|
405
|
+
let isDir = false;
|
|
406
|
+
try {
|
|
407
|
+
isDir = statSync(p).isDirectory();
|
|
408
|
+
} catch {
|
|
409
|
+
if (p.slice(p.lastIndexOf("/") + 1).startsWith(".")) isDir = true;
|
|
410
|
+
}
|
|
411
|
+
addEntry(p, "ignored", isDir);
|
|
412
|
+
}
|
|
413
|
+
for (const d of readOnly) addEntry(d, "read-only", true);
|
|
414
|
+
for (const d of WORK_DIRS) addEntry(d, "workdir", true);
|
|
415
|
+
function printTree(node, prefix) {
|
|
416
|
+
const entries = [...node.children.entries()].sort((a, b) => a[0].localeCompare(b[0]));
|
|
417
|
+
for (let i = 0; i < entries.length; i++) {
|
|
418
|
+
const [name, child] = entries[i];
|
|
419
|
+
const last = i === entries.length - 1;
|
|
420
|
+
const connector = last ? "└── " : "├── ";
|
|
421
|
+
const pipe = last ? " " : "│ ";
|
|
422
|
+
if (child.kind) {
|
|
423
|
+
const suffix = child.isDir ? "/" : "";
|
|
424
|
+
console.log(`${prefix}${connector}${icon(child.kind)} ${name}${suffix} ${tag(child.kind)}`);
|
|
425
|
+
} else console.log(`${prefix}${connector}${C}${name}/${X}`);
|
|
426
|
+
if (child.children.size > 0) printTree(child, prefix + pipe);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
console.log(`\n${C}~/${X}`);
|
|
430
|
+
printTree(root, "");
|
|
431
|
+
console.log(`\n${R}✖${X} = denied ${Y}◉${X} = read-only ${G}✔${X} = read-write\n`);
|
|
432
|
+
process.exit(0);
|
|
433
|
+
}
|
|
434
|
+
const profilePath = join("/tmp", `bx-${process.pid}.sb`);
|
|
435
|
+
writeFileSync(profilePath, profile);
|
|
359
436
|
const cmd = buildCommand(mode, WORK_DIRS, HOME, profileSandbox, execCmd);
|
|
360
437
|
spawn("sandbox-exec", [
|
|
361
438
|
"-f",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bx-mac",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Launch apps in a macOS sandbox — only the project directory is accessible",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"dist"
|
|
11
11
|
],
|
|
12
12
|
"scripts": {
|
|
13
|
+
"start": "npm run build && node ./dist/bx.js",
|
|
13
14
|
"build": "rolldown -c",
|
|
14
15
|
"build:native": "bun build src/index.ts --compile --outfile dist/bx-native --define \"__VERSION__=\\\"$(node -p \"require('./package.json').version\")\\\"\"",
|
|
15
16
|
"sign": "./scripts/sign.sh",
|