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.
Files changed (3) hide show
  1. package/README.md +4 -0
  2. package/dist/bx.js +87 -10
  3. 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.6.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
- if (!statSync(fullPath).isDirectory()) continue;
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
- return existsSync(p) && statSync(p).isDirectory() ? ` (subpath "${p}")` : ` (literal "${p}")`;
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
- checkOwnSandbox();
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.6.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",