bx-mac 0.5.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 +18 -3
- package/dist/bx-native +0 -0
- package/dist/bx.js +102 -13
- package/package.json +4 -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
|
|
|
@@ -128,14 +132,25 @@ Deny rules are applied **in addition** to the built-in protected list:
|
|
|
128
132
|
|
|
129
133
|
### `<project>/.bxignore`
|
|
130
134
|
|
|
131
|
-
Block paths within the working directory.
|
|
135
|
+
Block paths within the working directory. Uses [`.gitignore`-style pattern matching](https://git-scm.com/docs/gitignore#_pattern_format):
|
|
136
|
+
|
|
137
|
+
| Pattern | Matches | Why |
|
|
138
|
+
|---|---|---|
|
|
139
|
+
| `.env` | `.env` at any depth | No `/` → recursive |
|
|
140
|
+
| `.env.*` | `.env.local`, `sub/.env.production` | No `/` → recursive |
|
|
141
|
+
| `*.pem` | `key.pem`, `sub/deep/cert.pem` | No `/` → recursive |
|
|
142
|
+
| `secrets/` | `secrets/` at any depth | Trailing `/` is a dir marker, not a path separator |
|
|
143
|
+
| `/.env` | Only `<workdir>/.env` | Leading `/` → anchored to root |
|
|
144
|
+
| `config/secrets` | Only `<workdir>/config/secrets` | Contains `/` → relative to workdir |
|
|
145
|
+
|
|
146
|
+
bx searches for `.bxignore` files **recursively** through the entire project tree (skipping `.`-prefixed dirs and `node_modules`), so you can place them in subdirectories to hide secrets close to where they live.
|
|
132
147
|
|
|
133
148
|
```gitignore
|
|
134
149
|
.env
|
|
135
150
|
.env.*
|
|
136
151
|
secrets/
|
|
137
|
-
|
|
138
|
-
|
|
152
|
+
*.pem
|
|
153
|
+
*.key
|
|
139
154
|
```
|
|
140
155
|
|
|
141
156
|
For example, a monorepo might have:
|
package/dist/bx-native
ADDED
|
Binary file
|
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
|
|
@@ -121,10 +128,22 @@ function parseLines(filePath) {
|
|
|
121
128
|
return readFileSync(filePath, "utf-8").split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
|
|
122
129
|
}
|
|
123
130
|
/**
|
|
131
|
+
* Convert a .bxignore line to a glob pattern following .gitignore semantics:
|
|
132
|
+
* - Leading "/" anchors to the base dir (stripped before globbing)
|
|
133
|
+
* - Patterns without "/" (except trailing) match recursively via ** / prefix
|
|
134
|
+
* - Patterns with "/" (non-leading, non-trailing) are relative to baseDir
|
|
135
|
+
* - Trailing "/" marks directories only and doesn't count as path separator
|
|
136
|
+
*/
|
|
137
|
+
function toGlobPattern(line) {
|
|
138
|
+
if (line.startsWith("/")) return line.slice(1);
|
|
139
|
+
if ((line.endsWith("/") ? line.slice(0, -1) : line).includes("/")) return line;
|
|
140
|
+
return `**/${line}`;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
124
143
|
* Apply a single .bxignore file: resolve glob patterns relative to baseDir.
|
|
125
144
|
*/
|
|
126
145
|
function applyIgnoreFile(filePath, baseDir, ignored) {
|
|
127
|
-
for (const line of parseLines(filePath)) for (const match of globSync(line, { cwd: baseDir })) ignored.push(resolve(baseDir, match));
|
|
146
|
+
for (const line of parseLines(filePath)) for (const match of globSync(toGlobPattern(line), { cwd: baseDir })) ignored.push(resolve(baseDir, match));
|
|
128
147
|
}
|
|
129
148
|
/**
|
|
130
149
|
* Recursively find and apply .bxignore files in a directory tree.
|
|
@@ -190,7 +209,13 @@ function collectBlockedDirs(parentDir, home, scriptDir, allowedDirs) {
|
|
|
190
209
|
for (const name of readdirSync(parentDir)) {
|
|
191
210
|
if (name.startsWith(".")) continue;
|
|
192
211
|
const fullPath = join(parentDir, name);
|
|
193
|
-
|
|
212
|
+
let isDir;
|
|
213
|
+
try {
|
|
214
|
+
isDir = statSync(fullPath).isDirectory();
|
|
215
|
+
} catch {
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
if (!isDir) continue;
|
|
194
219
|
if (parentDir === home && name === "Library") continue;
|
|
195
220
|
if (scriptDir.startsWith(fullPath + "/") || scriptDir === fullPath) continue;
|
|
196
221
|
if (allowedDirs.has(fullPath)) continue;
|
|
@@ -211,7 +236,7 @@ function collectIgnoredPaths(home, workDirs) {
|
|
|
211
236
|
const globalIgnore = join(home, ".bxignore");
|
|
212
237
|
if (existsSync(globalIgnore)) {
|
|
213
238
|
const denyLines = parseLines(globalIgnore).filter((l) => !l.match(/^(RW|RO):/i));
|
|
214
|
-
for (const line of denyLines) for (const match of globSync(line, { cwd: home })) ignored.push(resolve(home, match));
|
|
239
|
+
for (const line of denyLines) for (const match of globSync(toGlobPattern(line), { cwd: home })) ignored.push(resolve(home, match));
|
|
215
240
|
}
|
|
216
241
|
for (const workDir of workDirs) collectIgnoreFilesRecursive(workDir, ignored);
|
|
217
242
|
return ignored;
|
|
@@ -222,7 +247,11 @@ function collectIgnoredPaths(home, workDirs) {
|
|
|
222
247
|
function generateProfile(workDirs, blockedDirs, ignoredPaths, readOnlyDirs = []) {
|
|
223
248
|
const denyRules = blockedDirs.map((dir) => ` (subpath "${dir}")`).join("\n");
|
|
224
249
|
const ignoredRules = ignoredPaths.length > 0 ? `\n; Hidden paths from .bxignore\n(deny file*\n${ignoredPaths.map((p) => {
|
|
225
|
-
|
|
250
|
+
let isDir = false;
|
|
251
|
+
try {
|
|
252
|
+
isDir = existsSync(p) && statSync(p).isDirectory();
|
|
253
|
+
} catch {}
|
|
254
|
+
return isDir ? ` (subpath "${p}")` : ` (literal "${p}")`;
|
|
226
255
|
}).join("\n")}\n)\n` : "";
|
|
227
256
|
const readOnlyRules = readOnlyDirs.length > 0 ? `\n; Read-only directories\n(deny file-write*\n${readOnlyDirs.map((dir) => ` (subpath "${dir}")`).join("\n")}\n)\n` : "";
|
|
228
257
|
return `; Auto-generated sandbox profile
|
|
@@ -305,6 +334,7 @@ Usage:
|
|
|
305
334
|
bx exec [workdir...] -- command [args...] arbitrary command
|
|
306
335
|
|
|
307
336
|
Options:
|
|
337
|
+
--dry show what will be protected, don't launch
|
|
308
338
|
--verbose print the generated sandbox profile
|
|
309
339
|
--profile-sandbox use an isolated VSCode profile (code mode only)
|
|
310
340
|
-v, --version show version
|
|
@@ -315,17 +345,30 @@ Configuration:
|
|
|
315
345
|
path block access (deny)
|
|
316
346
|
rw:path allow read-write access
|
|
317
347
|
ro:path allow read-only access
|
|
318
|
-
<workdir>/.bxignore blocked paths in project (
|
|
348
|
+
<workdir>/.bxignore blocked paths in project (.gitignore-style matching)
|
|
319
349
|
|
|
320
350
|
https://github.com/holtwick/bx-mac`);
|
|
321
351
|
process.exit(0);
|
|
322
352
|
}
|
|
323
|
-
|
|
324
|
-
checkVSCodeTerminal();
|
|
325
|
-
checkExternalSandbox();
|
|
326
|
-
const { mode, workArgs, verbose, profileSandbox, execCmd } = parseArgs();
|
|
353
|
+
const { mode, workArgs, verbose, dry, profileSandbox, execCmd, implicit } = parseArgs();
|
|
327
354
|
const HOME = process.env.HOME;
|
|
328
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
|
+
}
|
|
329
372
|
checkWorkDirs(WORK_DIRS, HOME);
|
|
330
373
|
if (mode === "code" && profileSandbox) setupVSCodeProfile(HOME);
|
|
331
374
|
const { allowed, readOnly } = parseHomeConfig(HOME, WORK_DIRS);
|
|
@@ -335,8 +378,6 @@ const extraIgnored = ignoredPaths.length - PROTECTED_DOTDIRS.length;
|
|
|
335
378
|
if (extraIgnored > 0) console.error(`sandbox: .bxignore hides ${extraIgnored} extra path(s)`);
|
|
336
379
|
if (readOnly.size > 0) console.error(`sandbox: ${readOnly.size} read-only director${readOnly.size === 1 ? "y" : "ies"}`);
|
|
337
380
|
const profile = generateProfile(WORK_DIRS, blockedDirs, ignoredPaths, [...readOnly]);
|
|
338
|
-
const profilePath = join("/tmp", `bx-${process.pid}.sb`);
|
|
339
|
-
writeFileSync(profilePath, profile);
|
|
340
381
|
const dirLabel = WORK_DIRS.length === 1 ? WORK_DIRS[0] : `${WORK_DIRS.length} directories`;
|
|
341
382
|
console.error(`sandbox: ${mode} mode, working directory: ${dirLabel}`);
|
|
342
383
|
if (verbose) {
|
|
@@ -344,6 +385,54 @@ if (verbose) {
|
|
|
344
385
|
console.error(profile);
|
|
345
386
|
console.error("--- End of profile ---\n");
|
|
346
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);
|
|
347
436
|
const cmd = buildCommand(mode, WORK_DIRS, HOME, profileSandbox, execCmd);
|
|
348
437
|
spawn("sandbox-exec", [
|
|
349
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,7 +10,10 @@
|
|
|
10
10
|
"dist"
|
|
11
11
|
],
|
|
12
12
|
"scripts": {
|
|
13
|
+
"start": "npm run build && node ./dist/bx.js",
|
|
13
14
|
"build": "rolldown -c",
|
|
15
|
+
"build:native": "bun build src/index.ts --compile --outfile dist/bx-native --define \"__VERSION__=\\\"$(node -p \"require('./package.json').version\")\\\"\"",
|
|
16
|
+
"sign": "./scripts/sign.sh",
|
|
14
17
|
"test": "vitest run",
|
|
15
18
|
"prepublishOnly": "npm run build",
|
|
16
19
|
"post:release": "./scripts/release.sh"
|