bx-mac 0.2.1 → 0.4.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 +31 -11
  2. package/dist/bx.js +241 -109
  3. package/package.json +7 -4
package/README.md CHANGED
@@ -14,6 +14,12 @@ bx ~/work/my-project
14
14
 
15
15
  That's it. 🎉 VSCode opens with full access to `~/work/my-project` and nothing else.
16
16
 
17
+ Need multiple directories? No problem:
18
+
19
+ ```bash
20
+ bx ~/work/my-project ~/work/shared-lib
21
+ ```
22
+
17
23
  ## ✅ What it does
18
24
 
19
25
  - 🔒 Blocks `~/Documents`, `~/Desktop`, `~/Downloads`, and all other personal folders
@@ -21,8 +27,9 @@ That's it. 🎉 VSCode opens with full access to `~/work/my-project` and nothing
21
27
  - 🛡️ Protects sensitive dotdirs like `~/.ssh`, `~/.gnupg`, `~/.docker`, `~/.cargo`
22
28
  - ⚙️ Keeps VSCode, extensions, shell, Node.js, and other tooling fully functional
23
29
  - 🔍 Generates sandbox rules dynamically based on your actual `$HOME` contents
24
- - 📝 Supports `.bxignore` to hide secrets like `.env` files within a project
30
+ - 📝 Supports `.bxignore` files (searched recursively) to hide secrets like `.env` files within a project
25
31
  - 📂 Supports `~/.bxallow` to grant access to shared utility directories
32
+ - 🗂️ Supports multiple working directories in a single sandbox
26
33
 
27
34
  ## 🚫 What it doesn't do
28
35
 
@@ -54,13 +61,13 @@ pnpm link -g
54
61
 
55
62
  | Command | What it launches |
56
63
  |---|---|
57
- | `bx [workdir]` | 🖥️ VSCode (default) |
58
- | `bx code [workdir]` | 🖥️ VSCode (explicit) |
59
- | `bx term [workdir]` | 💻 Sandboxed login shell (`$SHELL -l`) |
60
- | `bx claude [workdir]` | 🤖 Claude Code CLI |
61
- | `bx exec [workdir] -- cmd` | ⚡ Any command you want |
64
+ | `bx [workdir...]` | 🖥️ VSCode (default) |
65
+ | `bx code [workdir...]` | 🖥️ VSCode (explicit) |
66
+ | `bx term [workdir...]` | 💻 Sandboxed login shell (`$SHELL -l`) |
67
+ | `bx claude [workdir...]` | 🤖 Claude Code CLI |
68
+ | `bx exec [workdir...] -- cmd` | ⚡ Any command you want |
62
69
 
63
- If no directory is given, the current directory is used.
70
+ If no directory is given, the current directory is used. All modes accept multiple directories.
64
71
 
65
72
  ### Examples
66
73
 
@@ -68,6 +75,9 @@ If no directory is given, the current directory is used.
68
75
  # 🖥️ VSCode with sandbox protection
69
76
  bx ~/work/my-project
70
77
 
78
+ # 📂 Multiple working directories
79
+ bx ~/work/my-project ~/work/shared-lib
80
+
71
81
  # 💻 Work on a project in a sandboxed terminal
72
82
  bx term ~/work/my-project
73
83
 
@@ -90,7 +100,7 @@ bx --verbose ~/work/my-project
90
100
 
91
101
  ## 📝 Configuration
92
102
 
93
- bx uses three optional config files — one entry per line, `#` for comments.
103
+ bx uses three optional config files — one entry per line, `#` for comments. Project `.bxignore` files are discovered recursively.
94
104
 
95
105
  ### `~/.bxallow`
96
106
 
@@ -119,7 +129,7 @@ These are blocked **in addition** to the built-in protected list:
119
129
 
120
130
  ### `<project>/.bxignore`
121
131
 
122
- Block paths within the working directory. Supports glob patterns.
132
+ Block paths within the working directory. Supports glob patterns. 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.
123
133
 
124
134
  ```gitignore
125
135
  .env
@@ -129,15 +139,25 @@ secrets/
129
139
  **/*.key
130
140
  ```
131
141
 
142
+ For example, a monorepo might have:
143
+
144
+ ```text
145
+ my-project/.bxignore # top-level rules
146
+ my-project/services/api/.bxignore # API-specific secrets
147
+ my-project/deploy/.bxignore # deployment credentials
148
+ ```
149
+
150
+ Each `.bxignore` resolves its patterns relative to its own directory.
151
+
132
152
  ## 🔧 How it works
133
153
 
134
154
  bx generates a macOS sandbox profile at launch time:
135
155
 
136
156
  1. **Scan** `$HOME` for non-hidden directories
137
157
  2. **Block** each one individually with `(deny file* (subpath ...))`
138
- 3. **Skip** the working directory, `~/Library`, dotfiles, and `~/.bxallow` paths
158
+ 3. **Skip** all working directories, `~/Library`, dotfiles, and `~/.bxallow` paths
139
159
  4. **Descend** into parent directories of allowed paths to block only siblings (because SBPL deny rules always override allow rules)
140
- 5. **Append** deny rules for protected dotdirs and `.bxignore` entries
160
+ 5. **Append** deny rules for protected dotdirs, `~/.bxignore`, and `.bxignore` files found recursively in each working directory
141
161
  6. **Write** the profile to `/tmp`, launch the app via `sandbox-exec`, clean up on exit
142
162
 
143
163
  ### Why not a simple deny-all + allow?
package/dist/bx.js CHANGED
@@ -1,21 +1,53 @@
1
1
  #!/usr/bin/env node
2
+ const __VERSION__ = "0.4.0";
2
3
  import { accessSync, constants, cpSync, existsSync, globSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
3
4
  import { dirname, join, resolve } from "node:path";
4
5
  import { spawn } from "node:child_process";
5
6
  import { fileURLToPath } from "node:url";
6
- //#region src/index.ts
7
- const __dirname = dirname(fileURLToPath(import.meta.url));
8
- if (process.env.CODEBOX_SANDBOX === "1") {
9
- console.error("sandbox: ERROR already running inside a bx sandbox.");
10
- console.error("sandbox: Nesting sandbox-exec causes silent failures. Aborting.");
11
- process.exit(1);
7
+ import process from "node:process";
8
+ //#region src/guards.ts
9
+ /**
10
+ * Abort if we're already inside a bx sandbox (env var set by us).
11
+ */
12
+ function checkOwnSandbox() {
13
+ if (process.env.CODEBOX_SANDBOX === "1") {
14
+ console.error("sandbox: ERROR — already running inside a bx sandbox.");
15
+ console.error("sandbox: Nesting sandbox-exec causes silent failures. Aborting.");
16
+ process.exit(1);
17
+ }
18
+ }
19
+ /**
20
+ * Warn if launched from inside a VSCode terminal.
21
+ */
22
+ function checkVSCodeTerminal() {
23
+ if (process.env.VSCODE_PID) {
24
+ console.error("sandbox: WARNING — running from inside a VSCode terminal.");
25
+ console.error("sandbox: This will launch a *new* instance in a sandbox.");
26
+ console.error("sandbox: The current VSCode instance will NOT be sandboxed.");
27
+ }
12
28
  }
13
- if (process.env.VSCODE_PID) {
14
- console.error("sandbox: WARNING running from inside a VSCode terminal.");
15
- console.error("sandbox: This will launch a *new* instance in a sandbox.");
16
- console.error("sandbox: The current VSCode instance will NOT be sandboxed.");
29
+ /**
30
+ * Abort if any workdir IS $HOME or is not inside $HOME.
31
+ */
32
+ function checkWorkDirs(workDirs, home) {
33
+ for (const dir of workDirs) {
34
+ if (dir === home) {
35
+ console.error("sandbox: ERROR — working directory cannot be $HOME itself.");
36
+ console.error("sandbox: Sandboxing your entire home directory is not supported. Aborting.");
37
+ process.exit(1);
38
+ }
39
+ if (!dir.startsWith(home + "/")) {
40
+ console.error(`sandbox: ERROR — working directory is outside $HOME: ${dir}`);
41
+ console.error("sandbox: Only directories inside $HOME are supported. Aborting.");
42
+ process.exit(1);
43
+ }
44
+ }
17
45
  }
18
- function isAlreadySandboxed() {
46
+ /**
47
+ * Detect if we're inside an unknown sandbox by probing well-known
48
+ * directories that exist on every Mac but would be blocked.
49
+ */
50
+ function checkExternalSandbox() {
19
51
  for (const dir of [
20
52
  "Documents",
21
53
  "Desktop",
@@ -25,141 +57,187 @@ function isAlreadySandboxed() {
25
57
  try {
26
58
  accessSync(target, constants.R_OK);
27
59
  } catch (e) {
28
- if (e.code === "EPERM") return true;
60
+ if (e.code === "EPERM") {
61
+ console.error("sandbox: ERROR — already running inside a sandbox!");
62
+ console.error("sandbox: Nesting sandbox-exec may cause silent failures. Aborting.");
63
+ process.exit(1);
64
+ }
29
65
  }
30
66
  }
31
- return false;
32
- }
33
- if (isAlreadySandboxed()) {
34
- console.error("sandbox: ERROR — already running inside a sandbox!");
35
- console.error("sandbox: Nesting sandbox-exec may cause silent failures. Aborting.");
36
- process.exit(1);
37
67
  }
68
+ //#endregion
69
+ //#region src/args.ts
38
70
  const MODES = [
39
71
  "code",
40
72
  "term",
41
73
  "claude",
42
74
  "exec"
43
75
  ];
44
- const rawArgs = process.argv.slice(2);
45
- const verbose = rawArgs.includes("--verbose");
46
- const profileSandbox = rawArgs.includes("--profile-sandbox");
47
- const positional = rawArgs.filter((a) => !a.startsWith("--"));
48
- const doubleDashIdx = rawArgs.indexOf("--");
49
- const execCmd = doubleDashIdx >= 0 ? rawArgs.slice(doubleDashIdx + 1) : [];
50
- const beforeDash = doubleDashIdx >= 0 ? rawArgs.slice(0, doubleDashIdx).filter((a) => !a.startsWith("--")) : positional;
51
- let mode = "code";
52
- let workArg = ".";
53
- if (beforeDash.length > 0 && MODES.includes(beforeDash[0])) {
54
- mode = beforeDash[0];
55
- workArg = beforeDash[1] ?? ".";
56
- } else if (beforeDash.length > 0) workArg = beforeDash[0];
57
- if (mode === "exec" && execCmd.length === 0) {
58
- console.error("sandbox: exec mode requires a command after \"--\"");
59
- console.error("usage: bx exec [workdir] -- command [args...]");
60
- process.exit(1);
76
+ function parseArgs() {
77
+ const rawArgs = process.argv.slice(2);
78
+ const verbose = rawArgs.includes("--verbose");
79
+ const profileSandbox = rawArgs.includes("--profile-sandbox");
80
+ const positional = rawArgs.filter((a) => !a.startsWith("--"));
81
+ const doubleDashIdx = rawArgs.indexOf("--");
82
+ const execCmd = doubleDashIdx >= 0 ? rawArgs.slice(doubleDashIdx + 1) : [];
83
+ const beforeDash = doubleDashIdx >= 0 ? rawArgs.slice(0, doubleDashIdx).filter((a) => !a.startsWith("--")) : positional;
84
+ let mode = "code";
85
+ let workArgs;
86
+ if (beforeDash.length > 0 && MODES.includes(beforeDash[0])) {
87
+ mode = beforeDash[0];
88
+ workArgs = beforeDash.slice(1);
89
+ } else workArgs = beforeDash;
90
+ if (workArgs.length === 0) workArgs = ["."];
91
+ if (mode === "exec" && execCmd.length === 0) {
92
+ console.error("sandbox: exec mode requires a command after \"--\"");
93
+ console.error("usage: bx exec [workdir...] -- command [args...]");
94
+ process.exit(1);
95
+ }
96
+ return {
97
+ mode,
98
+ workArgs,
99
+ verbose,
100
+ profileSandbox,
101
+ execCmd
102
+ };
61
103
  }
62
- const HOME = process.env.HOME;
63
- const SCRIPT_DIR = __dirname;
64
- const WORK_DIR = resolve(workArg);
65
- const allowedDirs = new Set([WORK_DIR]);
66
- const sandboxAllowPath = join(HOME, ".bxallow");
67
- if (existsSync(sandboxAllowPath)) for (const raw of readFileSync(sandboxAllowPath, "utf-8").split("\n")) {
68
- const line = raw.trim();
69
- if (!line || line.startsWith("#")) continue;
70
- const absolute = resolve(HOME, line);
71
- if (existsSync(absolute) && statSync(absolute).isDirectory()) allowedDirs.add(absolute);
104
+ //#endregion
105
+ //#region src/profile.ts
106
+ const PROTECTED_DOTDIRS = [
107
+ ".Trash",
108
+ ".ssh",
109
+ ".gnupg",
110
+ ".docker",
111
+ ".zsh_sessions",
112
+ ".cargo",
113
+ ".gradle",
114
+ ".gem"
115
+ ];
116
+ /**
117
+ * Parse a config file with one entry per line (supports # comments).
118
+ */
119
+ function parseLines(filePath) {
120
+ if (!existsSync(filePath)) return [];
121
+ return readFileSync(filePath, "utf-8").split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
72
122
  }
73
- const VSCODE_APP = "/Applications/Visual Studio Code.app/Contents/MacOS/Electron";
74
- const VSCODE_DATA = join(HOME, ".vscode-sandbox");
75
- const VSCODE_EXTENSIONS_GLOBAL = join(HOME, ".vscode", "extensions");
76
- const VSCODE_EXTENSIONS_LOCAL = join(VSCODE_DATA, "extensions");
77
- if (mode === "code" && profileSandbox) {
78
- mkdirSync(VSCODE_DATA, { recursive: true });
79
- if (!existsSync(VSCODE_EXTENSIONS_LOCAL) && existsSync(VSCODE_EXTENSIONS_GLOBAL)) {
80
- console.error("sandbox: copying extensions from global install...");
81
- cpSync(VSCODE_EXTENSIONS_GLOBAL, VSCODE_EXTENSIONS_LOCAL, { recursive: true });
123
+ /**
124
+ * Apply a single .bxignore file: resolve glob patterns relative to baseDir.
125
+ */
126
+ 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));
128
+ }
129
+ /**
130
+ * Recursively find and apply .bxignore files in a directory tree.
131
+ */
132
+ function collectIgnoreFilesRecursive(dir, ignored) {
133
+ const ignoreFile = join(dir, ".bxignore");
134
+ if (existsSync(ignoreFile)) applyIgnoreFile(ignoreFile, dir, ignored);
135
+ let entries;
136
+ try {
137
+ entries = readdirSync(dir);
138
+ } catch {
139
+ return;
140
+ }
141
+ for (const name of entries) {
142
+ if (name.startsWith(".") || name === "node_modules") continue;
143
+ const fullPath = join(dir, name);
144
+ try {
145
+ if (statSync(fullPath).isDirectory()) collectIgnoreFilesRecursive(fullPath, ignored);
146
+ } catch {}
82
147
  }
83
148
  }
84
- function collectBlockedDirs(parentDir) {
149
+ /**
150
+ * Parse ~/.bxallow and return a set of all allowed directories.
151
+ */
152
+ function parseAllowedDirs(home, workDirs) {
153
+ const allowed = new Set(workDirs);
154
+ for (const line of parseLines(join(home, ".bxallow"))) {
155
+ const absolute = resolve(home, line);
156
+ if (existsSync(absolute) && statSync(absolute).isDirectory()) allowed.add(absolute);
157
+ }
158
+ return allowed;
159
+ }
160
+ /**
161
+ * Recursively collect directories to block under parentDir.
162
+ * Never blocks a parent of an allowed path — instead descends and blocks siblings.
163
+ */
164
+ function collectBlockedDirs(parentDir, home, scriptDir, allowedDirs) {
85
165
  const blocked = [];
86
166
  for (const name of readdirSync(parentDir)) {
87
167
  if (name.startsWith(".")) continue;
88
168
  const fullPath = join(parentDir, name);
89
169
  if (!statSync(fullPath).isDirectory()) continue;
90
- if (parentDir === HOME && name === "Library") continue;
91
- if (SCRIPT_DIR.startsWith(fullPath + "/") || SCRIPT_DIR === fullPath) continue;
170
+ if (parentDir === home && name === "Library") continue;
171
+ if (scriptDir.startsWith(fullPath + "/") || scriptDir === fullPath) continue;
92
172
  if (allowedDirs.has(fullPath)) continue;
93
173
  if ([...allowedDirs].some((d) => d.startsWith(fullPath + "/"))) {
94
- blocked.push(...collectBlockedDirs(fullPath));
174
+ blocked.push(...collectBlockedDirs(fullPath, home, scriptDir, allowedDirs));
95
175
  continue;
96
176
  }
97
177
  blocked.push(fullPath);
98
178
  }
99
179
  return blocked;
100
180
  }
101
- const blockedDirs = collectBlockedDirs(HOME);
102
- const PROTECTED_DOTDIRS = [
103
- ".Trash",
104
- ".ssh",
105
- ".gnupg",
106
- ".docker",
107
- ".zsh_sessions",
108
- ".cargo",
109
- ".gradle",
110
- ".gem"
111
- ];
112
- const ignoredPaths = PROTECTED_DOTDIRS.map((d) => join(HOME, d));
113
- function parseSandboxIgnore(filePath, baseDir) {
114
- if (!existsSync(filePath)) return;
115
- for (const raw of readFileSync(filePath, "utf-8").split("\n")) {
116
- const line = raw.trim();
117
- if (!line || line.startsWith("#")) continue;
118
- const matches = globSync(line, { cwd: baseDir });
119
- for (const match of matches) ignoredPaths.push(resolve(baseDir, match));
120
- }
181
+ /**
182
+ * Collect paths to deny from .bxignore files and built-in protected dotdirs.
183
+ * Searches ~/.bxignore and recursively through all workdirs.
184
+ */
185
+ function collectIgnoredPaths(home, workDirs) {
186
+ const ignored = PROTECTED_DOTDIRS.map((d) => join(home, d));
187
+ applyIgnoreFile(join(home, ".bxignore"), home, ignored);
188
+ for (const workDir of workDirs) collectIgnoreFilesRecursive(workDir, ignored);
189
+ return ignored;
121
190
  }
122
- parseSandboxIgnore(join(HOME, ".bxignore"), HOME);
123
- parseSandboxIgnore(join(WORK_DIR, ".bxignore"), WORK_DIR);
124
- const extraIgnored = ignoredPaths.length - PROTECTED_DOTDIRS.length;
125
- if (extraIgnored > 0) console.error(`sandbox: .bxignore hides ${extraIgnored} extra path(s)`);
126
- const profile = `; Auto-generated sandbox profile
127
- ; Working directory: ${WORK_DIR}
191
+ /**
192
+ * Generate the SBPL sandbox profile string.
193
+ */
194
+ function generateProfile(workDirs, blockedDirs, ignoredPaths) {
195
+ const denyRules = blockedDirs.map((dir) => ` (subpath "${dir}")`).join("\n");
196
+ const ignoredRules = ignoredPaths.length > 0 ? `\n; Hidden paths from .bxignore\n(deny file*\n${ignoredPaths.map((p) => {
197
+ return existsSync(p) && statSync(p).isDirectory() ? ` (subpath "${p}")` : ` (literal "${p}")`;
198
+ }).join("\n")}\n)\n` : "";
199
+ return `; Auto-generated sandbox profile
200
+ ; Working directories: ${workDirs.join(", ")}
128
201
 
129
202
  (version 1)
130
203
  (allow default)
131
204
 
132
205
  ; Blocked directories (auto-generated from $HOME contents)
133
206
  (deny file*
134
- ${blockedDirs.map((dir) => ` (subpath "${dir}")`).join("\n")}
207
+ ${denyRules}
135
208
  )
136
-
137
- ${ignoredPaths.length > 0 ? `
138
- ; Hidden paths from .bxignore
139
- (deny file*
140
- ${ignoredPaths.map((p) => {
141
- return existsSync(p) && statSync(p).isDirectory() ? ` (subpath "${p}")` : ` (literal "${p}")`;
142
- }).join("\n")}
143
- )
144
- ` : ""}
209
+ ${ignoredRules}
145
210
  `;
146
- const profilePath = join("/tmp", `bx-${process.pid}.sb`);
147
- writeFileSync(profilePath, profile);
148
- console.error(`sandbox: ${mode} mode, working directory: ${WORK_DIR}`);
149
- if (verbose) {
150
- console.error("\n--- Generated sandbox profile ---");
151
- console.error(profile);
152
- console.error("--- End of profile ---\n");
153
211
  }
154
- function buildCommand() {
212
+ //#endregion
213
+ //#region src/modes.ts
214
+ const VSCODE_APP = "/Applications/Visual Studio Code.app/Contents/MacOS/Electron";
215
+ /**
216
+ * Prepare VSCode isolated profile if --profile-sandbox is set.
217
+ */
218
+ function setupVSCodeProfile(home) {
219
+ const dataDir = join(home, ".vscode-sandbox");
220
+ const globalExt = join(home, ".vscode", "extensions");
221
+ const localExt = join(dataDir, "extensions");
222
+ mkdirSync(dataDir, { recursive: true });
223
+ if (!existsSync(localExt) && existsSync(globalExt)) {
224
+ console.error("sandbox: copying extensions from global install...");
225
+ cpSync(globalExt, localExt, { recursive: true });
226
+ }
227
+ }
228
+ /**
229
+ * Build the command + args to run inside the sandbox for the given mode.
230
+ */
231
+ function buildCommand(mode, workDirs, home, profileSandbox, execCmd) {
155
232
  switch (mode) {
156
233
  case "code": {
234
+ const dataDir = join(home, ".vscode-sandbox");
157
235
  const args = ["--no-sandbox"];
158
236
  if (profileSandbox) {
159
- args.push("--user-data-dir", join(VSCODE_DATA, "data"));
160
- args.push("--extensions-dir", VSCODE_EXTENSIONS_LOCAL);
237
+ args.push("--user-data-dir", join(dataDir, "data"));
238
+ args.push("--extensions-dir", join(dataDir, "extensions"));
161
239
  }
162
- args.push(WORK_DIR);
240
+ args.push(...workDirs);
163
241
  return {
164
242
  bin: VSCODE_APP,
165
243
  args
@@ -171,7 +249,7 @@ function buildCommand() {
171
249
  };
172
250
  case "claude": return {
173
251
  bin: "claude",
174
- args: [WORK_DIR]
252
+ args: []
175
253
  };
176
254
  case "exec": return {
177
255
  bin: execCmd[0],
@@ -179,18 +257,72 @@ function buildCommand() {
179
257
  };
180
258
  }
181
259
  }
182
- const cmd = buildCommand();
260
+ //#endregion
261
+ //#region src/index.ts
262
+ const __dirname = dirname(fileURLToPath(import.meta.url));
263
+ const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : "dev";
264
+ if (process.argv.includes("--version") || process.argv.includes("-v")) {
265
+ console.log(`bx ${VERSION}`);
266
+ process.exit(0);
267
+ }
268
+ if (process.argv.includes("--help") || process.argv.includes("-h")) {
269
+ console.log(`bx ${VERSION} — launch apps in a macOS sandbox
270
+
271
+ Usage:
272
+ bx [workdir...] VSCode (default)
273
+ bx code [workdir...] VSCode
274
+ bx term [workdir...] sandboxed login shell
275
+ bx claude [workdir...] Claude Code CLI
276
+ bx exec [workdir...] -- command [args...] arbitrary command
277
+
278
+ Options:
279
+ --verbose print the generated sandbox profile
280
+ --profile-sandbox use an isolated VSCode profile (code mode only)
281
+ -v, --version show version
282
+ -h, --help show this help
283
+
284
+ Configuration:
285
+ ~/.bxallow extra allowed directories (one per line)
286
+ ~/.bxignore extra blocked paths in $HOME (one per line)
287
+ <workdir>/.bxignore blocked paths in project (supports globs, searched recursively)
288
+
289
+ https://github.com/holtwick/bx-mac`);
290
+ process.exit(0);
291
+ }
292
+ checkOwnSandbox();
293
+ checkVSCodeTerminal();
294
+ checkExternalSandbox();
295
+ const { mode, workArgs, verbose, profileSandbox, execCmd } = parseArgs();
296
+ const HOME = process.env.HOME;
297
+ const WORK_DIRS = workArgs.map((a) => resolve(a));
298
+ checkWorkDirs(WORK_DIRS, HOME);
299
+ if (mode === "code" && profileSandbox) setupVSCodeProfile(HOME);
300
+ const blockedDirs = collectBlockedDirs(HOME, HOME, __dirname, parseAllowedDirs(HOME, WORK_DIRS));
301
+ const ignoredPaths = collectIgnoredPaths(HOME, WORK_DIRS);
302
+ const extraIgnored = ignoredPaths.length - PROTECTED_DOTDIRS.length;
303
+ if (extraIgnored > 0) console.error(`sandbox: .bxignore hides ${extraIgnored} extra path(s)`);
304
+ const profile = generateProfile(WORK_DIRS, blockedDirs, ignoredPaths);
305
+ const profilePath = join("/tmp", `bx-${process.pid}.sb`);
306
+ writeFileSync(profilePath, profile);
307
+ const dirLabel = WORK_DIRS.length === 1 ? WORK_DIRS[0] : `${WORK_DIRS.length} directories`;
308
+ console.error(`sandbox: ${mode} mode, working directory: ${dirLabel}`);
309
+ if (verbose) {
310
+ console.error("\n--- Generated sandbox profile ---");
311
+ console.error(profile);
312
+ console.error("--- End of profile ---\n");
313
+ }
314
+ const cmd = buildCommand(mode, WORK_DIRS, HOME, profileSandbox, execCmd);
183
315
  spawn("sandbox-exec", [
184
316
  "-f",
185
317
  profilePath,
186
318
  "-D",
187
319
  `HOME=${HOME}`,
188
320
  "-D",
189
- `WORK=${WORK_DIR}`,
321
+ `WORK=${WORK_DIRS[0]}`,
190
322
  cmd.bin,
191
323
  ...cmd.args
192
324
  ], {
193
- cwd: WORK_DIR,
325
+ cwd: WORK_DIRS[0],
194
326
  stdio: "inherit",
195
327
  env: {
196
328
  ...process.env,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bx-mac",
3
- "version": "0.2.1",
3
+ "version": "0.4.0",
4
4
  "description": "Launch apps in a macOS sandbox — only the project directory is accessible",
5
5
  "type": "module",
6
6
  "bin": {
@@ -11,8 +11,9 @@
11
11
  ],
12
12
  "scripts": {
13
13
  "build": "rolldown -c",
14
- "release": "./scripts/release.sh",
15
- "prepublishOnly": "npm run build"
14
+ "test": "vitest run",
15
+ "prepublishOnly": "npm run build",
16
+ "post:release": "./scripts/release.sh"
16
17
  },
17
18
  "keywords": [
18
19
  "sandbox",
@@ -24,7 +25,9 @@
24
25
  "author": "Dirk Holtwick",
25
26
  "license": "MIT",
26
27
  "devDependencies": {
27
- "rolldown": "^1.0.0-rc.12"
28
+ "@types/node": "^25.5.0",
29
+ "rolldown": "^1.0.0-rc.12",
30
+ "vitest": "^4.1.2"
28
31
  },
29
32
  "engines": {
30
33
  "node": ">=22"