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/README.md +96 -6
- package/dist/bx-native +0 -0
- package/dist/bx.js +1308 -240
- package/package.json +2 -1
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 = `[36mbx${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("
|
|
16
|
-
console.error("
|
|
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("
|
|
26
|
-
console.error("
|
|
27
|
-
console.error("
|
|
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("
|
|
37
|
-
console.error("
|
|
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(`
|
|
42
|
-
console.error("
|
|
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("
|
|
63
|
-
console.error("
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
|
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
|
|
89
|
-
if (beforeDash.length > 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)
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
108
|
-
implicit:
|
|
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
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
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
|
-
|
|
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))
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
const
|
|
188
|
-
|
|
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
|
-
|
|
205
|
-
|
|
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
|
-
|
|
222
|
-
if (
|
|
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) => !
|
|
239
|
-
for (const line of denyLines)
|
|
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
|
-
|
|
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
|
|
249
|
-
const ignoredRules =
|
|
250
|
-
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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("
|
|
1194
|
+
console.error(fmt.detail("copying extensions from global install..."));
|
|
283
1195
|
cpSync(globalExt, localExt, { recursive: true });
|
|
284
1196
|
}
|
|
285
1197
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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:
|
|
314
|
-
args:
|
|
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/
|
|
320
|
-
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
console.log(
|
|
324
|
-
|
|
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
|
-
|
|
327
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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(`
|
|
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
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
if (
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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 {};
|