bx-mac 0.7.0 → 0.8.2

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/dist/bx.js CHANGED
@@ -1,30 +1,900 @@
1
1
  #!/usr/bin/env node
2
- const __VERSION__ = "0.7.0";
3
2
  import { accessSync, constants, cpSync, existsSync, globSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
4
- import { dirname, join, resolve } from "node:path";
5
- import { spawn } from "node:child_process";
3
+ import { basename, dirname, join, resolve } from "node:path";
4
+ import { execFileSync, spawn } from "node:child_process";
6
5
  import { createInterface } from "node:readline";
7
6
  import { fileURLToPath } from "node:url";
8
- import process from "node:process";
7
+ import process$1 from "node:process";
8
+ //#region node_modules/.pnpm/smol-toml@1.6.1/node_modules/smol-toml/dist/error.js
9
+ /*!
10
+ * Copyright (c) Squirrel Chat et al., All rights reserved.
11
+ * SPDX-License-Identifier: BSD-3-Clause
12
+ *
13
+ * Redistribution and use in source and binary forms, with or without
14
+ * modification, are permitted provided that the following conditions are met:
15
+ *
16
+ * 1. Redistributions of source code must retain the above copyright notice, this
17
+ * list of conditions and the following disclaimer.
18
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
19
+ * this list of conditions and the following disclaimer in the
20
+ * documentation and/or other materials provided with the distribution.
21
+ * 3. Neither the name of the copyright holder nor the names of its contributors
22
+ * may be used to endorse or promote products derived from this software without
23
+ * specific prior written permission.
24
+ *
25
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
26
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
27
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
28
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
29
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
30
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
31
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
32
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
33
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
34
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
35
+ */
36
+ function getLineColFromPtr(string, ptr) {
37
+ let lines = string.slice(0, ptr).split(/\r\n|\n|\r/g);
38
+ return [lines.length, lines.pop().length + 1];
39
+ }
40
+ function makeCodeBlock(string, line, column) {
41
+ let lines = string.split(/\r\n|\n|\r/g);
42
+ let codeblock = "";
43
+ let numberLen = (Math.log10(line + 1) | 0) + 1;
44
+ for (let i = line - 1; i <= line + 1; i++) {
45
+ let l = lines[i - 1];
46
+ if (!l) continue;
47
+ codeblock += i.toString().padEnd(numberLen, " ");
48
+ codeblock += ": ";
49
+ codeblock += l;
50
+ codeblock += "\n";
51
+ if (i === line) {
52
+ codeblock += " ".repeat(numberLen + column + 2);
53
+ codeblock += "^\n";
54
+ }
55
+ }
56
+ return codeblock;
57
+ }
58
+ var TomlError = class extends Error {
59
+ line;
60
+ column;
61
+ codeblock;
62
+ constructor(message, options) {
63
+ const [line, column] = getLineColFromPtr(options.toml, options.ptr);
64
+ const codeblock = makeCodeBlock(options.toml, line, column);
65
+ super(`Invalid TOML document: ${message}\n\n${codeblock}`, options);
66
+ this.line = line;
67
+ this.column = column;
68
+ this.codeblock = codeblock;
69
+ }
70
+ };
71
+ //#endregion
72
+ //#region node_modules/.pnpm/smol-toml@1.6.1/node_modules/smol-toml/dist/util.js
73
+ /*!
74
+ * Copyright (c) Squirrel Chat et al., All rights reserved.
75
+ * SPDX-License-Identifier: BSD-3-Clause
76
+ *
77
+ * Redistribution and use in source and binary forms, with or without
78
+ * modification, are permitted provided that the following conditions are met:
79
+ *
80
+ * 1. Redistributions of source code must retain the above copyright notice, this
81
+ * list of conditions and the following disclaimer.
82
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
83
+ * this list of conditions and the following disclaimer in the
84
+ * documentation and/or other materials provided with the distribution.
85
+ * 3. Neither the name of the copyright holder nor the names of its contributors
86
+ * may be used to endorse or promote products derived from this software without
87
+ * specific prior written permission.
88
+ *
89
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
90
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
91
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
92
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
93
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
94
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
95
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
96
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
97
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
98
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
99
+ */
100
+ function isEscaped(str, ptr) {
101
+ let i = 0;
102
+ while (str[ptr - ++i] === "\\");
103
+ return --i && i % 2;
104
+ }
105
+ function indexOfNewline(str, start = 0, end = str.length) {
106
+ let idx = str.indexOf("\n", start);
107
+ if (str[idx - 1] === "\r") idx--;
108
+ return idx <= end ? idx : -1;
109
+ }
110
+ function skipComment(str, ptr) {
111
+ for (let i = ptr; i < str.length; i++) {
112
+ let c = str[i];
113
+ if (c === "\n") return i;
114
+ if (c === "\r" && str[i + 1] === "\n") return i + 1;
115
+ if (c < " " && c !== " " || c === "") throw new TomlError("control characters are not allowed in comments", {
116
+ toml: str,
117
+ ptr
118
+ });
119
+ }
120
+ return str.length;
121
+ }
122
+ function skipVoid(str, ptr, banNewLines, banComments) {
123
+ let c;
124
+ while (1) {
125
+ while ((c = str[ptr]) === " " || c === " " || !banNewLines && (c === "\n" || c === "\r" && str[ptr + 1] === "\n")) ptr++;
126
+ if (banComments || c !== "#") break;
127
+ ptr = skipComment(str, ptr);
128
+ }
129
+ return ptr;
130
+ }
131
+ function skipUntil(str, ptr, sep, end, banNewLines = false) {
132
+ if (!end) {
133
+ ptr = indexOfNewline(str, ptr);
134
+ return ptr < 0 ? str.length : ptr;
135
+ }
136
+ for (let i = ptr; i < str.length; i++) {
137
+ let c = str[i];
138
+ if (c === "#") i = indexOfNewline(str, i);
139
+ else if (c === sep) return i + 1;
140
+ else if (c === end || banNewLines && (c === "\n" || c === "\r" && str[i + 1] === "\n")) return i;
141
+ }
142
+ throw new TomlError("cannot find end of structure", {
143
+ toml: str,
144
+ ptr
145
+ });
146
+ }
147
+ function getStringEnd(str, seek) {
148
+ let first = str[seek];
149
+ let target = first === str[seek + 1] && str[seek + 1] === str[seek + 2] ? str.slice(seek, seek + 3) : first;
150
+ seek += target.length - 1;
151
+ do
152
+ seek = str.indexOf(target, ++seek);
153
+ while (seek > -1 && first !== "'" && isEscaped(str, seek));
154
+ if (seek > -1) {
155
+ seek += target.length;
156
+ if (target.length > 1) {
157
+ if (str[seek] === first) seek++;
158
+ if (str[seek] === first) seek++;
159
+ }
160
+ }
161
+ return seek;
162
+ }
163
+ //#endregion
164
+ //#region node_modules/.pnpm/smol-toml@1.6.1/node_modules/smol-toml/dist/date.js
165
+ /*!
166
+ * Copyright (c) Squirrel Chat et al., All rights reserved.
167
+ * SPDX-License-Identifier: BSD-3-Clause
168
+ *
169
+ * Redistribution and use in source and binary forms, with or without
170
+ * modification, are permitted provided that the following conditions are met:
171
+ *
172
+ * 1. Redistributions of source code must retain the above copyright notice, this
173
+ * list of conditions and the following disclaimer.
174
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
175
+ * this list of conditions and the following disclaimer in the
176
+ * documentation and/or other materials provided with the distribution.
177
+ * 3. Neither the name of the copyright holder nor the names of its contributors
178
+ * may be used to endorse or promote products derived from this software without
179
+ * specific prior written permission.
180
+ *
181
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
182
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
183
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
184
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
185
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
186
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
187
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
188
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
189
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
190
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
191
+ */
192
+ let DATE_TIME_RE = /^(\d{4}-\d{2}-\d{2})?[T ]?(?:(\d{2}):\d{2}(?::\d{2}(?:\.\d+)?)?)?(Z|[-+]\d{2}:\d{2})?$/i;
193
+ var TomlDate = class TomlDate extends Date {
194
+ #hasDate = false;
195
+ #hasTime = false;
196
+ #offset = null;
197
+ constructor(date) {
198
+ let hasDate = true;
199
+ let hasTime = true;
200
+ let offset = "Z";
201
+ if (typeof date === "string") {
202
+ let match = date.match(DATE_TIME_RE);
203
+ if (match) {
204
+ if (!match[1]) {
205
+ hasDate = false;
206
+ date = `0000-01-01T${date}`;
207
+ }
208
+ hasTime = !!match[2];
209
+ hasTime && date[10] === " " && (date = date.replace(" ", "T"));
210
+ if (match[2] && +match[2] > 23) date = "";
211
+ else {
212
+ offset = match[3] || null;
213
+ date = date.toUpperCase();
214
+ if (!offset && hasTime) date += "Z";
215
+ }
216
+ } else date = "";
217
+ }
218
+ super(date);
219
+ if (!isNaN(this.getTime())) {
220
+ this.#hasDate = hasDate;
221
+ this.#hasTime = hasTime;
222
+ this.#offset = offset;
223
+ }
224
+ }
225
+ isDateTime() {
226
+ return this.#hasDate && this.#hasTime;
227
+ }
228
+ isLocal() {
229
+ return !this.#hasDate || !this.#hasTime || !this.#offset;
230
+ }
231
+ isDate() {
232
+ return this.#hasDate && !this.#hasTime;
233
+ }
234
+ isTime() {
235
+ return this.#hasTime && !this.#hasDate;
236
+ }
237
+ isValid() {
238
+ return this.#hasDate || this.#hasTime;
239
+ }
240
+ toISOString() {
241
+ let iso = super.toISOString();
242
+ if (this.isDate()) return iso.slice(0, 10);
243
+ if (this.isTime()) return iso.slice(11, 23);
244
+ if (this.#offset === null) return iso.slice(0, -1);
245
+ if (this.#offset === "Z") return iso;
246
+ let offset = +this.#offset.slice(1, 3) * 60 + +this.#offset.slice(4, 6);
247
+ offset = this.#offset[0] === "-" ? offset : -offset;
248
+ return (/* @__PURE__ */ new Date(this.getTime() - offset * 6e4)).toISOString().slice(0, -1) + this.#offset;
249
+ }
250
+ static wrapAsOffsetDateTime(jsDate, offset = "Z") {
251
+ let date = new TomlDate(jsDate);
252
+ date.#offset = offset;
253
+ return date;
254
+ }
255
+ static wrapAsLocalDateTime(jsDate) {
256
+ let date = new TomlDate(jsDate);
257
+ date.#offset = null;
258
+ return date;
259
+ }
260
+ static wrapAsLocalDate(jsDate) {
261
+ let date = new TomlDate(jsDate);
262
+ date.#hasTime = false;
263
+ date.#offset = null;
264
+ return date;
265
+ }
266
+ static wrapAsLocalTime(jsDate) {
267
+ let date = new TomlDate(jsDate);
268
+ date.#hasDate = false;
269
+ date.#offset = null;
270
+ return date;
271
+ }
272
+ };
273
+ //#endregion
274
+ //#region node_modules/.pnpm/smol-toml@1.6.1/node_modules/smol-toml/dist/primitive.js
275
+ /*!
276
+ * Copyright (c) Squirrel Chat et al., All rights reserved.
277
+ * SPDX-License-Identifier: BSD-3-Clause
278
+ *
279
+ * Redistribution and use in source and binary forms, with or without
280
+ * modification, are permitted provided that the following conditions are met:
281
+ *
282
+ * 1. Redistributions of source code must retain the above copyright notice, this
283
+ * list of conditions and the following disclaimer.
284
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
285
+ * this list of conditions and the following disclaimer in the
286
+ * documentation and/or other materials provided with the distribution.
287
+ * 3. Neither the name of the copyright holder nor the names of its contributors
288
+ * may be used to endorse or promote products derived from this software without
289
+ * specific prior written permission.
290
+ *
291
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
292
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
293
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
294
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
295
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
296
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
297
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
298
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
299
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
300
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
301
+ */
302
+ let INT_REGEX = /^((0x[0-9a-fA-F](_?[0-9a-fA-F])*)|(([+-]|0[ob])?\d(_?\d)*))$/;
303
+ let FLOAT_REGEX = /^[+-]?\d(_?\d)*(\.\d(_?\d)*)?([eE][+-]?\d(_?\d)*)?$/;
304
+ let LEADING_ZERO = /^[+-]?0[0-9_]/;
305
+ let ESCAPE_REGEX = /^[0-9a-f]{2,8}$/i;
306
+ let ESC_MAP = {
307
+ b: "\b",
308
+ t: " ",
309
+ n: "\n",
310
+ f: "\f",
311
+ r: "\r",
312
+ e: "\x1B",
313
+ "\"": "\"",
314
+ "\\": "\\"
315
+ };
316
+ function parseString(str, ptr = 0, endPtr = str.length) {
317
+ let isLiteral = str[ptr] === "'";
318
+ let isMultiline = str[ptr++] === str[ptr] && str[ptr] === str[ptr + 1];
319
+ if (isMultiline) {
320
+ endPtr -= 2;
321
+ if (str[ptr += 2] === "\r") ptr++;
322
+ if (str[ptr] === "\n") ptr++;
323
+ }
324
+ let tmp = 0;
325
+ let isEscape;
326
+ let parsed = "";
327
+ let sliceStart = ptr;
328
+ while (ptr < endPtr - 1) {
329
+ let c = str[ptr++];
330
+ if (c === "\n" || c === "\r" && str[ptr] === "\n") {
331
+ if (!isMultiline) throw new TomlError("newlines are not allowed in strings", {
332
+ toml: str,
333
+ ptr: ptr - 1
334
+ });
335
+ } else if (c < " " && c !== " " || c === "") throw new TomlError("control characters are not allowed in strings", {
336
+ toml: str,
337
+ ptr: ptr - 1
338
+ });
339
+ if (isEscape) {
340
+ isEscape = false;
341
+ if (c === "x" || c === "u" || c === "U") {
342
+ let code = str.slice(ptr, ptr += c === "x" ? 2 : c === "u" ? 4 : 8);
343
+ if (!ESCAPE_REGEX.test(code)) throw new TomlError("invalid unicode escape", {
344
+ toml: str,
345
+ ptr: tmp
346
+ });
347
+ try {
348
+ parsed += String.fromCodePoint(parseInt(code, 16));
349
+ } catch {
350
+ throw new TomlError("invalid unicode escape", {
351
+ toml: str,
352
+ ptr: tmp
353
+ });
354
+ }
355
+ } else if (isMultiline && (c === "\n" || c === " " || c === " " || c === "\r")) {
356
+ ptr = skipVoid(str, ptr - 1, true);
357
+ if (str[ptr] !== "\n" && str[ptr] !== "\r") throw new TomlError("invalid escape: only line-ending whitespace may be escaped", {
358
+ toml: str,
359
+ ptr: tmp
360
+ });
361
+ ptr = skipVoid(str, ptr);
362
+ } else if (c in ESC_MAP) parsed += ESC_MAP[c];
363
+ else throw new TomlError("unrecognized escape sequence", {
364
+ toml: str,
365
+ ptr: tmp
366
+ });
367
+ sliceStart = ptr;
368
+ } else if (!isLiteral && c === "\\") {
369
+ tmp = ptr - 1;
370
+ isEscape = true;
371
+ parsed += str.slice(sliceStart, tmp);
372
+ }
373
+ }
374
+ return parsed + str.slice(sliceStart, endPtr - 1);
375
+ }
376
+ function parseValue(value, toml, ptr, integersAsBigInt) {
377
+ if (value === "true") return true;
378
+ if (value === "false") return false;
379
+ if (value === "-inf") return -Infinity;
380
+ if (value === "inf" || value === "+inf") return Infinity;
381
+ if (value === "nan" || value === "+nan" || value === "-nan") return NaN;
382
+ if (value === "-0") return integersAsBigInt ? 0n : 0;
383
+ let isInt = INT_REGEX.test(value);
384
+ if (isInt || FLOAT_REGEX.test(value)) {
385
+ if (LEADING_ZERO.test(value)) throw new TomlError("leading zeroes are not allowed", {
386
+ toml,
387
+ ptr
388
+ });
389
+ value = value.replace(/_/g, "");
390
+ let numeric = +value;
391
+ if (isNaN(numeric)) throw new TomlError("invalid number", {
392
+ toml,
393
+ ptr
394
+ });
395
+ if (isInt) {
396
+ if ((isInt = !Number.isSafeInteger(numeric)) && !integersAsBigInt) throw new TomlError("integer value cannot be represented losslessly", {
397
+ toml,
398
+ ptr
399
+ });
400
+ if (isInt || integersAsBigInt === true) numeric = BigInt(value);
401
+ }
402
+ return numeric;
403
+ }
404
+ const date = new TomlDate(value);
405
+ if (!date.isValid()) throw new TomlError("invalid value", {
406
+ toml,
407
+ ptr
408
+ });
409
+ return date;
410
+ }
411
+ //#endregion
412
+ //#region node_modules/.pnpm/smol-toml@1.6.1/node_modules/smol-toml/dist/extract.js
413
+ /*!
414
+ * Copyright (c) Squirrel Chat et al., All rights reserved.
415
+ * SPDX-License-Identifier: BSD-3-Clause
416
+ *
417
+ * Redistribution and use in source and binary forms, with or without
418
+ * modification, are permitted provided that the following conditions are met:
419
+ *
420
+ * 1. Redistributions of source code must retain the above copyright notice, this
421
+ * list of conditions and the following disclaimer.
422
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
423
+ * this list of conditions and the following disclaimer in the
424
+ * documentation and/or other materials provided with the distribution.
425
+ * 3. Neither the name of the copyright holder nor the names of its contributors
426
+ * may be used to endorse or promote products derived from this software without
427
+ * specific prior written permission.
428
+ *
429
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
430
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
431
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
432
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
433
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
434
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
435
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
436
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
437
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
438
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
439
+ */
440
+ function sliceAndTrimEndOf(str, startPtr, endPtr) {
441
+ let value = str.slice(startPtr, endPtr);
442
+ let commentIdx = value.indexOf("#");
443
+ if (commentIdx > -1) {
444
+ skipComment(str, commentIdx);
445
+ value = value.slice(0, commentIdx);
446
+ }
447
+ return [value.trimEnd(), commentIdx];
448
+ }
449
+ function extractValue(str, ptr, end, depth, integersAsBigInt) {
450
+ if (depth === 0) throw new TomlError("document contains excessively nested structures. aborting.", {
451
+ toml: str,
452
+ ptr
453
+ });
454
+ let c = str[ptr];
455
+ if (c === "[" || c === "{") {
456
+ let [value, endPtr] = c === "[" ? parseArray(str, ptr, depth, integersAsBigInt) : parseInlineTable(str, ptr, depth, integersAsBigInt);
457
+ if (end) {
458
+ endPtr = skipVoid(str, endPtr);
459
+ if (str[endPtr] === ",") endPtr++;
460
+ else if (str[endPtr] !== end) throw new TomlError("expected comma or end of structure", {
461
+ toml: str,
462
+ ptr: endPtr
463
+ });
464
+ }
465
+ return [value, endPtr];
466
+ }
467
+ let endPtr;
468
+ if (c === "\"" || c === "'") {
469
+ endPtr = getStringEnd(str, ptr);
470
+ let parsed = parseString(str, ptr, endPtr);
471
+ if (end) {
472
+ endPtr = skipVoid(str, endPtr);
473
+ if (str[endPtr] && str[endPtr] !== "," && str[endPtr] !== end && str[endPtr] !== "\n" && str[endPtr] !== "\r") throw new TomlError("unexpected character encountered", {
474
+ toml: str,
475
+ ptr: endPtr
476
+ });
477
+ endPtr += +(str[endPtr] === ",");
478
+ }
479
+ return [parsed, endPtr];
480
+ }
481
+ endPtr = skipUntil(str, ptr, ",", end);
482
+ let slice = sliceAndTrimEndOf(str, ptr, endPtr - +(str[endPtr - 1] === ","));
483
+ if (!slice[0]) throw new TomlError("incomplete key-value declaration: no value specified", {
484
+ toml: str,
485
+ ptr
486
+ });
487
+ if (end && slice[1] > -1) {
488
+ endPtr = skipVoid(str, ptr + slice[1]);
489
+ endPtr += +(str[endPtr] === ",");
490
+ }
491
+ return [parseValue(slice[0], str, ptr, integersAsBigInt), endPtr];
492
+ }
493
+ //#endregion
494
+ //#region node_modules/.pnpm/smol-toml@1.6.1/node_modules/smol-toml/dist/struct.js
495
+ /*!
496
+ * Copyright (c) Squirrel Chat et al., All rights reserved.
497
+ * SPDX-License-Identifier: BSD-3-Clause
498
+ *
499
+ * Redistribution and use in source and binary forms, with or without
500
+ * modification, are permitted provided that the following conditions are met:
501
+ *
502
+ * 1. Redistributions of source code must retain the above copyright notice, this
503
+ * list of conditions and the following disclaimer.
504
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
505
+ * this list of conditions and the following disclaimer in the
506
+ * documentation and/or other materials provided with the distribution.
507
+ * 3. Neither the name of the copyright holder nor the names of its contributors
508
+ * may be used to endorse or promote products derived from this software without
509
+ * specific prior written permission.
510
+ *
511
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
512
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
513
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
514
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
515
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
516
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
517
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
518
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
519
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
520
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
521
+ */
522
+ let KEY_PART_RE = /^[a-zA-Z0-9-_]+[ \t]*$/;
523
+ function parseKey(str, ptr, end = "=") {
524
+ let dot = ptr - 1;
525
+ let parsed = [];
526
+ let endPtr = str.indexOf(end, ptr);
527
+ if (endPtr < 0) throw new TomlError("incomplete key-value: cannot find end of key", {
528
+ toml: str,
529
+ ptr
530
+ });
531
+ do {
532
+ let c = str[ptr = ++dot];
533
+ if (c !== " " && c !== " ") if (c === "\"" || c === "'") {
534
+ if (c === str[ptr + 1] && c === str[ptr + 2]) throw new TomlError("multiline strings are not allowed in keys", {
535
+ toml: str,
536
+ ptr
537
+ });
538
+ let eos = getStringEnd(str, ptr);
539
+ if (eos < 0) throw new TomlError("unfinished string encountered", {
540
+ toml: str,
541
+ ptr
542
+ });
543
+ dot = str.indexOf(".", eos);
544
+ let strEnd = str.slice(eos, dot < 0 || dot > endPtr ? endPtr : dot);
545
+ let newLine = indexOfNewline(strEnd);
546
+ if (newLine > -1) throw new TomlError("newlines are not allowed in keys", {
547
+ toml: str,
548
+ ptr: ptr + dot + newLine
549
+ });
550
+ if (strEnd.trimStart()) throw new TomlError("found extra tokens after the string part", {
551
+ toml: str,
552
+ ptr: eos
553
+ });
554
+ if (endPtr < eos) {
555
+ endPtr = str.indexOf(end, eos);
556
+ if (endPtr < 0) throw new TomlError("incomplete key-value: cannot find end of key", {
557
+ toml: str,
558
+ ptr
559
+ });
560
+ }
561
+ parsed.push(parseString(str, ptr, eos));
562
+ } else {
563
+ dot = str.indexOf(".", ptr);
564
+ let part = str.slice(ptr, dot < 0 || dot > endPtr ? endPtr : dot);
565
+ if (!KEY_PART_RE.test(part)) throw new TomlError("only letter, numbers, dashes and underscores are allowed in keys", {
566
+ toml: str,
567
+ ptr
568
+ });
569
+ parsed.push(part.trimEnd());
570
+ }
571
+ } while (dot + 1 && dot < endPtr);
572
+ return [parsed, skipVoid(str, endPtr + 1, true, true)];
573
+ }
574
+ function parseInlineTable(str, ptr, depth, integersAsBigInt) {
575
+ let res = {};
576
+ let seen = /* @__PURE__ */ new Set();
577
+ let c;
578
+ ptr++;
579
+ while ((c = str[ptr++]) !== "}" && c) if (c === ",") throw new TomlError("expected value, found comma", {
580
+ toml: str,
581
+ ptr: ptr - 1
582
+ });
583
+ else if (c === "#") ptr = skipComment(str, ptr);
584
+ else if (c !== " " && c !== " " && c !== "\n" && c !== "\r") {
585
+ let k;
586
+ let t = res;
587
+ let hasOwn = false;
588
+ let [key, keyEndPtr] = parseKey(str, ptr - 1);
589
+ for (let i = 0; i < key.length; i++) {
590
+ if (i) t = hasOwn ? t[k] : t[k] = {};
591
+ k = key[i];
592
+ if ((hasOwn = Object.hasOwn(t, k)) && (typeof t[k] !== "object" || seen.has(t[k]))) throw new TomlError("trying to redefine an already defined value", {
593
+ toml: str,
594
+ ptr
595
+ });
596
+ if (!hasOwn && k === "__proto__") Object.defineProperty(t, k, {
597
+ enumerable: true,
598
+ configurable: true,
599
+ writable: true
600
+ });
601
+ }
602
+ if (hasOwn) throw new TomlError("trying to redefine an already defined value", {
603
+ toml: str,
604
+ ptr
605
+ });
606
+ let [value, valueEndPtr] = extractValue(str, keyEndPtr, "}", depth - 1, integersAsBigInt);
607
+ seen.add(value);
608
+ t[k] = value;
609
+ ptr = valueEndPtr;
610
+ }
611
+ if (!c) throw new TomlError("unfinished table encountered", {
612
+ toml: str,
613
+ ptr
614
+ });
615
+ return [res, ptr];
616
+ }
617
+ function parseArray(str, ptr, depth, integersAsBigInt) {
618
+ let res = [];
619
+ let c;
620
+ ptr++;
621
+ while ((c = str[ptr++]) !== "]" && c) if (c === ",") throw new TomlError("expected value, found comma", {
622
+ toml: str,
623
+ ptr: ptr - 1
624
+ });
625
+ else if (c === "#") ptr = skipComment(str, ptr);
626
+ else if (c !== " " && c !== " " && c !== "\n" && c !== "\r") {
627
+ let e = extractValue(str, ptr - 1, "]", depth - 1, integersAsBigInt);
628
+ res.push(e[0]);
629
+ ptr = e[1];
630
+ }
631
+ if (!c) throw new TomlError("unfinished array encountered", {
632
+ toml: str,
633
+ ptr
634
+ });
635
+ return [res, ptr];
636
+ }
637
+ //#endregion
638
+ //#region node_modules/.pnpm/smol-toml@1.6.1/node_modules/smol-toml/dist/parse.js
639
+ /*!
640
+ * Copyright (c) Squirrel Chat et al., All rights reserved.
641
+ * SPDX-License-Identifier: BSD-3-Clause
642
+ *
643
+ * Redistribution and use in source and binary forms, with or without
644
+ * modification, are permitted provided that the following conditions are met:
645
+ *
646
+ * 1. Redistributions of source code must retain the above copyright notice, this
647
+ * list of conditions and the following disclaimer.
648
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
649
+ * this list of conditions and the following disclaimer in the
650
+ * documentation and/or other materials provided with the distribution.
651
+ * 3. Neither the name of the copyright holder nor the names of its contributors
652
+ * may be used to endorse or promote products derived from this software without
653
+ * specific prior written permission.
654
+ *
655
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
656
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
657
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
658
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
659
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
660
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
661
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
662
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
663
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
664
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
665
+ */
666
+ function peekTable(key, table, meta, type) {
667
+ let t = table;
668
+ let m = meta;
669
+ let k;
670
+ let hasOwn = false;
671
+ let state;
672
+ for (let i = 0; i < key.length; i++) {
673
+ if (i) {
674
+ t = hasOwn ? t[k] : t[k] = {};
675
+ m = (state = m[k]).c;
676
+ if (type === 0 && (state.t === 1 || state.t === 2)) return null;
677
+ if (state.t === 2) {
678
+ let l = t.length - 1;
679
+ t = t[l];
680
+ m = m[l].c;
681
+ }
682
+ }
683
+ k = key[i];
684
+ if ((hasOwn = Object.hasOwn(t, k)) && m[k]?.t === 0 && m[k]?.d) return null;
685
+ if (!hasOwn) {
686
+ if (k === "__proto__") {
687
+ Object.defineProperty(t, k, {
688
+ enumerable: true,
689
+ configurable: true,
690
+ writable: true
691
+ });
692
+ Object.defineProperty(m, k, {
693
+ enumerable: true,
694
+ configurable: true,
695
+ writable: true
696
+ });
697
+ }
698
+ m[k] = {
699
+ t: i < key.length - 1 && type === 2 ? 3 : type,
700
+ d: false,
701
+ i: 0,
702
+ c: {}
703
+ };
704
+ }
705
+ }
706
+ state = m[k];
707
+ if (state.t !== type && !(type === 1 && state.t === 3)) return null;
708
+ if (type === 2) {
709
+ if (!state.d) {
710
+ state.d = true;
711
+ t[k] = [];
712
+ }
713
+ t[k].push(t = {});
714
+ state.c[state.i++] = state = {
715
+ t: 1,
716
+ d: false,
717
+ i: 0,
718
+ c: {}
719
+ };
720
+ }
721
+ if (state.d) return null;
722
+ state.d = true;
723
+ if (type === 1) t = hasOwn ? t[k] : t[k] = {};
724
+ else if (type === 0 && hasOwn) return null;
725
+ return [
726
+ k,
727
+ t,
728
+ state.c
729
+ ];
730
+ }
731
+ function parse(toml, { maxDepth = 1e3, integersAsBigInt } = {}) {
732
+ let res = {};
733
+ let meta = {};
734
+ let tbl = res;
735
+ let m = meta;
736
+ for (let ptr = skipVoid(toml, 0); ptr < toml.length;) {
737
+ if (toml[ptr] === "[") {
738
+ let isTableArray = toml[++ptr] === "[";
739
+ let k = parseKey(toml, ptr += +isTableArray, "]");
740
+ if (isTableArray) {
741
+ if (toml[k[1] - 1] !== "]") throw new TomlError("expected end of table declaration", {
742
+ toml,
743
+ ptr: k[1] - 1
744
+ });
745
+ k[1]++;
746
+ }
747
+ let p = peekTable(k[0], res, meta, isTableArray ? 2 : 1);
748
+ if (!p) throw new TomlError("trying to redefine an already defined table or value", {
749
+ toml,
750
+ ptr
751
+ });
752
+ m = p[2];
753
+ tbl = p[1];
754
+ ptr = k[1];
755
+ } else {
756
+ let k = parseKey(toml, ptr);
757
+ let p = peekTable(k[0], tbl, m, 0);
758
+ if (!p) throw new TomlError("trying to redefine an already defined table or value", {
759
+ toml,
760
+ ptr
761
+ });
762
+ let v = extractValue(toml, k[1], void 0, maxDepth, integersAsBigInt);
763
+ p[1][p[0]] = v[0];
764
+ ptr = v[1];
765
+ }
766
+ ptr = skipVoid(toml, ptr, true);
767
+ if (toml[ptr] && toml[ptr] !== "\n" && toml[ptr] !== "\r") throw new TomlError("each key-value declaration must be followed by an end-of-line", {
768
+ toml,
769
+ ptr
770
+ });
771
+ ptr = skipVoid(toml, ptr);
772
+ }
773
+ return res;
774
+ }
775
+ //#endregion
776
+ //#region src/fmt.ts
777
+ const DIM$1 = "\x1B[2m";
778
+ const RESET$1 = "\x1B[0m";
779
+ const PREFIX = `bx${RESET$1}`;
780
+ const fmt = {
781
+ info: (msg) => `🔒 ${PREFIX} · ${msg}`,
782
+ warn: (msg) => `⚠️ ${PREFIX} · ${msg}`,
783
+ error: (msg) => `🚫 ${PREFIX} · ${msg}`,
784
+ detail: (msg) => ` ${DIM$1}${msg}${RESET$1}`
785
+ };
786
+ //#endregion
787
+ //#region src/config.ts
788
+ /** Built-in app definitions — always available, can be overridden via config */
789
+ const BUILTIN_APPS = {
790
+ code: {
791
+ bundle: "com.microsoft.VSCode",
792
+ binary: "Contents/MacOS/Electron",
793
+ fallback: "/Applications/Visual Studio Code.app/Contents/MacOS/Electron",
794
+ args: ["--no-sandbox"]
795
+ },
796
+ xcode: {
797
+ bundle: "com.apple.dt.Xcode",
798
+ binary: "Contents/MacOS/Xcode",
799
+ fallback: "/Applications/Xcode.app/Contents/MacOS/Xcode"
800
+ }
801
+ };
802
+ /** Shell-only built-in modes that are not app definitions */
803
+ const BUILTIN_MODES = [
804
+ "term",
805
+ "claude",
806
+ "exec"
807
+ ];
808
+ /**
809
+ * Load and parse ~/.bxconfig.toml. Returns empty apps if file missing or invalid.
810
+ */
811
+ function loadConfig(home) {
812
+ const configPath = join(home, ".bxconfig.toml");
813
+ if (!existsSync(configPath)) return { apps: {} };
814
+ try {
815
+ const doc = parse(readFileSync(configPath, "utf-8"));
816
+ const apps = {};
817
+ if (doc.apps && typeof doc.apps === "object") for (const [name, def] of Object.entries(doc.apps)) apps[name] = {
818
+ bundle: typeof def.bundle === "string" ? def.bundle : void 0,
819
+ binary: typeof def.binary === "string" ? def.binary : void 0,
820
+ path: typeof def.path === "string" ? def.path : void 0,
821
+ fallback: typeof def.fallback === "string" ? def.fallback : void 0,
822
+ args: Array.isArray(def.args) ? def.args.filter((a) => typeof a === "string") : void 0,
823
+ passWorkdirs: typeof def.passWorkdirs === "boolean" ? def.passWorkdirs : void 0,
824
+ workdirs: Array.isArray(def.workdirs) ? def.workdirs.filter((a) => typeof a === "string") : void 0
825
+ };
826
+ return { apps };
827
+ } catch (err) {
828
+ console.error(`\n${fmt.warn(`failed to parse ${configPath}: ${err instanceof Error ? err.message : err}`)}`);
829
+ return { apps: {} };
830
+ }
831
+ }
832
+ /**
833
+ * Merge built-in apps with user config (config wins on conflict).
834
+ */
835
+ function getAvailableApps(config) {
836
+ const merged = {};
837
+ for (const [name, def] of Object.entries(BUILTIN_APPS)) merged[name] = { ...def };
838
+ for (const [name, def] of Object.entries(config.apps)) if (merged[name]) merged[name] = {
839
+ ...merged[name],
840
+ ...stripUndefined(def)
841
+ };
842
+ else merged[name] = def;
843
+ return merged;
844
+ }
845
+ function stripUndefined(obj) {
846
+ const result = {};
847
+ for (const [k, v] of Object.entries(obj)) if (v !== void 0) result[k] = v;
848
+ return result;
849
+ }
850
+ /**
851
+ * Get all valid mode names (builtin modes + app names).
852
+ */
853
+ function getValidModes(apps) {
854
+ return [...BUILTIN_MODES, ...Object.keys(apps)];
855
+ }
856
+ /**
857
+ * Resolve an AppDefinition to an executable path.
858
+ * Resolution chain: path (explicit) → mdfind + binary → fallback
859
+ */
860
+ function resolveAppPath(app) {
861
+ if (app.path) {
862
+ if (existsSync(app.path)) return app.path;
863
+ console.error(`\n${fmt.warn(`configured path not found: ${app.path}`)}`);
864
+ }
865
+ if (app.bundle) try {
866
+ const appPath = execFileSync("mdfind", [`kMDItemCFBundleIdentifier == '${app.bundle.replace(/'/g, "'\\''")}'`], {
867
+ encoding: "utf-8",
868
+ timeout: 5e3
869
+ }).trim().split("\n")[0];
870
+ if (appPath) if (app.binary) {
871
+ const fullPath = join(appPath, app.binary);
872
+ if (existsSync(fullPath)) return fullPath;
873
+ } else return appPath;
874
+ } catch {}
875
+ if (app.fallback && existsSync(app.fallback)) return app.fallback;
876
+ return null;
877
+ }
878
+ //#endregion
9
879
  //#region src/guards.ts
10
880
  /**
11
881
  * Abort if we're already inside a bx sandbox (env var set by us).
12
882
  */
13
883
  function checkOwnSandbox() {
14
- if (process.env.CODEBOX_SANDBOX === "1") {
15
- console.error("sandbox: ERROR — already running inside a bx sandbox.");
16
- console.error("sandbox: Nesting sandbox-exec causes silent failures. Aborting.");
17
- process.exit(1);
884
+ if (process$1.env.CODEBOX_SANDBOX === "1") {
885
+ console.error(`\n${fmt.error("already running inside a bx sandbox")}`);
886
+ console.error(fmt.detail("nesting sandbox-exec causes silent failures\n"));
887
+ process$1.exit(1);
18
888
  }
19
889
  }
20
890
  /**
21
891
  * Warn if launched from inside a VSCode terminal.
22
892
  */
23
893
  function checkVSCodeTerminal() {
24
- if (process.env.VSCODE_PID) {
25
- console.error("sandbox: WARNING — running from inside a VSCode terminal.");
26
- console.error("sandbox: This will launch a *new* instance in a sandbox.");
27
- console.error("sandbox: The current VSCode instance will NOT be sandboxed.");
894
+ if (process$1.env.VSCODE_PID) {
895
+ console.error(`\n${fmt.warn("running from inside a VSCode terminal")}`);
896
+ console.error(fmt.detail("this will launch a *new* instance in a sandbox"));
897
+ console.error(fmt.detail("the current VSCode instance will NOT be sandboxed"));
28
898
  }
29
899
  }
30
900
  /**
@@ -33,18 +903,50 @@ function checkVSCodeTerminal() {
33
903
  function checkWorkDirs(workDirs, home) {
34
904
  for (const dir of workDirs) {
35
905
  if (dir === home) {
36
- console.error("sandbox: ERROR — working directory cannot be $HOME itself.");
37
- console.error("sandbox: Sandboxing your entire home directory is not supported. Aborting.");
38
- process.exit(1);
906
+ console.error(`\n${fmt.error("working directory cannot be $HOME itself")}`);
907
+ console.error(fmt.detail("sandboxing your entire home directory is not supported\n"));
908
+ process$1.exit(1);
39
909
  }
40
910
  if (!dir.startsWith(home + "/")) {
41
- console.error(`sandbox: ERROR — working directory is outside $HOME: ${dir}`);
42
- console.error("sandbox: Only directories inside $HOME are supported. Aborting.");
43
- process.exit(1);
911
+ console.error(`\n${fmt.error(`working directory is outside $HOME: ${dir}`)}`);
912
+ console.error(fmt.detail("only directories inside $HOME are supported\n"));
913
+ process$1.exit(1);
44
914
  }
45
915
  }
46
916
  }
47
917
  /**
918
+ * Warn if the target app is already running — the new workspace will open
919
+ * in the existing (unsandboxed) instance, bypassing our sandbox profile.
920
+ */
921
+ async function checkAppAlreadyRunning(mode, apps) {
922
+ if (BUILTIN_MODES.includes(mode)) return;
923
+ const app = apps[mode];
924
+ if (!app?.bundle) return;
925
+ let running = false;
926
+ try {
927
+ running = execFileSync("lsappinfo", ["list"], {
928
+ encoding: "utf-8",
929
+ timeout: 3e3
930
+ }).includes(`bundleID="${app.bundle}"`);
931
+ } catch {
932
+ return;
933
+ }
934
+ if (!running) return;
935
+ console.error(`\n${fmt.warn(`"${mode}" is already running`)}`);
936
+ console.error(fmt.detail("the workspace will open in the EXISTING instance — sandbox will NOT apply"));
937
+ if (mode === "code") console.error(fmt.detail("quit the app first, or use --profile-sandbox for an isolated instance"));
938
+ else console.error(fmt.detail("quit the app first to ensure sandbox protection"));
939
+ const rl = createInterface({
940
+ input: process$1.stdin,
941
+ output: process$1.stderr
942
+ });
943
+ const answer = await new Promise((res) => {
944
+ rl.question(` continue without sandbox? [y/N] `, res);
945
+ });
946
+ rl.close();
947
+ if (!answer.match(/^y(es)?$/i)) process$1.exit(0);
948
+ }
949
+ /**
48
950
  * Detect if we're inside an unknown sandbox by probing well-known
49
951
  * directories that exist on every Mac but would be blocked.
50
952
  */
@@ -54,49 +956,48 @@ function checkExternalSandbox() {
54
956
  "Desktop",
55
957
  "Downloads"
56
958
  ]) {
57
- const target = join(process.env.HOME, dir);
959
+ const target = join(process$1.env.HOME, dir);
58
960
  try {
59
961
  accessSync(target, constants.R_OK);
60
962
  } catch (e) {
61
963
  if (e.code === "EPERM") {
62
- console.error("sandbox: ERROR — already running inside a sandbox!");
63
- console.error("sandbox: Nesting sandbox-exec may cause silent failures. Aborting.");
64
- process.exit(1);
964
+ console.error(`\n${fmt.error("already running inside a sandbox")}`);
965
+ console.error(fmt.detail("nesting sandbox-exec may cause silent failures\n"));
966
+ process$1.exit(1);
65
967
  }
66
968
  }
67
969
  }
68
970
  }
69
971
  //#endregion
70
972
  //#region src/args.ts
71
- const MODES = [
72
- "code",
73
- "term",
74
- "claude",
75
- "exec"
76
- ];
77
- function parseArgs() {
78
- const rawArgs = process.argv.slice(2);
973
+ /**
974
+ * Parse CLI arguments. `validModes` is the list of recognized mode names
975
+ * (builtin modes + app names from config).
976
+ */
977
+ function parseArgs(validModes) {
978
+ const rawArgs = process$1.argv.slice(2);
79
979
  const verbose = rawArgs.includes("--verbose");
80
980
  const dry = rawArgs.includes("--dry");
81
981
  const profileSandbox = rawArgs.includes("--profile-sandbox");
82
982
  const positional = rawArgs.filter((a) => !a.startsWith("--"));
83
983
  const doubleDashIdx = rawArgs.indexOf("--");
84
- const execCmd = doubleDashIdx >= 0 ? rawArgs.slice(doubleDashIdx + 1) : [];
984
+ const appArgs = doubleDashIdx >= 0 ? rawArgs.slice(doubleDashIdx + 1) : [];
85
985
  const beforeDash = doubleDashIdx >= 0 ? rawArgs.slice(0, doubleDashIdx).filter((a) => !a.startsWith("--")) : positional;
86
986
  let mode = "code";
87
987
  let workArgs;
88
- let explicit = false;
89
- if (beforeDash.length > 0 && MODES.includes(beforeDash[0])) {
988
+ let implicitWorkdirs = false;
989
+ if (beforeDash.length > 0 && validModes.includes(beforeDash[0])) {
90
990
  mode = beforeDash[0];
91
991
  workArgs = beforeDash.slice(1);
92
- explicit = true;
93
992
  } else workArgs = beforeDash;
94
- if (workArgs.length === 0) workArgs = ["."];
95
- else explicit = true;
96
- if (mode === "exec" && execCmd.length === 0) {
97
- console.error("sandbox: exec mode requires a command after \"--\"");
98
- console.error("usage: bx exec [workdir...] -- command [args...]");
99
- process.exit(1);
993
+ if (workArgs.length === 0) {
994
+ workArgs = ["."];
995
+ implicitWorkdirs = true;
996
+ }
997
+ if (mode === "exec" && appArgs.length === 0) {
998
+ console.error(`\n${fmt.error("exec mode requires a command after \"--\"")}`);
999
+ console.error(fmt.detail("usage: bx exec [workdir...] -- command [args...]\n"));
1000
+ process$1.exit(1);
100
1001
  }
101
1002
  return {
102
1003
  mode,
@@ -104,8 +1005,8 @@ function parseArgs() {
104
1005
  verbose,
105
1006
  dry,
106
1007
  profileSandbox,
107
- execCmd,
108
- implicit: !explicit
1008
+ appArgs,
1009
+ implicit: implicitWorkdirs
109
1010
  };
110
1011
  }
111
1012
  //#endregion
@@ -120,34 +1021,29 @@ const PROTECTED_DOTDIRS = [
120
1021
  ".gradle",
121
1022
  ".gem"
122
1023
  ];
123
- /**
124
- * Parse a config file with one entry per line (supports # comments).
125
- */
126
1024
  function parseLines(filePath) {
127
1025
  if (!existsSync(filePath)) return [];
128
1026
  return readFileSync(filePath, "utf-8").split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
129
1027
  }
130
1028
  /**
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
1029
+ * Convert a .bxignore line to a glob pattern (.gitignore semantics):
1030
+ *
1031
+ * "/foo" anchored to base dir → "foo"
1032
+ * "foo/bar" relative path, use as-is → "foo/bar"
1033
+ * "foo" no slash, match recursively "** /foo"
1034
+ * "secrets/" → trailing slash = dir marker, still recursive
136
1035
  */
137
1036
  function toGlobPattern(line) {
138
1037
  if (line.startsWith("/")) return line.slice(1);
139
1038
  if ((line.endsWith("/") ? line.slice(0, -1) : line).includes("/")) return line;
140
1039
  return `**/${line}`;
141
1040
  }
142
- /**
143
- * Apply a single .bxignore file: resolve glob patterns relative to baseDir.
144
- */
1041
+ function resolveGlobMatches(pattern, baseDir) {
1042
+ return globSync(toGlobPattern(pattern), { cwd: baseDir }).map((match) => resolve(baseDir, match));
1043
+ }
145
1044
  function applyIgnoreFile(filePath, baseDir, ignored) {
146
- for (const line of parseLines(filePath)) for (const match of globSync(toGlobPattern(line), { cwd: baseDir })) ignored.push(resolve(baseDir, match));
1045
+ for (const line of parseLines(filePath)) ignored.push(...resolveGlobMatches(line, baseDir));
147
1046
  }
148
- /**
149
- * Recursively find and apply .bxignore files in a directory tree.
150
- */
151
1047
  function collectIgnoreFilesRecursive(dir, ignored) {
152
1048
  const ignoreFile = join(dir, ".bxignore");
153
1049
  if (existsSync(ignoreFile)) applyIgnoreFile(ignoreFile, dir, ignored);
@@ -165,11 +1061,7 @@ function collectIgnoreFilesRecursive(dir, ignored) {
165
1061
  } catch {}
166
1062
  }
167
1063
  }
168
- /**
169
- * Parse ~/.bxignore for RW:/RO: prefixed lines and return allowed directories.
170
- * Lines without prefix are ignored here (handled by collectIgnoredPaths).
171
- * Also checks for deprecated ~/.bxallow and migrates its entries.
172
- */
1064
+ const ACCESS_PREFIX_RE = /^(RW|RO):(.+)$/i;
173
1065
  function parseHomeConfig(home, workDirs) {
174
1066
  const allowed = new Set(workDirs);
175
1067
  const readOnly = /* @__PURE__ */ new Set();
@@ -182,17 +1074,12 @@ function parseHomeConfig(home, workDirs) {
182
1074
  }
183
1075
  }
184
1076
  for (const line of parseLines(join(home, ".bxignore"))) {
185
- let prefix = "";
186
- let path = line;
187
- const match = line.match(/^(RW|RO):(.+)$/i);
188
- if (match) {
189
- prefix = match[1].toUpperCase();
190
- path = match[2].trim();
191
- }
192
- if (!prefix) continue;
193
- const absolute = resolve(home, path);
1077
+ const match = line.match(ACCESS_PREFIX_RE);
1078
+ if (!match) continue;
1079
+ const [, prefix, rawPath] = match;
1080
+ const absolute = resolve(home, rawPath.trim());
194
1081
  if (!existsSync(absolute) || !statSync(absolute).isDirectory()) continue;
195
- if (prefix === "RW") allowed.add(absolute);
1082
+ if (prefix.toUpperCase() === "RW") allowed.add(absolute);
196
1083
  else readOnly.add(absolute);
197
1084
  }
198
1085
  return {
@@ -200,10 +1087,12 @@ function parseHomeConfig(home, workDirs) {
200
1087
  readOnly
201
1088
  };
202
1089
  }
203
- /**
204
- * Recursively collect directories to block under parentDir.
205
- * Never blocks a parent of an allowed path — instead descends and blocks siblings.
206
- */
1090
+ function isAllowedOrAncestor(fullPath, allowedDirs) {
1091
+ if (allowedDirs.has(fullPath)) return "allowed";
1092
+ const prefix = fullPath + "/";
1093
+ for (const dir of allowedDirs) if (dir.startsWith(prefix)) return "ancestor";
1094
+ return "none";
1095
+ }
207
1096
  function collectBlockedDirs(parentDir, home, scriptDir, allowedDirs) {
208
1097
  const blocked = [];
209
1098
  for (const name of readdirSync(parentDir)) {
@@ -218,8 +1107,9 @@ function collectBlockedDirs(parentDir, home, scriptDir, allowedDirs) {
218
1107
  if (!isDir) continue;
219
1108
  if (parentDir === home && name === "Library") continue;
220
1109
  if (scriptDir.startsWith(fullPath + "/") || scriptDir === fullPath) continue;
221
- if (allowedDirs.has(fullPath)) continue;
222
- if ([...allowedDirs].some((d) => d.startsWith(fullPath + "/"))) {
1110
+ const status = isAllowedOrAncestor(fullPath, allowedDirs);
1111
+ if (status === "allowed") continue;
1112
+ if (status === "ancestor") {
223
1113
  blocked.push(...collectBlockedDirs(fullPath, home, scriptDir, allowedDirs));
224
1114
  continue;
225
1115
  }
@@ -227,82 +1117,92 @@ function collectBlockedDirs(parentDir, home, scriptDir, allowedDirs) {
227
1117
  }
228
1118
  return blocked;
229
1119
  }
230
- /**
231
- * Collect paths to deny from .bxignore files and built-in protected dotdirs.
232
- * Searches ~/.bxignore (skipping RW:/RO: lines) and recursively through all workdirs.
233
- */
234
1120
  function collectIgnoredPaths(home, workDirs) {
235
1121
  const ignored = PROTECTED_DOTDIRS.map((d) => join(home, d));
236
1122
  const globalIgnore = join(home, ".bxignore");
237
1123
  if (existsSync(globalIgnore)) {
238
- const denyLines = parseLines(globalIgnore).filter((l) => !l.match(/^(RW|RO):/i));
239
- for (const line of denyLines) for (const match of globSync(toGlobPattern(line), { cwd: home })) ignored.push(resolve(home, match));
1124
+ const denyLines = parseLines(globalIgnore).filter((l) => !ACCESS_PREFIX_RE.test(l));
1125
+ for (const line of denyLines) ignored.push(...resolveGlobMatches(line, home));
240
1126
  }
241
1127
  for (const workDir of workDirs) collectIgnoreFilesRecursive(workDir, ignored);
242
1128
  return ignored;
243
1129
  }
244
- /**
245
- * Generate the SBPL sandbox profile string.
246
- */
1130
+ function sbplEscape(path) {
1131
+ return path.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
1132
+ }
1133
+ function sbplSubpath(path) {
1134
+ return ` (subpath "${sbplEscape(path)}")`;
1135
+ }
1136
+ function sbplLiteral(path) {
1137
+ return ` (literal "${sbplEscape(path)}")`;
1138
+ }
1139
+ function sbplPathRule(path) {
1140
+ let isDir = false;
1141
+ try {
1142
+ isDir = existsSync(path) && statSync(path).isDirectory();
1143
+ } catch {}
1144
+ return isDir ? sbplSubpath(path) : sbplLiteral(path);
1145
+ }
1146
+ function sbplDenyBlock(comment, verb, rules) {
1147
+ if (rules.length === 0) return "";
1148
+ return `\n; ${comment}\n(deny ${verb}\n${rules.join("\n")}\n)\n`;
1149
+ }
247
1150
  function generateProfile(workDirs, blockedDirs, ignoredPaths, readOnlyDirs = []) {
248
- const denyRules = blockedDirs.map((dir) => ` (subpath "${dir}")`).join("\n");
249
- const ignoredRules = ignoredPaths.length > 0 ? `\n; Hidden paths from .bxignore\n(deny file*\n${ignoredPaths.map((p) => {
250
- let isDir = false;
251
- try {
252
- isDir = existsSync(p) && statSync(p).isDirectory();
253
- } catch {}
254
- return isDir ? ` (subpath "${p}")` : ` (literal "${p}")`;
255
- }).join("\n")}\n)\n` : "";
256
- const readOnlyRules = readOnlyDirs.length > 0 ? `\n; Read-only directories\n(deny file-write*\n${readOnlyDirs.map((dir) => ` (subpath "${dir}")`).join("\n")}\n)\n` : "";
1151
+ const blockedRules = sbplDenyBlock("Blocked directories (auto-generated from $HOME contents)", "file*", blockedDirs.map(sbplSubpath));
1152
+ const ignoredRules = sbplDenyBlock("Hidden paths from .bxignore", "file*", ignoredPaths.map(sbplPathRule));
1153
+ const readOnlyRules = sbplDenyBlock("Read-only directories", "file-write*", readOnlyDirs.map(sbplSubpath));
257
1154
  return `; Auto-generated sandbox profile
258
1155
  ; Working directories: ${workDirs.join(", ")}
259
1156
 
260
1157
  (version 1)
261
1158
  (allow default)
262
-
263
- ; Blocked directories (auto-generated from $HOME contents)
264
- (deny file*
265
- ${denyRules}
266
- )
267
- ${ignoredRules}${readOnlyRules}
1159
+ ${blockedRules}${ignoredRules}${readOnlyRules}
268
1160
  `;
269
1161
  }
270
1162
  //#endregion
271
1163
  //#region src/modes.ts
272
- const VSCODE_APP = "/Applications/Visual Studio Code.app/Contents/MacOS/Electron";
273
- /**
274
- * Prepare VSCode isolated profile if --profile-sandbox is set.
275
- */
1164
+ function isBuiltinMode(mode) {
1165
+ return BUILTIN_MODES.includes(mode);
1166
+ }
1167
+ function shouldPassWorkdirs(app, mode) {
1168
+ if (typeof app.passWorkdirs === "boolean") return app.passWorkdirs;
1169
+ return mode !== "xcode";
1170
+ }
1171
+ function appBundleFromPath(path) {
1172
+ if (path.endsWith(".app")) return path;
1173
+ const idx = path.indexOf(".app/");
1174
+ if (idx < 0) return null;
1175
+ return path.slice(0, idx + 4);
1176
+ }
1177
+ function executableFromBundle(bundlePath, app) {
1178
+ if (!bundlePath.endsWith(".app")) return bundlePath;
1179
+ if (app.binary) return join(bundlePath, app.binary);
1180
+ return join(bundlePath, "Contents", "MacOS", basename(bundlePath, ".app"));
1181
+ }
1182
+ const SANDBOX_KEY = "com.apple.security.app-sandbox";
1183
+ function hasAppSandboxEntitlement(entitlements) {
1184
+ if (new RegExp(`<key>\\s*${SANDBOX_KEY.replace(/\./g, "\\.")}\\s*</key>\\s*<true\\s*/>`, "i").test(entitlements)) return true;
1185
+ if (new RegExp(`<key>\\s*${SANDBOX_KEY.replace(/\./g, "\\.")}\\s*</key>\\s*<false\\s*/>`, "i").test(entitlements)) return false;
1186
+ return new RegExp(`${SANDBOX_KEY.replace(/\./g, "\\.")}\\s*[=:]\\s*(1|true)`, "i").test(entitlements);
1187
+ }
276
1188
  function setupVSCodeProfile(home) {
277
1189
  const dataDir = join(home, ".vscode-sandbox");
278
1190
  const globalExt = join(home, ".vscode", "extensions");
279
1191
  const localExt = join(dataDir, "extensions");
280
1192
  mkdirSync(dataDir, { recursive: true });
281
1193
  if (!existsSync(localExt) && existsSync(globalExt)) {
282
- console.error("sandbox: copying extensions from global install...");
1194
+ console.error(fmt.detail("copying extensions from global install..."));
283
1195
  cpSync(globalExt, localExt, { recursive: true });
284
1196
  }
285
1197
  }
286
- /**
287
- * Build the command + args to run inside the sandbox for the given mode.
288
- */
289
- function buildCommand(mode, workDirs, home, profileSandbox, execCmd) {
1198
+ function buildCommand(mode, workDirs, home, profileSandbox, appArgs, apps) {
1199
+ if (isBuiltinMode(mode)) return buildBuiltinCommand(mode, appArgs);
1200
+ return buildAppCommand(mode, workDirs, home, profileSandbox, appArgs, apps);
1201
+ }
1202
+ function buildBuiltinCommand(mode, appArgs) {
290
1203
  switch (mode) {
291
- case "code": {
292
- const dataDir = join(home, ".vscode-sandbox");
293
- const args = ["--no-sandbox"];
294
- if (profileSandbox) {
295
- args.push("--user-data-dir", join(dataDir, "data"));
296
- args.push("--extensions-dir", join(dataDir, "extensions"));
297
- }
298
- args.push(...workDirs);
299
- return {
300
- bin: VSCODE_APP,
301
- args
302
- };
303
- }
304
1204
  case "term": return {
305
- bin: process.env.SHELL ?? "/bin/zsh",
1205
+ bin: process$1.env.SHELL ?? "/bin/zsh",
306
1206
  args: ["-l"]
307
1207
  };
308
1208
  case "claude": return {
@@ -310,29 +1210,120 @@ function buildCommand(mode, workDirs, home, profileSandbox, execCmd) {
310
1210
  args: []
311
1211
  };
312
1212
  case "exec": return {
313
- bin: execCmd[0],
314
- args: execCmd.slice(1)
1213
+ bin: appArgs[0],
1214
+ args: appArgs.slice(1)
315
1215
  };
316
1216
  }
317
1217
  }
1218
+ function buildAppCommand(mode, workDirs, home, profileSandbox, appArgs, apps) {
1219
+ const app = apps[mode];
1220
+ if (!app) {
1221
+ console.error(`\n${fmt.error(`unknown mode "${mode}"`)}\n`);
1222
+ process$1.exit(1);
1223
+ }
1224
+ const resolvedPath = resolveAppPath(app);
1225
+ if (!resolvedPath) {
1226
+ console.error(`\n${fmt.error(`could not find application for "${mode}"`)}`);
1227
+ if (app.bundle) console.error(fmt.detail(`bundle: ${app.bundle}`));
1228
+ console.error(fmt.detail("hint: set an explicit path in ~/.bxconfig.toml\n"));
1229
+ process$1.exit(1);
1230
+ }
1231
+ const bin = executableFromBundle(resolvedPath, app);
1232
+ const args = [];
1233
+ if (mode === "code" && profileSandbox) {
1234
+ const dataDir = join(home, ".vscode-sandbox");
1235
+ args.push("--user-data-dir", join(dataDir, "data"));
1236
+ args.push("--extensions-dir", join(dataDir, "extensions"));
1237
+ }
1238
+ if (app.args) args.push(...app.args);
1239
+ if (appArgs.length > 0) args.push(...appArgs);
1240
+ if (shouldPassWorkdirs(app, mode)) args.push(...workDirs);
1241
+ return {
1242
+ bin,
1243
+ args
1244
+ };
1245
+ }
1246
+ function getActivationCommand(mode, apps) {
1247
+ if (isBuiltinMode(mode)) return null;
1248
+ const app = apps[mode];
1249
+ if (!app) return null;
1250
+ if (app.bundle) return {
1251
+ bin: "/usr/bin/open",
1252
+ args: ["-b", app.bundle]
1253
+ };
1254
+ const resolved = resolveAppPath(app);
1255
+ if (!resolved) return null;
1256
+ const bundlePath = appBundleFromPath(resolved);
1257
+ if (!bundlePath) return null;
1258
+ return {
1259
+ bin: "/usr/bin/open",
1260
+ args: ["-a", bundlePath]
1261
+ };
1262
+ }
1263
+ function getNestedSandboxWarning(mode, apps) {
1264
+ if (isBuiltinMode(mode)) return null;
1265
+ const app = apps[mode];
1266
+ if (!app) return null;
1267
+ const resolvedPath = resolveAppPath(app);
1268
+ if (!resolvedPath) return null;
1269
+ const target = appBundleFromPath(resolvedPath) ?? resolvedPath;
1270
+ try {
1271
+ if (hasAppSandboxEntitlement(execFileSync("codesign", [
1272
+ "-d",
1273
+ "--entitlements",
1274
+ "-",
1275
+ target
1276
+ ], {
1277
+ encoding: "utf-8",
1278
+ stdio: [
1279
+ "ignore",
1280
+ "pipe",
1281
+ "pipe"
1282
+ ]
1283
+ }))) return `⚠️ "${mode}" has Apple App Sandbox enabled — nested sandboxing may cause issues`;
1284
+ } catch {}
1285
+ return null;
1286
+ }
1287
+ function bringAppToFront(mode, apps) {
1288
+ const cmd = getActivationCommand(mode, apps);
1289
+ if (!cmd) return;
1290
+ setTimeout(() => {
1291
+ try {
1292
+ spawn(cmd.bin, cmd.args, {
1293
+ stdio: "ignore",
1294
+ detached: true
1295
+ }).unref();
1296
+ } catch {}
1297
+ }, 250);
1298
+ }
318
1299
  //#endregion
319
- //#region src/index.ts
320
- const __dirname = dirname(fileURLToPath(import.meta.url));
321
- const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : "dev";
322
- if (process.argv.includes("--version") || process.argv.includes("-v")) {
323
- console.log(`bx ${VERSION}`);
324
- process.exit(0);
1300
+ //#region src/help.ts
1301
+ function printHelp(version) {
1302
+ const HOME = process.env.HOME;
1303
+ const usageLines = buildUsageLines(HOME, version);
1304
+ console.log(usageLines);
1305
+ console.log(OPTIONS_TEXT);
325
1306
  }
326
- if (process.argv.includes("--help") || process.argv.includes("-h")) {
327
- console.log(`bx ${VERSION} — launch apps in a macOS sandbox
1307
+ function buildUsageLines(home, version) {
1308
+ return `${`bx ${version} — launch apps in a macOS sandbox`}
328
1309
 
329
1310
  Usage:
330
1311
  bx [workdir...] VSCode (default)
331
- bx code [workdir...] VSCode
332
- bx term [workdir...] sandboxed login shell
1312
+ ${home ? buildAppUsageLines(home) : ""} bx term [workdir...] sandboxed login shell
333
1313
  bx claude [workdir...] Claude Code CLI
334
- bx exec [workdir...] -- command [args...] arbitrary command
335
-
1314
+ bx exec [workdir...] -- command [args...] arbitrary command`;
1315
+ }
1316
+ function buildAppUsageLines(home) {
1317
+ const apps = getAvailableApps(loadConfig(home));
1318
+ const names = Object.keys(apps);
1319
+ const maxLen = Math.max(...names.map((n) => n.length));
1320
+ const colWidth = 41;
1321
+ return names.map((name) => {
1322
+ const left = ` bx ${name} [workdir...] [-- app-args...]`;
1323
+ return `${left}${" ".repeat(Math.max(1, colWidth + maxLen - name.length - left.length + 2))}${name} (app)`;
1324
+ }).join("\n") + "\n";
1325
+ }
1326
+ const OPTIONS_TEXT = `
336
1327
  Options:
337
1328
  --dry show what will be protected, don't launch
338
1329
  --verbose print the generated sandbox profile
@@ -341,118 +1332,195 @@ Options:
341
1332
  -h, --help show this help
342
1333
 
343
1334
  Configuration:
1335
+ ~/.bxconfig.toml app definitions (TOML):
1336
+ [apps.name] add a new app
1337
+ bundle = "..." macOS bundle ID (auto-discovery)
1338
+ binary = "..." relative path in .app bundle
1339
+ path = "..." explicit executable path
1340
+ args = ["..."] extra arguments
1341
+ passWorkdirs = true|false pass workdirs as launch args
1342
+ built-in apps (code, xcode) can be overridden
344
1343
  ~/.bxignore sandbox rules (one per line):
345
1344
  path block access (deny)
346
1345
  rw:path allow read-write access
347
1346
  ro:path allow read-only access
348
1347
  <workdir>/.bxignore blocked paths in project (.gitignore-style matching)
349
1348
 
350
- https://github.com/holtwick/bx-mac`);
351
- process.exit(0);
1349
+ https://github.com/holtwick/bx-mac`;
1350
+ //#endregion
1351
+ //#region src/drytree.ts
1352
+ const RED = "\x1B[31m";
1353
+ const GREEN = "\x1B[32m";
1354
+ const YELLOW = "\x1B[33m";
1355
+ const CYAN = "\x1B[36m";
1356
+ const DIM = "\x1B[2m";
1357
+ const RESET = "\x1B[0m";
1358
+ function kindIcon(kind) {
1359
+ switch (kind) {
1360
+ case "read-only": return `${YELLOW}◉${RESET}`;
1361
+ case "workdir": return `${GREEN}✔${RESET}`;
1362
+ default: return `${RED}✖${RESET}`;
1363
+ }
1364
+ }
1365
+ function insertPath(root, home, absPath, kind, isDir) {
1366
+ const rel = absPath.startsWith(home + "/") ? absPath.slice(home.length + 1) : absPath;
1367
+ let node = root;
1368
+ for (const part of rel.split("/")) {
1369
+ if (!node.children.has(part)) node.children.set(part, { children: /* @__PURE__ */ new Map() });
1370
+ node = node.children.get(part);
1371
+ }
1372
+ node.kind = kind;
1373
+ node.isDir = isDir;
1374
+ }
1375
+ function isDirectory(path) {
1376
+ try {
1377
+ return statSync(path).isDirectory();
1378
+ } catch {
1379
+ return path.slice(path.lastIndexOf("/") + 1).startsWith(".");
1380
+ }
1381
+ }
1382
+ function printNode(node, prefix) {
1383
+ const entries = [...node.children.entries()].sort((a, b) => a[0].localeCompare(b[0]));
1384
+ for (let i = 0; i < entries.length; i++) {
1385
+ const [name, child] = entries[i];
1386
+ const isLast = i === entries.length - 1;
1387
+ const connector = isLast ? "└── " : "├── ";
1388
+ const continuation = isLast ? " " : "│ ";
1389
+ if (child.kind) {
1390
+ const suffix = child.isDir ? "/" : "";
1391
+ console.log(`${prefix}${connector}${kindIcon(child.kind)} ${name}${suffix} ${DIM}${child.kind}${RESET}`);
1392
+ } else console.log(`${prefix}${connector}${CYAN}${name}/${RESET}`);
1393
+ if (child.children.size > 0) printNode(child, prefix + continuation);
1394
+ }
1395
+ }
1396
+ function printDryRunTree({ home, blockedDirs, ignoredPaths, readOnlyDirs, workDirs }) {
1397
+ const root = { children: /* @__PURE__ */ new Map() };
1398
+ for (const dir of blockedDirs) insertPath(root, home, dir, "blocked", true);
1399
+ for (const path of ignoredPaths) insertPath(root, home, path, "ignored", isDirectory(path));
1400
+ for (const dir of readOnlyDirs) insertPath(root, home, dir, "read-only", true);
1401
+ for (const dir of workDirs) insertPath(root, home, dir, "workdir", true);
1402
+ console.log(`\n${CYAN}~/${RESET}`);
1403
+ printNode(root, "");
1404
+ console.log(`\n${RED}✖${RESET} = denied ${YELLOW}◉${RESET} = read-only ${GREEN}✔${RESET} = read-write\n`);
1405
+ }
1406
+ //#endregion
1407
+ //#region src/index.ts
1408
+ const __dirname = dirname(fileURLToPath(import.meta.url));
1409
+ const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : "dev";
1410
+ if (process$1.argv.includes("--version") || process$1.argv.includes("-v")) {
1411
+ console.log(`bx ${VERSION}`);
1412
+ process$1.exit(0);
1413
+ }
1414
+ if (process$1.argv.includes("--help") || process$1.argv.includes("-h")) {
1415
+ printHelp(VERSION);
1416
+ process$1.exit(0);
352
1417
  }
353
- const { mode, workArgs, verbose, dry, profileSandbox, execCmd, implicit } = parseArgs();
354
- const HOME = process.env.HOME;
355
- const WORK_DIRS = workArgs.map((a) => resolve(a));
356
- if (implicit && !dry) {
1418
+ if (!process$1.env.HOME) {
1419
+ console.error(`\n${fmt.error("$HOME environment variable is not set")}\n`);
1420
+ process$1.exit(1);
1421
+ }
1422
+ const HOME = process$1.env.HOME;
1423
+ async function main() {
1424
+ const apps = getAvailableApps(loadConfig(HOME));
1425
+ const { mode, workArgs, verbose, dry, profileSandbox, appArgs, implicit } = parseArgs(getValidModes(apps));
1426
+ const app = apps[mode];
1427
+ const workDirs = (implicit && app?.workdirs?.length ? app.workdirs : workArgs).map((a) => resolve(a.replace(/^~\//, HOME + "/")));
1428
+ if (implicit && !app?.workdirs?.length) {
1429
+ if (workDirs.some((d) => d === HOME)) {
1430
+ console.error(`\n${fmt.error("no working directory specified and current directory is $HOME")}\n`);
1431
+ console.error(fmt.detail(`Usage: bx ${mode} <workdir>`));
1432
+ console.error(fmt.detail(`Config: set default workdirs in ~/.bxconfig.toml:\n`));
1433
+ console.error(fmt.detail(`[apps.${mode}]`));
1434
+ console.error(fmt.detail(`workdirs = ["~/work/my-project"]\n`));
1435
+ process$1.exit(1);
1436
+ }
1437
+ if (!dry) await confirmLaunch(workDirs[0], mode);
1438
+ }
1439
+ if (!dry) {
1440
+ checkOwnSandbox();
1441
+ checkVSCodeTerminal();
1442
+ checkExternalSandbox();
1443
+ }
1444
+ checkWorkDirs(workDirs, HOME);
1445
+ await checkAppAlreadyRunning(mode, apps);
1446
+ if (mode === "code" && profileSandbox) setupVSCodeProfile(HOME);
1447
+ const { allowed, readOnly } = parseHomeConfig(HOME, workDirs);
1448
+ const blockedDirs = collectBlockedDirs(HOME, HOME, __dirname, new Set([...allowed, ...readOnly]));
1449
+ const ignoredPaths = collectIgnoredPaths(HOME, workDirs);
1450
+ printPolicySummary(mode, workDirs, blockedDirs, ignoredPaths, readOnly);
1451
+ const profile = generateProfile(workDirs, blockedDirs, ignoredPaths, [...readOnly]);
1452
+ if (verbose) {
1453
+ console.error("\n--- Generated sandbox profile ---");
1454
+ console.error(profile);
1455
+ console.error("--- End of profile ---\n");
1456
+ }
1457
+ if (dry) {
1458
+ printDryRunTree({
1459
+ home: HOME,
1460
+ blockedDirs,
1461
+ ignoredPaths,
1462
+ readOnlyDirs: readOnly,
1463
+ workDirs
1464
+ });
1465
+ process$1.exit(0);
1466
+ }
1467
+ const profilePath = join("/tmp", `bx-${process$1.pid}.sb`);
1468
+ writeFileSync(profilePath, profile);
1469
+ const cmd = buildCommand(mode, workDirs, HOME, profileSandbox, appArgs, apps);
1470
+ const nestedSandboxWarning = getNestedSandboxWarning(mode, apps);
1471
+ if (nestedSandboxWarning) console.error(fmt.detail(nestedSandboxWarning));
1472
+ if (verbose) printLaunchDetails(cmd, workDirs[0], getActivationCommand(mode, apps));
1473
+ console.error("");
1474
+ const child = spawn("sandbox-exec", [
1475
+ "-f",
1476
+ profilePath,
1477
+ "-D",
1478
+ `HOME=${HOME}`,
1479
+ "-D",
1480
+ `WORK=${workDirs[0]}`,
1481
+ cmd.bin,
1482
+ ...cmd.args
1483
+ ], {
1484
+ cwd: workDirs[0],
1485
+ stdio: "inherit",
1486
+ env: {
1487
+ ...process$1.env,
1488
+ CODEBOX_SANDBOX: "1"
1489
+ }
1490
+ });
1491
+ bringAppToFront(mode, apps);
1492
+ child.on("close", (code) => {
1493
+ rmSync(profilePath, { force: true });
1494
+ process$1.exit(code ?? 0);
1495
+ });
1496
+ }
1497
+ async function confirmLaunch(workDir, mode) {
357
1498
  const rl = createInterface({
358
- input: process.stdin,
359
- output: process.stderr
1499
+ input: process$1.stdin,
1500
+ output: process$1.stderr
360
1501
  });
361
1502
  const answer = await new Promise((res) => {
362
- rl.question(`sandbox: open ${WORK_DIRS[0]} in VSCode? [Y/n] `, res);
1503
+ rl.question(`${fmt.info(`open ${workDir} in ${mode}?`)} [Y/n] `, res);
363
1504
  });
364
1505
  rl.close();
365
- if (answer && !answer.match(/^y(es)?$/i)) process.exit(0);
366
- }
367
- if (!dry) {
368
- checkOwnSandbox();
369
- checkVSCodeTerminal();
370
- checkExternalSandbox();
371
- }
372
- checkWorkDirs(WORK_DIRS, HOME);
373
- if (mode === "code" && profileSandbox) setupVSCodeProfile(HOME);
374
- const { allowed, readOnly } = parseHomeConfig(HOME, WORK_DIRS);
375
- const blockedDirs = collectBlockedDirs(HOME, HOME, __dirname, new Set([...allowed, ...readOnly]));
376
- const ignoredPaths = collectIgnoredPaths(HOME, WORK_DIRS);
377
- const extraIgnored = ignoredPaths.length - PROTECTED_DOTDIRS.length;
378
- if (extraIgnored > 0) console.error(`sandbox: .bxignore hides ${extraIgnored} extra path(s)`);
379
- if (readOnly.size > 0) console.error(`sandbox: ${readOnly.size} read-only director${readOnly.size === 1 ? "y" : "ies"}`);
380
- const profile = generateProfile(WORK_DIRS, blockedDirs, ignoredPaths, [...readOnly]);
381
- const dirLabel = WORK_DIRS.length === 1 ? WORK_DIRS[0] : `${WORK_DIRS.length} directories`;
382
- console.error(`sandbox: ${mode} mode, working directory: ${dirLabel}`);
383
- if (verbose) {
384
- console.error("\n--- Generated sandbox profile ---");
385
- console.error(profile);
386
- console.error("--- End of profile ---\n");
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);
436
- const cmd = buildCommand(mode, WORK_DIRS, HOME, profileSandbox, execCmd);
437
- spawn("sandbox-exec", [
438
- "-f",
439
- profilePath,
440
- "-D",
441
- `HOME=${HOME}`,
442
- "-D",
443
- `WORK=${WORK_DIRS[0]}`,
444
- cmd.bin,
445
- ...cmd.args
446
- ], {
447
- cwd: WORK_DIRS[0],
448
- stdio: "inherit",
449
- env: {
450
- ...process.env,
451
- CODEBOX_SANDBOX: "1"
452
- }
453
- }).on("close", (code) => {
454
- rmSync(profilePath, { force: true });
455
- process.exit(code ?? 0);
456
- });
1506
+ if (answer && !answer.match(/^y(es)?$/i)) process$1.exit(0);
1507
+ }
1508
+ function printPolicySummary(mode, workDirs, blockedDirs, ignoredPaths, readOnly) {
1509
+ const dirLabel = workDirs.length === 1 ? workDirs[0] : `${workDirs.length} directories`;
1510
+ console.error(`\n${fmt.info(`${mode} → ${dirLabel}`)}`);
1511
+ const parts = [`${blockedDirs.length} blocked`, `${ignoredPaths.length} hidden`];
1512
+ if (readOnly.size > 0) parts.push(`${readOnly.size} read-only`);
1513
+ const extraIgnored = ignoredPaths.length - PROTECTED_DOTDIRS.length;
1514
+ if (extraIgnored > 0) parts.push(`${extraIgnored} from .bxignore`);
1515
+ console.error(fmt.detail(parts.join(" · ")));
1516
+ }
1517
+ function printLaunchDetails(cmd, cwd, activationCmd) {
1518
+ const quote = (a) => JSON.stringify(a);
1519
+ console.error(fmt.detail(`bin: ${cmd.bin}`));
1520
+ console.error(fmt.detail(`args: ${cmd.args.map(quote).join(" ") || "(none)"}`));
1521
+ console.error(fmt.detail(`cwd: ${cwd}`));
1522
+ if (activationCmd) console.error(fmt.detail(`focus: ${activationCmd.bin} ${activationCmd.args.map(quote).join(" ")}`));
1523
+ }
1524
+ main();
457
1525
  //#endregion
458
1526
  export {};