@pablozaiden/devbox 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +105 -0
- package/dist/devbox.js +1802 -0
- package/package.json +49 -0
package/dist/devbox.js
ADDED
|
@@ -0,0 +1,1802 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { realpath } from "node:fs/promises";
|
|
5
|
+
import path3 from "node:path";
|
|
6
|
+
|
|
7
|
+
// src/core.ts
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
9
|
+
import { existsSync } from "node:fs";
|
|
10
|
+
import { access, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
11
|
+
import os from "node:os";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
|
|
14
|
+
// node_modules/jsonc-parser/lib/esm/impl/scanner.js
|
|
15
|
+
function createScanner(text, ignoreTrivia = false) {
|
|
16
|
+
const len = text.length;
|
|
17
|
+
let pos = 0, value = "", tokenOffset = 0, token = 16, lineNumber = 0, lineStartOffset = 0, tokenLineStartOffset = 0, prevTokenLineStartOffset = 0, scanError = 0;
|
|
18
|
+
function scanHexDigits(count, exact) {
|
|
19
|
+
let digits = 0;
|
|
20
|
+
let value2 = 0;
|
|
21
|
+
while (digits < count || !exact) {
|
|
22
|
+
let ch = text.charCodeAt(pos);
|
|
23
|
+
if (ch >= 48 && ch <= 57) {
|
|
24
|
+
value2 = value2 * 16 + ch - 48;
|
|
25
|
+
} else if (ch >= 65 && ch <= 70) {
|
|
26
|
+
value2 = value2 * 16 + ch - 65 + 10;
|
|
27
|
+
} else if (ch >= 97 && ch <= 102) {
|
|
28
|
+
value2 = value2 * 16 + ch - 97 + 10;
|
|
29
|
+
} else {
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
pos++;
|
|
33
|
+
digits++;
|
|
34
|
+
}
|
|
35
|
+
if (digits < count) {
|
|
36
|
+
value2 = -1;
|
|
37
|
+
}
|
|
38
|
+
return value2;
|
|
39
|
+
}
|
|
40
|
+
function setPosition(newPosition) {
|
|
41
|
+
pos = newPosition;
|
|
42
|
+
value = "";
|
|
43
|
+
tokenOffset = 0;
|
|
44
|
+
token = 16;
|
|
45
|
+
scanError = 0;
|
|
46
|
+
}
|
|
47
|
+
function scanNumber() {
|
|
48
|
+
let start = pos;
|
|
49
|
+
if (text.charCodeAt(pos) === 48) {
|
|
50
|
+
pos++;
|
|
51
|
+
} else {
|
|
52
|
+
pos++;
|
|
53
|
+
while (pos < text.length && isDigit(text.charCodeAt(pos))) {
|
|
54
|
+
pos++;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (pos < text.length && text.charCodeAt(pos) === 46) {
|
|
58
|
+
pos++;
|
|
59
|
+
if (pos < text.length && isDigit(text.charCodeAt(pos))) {
|
|
60
|
+
pos++;
|
|
61
|
+
while (pos < text.length && isDigit(text.charCodeAt(pos))) {
|
|
62
|
+
pos++;
|
|
63
|
+
}
|
|
64
|
+
} else {
|
|
65
|
+
scanError = 3;
|
|
66
|
+
return text.substring(start, pos);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
let end = pos;
|
|
70
|
+
if (pos < text.length && (text.charCodeAt(pos) === 69 || text.charCodeAt(pos) === 101)) {
|
|
71
|
+
pos++;
|
|
72
|
+
if (pos < text.length && text.charCodeAt(pos) === 43 || text.charCodeAt(pos) === 45) {
|
|
73
|
+
pos++;
|
|
74
|
+
}
|
|
75
|
+
if (pos < text.length && isDigit(text.charCodeAt(pos))) {
|
|
76
|
+
pos++;
|
|
77
|
+
while (pos < text.length && isDigit(text.charCodeAt(pos))) {
|
|
78
|
+
pos++;
|
|
79
|
+
}
|
|
80
|
+
end = pos;
|
|
81
|
+
} else {
|
|
82
|
+
scanError = 3;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return text.substring(start, end);
|
|
86
|
+
}
|
|
87
|
+
function scanString() {
|
|
88
|
+
let result = "", start = pos;
|
|
89
|
+
while (true) {
|
|
90
|
+
if (pos >= len) {
|
|
91
|
+
result += text.substring(start, pos);
|
|
92
|
+
scanError = 2;
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
const ch = text.charCodeAt(pos);
|
|
96
|
+
if (ch === 34) {
|
|
97
|
+
result += text.substring(start, pos);
|
|
98
|
+
pos++;
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
if (ch === 92) {
|
|
102
|
+
result += text.substring(start, pos);
|
|
103
|
+
pos++;
|
|
104
|
+
if (pos >= len) {
|
|
105
|
+
scanError = 2;
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
const ch2 = text.charCodeAt(pos++);
|
|
109
|
+
switch (ch2) {
|
|
110
|
+
case 34:
|
|
111
|
+
result += '"';
|
|
112
|
+
break;
|
|
113
|
+
case 92:
|
|
114
|
+
result += "\\";
|
|
115
|
+
break;
|
|
116
|
+
case 47:
|
|
117
|
+
result += "/";
|
|
118
|
+
break;
|
|
119
|
+
case 98:
|
|
120
|
+
result += "\b";
|
|
121
|
+
break;
|
|
122
|
+
case 102:
|
|
123
|
+
result += "\f";
|
|
124
|
+
break;
|
|
125
|
+
case 110:
|
|
126
|
+
result += `
|
|
127
|
+
`;
|
|
128
|
+
break;
|
|
129
|
+
case 114:
|
|
130
|
+
result += "\r";
|
|
131
|
+
break;
|
|
132
|
+
case 116:
|
|
133
|
+
result += "\t";
|
|
134
|
+
break;
|
|
135
|
+
case 117:
|
|
136
|
+
const ch3 = scanHexDigits(4, true);
|
|
137
|
+
if (ch3 >= 0) {
|
|
138
|
+
result += String.fromCharCode(ch3);
|
|
139
|
+
} else {
|
|
140
|
+
scanError = 4;
|
|
141
|
+
}
|
|
142
|
+
break;
|
|
143
|
+
default:
|
|
144
|
+
scanError = 5;
|
|
145
|
+
}
|
|
146
|
+
start = pos;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (ch >= 0 && ch <= 31) {
|
|
150
|
+
if (isLineBreak(ch)) {
|
|
151
|
+
result += text.substring(start, pos);
|
|
152
|
+
scanError = 2;
|
|
153
|
+
break;
|
|
154
|
+
} else {
|
|
155
|
+
scanError = 6;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
pos++;
|
|
159
|
+
}
|
|
160
|
+
return result;
|
|
161
|
+
}
|
|
162
|
+
function scanNext() {
|
|
163
|
+
value = "";
|
|
164
|
+
scanError = 0;
|
|
165
|
+
tokenOffset = pos;
|
|
166
|
+
lineStartOffset = lineNumber;
|
|
167
|
+
prevTokenLineStartOffset = tokenLineStartOffset;
|
|
168
|
+
if (pos >= len) {
|
|
169
|
+
tokenOffset = len;
|
|
170
|
+
return token = 17;
|
|
171
|
+
}
|
|
172
|
+
let code = text.charCodeAt(pos);
|
|
173
|
+
if (isWhiteSpace(code)) {
|
|
174
|
+
do {
|
|
175
|
+
pos++;
|
|
176
|
+
value += String.fromCharCode(code);
|
|
177
|
+
code = text.charCodeAt(pos);
|
|
178
|
+
} while (isWhiteSpace(code));
|
|
179
|
+
return token = 15;
|
|
180
|
+
}
|
|
181
|
+
if (isLineBreak(code)) {
|
|
182
|
+
pos++;
|
|
183
|
+
value += String.fromCharCode(code);
|
|
184
|
+
if (code === 13 && text.charCodeAt(pos) === 10) {
|
|
185
|
+
pos++;
|
|
186
|
+
value += `
|
|
187
|
+
`;
|
|
188
|
+
}
|
|
189
|
+
lineNumber++;
|
|
190
|
+
tokenLineStartOffset = pos;
|
|
191
|
+
return token = 14;
|
|
192
|
+
}
|
|
193
|
+
switch (code) {
|
|
194
|
+
case 123:
|
|
195
|
+
pos++;
|
|
196
|
+
return token = 1;
|
|
197
|
+
case 125:
|
|
198
|
+
pos++;
|
|
199
|
+
return token = 2;
|
|
200
|
+
case 91:
|
|
201
|
+
pos++;
|
|
202
|
+
return token = 3;
|
|
203
|
+
case 93:
|
|
204
|
+
pos++;
|
|
205
|
+
return token = 4;
|
|
206
|
+
case 58:
|
|
207
|
+
pos++;
|
|
208
|
+
return token = 6;
|
|
209
|
+
case 44:
|
|
210
|
+
pos++;
|
|
211
|
+
return token = 5;
|
|
212
|
+
case 34:
|
|
213
|
+
pos++;
|
|
214
|
+
value = scanString();
|
|
215
|
+
return token = 10;
|
|
216
|
+
case 47:
|
|
217
|
+
const start = pos - 1;
|
|
218
|
+
if (text.charCodeAt(pos + 1) === 47) {
|
|
219
|
+
pos += 2;
|
|
220
|
+
while (pos < len) {
|
|
221
|
+
if (isLineBreak(text.charCodeAt(pos))) {
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
pos++;
|
|
225
|
+
}
|
|
226
|
+
value = text.substring(start, pos);
|
|
227
|
+
return token = 12;
|
|
228
|
+
}
|
|
229
|
+
if (text.charCodeAt(pos + 1) === 42) {
|
|
230
|
+
pos += 2;
|
|
231
|
+
const safeLength = len - 1;
|
|
232
|
+
let commentClosed = false;
|
|
233
|
+
while (pos < safeLength) {
|
|
234
|
+
const ch = text.charCodeAt(pos);
|
|
235
|
+
if (ch === 42 && text.charCodeAt(pos + 1) === 47) {
|
|
236
|
+
pos += 2;
|
|
237
|
+
commentClosed = true;
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
pos++;
|
|
241
|
+
if (isLineBreak(ch)) {
|
|
242
|
+
if (ch === 13 && text.charCodeAt(pos) === 10) {
|
|
243
|
+
pos++;
|
|
244
|
+
}
|
|
245
|
+
lineNumber++;
|
|
246
|
+
tokenLineStartOffset = pos;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (!commentClosed) {
|
|
250
|
+
pos++;
|
|
251
|
+
scanError = 1;
|
|
252
|
+
}
|
|
253
|
+
value = text.substring(start, pos);
|
|
254
|
+
return token = 13;
|
|
255
|
+
}
|
|
256
|
+
value += String.fromCharCode(code);
|
|
257
|
+
pos++;
|
|
258
|
+
return token = 16;
|
|
259
|
+
case 45:
|
|
260
|
+
value += String.fromCharCode(code);
|
|
261
|
+
pos++;
|
|
262
|
+
if (pos === len || !isDigit(text.charCodeAt(pos))) {
|
|
263
|
+
return token = 16;
|
|
264
|
+
}
|
|
265
|
+
case 48:
|
|
266
|
+
case 49:
|
|
267
|
+
case 50:
|
|
268
|
+
case 51:
|
|
269
|
+
case 52:
|
|
270
|
+
case 53:
|
|
271
|
+
case 54:
|
|
272
|
+
case 55:
|
|
273
|
+
case 56:
|
|
274
|
+
case 57:
|
|
275
|
+
value += scanNumber();
|
|
276
|
+
return token = 11;
|
|
277
|
+
default:
|
|
278
|
+
while (pos < len && isUnknownContentCharacter(code)) {
|
|
279
|
+
pos++;
|
|
280
|
+
code = text.charCodeAt(pos);
|
|
281
|
+
}
|
|
282
|
+
if (tokenOffset !== pos) {
|
|
283
|
+
value = text.substring(tokenOffset, pos);
|
|
284
|
+
switch (value) {
|
|
285
|
+
case "true":
|
|
286
|
+
return token = 8;
|
|
287
|
+
case "false":
|
|
288
|
+
return token = 9;
|
|
289
|
+
case "null":
|
|
290
|
+
return token = 7;
|
|
291
|
+
}
|
|
292
|
+
return token = 16;
|
|
293
|
+
}
|
|
294
|
+
value += String.fromCharCode(code);
|
|
295
|
+
pos++;
|
|
296
|
+
return token = 16;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
function isUnknownContentCharacter(code) {
|
|
300
|
+
if (isWhiteSpace(code) || isLineBreak(code)) {
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
switch (code) {
|
|
304
|
+
case 125:
|
|
305
|
+
case 93:
|
|
306
|
+
case 123:
|
|
307
|
+
case 91:
|
|
308
|
+
case 34:
|
|
309
|
+
case 58:
|
|
310
|
+
case 44:
|
|
311
|
+
case 47:
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
316
|
+
function scanNextNonTrivia() {
|
|
317
|
+
let result;
|
|
318
|
+
do {
|
|
319
|
+
result = scanNext();
|
|
320
|
+
} while (result >= 12 && result <= 15);
|
|
321
|
+
return result;
|
|
322
|
+
}
|
|
323
|
+
return {
|
|
324
|
+
setPosition,
|
|
325
|
+
getPosition: () => pos,
|
|
326
|
+
scan: ignoreTrivia ? scanNextNonTrivia : scanNext,
|
|
327
|
+
getToken: () => token,
|
|
328
|
+
getTokenValue: () => value,
|
|
329
|
+
getTokenOffset: () => tokenOffset,
|
|
330
|
+
getTokenLength: () => pos - tokenOffset,
|
|
331
|
+
getTokenStartLine: () => lineStartOffset,
|
|
332
|
+
getTokenStartCharacter: () => tokenOffset - prevTokenLineStartOffset,
|
|
333
|
+
getTokenError: () => scanError
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
function isWhiteSpace(ch) {
|
|
337
|
+
return ch === 32 || ch === 9;
|
|
338
|
+
}
|
|
339
|
+
function isLineBreak(ch) {
|
|
340
|
+
return ch === 10 || ch === 13;
|
|
341
|
+
}
|
|
342
|
+
function isDigit(ch) {
|
|
343
|
+
return ch >= 48 && ch <= 57;
|
|
344
|
+
}
|
|
345
|
+
var CharacterCodes;
|
|
346
|
+
(function(CharacterCodes2) {
|
|
347
|
+
CharacterCodes2[CharacterCodes2["lineFeed"] = 10] = "lineFeed";
|
|
348
|
+
CharacterCodes2[CharacterCodes2["carriageReturn"] = 13] = "carriageReturn";
|
|
349
|
+
CharacterCodes2[CharacterCodes2["space"] = 32] = "space";
|
|
350
|
+
CharacterCodes2[CharacterCodes2["_0"] = 48] = "_0";
|
|
351
|
+
CharacterCodes2[CharacterCodes2["_1"] = 49] = "_1";
|
|
352
|
+
CharacterCodes2[CharacterCodes2["_2"] = 50] = "_2";
|
|
353
|
+
CharacterCodes2[CharacterCodes2["_3"] = 51] = "_3";
|
|
354
|
+
CharacterCodes2[CharacterCodes2["_4"] = 52] = "_4";
|
|
355
|
+
CharacterCodes2[CharacterCodes2["_5"] = 53] = "_5";
|
|
356
|
+
CharacterCodes2[CharacterCodes2["_6"] = 54] = "_6";
|
|
357
|
+
CharacterCodes2[CharacterCodes2["_7"] = 55] = "_7";
|
|
358
|
+
CharacterCodes2[CharacterCodes2["_8"] = 56] = "_8";
|
|
359
|
+
CharacterCodes2[CharacterCodes2["_9"] = 57] = "_9";
|
|
360
|
+
CharacterCodes2[CharacterCodes2["a"] = 97] = "a";
|
|
361
|
+
CharacterCodes2[CharacterCodes2["b"] = 98] = "b";
|
|
362
|
+
CharacterCodes2[CharacterCodes2["c"] = 99] = "c";
|
|
363
|
+
CharacterCodes2[CharacterCodes2["d"] = 100] = "d";
|
|
364
|
+
CharacterCodes2[CharacterCodes2["e"] = 101] = "e";
|
|
365
|
+
CharacterCodes2[CharacterCodes2["f"] = 102] = "f";
|
|
366
|
+
CharacterCodes2[CharacterCodes2["g"] = 103] = "g";
|
|
367
|
+
CharacterCodes2[CharacterCodes2["h"] = 104] = "h";
|
|
368
|
+
CharacterCodes2[CharacterCodes2["i"] = 105] = "i";
|
|
369
|
+
CharacterCodes2[CharacterCodes2["j"] = 106] = "j";
|
|
370
|
+
CharacterCodes2[CharacterCodes2["k"] = 107] = "k";
|
|
371
|
+
CharacterCodes2[CharacterCodes2["l"] = 108] = "l";
|
|
372
|
+
CharacterCodes2[CharacterCodes2["m"] = 109] = "m";
|
|
373
|
+
CharacterCodes2[CharacterCodes2["n"] = 110] = "n";
|
|
374
|
+
CharacterCodes2[CharacterCodes2["o"] = 111] = "o";
|
|
375
|
+
CharacterCodes2[CharacterCodes2["p"] = 112] = "p";
|
|
376
|
+
CharacterCodes2[CharacterCodes2["q"] = 113] = "q";
|
|
377
|
+
CharacterCodes2[CharacterCodes2["r"] = 114] = "r";
|
|
378
|
+
CharacterCodes2[CharacterCodes2["s"] = 115] = "s";
|
|
379
|
+
CharacterCodes2[CharacterCodes2["t"] = 116] = "t";
|
|
380
|
+
CharacterCodes2[CharacterCodes2["u"] = 117] = "u";
|
|
381
|
+
CharacterCodes2[CharacterCodes2["v"] = 118] = "v";
|
|
382
|
+
CharacterCodes2[CharacterCodes2["w"] = 119] = "w";
|
|
383
|
+
CharacterCodes2[CharacterCodes2["x"] = 120] = "x";
|
|
384
|
+
CharacterCodes2[CharacterCodes2["y"] = 121] = "y";
|
|
385
|
+
CharacterCodes2[CharacterCodes2["z"] = 122] = "z";
|
|
386
|
+
CharacterCodes2[CharacterCodes2["A"] = 65] = "A";
|
|
387
|
+
CharacterCodes2[CharacterCodes2["B"] = 66] = "B";
|
|
388
|
+
CharacterCodes2[CharacterCodes2["C"] = 67] = "C";
|
|
389
|
+
CharacterCodes2[CharacterCodes2["D"] = 68] = "D";
|
|
390
|
+
CharacterCodes2[CharacterCodes2["E"] = 69] = "E";
|
|
391
|
+
CharacterCodes2[CharacterCodes2["F"] = 70] = "F";
|
|
392
|
+
CharacterCodes2[CharacterCodes2["G"] = 71] = "G";
|
|
393
|
+
CharacterCodes2[CharacterCodes2["H"] = 72] = "H";
|
|
394
|
+
CharacterCodes2[CharacterCodes2["I"] = 73] = "I";
|
|
395
|
+
CharacterCodes2[CharacterCodes2["J"] = 74] = "J";
|
|
396
|
+
CharacterCodes2[CharacterCodes2["K"] = 75] = "K";
|
|
397
|
+
CharacterCodes2[CharacterCodes2["L"] = 76] = "L";
|
|
398
|
+
CharacterCodes2[CharacterCodes2["M"] = 77] = "M";
|
|
399
|
+
CharacterCodes2[CharacterCodes2["N"] = 78] = "N";
|
|
400
|
+
CharacterCodes2[CharacterCodes2["O"] = 79] = "O";
|
|
401
|
+
CharacterCodes2[CharacterCodes2["P"] = 80] = "P";
|
|
402
|
+
CharacterCodes2[CharacterCodes2["Q"] = 81] = "Q";
|
|
403
|
+
CharacterCodes2[CharacterCodes2["R"] = 82] = "R";
|
|
404
|
+
CharacterCodes2[CharacterCodes2["S"] = 83] = "S";
|
|
405
|
+
CharacterCodes2[CharacterCodes2["T"] = 84] = "T";
|
|
406
|
+
CharacterCodes2[CharacterCodes2["U"] = 85] = "U";
|
|
407
|
+
CharacterCodes2[CharacterCodes2["V"] = 86] = "V";
|
|
408
|
+
CharacterCodes2[CharacterCodes2["W"] = 87] = "W";
|
|
409
|
+
CharacterCodes2[CharacterCodes2["X"] = 88] = "X";
|
|
410
|
+
CharacterCodes2[CharacterCodes2["Y"] = 89] = "Y";
|
|
411
|
+
CharacterCodes2[CharacterCodes2["Z"] = 90] = "Z";
|
|
412
|
+
CharacterCodes2[CharacterCodes2["asterisk"] = 42] = "asterisk";
|
|
413
|
+
CharacterCodes2[CharacterCodes2["backslash"] = 92] = "backslash";
|
|
414
|
+
CharacterCodes2[CharacterCodes2["closeBrace"] = 125] = "closeBrace";
|
|
415
|
+
CharacterCodes2[CharacterCodes2["closeBracket"] = 93] = "closeBracket";
|
|
416
|
+
CharacterCodes2[CharacterCodes2["colon"] = 58] = "colon";
|
|
417
|
+
CharacterCodes2[CharacterCodes2["comma"] = 44] = "comma";
|
|
418
|
+
CharacterCodes2[CharacterCodes2["dot"] = 46] = "dot";
|
|
419
|
+
CharacterCodes2[CharacterCodes2["doubleQuote"] = 34] = "doubleQuote";
|
|
420
|
+
CharacterCodes2[CharacterCodes2["minus"] = 45] = "minus";
|
|
421
|
+
CharacterCodes2[CharacterCodes2["openBrace"] = 123] = "openBrace";
|
|
422
|
+
CharacterCodes2[CharacterCodes2["openBracket"] = 91] = "openBracket";
|
|
423
|
+
CharacterCodes2[CharacterCodes2["plus"] = 43] = "plus";
|
|
424
|
+
CharacterCodes2[CharacterCodes2["slash"] = 47] = "slash";
|
|
425
|
+
CharacterCodes2[CharacterCodes2["formFeed"] = 12] = "formFeed";
|
|
426
|
+
CharacterCodes2[CharacterCodes2["tab"] = 9] = "tab";
|
|
427
|
+
})(CharacterCodes || (CharacterCodes = {}));
|
|
428
|
+
|
|
429
|
+
// node_modules/jsonc-parser/lib/esm/impl/string-intern.js
|
|
430
|
+
var cachedSpaces = new Array(20).fill(0).map((_, index) => {
|
|
431
|
+
return " ".repeat(index);
|
|
432
|
+
});
|
|
433
|
+
var maxCachedValues = 200;
|
|
434
|
+
var cachedBreakLinesWithSpaces = {
|
|
435
|
+
" ": {
|
|
436
|
+
"\n": new Array(maxCachedValues).fill(0).map((_, index) => {
|
|
437
|
+
return `
|
|
438
|
+
` + " ".repeat(index);
|
|
439
|
+
}),
|
|
440
|
+
"\r": new Array(maxCachedValues).fill(0).map((_, index) => {
|
|
441
|
+
return "\r" + " ".repeat(index);
|
|
442
|
+
}),
|
|
443
|
+
"\r\n": new Array(maxCachedValues).fill(0).map((_, index) => {
|
|
444
|
+
return `\r
|
|
445
|
+
` + " ".repeat(index);
|
|
446
|
+
})
|
|
447
|
+
},
|
|
448
|
+
"\t": {
|
|
449
|
+
"\n": new Array(maxCachedValues).fill(0).map((_, index) => {
|
|
450
|
+
return `
|
|
451
|
+
` + "\t".repeat(index);
|
|
452
|
+
}),
|
|
453
|
+
"\r": new Array(maxCachedValues).fill(0).map((_, index) => {
|
|
454
|
+
return "\r" + "\t".repeat(index);
|
|
455
|
+
}),
|
|
456
|
+
"\r\n": new Array(maxCachedValues).fill(0).map((_, index) => {
|
|
457
|
+
return `\r
|
|
458
|
+
` + "\t".repeat(index);
|
|
459
|
+
})
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
// node_modules/jsonc-parser/lib/esm/impl/parser.js
|
|
464
|
+
var ParseOptions;
|
|
465
|
+
(function(ParseOptions2) {
|
|
466
|
+
ParseOptions2.DEFAULT = {
|
|
467
|
+
allowTrailingComma: false
|
|
468
|
+
};
|
|
469
|
+
})(ParseOptions || (ParseOptions = {}));
|
|
470
|
+
function parse(text, errors = [], options = ParseOptions.DEFAULT) {
|
|
471
|
+
let currentProperty = null;
|
|
472
|
+
let currentParent = [];
|
|
473
|
+
const previousParents = [];
|
|
474
|
+
function onValue(value) {
|
|
475
|
+
if (Array.isArray(currentParent)) {
|
|
476
|
+
currentParent.push(value);
|
|
477
|
+
} else if (currentProperty !== null) {
|
|
478
|
+
currentParent[currentProperty] = value;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
const visitor = {
|
|
482
|
+
onObjectBegin: () => {
|
|
483
|
+
const object = {};
|
|
484
|
+
onValue(object);
|
|
485
|
+
previousParents.push(currentParent);
|
|
486
|
+
currentParent = object;
|
|
487
|
+
currentProperty = null;
|
|
488
|
+
},
|
|
489
|
+
onObjectProperty: (name) => {
|
|
490
|
+
currentProperty = name;
|
|
491
|
+
},
|
|
492
|
+
onObjectEnd: () => {
|
|
493
|
+
currentParent = previousParents.pop();
|
|
494
|
+
},
|
|
495
|
+
onArrayBegin: () => {
|
|
496
|
+
const array = [];
|
|
497
|
+
onValue(array);
|
|
498
|
+
previousParents.push(currentParent);
|
|
499
|
+
currentParent = array;
|
|
500
|
+
currentProperty = null;
|
|
501
|
+
},
|
|
502
|
+
onArrayEnd: () => {
|
|
503
|
+
currentParent = previousParents.pop();
|
|
504
|
+
},
|
|
505
|
+
onLiteralValue: onValue,
|
|
506
|
+
onError: (error, offset, length) => {
|
|
507
|
+
errors.push({ error, offset, length });
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
visit(text, visitor, options);
|
|
511
|
+
return currentParent[0];
|
|
512
|
+
}
|
|
513
|
+
function visit(text, visitor, options = ParseOptions.DEFAULT) {
|
|
514
|
+
const _scanner = createScanner(text, false);
|
|
515
|
+
const _jsonPath = [];
|
|
516
|
+
let suppressedCallbacks = 0;
|
|
517
|
+
function toNoArgVisit(visitFunction) {
|
|
518
|
+
return visitFunction ? () => suppressedCallbacks === 0 && visitFunction(_scanner.getTokenOffset(), _scanner.getTokenLength(), _scanner.getTokenStartLine(), _scanner.getTokenStartCharacter()) : () => true;
|
|
519
|
+
}
|
|
520
|
+
function toOneArgVisit(visitFunction) {
|
|
521
|
+
return visitFunction ? (arg) => suppressedCallbacks === 0 && visitFunction(arg, _scanner.getTokenOffset(), _scanner.getTokenLength(), _scanner.getTokenStartLine(), _scanner.getTokenStartCharacter()) : () => true;
|
|
522
|
+
}
|
|
523
|
+
function toOneArgVisitWithPath(visitFunction) {
|
|
524
|
+
return visitFunction ? (arg) => suppressedCallbacks === 0 && visitFunction(arg, _scanner.getTokenOffset(), _scanner.getTokenLength(), _scanner.getTokenStartLine(), _scanner.getTokenStartCharacter(), () => _jsonPath.slice()) : () => true;
|
|
525
|
+
}
|
|
526
|
+
function toBeginVisit(visitFunction) {
|
|
527
|
+
return visitFunction ? () => {
|
|
528
|
+
if (suppressedCallbacks > 0) {
|
|
529
|
+
suppressedCallbacks++;
|
|
530
|
+
} else {
|
|
531
|
+
let cbReturn = visitFunction(_scanner.getTokenOffset(), _scanner.getTokenLength(), _scanner.getTokenStartLine(), _scanner.getTokenStartCharacter(), () => _jsonPath.slice());
|
|
532
|
+
if (cbReturn === false) {
|
|
533
|
+
suppressedCallbacks = 1;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
} : () => true;
|
|
537
|
+
}
|
|
538
|
+
function toEndVisit(visitFunction) {
|
|
539
|
+
return visitFunction ? () => {
|
|
540
|
+
if (suppressedCallbacks > 0) {
|
|
541
|
+
suppressedCallbacks--;
|
|
542
|
+
}
|
|
543
|
+
if (suppressedCallbacks === 0) {
|
|
544
|
+
visitFunction(_scanner.getTokenOffset(), _scanner.getTokenLength(), _scanner.getTokenStartLine(), _scanner.getTokenStartCharacter());
|
|
545
|
+
}
|
|
546
|
+
} : () => true;
|
|
547
|
+
}
|
|
548
|
+
const onObjectBegin = toBeginVisit(visitor.onObjectBegin), onObjectProperty = toOneArgVisitWithPath(visitor.onObjectProperty), onObjectEnd = toEndVisit(visitor.onObjectEnd), onArrayBegin = toBeginVisit(visitor.onArrayBegin), onArrayEnd = toEndVisit(visitor.onArrayEnd), onLiteralValue = toOneArgVisitWithPath(visitor.onLiteralValue), onSeparator = toOneArgVisit(visitor.onSeparator), onComment = toNoArgVisit(visitor.onComment), onError = toOneArgVisit(visitor.onError);
|
|
549
|
+
const disallowComments = options && options.disallowComments;
|
|
550
|
+
const allowTrailingComma = options && options.allowTrailingComma;
|
|
551
|
+
function scanNext() {
|
|
552
|
+
while (true) {
|
|
553
|
+
const token = _scanner.scan();
|
|
554
|
+
switch (_scanner.getTokenError()) {
|
|
555
|
+
case 4:
|
|
556
|
+
handleError(14);
|
|
557
|
+
break;
|
|
558
|
+
case 5:
|
|
559
|
+
handleError(15);
|
|
560
|
+
break;
|
|
561
|
+
case 3:
|
|
562
|
+
handleError(13);
|
|
563
|
+
break;
|
|
564
|
+
case 1:
|
|
565
|
+
if (!disallowComments) {
|
|
566
|
+
handleError(11);
|
|
567
|
+
}
|
|
568
|
+
break;
|
|
569
|
+
case 2:
|
|
570
|
+
handleError(12);
|
|
571
|
+
break;
|
|
572
|
+
case 6:
|
|
573
|
+
handleError(16);
|
|
574
|
+
break;
|
|
575
|
+
}
|
|
576
|
+
switch (token) {
|
|
577
|
+
case 12:
|
|
578
|
+
case 13:
|
|
579
|
+
if (disallowComments) {
|
|
580
|
+
handleError(10);
|
|
581
|
+
} else {
|
|
582
|
+
onComment();
|
|
583
|
+
}
|
|
584
|
+
break;
|
|
585
|
+
case 16:
|
|
586
|
+
handleError(1);
|
|
587
|
+
break;
|
|
588
|
+
case 15:
|
|
589
|
+
case 14:
|
|
590
|
+
break;
|
|
591
|
+
default:
|
|
592
|
+
return token;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
function handleError(error, skipUntilAfter = [], skipUntil = []) {
|
|
597
|
+
onError(error);
|
|
598
|
+
if (skipUntilAfter.length + skipUntil.length > 0) {
|
|
599
|
+
let token = _scanner.getToken();
|
|
600
|
+
while (token !== 17) {
|
|
601
|
+
if (skipUntilAfter.indexOf(token) !== -1) {
|
|
602
|
+
scanNext();
|
|
603
|
+
break;
|
|
604
|
+
} else if (skipUntil.indexOf(token) !== -1) {
|
|
605
|
+
break;
|
|
606
|
+
}
|
|
607
|
+
token = scanNext();
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
function parseString(isValue) {
|
|
612
|
+
const value = _scanner.getTokenValue();
|
|
613
|
+
if (isValue) {
|
|
614
|
+
onLiteralValue(value);
|
|
615
|
+
} else {
|
|
616
|
+
onObjectProperty(value);
|
|
617
|
+
_jsonPath.push(value);
|
|
618
|
+
}
|
|
619
|
+
scanNext();
|
|
620
|
+
return true;
|
|
621
|
+
}
|
|
622
|
+
function parseLiteral() {
|
|
623
|
+
switch (_scanner.getToken()) {
|
|
624
|
+
case 11:
|
|
625
|
+
const tokenValue = _scanner.getTokenValue();
|
|
626
|
+
let value = Number(tokenValue);
|
|
627
|
+
if (isNaN(value)) {
|
|
628
|
+
handleError(2);
|
|
629
|
+
value = 0;
|
|
630
|
+
}
|
|
631
|
+
onLiteralValue(value);
|
|
632
|
+
break;
|
|
633
|
+
case 7:
|
|
634
|
+
onLiteralValue(null);
|
|
635
|
+
break;
|
|
636
|
+
case 8:
|
|
637
|
+
onLiteralValue(true);
|
|
638
|
+
break;
|
|
639
|
+
case 9:
|
|
640
|
+
onLiteralValue(false);
|
|
641
|
+
break;
|
|
642
|
+
default:
|
|
643
|
+
return false;
|
|
644
|
+
}
|
|
645
|
+
scanNext();
|
|
646
|
+
return true;
|
|
647
|
+
}
|
|
648
|
+
function parseProperty() {
|
|
649
|
+
if (_scanner.getToken() !== 10) {
|
|
650
|
+
handleError(3, [], [2, 5]);
|
|
651
|
+
return false;
|
|
652
|
+
}
|
|
653
|
+
parseString(false);
|
|
654
|
+
if (_scanner.getToken() === 6) {
|
|
655
|
+
onSeparator(":");
|
|
656
|
+
scanNext();
|
|
657
|
+
if (!parseValue()) {
|
|
658
|
+
handleError(4, [], [2, 5]);
|
|
659
|
+
}
|
|
660
|
+
} else {
|
|
661
|
+
handleError(5, [], [2, 5]);
|
|
662
|
+
}
|
|
663
|
+
_jsonPath.pop();
|
|
664
|
+
return true;
|
|
665
|
+
}
|
|
666
|
+
function parseObject() {
|
|
667
|
+
onObjectBegin();
|
|
668
|
+
scanNext();
|
|
669
|
+
let needsComma = false;
|
|
670
|
+
while (_scanner.getToken() !== 2 && _scanner.getToken() !== 17) {
|
|
671
|
+
if (_scanner.getToken() === 5) {
|
|
672
|
+
if (!needsComma) {
|
|
673
|
+
handleError(4, [], []);
|
|
674
|
+
}
|
|
675
|
+
onSeparator(",");
|
|
676
|
+
scanNext();
|
|
677
|
+
if (_scanner.getToken() === 2 && allowTrailingComma) {
|
|
678
|
+
break;
|
|
679
|
+
}
|
|
680
|
+
} else if (needsComma) {
|
|
681
|
+
handleError(6, [], []);
|
|
682
|
+
}
|
|
683
|
+
if (!parseProperty()) {
|
|
684
|
+
handleError(4, [], [2, 5]);
|
|
685
|
+
}
|
|
686
|
+
needsComma = true;
|
|
687
|
+
}
|
|
688
|
+
onObjectEnd();
|
|
689
|
+
if (_scanner.getToken() !== 2) {
|
|
690
|
+
handleError(7, [2], []);
|
|
691
|
+
} else {
|
|
692
|
+
scanNext();
|
|
693
|
+
}
|
|
694
|
+
return true;
|
|
695
|
+
}
|
|
696
|
+
function parseArray() {
|
|
697
|
+
onArrayBegin();
|
|
698
|
+
scanNext();
|
|
699
|
+
let isFirstElement = true;
|
|
700
|
+
let needsComma = false;
|
|
701
|
+
while (_scanner.getToken() !== 4 && _scanner.getToken() !== 17) {
|
|
702
|
+
if (_scanner.getToken() === 5) {
|
|
703
|
+
if (!needsComma) {
|
|
704
|
+
handleError(4, [], []);
|
|
705
|
+
}
|
|
706
|
+
onSeparator(",");
|
|
707
|
+
scanNext();
|
|
708
|
+
if (_scanner.getToken() === 4 && allowTrailingComma) {
|
|
709
|
+
break;
|
|
710
|
+
}
|
|
711
|
+
} else if (needsComma) {
|
|
712
|
+
handleError(6, [], []);
|
|
713
|
+
}
|
|
714
|
+
if (isFirstElement) {
|
|
715
|
+
_jsonPath.push(0);
|
|
716
|
+
isFirstElement = false;
|
|
717
|
+
} else {
|
|
718
|
+
_jsonPath[_jsonPath.length - 1]++;
|
|
719
|
+
}
|
|
720
|
+
if (!parseValue()) {
|
|
721
|
+
handleError(4, [], [4, 5]);
|
|
722
|
+
}
|
|
723
|
+
needsComma = true;
|
|
724
|
+
}
|
|
725
|
+
onArrayEnd();
|
|
726
|
+
if (!isFirstElement) {
|
|
727
|
+
_jsonPath.pop();
|
|
728
|
+
}
|
|
729
|
+
if (_scanner.getToken() !== 4) {
|
|
730
|
+
handleError(8, [4], []);
|
|
731
|
+
} else {
|
|
732
|
+
scanNext();
|
|
733
|
+
}
|
|
734
|
+
return true;
|
|
735
|
+
}
|
|
736
|
+
function parseValue() {
|
|
737
|
+
switch (_scanner.getToken()) {
|
|
738
|
+
case 3:
|
|
739
|
+
return parseArray();
|
|
740
|
+
case 1:
|
|
741
|
+
return parseObject();
|
|
742
|
+
case 10:
|
|
743
|
+
return parseString(true);
|
|
744
|
+
default:
|
|
745
|
+
return parseLiteral();
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
scanNext();
|
|
749
|
+
if (_scanner.getToken() === 17) {
|
|
750
|
+
if (options.allowEmptyContent) {
|
|
751
|
+
return true;
|
|
752
|
+
}
|
|
753
|
+
handleError(4, [], []);
|
|
754
|
+
return false;
|
|
755
|
+
}
|
|
756
|
+
if (!parseValue()) {
|
|
757
|
+
handleError(4, [], []);
|
|
758
|
+
return false;
|
|
759
|
+
}
|
|
760
|
+
if (_scanner.getToken() !== 17) {
|
|
761
|
+
handleError(9, [], []);
|
|
762
|
+
}
|
|
763
|
+
return true;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// node_modules/jsonc-parser/lib/esm/main.js
|
|
767
|
+
var ScanError;
|
|
768
|
+
(function(ScanError2) {
|
|
769
|
+
ScanError2[ScanError2["None"] = 0] = "None";
|
|
770
|
+
ScanError2[ScanError2["UnexpectedEndOfComment"] = 1] = "UnexpectedEndOfComment";
|
|
771
|
+
ScanError2[ScanError2["UnexpectedEndOfString"] = 2] = "UnexpectedEndOfString";
|
|
772
|
+
ScanError2[ScanError2["UnexpectedEndOfNumber"] = 3] = "UnexpectedEndOfNumber";
|
|
773
|
+
ScanError2[ScanError2["InvalidUnicode"] = 4] = "InvalidUnicode";
|
|
774
|
+
ScanError2[ScanError2["InvalidEscapeCharacter"] = 5] = "InvalidEscapeCharacter";
|
|
775
|
+
ScanError2[ScanError2["InvalidCharacter"] = 6] = "InvalidCharacter";
|
|
776
|
+
})(ScanError || (ScanError = {}));
|
|
777
|
+
var SyntaxKind;
|
|
778
|
+
(function(SyntaxKind2) {
|
|
779
|
+
SyntaxKind2[SyntaxKind2["OpenBraceToken"] = 1] = "OpenBraceToken";
|
|
780
|
+
SyntaxKind2[SyntaxKind2["CloseBraceToken"] = 2] = "CloseBraceToken";
|
|
781
|
+
SyntaxKind2[SyntaxKind2["OpenBracketToken"] = 3] = "OpenBracketToken";
|
|
782
|
+
SyntaxKind2[SyntaxKind2["CloseBracketToken"] = 4] = "CloseBracketToken";
|
|
783
|
+
SyntaxKind2[SyntaxKind2["CommaToken"] = 5] = "CommaToken";
|
|
784
|
+
SyntaxKind2[SyntaxKind2["ColonToken"] = 6] = "ColonToken";
|
|
785
|
+
SyntaxKind2[SyntaxKind2["NullKeyword"] = 7] = "NullKeyword";
|
|
786
|
+
SyntaxKind2[SyntaxKind2["TrueKeyword"] = 8] = "TrueKeyword";
|
|
787
|
+
SyntaxKind2[SyntaxKind2["FalseKeyword"] = 9] = "FalseKeyword";
|
|
788
|
+
SyntaxKind2[SyntaxKind2["StringLiteral"] = 10] = "StringLiteral";
|
|
789
|
+
SyntaxKind2[SyntaxKind2["NumericLiteral"] = 11] = "NumericLiteral";
|
|
790
|
+
SyntaxKind2[SyntaxKind2["LineCommentTrivia"] = 12] = "LineCommentTrivia";
|
|
791
|
+
SyntaxKind2[SyntaxKind2["BlockCommentTrivia"] = 13] = "BlockCommentTrivia";
|
|
792
|
+
SyntaxKind2[SyntaxKind2["LineBreakTrivia"] = 14] = "LineBreakTrivia";
|
|
793
|
+
SyntaxKind2[SyntaxKind2["Trivia"] = 15] = "Trivia";
|
|
794
|
+
SyntaxKind2[SyntaxKind2["Unknown"] = 16] = "Unknown";
|
|
795
|
+
SyntaxKind2[SyntaxKind2["EOF"] = 17] = "EOF";
|
|
796
|
+
})(SyntaxKind || (SyntaxKind = {}));
|
|
797
|
+
var parse2 = parse;
|
|
798
|
+
var ParseErrorCode;
|
|
799
|
+
(function(ParseErrorCode2) {
|
|
800
|
+
ParseErrorCode2[ParseErrorCode2["InvalidSymbol"] = 1] = "InvalidSymbol";
|
|
801
|
+
ParseErrorCode2[ParseErrorCode2["InvalidNumberFormat"] = 2] = "InvalidNumberFormat";
|
|
802
|
+
ParseErrorCode2[ParseErrorCode2["PropertyNameExpected"] = 3] = "PropertyNameExpected";
|
|
803
|
+
ParseErrorCode2[ParseErrorCode2["ValueExpected"] = 4] = "ValueExpected";
|
|
804
|
+
ParseErrorCode2[ParseErrorCode2["ColonExpected"] = 5] = "ColonExpected";
|
|
805
|
+
ParseErrorCode2[ParseErrorCode2["CommaExpected"] = 6] = "CommaExpected";
|
|
806
|
+
ParseErrorCode2[ParseErrorCode2["CloseBraceExpected"] = 7] = "CloseBraceExpected";
|
|
807
|
+
ParseErrorCode2[ParseErrorCode2["CloseBracketExpected"] = 8] = "CloseBracketExpected";
|
|
808
|
+
ParseErrorCode2[ParseErrorCode2["EndOfFileExpected"] = 9] = "EndOfFileExpected";
|
|
809
|
+
ParseErrorCode2[ParseErrorCode2["InvalidCommentToken"] = 10] = "InvalidCommentToken";
|
|
810
|
+
ParseErrorCode2[ParseErrorCode2["UnexpectedEndOfComment"] = 11] = "UnexpectedEndOfComment";
|
|
811
|
+
ParseErrorCode2[ParseErrorCode2["UnexpectedEndOfString"] = 12] = "UnexpectedEndOfString";
|
|
812
|
+
ParseErrorCode2[ParseErrorCode2["UnexpectedEndOfNumber"] = 13] = "UnexpectedEndOfNumber";
|
|
813
|
+
ParseErrorCode2[ParseErrorCode2["InvalidUnicode"] = 14] = "InvalidUnicode";
|
|
814
|
+
ParseErrorCode2[ParseErrorCode2["InvalidEscapeCharacter"] = 15] = "InvalidEscapeCharacter";
|
|
815
|
+
ParseErrorCode2[ParseErrorCode2["InvalidCharacter"] = 16] = "InvalidCharacter";
|
|
816
|
+
})(ParseErrorCode || (ParseErrorCode = {}));
|
|
817
|
+
|
|
818
|
+
// src/constants.ts
|
|
819
|
+
var CLI_NAME = "devbox";
|
|
820
|
+
var LEGACY_GENERATED_CONFIG_BASENAME = ".devbox.generated.devcontainer.json";
|
|
821
|
+
var MANAGED_LABEL_KEY = "devbox.managed";
|
|
822
|
+
var WORKSPACE_LABEL_KEY = "devbox.workspace";
|
|
823
|
+
var SSH_AUTH_SOCK_TARGET = "/tmp/devbox-ssh-auth.sock";
|
|
824
|
+
var DOCKER_DESKTOP_SSH_AUTH_SOCK_SOURCE = "/run/host-services/ssh-auth.sock";
|
|
825
|
+
var KNOWN_HOSTS_TARGET = "/tmp/devbox-known_hosts";
|
|
826
|
+
var RUNNER_CRED_FILENAME = ".sshcred";
|
|
827
|
+
var RUNNER_HOST_KEYS_DIRNAME = ".devbox-ssh-host-keys";
|
|
828
|
+
var RUNNER_URL = "https://raw.githubusercontent.com/PabloZaiden/ssh-server-runner/main/ssh-server.sh";
|
|
829
|
+
var STATE_VERSION = 1;
|
|
830
|
+
|
|
831
|
+
// src/core.ts
|
|
832
|
+
class UserError extends Error {
|
|
833
|
+
constructor(message) {
|
|
834
|
+
super(message);
|
|
835
|
+
this.name = "UserError";
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
function helpText() {
|
|
839
|
+
return `${CLI_NAME} - manage a devcontainer plus ssh-server-runner
|
|
840
|
+
|
|
841
|
+
Usage:
|
|
842
|
+
${CLI_NAME} [port] [--allow-missing-ssh]
|
|
843
|
+
${CLI_NAME} up [port] [--allow-missing-ssh]
|
|
844
|
+
${CLI_NAME} rebuild [port] [--allow-missing-ssh]
|
|
845
|
+
${CLI_NAME} down
|
|
846
|
+
${CLI_NAME} --help
|
|
847
|
+
|
|
848
|
+
Notes:
|
|
849
|
+
- The same port is published on host and container.
|
|
850
|
+
- If no port is provided for up/rebuild, the last stored port for this workspace is reused.
|
|
851
|
+
- Pass --allow-missing-ssh to continue without SSH agent sharing when no usable SSH agent socket is available.
|
|
852
|
+
- Only image/Dockerfile-based devcontainers are supported in v1.`;
|
|
853
|
+
}
|
|
854
|
+
function parseArgs(argv) {
|
|
855
|
+
const args = [...argv];
|
|
856
|
+
if (args.length === 0) {
|
|
857
|
+
return { command: "up", allowMissingSsh: false };
|
|
858
|
+
}
|
|
859
|
+
let command = "up";
|
|
860
|
+
const first = args[0];
|
|
861
|
+
if (first === "up" || first === "down" || first === "rebuild" || first === "help") {
|
|
862
|
+
command = first;
|
|
863
|
+
args.shift();
|
|
864
|
+
} else if (first === "--help" || first === "-h") {
|
|
865
|
+
return { command: "help", allowMissingSsh: false };
|
|
866
|
+
}
|
|
867
|
+
let port;
|
|
868
|
+
let allowMissingSsh = false;
|
|
869
|
+
const positionals = [];
|
|
870
|
+
for (let index = 0;index < args.length; index += 1) {
|
|
871
|
+
const arg = args[index];
|
|
872
|
+
if (arg === "--help" || arg === "-h") {
|
|
873
|
+
return { command: "help", allowMissingSsh: false };
|
|
874
|
+
}
|
|
875
|
+
if (arg === "--allow-missing-ssh") {
|
|
876
|
+
allowMissingSsh = true;
|
|
877
|
+
continue;
|
|
878
|
+
}
|
|
879
|
+
if (arg === "--port" || arg === "-p") {
|
|
880
|
+
const value = args[index + 1];
|
|
881
|
+
if (!value) {
|
|
882
|
+
throw new UserError("Expected a value after --port.");
|
|
883
|
+
}
|
|
884
|
+
port = parsePort(value);
|
|
885
|
+
index += 1;
|
|
886
|
+
continue;
|
|
887
|
+
}
|
|
888
|
+
if (arg.startsWith("--port=")) {
|
|
889
|
+
port = parsePort(arg.slice("--port=".length));
|
|
890
|
+
continue;
|
|
891
|
+
}
|
|
892
|
+
if (arg.startsWith("-")) {
|
|
893
|
+
throw new UserError(`Unknown option: ${arg}`);
|
|
894
|
+
}
|
|
895
|
+
positionals.push(arg);
|
|
896
|
+
}
|
|
897
|
+
if (positionals.length > 1) {
|
|
898
|
+
throw new UserError(`Unexpected extra argument: ${positionals[1]}`);
|
|
899
|
+
}
|
|
900
|
+
if (positionals[0]) {
|
|
901
|
+
if (command === "down") {
|
|
902
|
+
throw new UserError("The down command does not accept a port.");
|
|
903
|
+
}
|
|
904
|
+
port = parsePort(positionals[0]);
|
|
905
|
+
}
|
|
906
|
+
if (command === "down" && port !== undefined) {
|
|
907
|
+
throw new UserError("The down command does not accept a port.");
|
|
908
|
+
}
|
|
909
|
+
return { command, port, allowMissingSsh };
|
|
910
|
+
}
|
|
911
|
+
function parsePort(raw) {
|
|
912
|
+
if (!/^\d+$/.test(raw)) {
|
|
913
|
+
throw new UserError(`Invalid port: ${raw}`);
|
|
914
|
+
}
|
|
915
|
+
const port = Number(raw);
|
|
916
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
917
|
+
throw new UserError(`Port must be between 1 and 65535. Received: ${raw}`);
|
|
918
|
+
}
|
|
919
|
+
return port;
|
|
920
|
+
}
|
|
921
|
+
function hashWorkspacePath(workspacePath) {
|
|
922
|
+
return createHash("sha256").update(workspacePath).digest("hex").slice(0, 16);
|
|
923
|
+
}
|
|
924
|
+
function getManagedLabels(workspaceHash) {
|
|
925
|
+
return {
|
|
926
|
+
[MANAGED_LABEL_KEY]: "true",
|
|
927
|
+
[WORKSPACE_LABEL_KEY]: workspaceHash
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
function getStateRoot() {
|
|
931
|
+
if (process.platform === "darwin") {
|
|
932
|
+
return path.join(os.homedir(), "Library", "Application Support", CLI_NAME);
|
|
933
|
+
}
|
|
934
|
+
const xdgStateHome = process.env.XDG_STATE_HOME;
|
|
935
|
+
if (xdgStateHome) {
|
|
936
|
+
return path.join(xdgStateHome, CLI_NAME);
|
|
937
|
+
}
|
|
938
|
+
return path.join(os.homedir(), ".local", "state", CLI_NAME);
|
|
939
|
+
}
|
|
940
|
+
function getWorkspaceStateDir(workspacePath) {
|
|
941
|
+
return path.join(getStateRoot(), "workspaces", hashWorkspacePath(workspacePath));
|
|
942
|
+
}
|
|
943
|
+
function getWorkspaceStateFile(workspacePath) {
|
|
944
|
+
return path.join(getWorkspaceStateDir(workspacePath), "state.json");
|
|
945
|
+
}
|
|
946
|
+
function getWorkspaceUserDataDir(workspacePath) {
|
|
947
|
+
return path.join(getWorkspaceStateDir(workspacePath), "user-data");
|
|
948
|
+
}
|
|
949
|
+
function getDefaultRemoteWorkspaceFolder(workspacePath) {
|
|
950
|
+
return path.posix.join("/workspaces", path.basename(workspacePath));
|
|
951
|
+
}
|
|
952
|
+
function getManagedContainerName(workspacePath, port) {
|
|
953
|
+
const projectName = path.basename(workspacePath);
|
|
954
|
+
const normalized = projectName.toLowerCase().replace(/[^a-z0-9_.-]+/g, "-").replace(/^-+/, "").replace(/-+$/, "").replace(/-+/g, "-");
|
|
955
|
+
const safeProjectName = normalized.length > 0 ? normalized : hashWorkspacePath(workspacePath);
|
|
956
|
+
return `devbox-${safeProjectName.slice(0, 48)}-${port}`;
|
|
957
|
+
}
|
|
958
|
+
async function loadWorkspaceState(workspacePath) {
|
|
959
|
+
const statePath = getWorkspaceStateFile(workspacePath);
|
|
960
|
+
if (!existsSync(statePath)) {
|
|
961
|
+
return null;
|
|
962
|
+
}
|
|
963
|
+
const raw = await readFile(statePath, "utf8");
|
|
964
|
+
const parsed = JSON.parse(raw);
|
|
965
|
+
if (parsed.version !== STATE_VERSION || typeof parsed.workspacePath !== "string" || typeof parsed.workspaceHash !== "string" || typeof parsed.port !== "number" || typeof parsed.generatedConfigPath !== "string" || typeof parsed.sourceConfigPath !== "string" || typeof parsed.userDataDir !== "string" || !parsed.labels || typeof parsed.labels !== "object") {
|
|
966
|
+
throw new UserError(`State file is invalid: ${statePath}`);
|
|
967
|
+
}
|
|
968
|
+
return parsed;
|
|
969
|
+
}
|
|
970
|
+
async function saveWorkspaceState(state) {
|
|
971
|
+
const stateDir = getWorkspaceStateDir(state.workspacePath);
|
|
972
|
+
await mkdir(stateDir, { recursive: true });
|
|
973
|
+
await writeFile(getWorkspaceStateFile(state.workspacePath), `${JSON.stringify(state, null, 2)}
|
|
974
|
+
`, "utf8");
|
|
975
|
+
}
|
|
976
|
+
async function deleteWorkspaceState(workspacePath) {
|
|
977
|
+
await rm(getWorkspaceStateDir(workspacePath), { recursive: true, force: true });
|
|
978
|
+
}
|
|
979
|
+
function resolvePort(command, explicitPort, state) {
|
|
980
|
+
if (command === "down" || command === "help") {
|
|
981
|
+
throw new UserError(`resolvePort cannot be used for ${command}.`);
|
|
982
|
+
}
|
|
983
|
+
if (explicitPort !== undefined) {
|
|
984
|
+
return explicitPort;
|
|
985
|
+
}
|
|
986
|
+
if (state) {
|
|
987
|
+
return state.port;
|
|
988
|
+
}
|
|
989
|
+
throw new UserError(`No port was provided and no previous port is stored for this workspace. Run \`${CLI_NAME} <port>\` first.`);
|
|
990
|
+
}
|
|
991
|
+
async function discoverDevcontainerConfig(workspacePath) {
|
|
992
|
+
const candidates = [
|
|
993
|
+
path.join(workspacePath, ".devcontainer", "devcontainer.json"),
|
|
994
|
+
path.join(workspacePath, ".devcontainer.json")
|
|
995
|
+
];
|
|
996
|
+
for (const candidate of candidates) {
|
|
997
|
+
if (!existsSync(candidate)) {
|
|
998
|
+
continue;
|
|
999
|
+
}
|
|
1000
|
+
const content = await readFile(candidate, "utf8");
|
|
1001
|
+
const errors = [];
|
|
1002
|
+
const parsed = parse2(content, errors, {
|
|
1003
|
+
allowTrailingComma: true,
|
|
1004
|
+
disallowComments: false
|
|
1005
|
+
});
|
|
1006
|
+
if (errors.length > 0) {
|
|
1007
|
+
const details = errors.map((error) => `${error.error}@${error.offset}`).join(", ");
|
|
1008
|
+
throw new UserError(`Could not parse ${candidate} as JSONC (${details}).`);
|
|
1009
|
+
}
|
|
1010
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1011
|
+
throw new UserError(`${candidate} must contain a JSON object.`);
|
|
1012
|
+
}
|
|
1013
|
+
const config = parsed;
|
|
1014
|
+
validateSupportedDevcontainerConfig(config);
|
|
1015
|
+
return { path: candidate, config };
|
|
1016
|
+
}
|
|
1017
|
+
throw new UserError(`No devcontainer definition was found in ${workspacePath}. Expected .devcontainer/devcontainer.json or .devcontainer.json.`);
|
|
1018
|
+
}
|
|
1019
|
+
function validateSupportedDevcontainerConfig(config) {
|
|
1020
|
+
if (config.dockerComposeFile !== undefined) {
|
|
1021
|
+
throw new UserError("dockerComposeFile-based devcontainers are not supported in v1.");
|
|
1022
|
+
}
|
|
1023
|
+
const hasImage = typeof config.image === "string" && config.image.trim().length > 0;
|
|
1024
|
+
const hasDockerFile = typeof config.dockerFile === "string" && config.dockerFile.trim().length > 0;
|
|
1025
|
+
const build = asRecord(config.build);
|
|
1026
|
+
const hasBuildDockerfile = typeof build?.dockerfile === "string" && build.dockerfile.trim().length > 0;
|
|
1027
|
+
if (!hasImage && !hasDockerFile && !hasBuildDockerfile) {
|
|
1028
|
+
throw new UserError("Only image- or Dockerfile-based devcontainers are supported in v1.");
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
function getGeneratedConfigPath(sourceConfigPath) {
|
|
1032
|
+
const sourceBasename = path.basename(sourceConfigPath);
|
|
1033
|
+
if (sourceBasename === "devcontainer.json") {
|
|
1034
|
+
return path.join(path.dirname(sourceConfigPath), ".devcontainer.json");
|
|
1035
|
+
}
|
|
1036
|
+
if (sourceBasename === ".devcontainer.json") {
|
|
1037
|
+
return path.join(path.dirname(sourceConfigPath), "devcontainer.json");
|
|
1038
|
+
}
|
|
1039
|
+
throw new UserError(`Unsupported devcontainer config filename: ${sourceConfigPath}. Expected devcontainer.json or .devcontainer.json.`);
|
|
1040
|
+
}
|
|
1041
|
+
function getLegacyGeneratedConfigPath(sourceConfigPath) {
|
|
1042
|
+
return path.join(path.dirname(sourceConfigPath), LEGACY_GENERATED_CONFIG_BASENAME);
|
|
1043
|
+
}
|
|
1044
|
+
async function writeManagedConfig(generatedConfigPath, config) {
|
|
1045
|
+
await mkdir(path.dirname(generatedConfigPath), { recursive: true });
|
|
1046
|
+
await writeFile(generatedConfigPath, `${JSON.stringify(config, null, 2)}
|
|
1047
|
+
`, "utf8");
|
|
1048
|
+
}
|
|
1049
|
+
async function removeGeneratedConfig(generatedConfigPath) {
|
|
1050
|
+
await rm(generatedConfigPath, { force: true });
|
|
1051
|
+
}
|
|
1052
|
+
function buildManagedConfig(baseConfig, options) {
|
|
1053
|
+
const managedConfig = structuredClone(baseConfig);
|
|
1054
|
+
const runArgs = withManagedContainerName(getStringArray(managedConfig.runArgs, "runArgs"), options.containerName);
|
|
1055
|
+
if (!hasPublishedPort(runArgs, options.port)) {
|
|
1056
|
+
runArgs.push("-p", `${options.port}:${options.port}`);
|
|
1057
|
+
}
|
|
1058
|
+
managedConfig.runArgs = runArgs;
|
|
1059
|
+
const mounts = getStringArray(managedConfig.mounts, "mounts");
|
|
1060
|
+
if (options.sshAuthSock) {
|
|
1061
|
+
mounts.push(`type=bind,source=${options.sshAuthSock},target=${SSH_AUTH_SOCK_TARGET}`);
|
|
1062
|
+
}
|
|
1063
|
+
if (options.knownHostsPath) {
|
|
1064
|
+
mounts.push(`type=bind,source=${options.knownHostsPath},target=${KNOWN_HOSTS_TARGET},readonly`);
|
|
1065
|
+
}
|
|
1066
|
+
managedConfig.mounts = dedupe(mounts);
|
|
1067
|
+
const containerEnv = getStringRecord(managedConfig.containerEnv, "containerEnv");
|
|
1068
|
+
if (options.sshAuthSock) {
|
|
1069
|
+
containerEnv.SSH_AUTH_SOCK = SSH_AUTH_SOCK_TARGET;
|
|
1070
|
+
}
|
|
1071
|
+
managedConfig.containerEnv = containerEnv;
|
|
1072
|
+
return managedConfig;
|
|
1073
|
+
}
|
|
1074
|
+
function createWorkspaceState(input) {
|
|
1075
|
+
return {
|
|
1076
|
+
version: STATE_VERSION,
|
|
1077
|
+
workspacePath: input.workspacePath,
|
|
1078
|
+
workspaceHash: hashWorkspacePath(input.workspacePath),
|
|
1079
|
+
port: input.port,
|
|
1080
|
+
sourceConfigPath: input.sourceConfigPath,
|
|
1081
|
+
generatedConfigPath: input.generatedConfigPath,
|
|
1082
|
+
labels: input.labels,
|
|
1083
|
+
userDataDir: input.userDataDir,
|
|
1084
|
+
lastContainerId: input.containerId,
|
|
1085
|
+
updatedAt: new Date().toISOString()
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
async function getKnownHostsPath() {
|
|
1089
|
+
const candidate = path.join(os.homedir(), ".ssh", "known_hosts");
|
|
1090
|
+
try {
|
|
1091
|
+
await access(candidate);
|
|
1092
|
+
return candidate;
|
|
1093
|
+
} catch {
|
|
1094
|
+
return null;
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
function quoteShell(value) {
|
|
1098
|
+
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
1099
|
+
}
|
|
1100
|
+
function dedupe(values) {
|
|
1101
|
+
return [...new Set(values)];
|
|
1102
|
+
}
|
|
1103
|
+
function withManagedContainerName(runArgs, containerName) {
|
|
1104
|
+
const next = [];
|
|
1105
|
+
for (let index = 0;index < runArgs.length; index += 1) {
|
|
1106
|
+
const current = runArgs[index];
|
|
1107
|
+
if (current === "--name") {
|
|
1108
|
+
index += 1;
|
|
1109
|
+
continue;
|
|
1110
|
+
}
|
|
1111
|
+
if (current.startsWith("--name=")) {
|
|
1112
|
+
continue;
|
|
1113
|
+
}
|
|
1114
|
+
next.push(current);
|
|
1115
|
+
}
|
|
1116
|
+
next.push("--name", containerName);
|
|
1117
|
+
return next;
|
|
1118
|
+
}
|
|
1119
|
+
function hasPublishedPort(runArgs, port) {
|
|
1120
|
+
const expected = `${port}:${port}`;
|
|
1121
|
+
for (let index = 0;index < runArgs.length; index += 1) {
|
|
1122
|
+
const current = runArgs[index];
|
|
1123
|
+
const next = runArgs[index + 1];
|
|
1124
|
+
if ((current === "-p" || current === "--publish") && next === expected) {
|
|
1125
|
+
return true;
|
|
1126
|
+
}
|
|
1127
|
+
if (current.startsWith("-p") && current.slice(2) === expected) {
|
|
1128
|
+
return true;
|
|
1129
|
+
}
|
|
1130
|
+
if (current.startsWith("--publish=") && current.slice("--publish=".length) === expected) {
|
|
1131
|
+
return true;
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
return false;
|
|
1135
|
+
}
|
|
1136
|
+
function getStringArray(value, fieldName) {
|
|
1137
|
+
if (value === undefined) {
|
|
1138
|
+
return [];
|
|
1139
|
+
}
|
|
1140
|
+
if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) {
|
|
1141
|
+
throw new UserError(`${fieldName} must be an array of strings.`);
|
|
1142
|
+
}
|
|
1143
|
+
return [...value];
|
|
1144
|
+
}
|
|
1145
|
+
function getStringRecord(value, fieldName) {
|
|
1146
|
+
if (value === undefined) {
|
|
1147
|
+
return {};
|
|
1148
|
+
}
|
|
1149
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1150
|
+
throw new UserError(`${fieldName} must be an object with string values.`);
|
|
1151
|
+
}
|
|
1152
|
+
const entries = Object.entries(value);
|
|
1153
|
+
if (entries.some(([, entryValue]) => typeof entryValue !== "string")) {
|
|
1154
|
+
throw new UserError(`${fieldName} must be an object with string values.`);
|
|
1155
|
+
}
|
|
1156
|
+
return Object.fromEntries(entries);
|
|
1157
|
+
}
|
|
1158
|
+
function asRecord(value) {
|
|
1159
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1160
|
+
return null;
|
|
1161
|
+
}
|
|
1162
|
+
return value;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
// src/runtime.ts
|
|
1166
|
+
import { spawn } from "node:child_process";
|
|
1167
|
+
import { access as access2, appendFile, mkdir as mkdir2, readFile as readFile2 } from "node:fs/promises";
|
|
1168
|
+
import { accessSync, constants as fsConstants } from "node:fs";
|
|
1169
|
+
import path2 from "node:path";
|
|
1170
|
+
class CommandError extends Error {
|
|
1171
|
+
command;
|
|
1172
|
+
result;
|
|
1173
|
+
constructor(command, result) {
|
|
1174
|
+
super(`Command failed: ${command.join(" ")}`);
|
|
1175
|
+
this.command = command;
|
|
1176
|
+
this.result = result;
|
|
1177
|
+
this.name = "CommandError";
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
function isExecutableAvailable(command) {
|
|
1181
|
+
return findExecutableOnPath(command) !== null;
|
|
1182
|
+
}
|
|
1183
|
+
function buildStopManagedSshdScript() {
|
|
1184
|
+
return [
|
|
1185
|
+
"pids=$(ps -eo pid=,comm= | while read -r pid comm; do",
|
|
1186
|
+
` if [ "$comm" = "sshd" ]; then printf '%s\\n' "$pid"; fi`,
|
|
1187
|
+
"done)",
|
|
1188
|
+
'if [ -n "$pids" ]; then kill $pids; fi'
|
|
1189
|
+
].join(`
|
|
1190
|
+
`);
|
|
1191
|
+
}
|
|
1192
|
+
function getRunnerCredFile(remoteWorkspaceFolder) {
|
|
1193
|
+
const trimmed = remoteWorkspaceFolder.endsWith("/") ? remoteWorkspaceFolder.slice(0, -1) : remoteWorkspaceFolder;
|
|
1194
|
+
return `${trimmed}/${RUNNER_CRED_FILENAME}`;
|
|
1195
|
+
}
|
|
1196
|
+
function getRunnerSummaryLines(output) {
|
|
1197
|
+
return output.split(/\r?\n/).map((line) => line.trimEnd()).filter((line) => /^(SSH user|SSH pass|SSH port|PermitRootLogin): /.test(line));
|
|
1198
|
+
}
|
|
1199
|
+
function formatDevcontainerProgressLine(line) {
|
|
1200
|
+
const cleaned = stripAnsi(line).trim();
|
|
1201
|
+
if (!cleaned) {
|
|
1202
|
+
return null;
|
|
1203
|
+
}
|
|
1204
|
+
try {
|
|
1205
|
+
const parsed = JSON.parse(cleaned);
|
|
1206
|
+
const text = typeof parsed.text === "string" ? stripAnsi(parsed.text).trim() : "";
|
|
1207
|
+
const type = typeof parsed.type === "string" ? parsed.type : "";
|
|
1208
|
+
const level = typeof parsed.level === "number" ? parsed.level : undefined;
|
|
1209
|
+
if (parsed.outcome !== undefined) {
|
|
1210
|
+
return null;
|
|
1211
|
+
}
|
|
1212
|
+
if (!text) {
|
|
1213
|
+
return null;
|
|
1214
|
+
}
|
|
1215
|
+
if (text.startsWith("Error:")) {
|
|
1216
|
+
return text;
|
|
1217
|
+
}
|
|
1218
|
+
if (type === "start") {
|
|
1219
|
+
if (text === "Resolving Remote") {
|
|
1220
|
+
return "Preparing devcontainer...";
|
|
1221
|
+
}
|
|
1222
|
+
if (text === "Starting container") {
|
|
1223
|
+
return "Starting container...";
|
|
1224
|
+
}
|
|
1225
|
+
return null;
|
|
1226
|
+
}
|
|
1227
|
+
if (type === "raw" && text === "Container started") {
|
|
1228
|
+
return "Container started.";
|
|
1229
|
+
}
|
|
1230
|
+
if (text.startsWith("workspace root: ")) {
|
|
1231
|
+
return `Workspace: ${text.slice("workspace root: ".length)}`;
|
|
1232
|
+
}
|
|
1233
|
+
if (text === "No user features to update" || text === "Inspecting container" || text.startsWith("Run: ") || text.startsWith("Run in container: ") || text.startsWith("userEnvProbe") || text.startsWith("LifecycleCommandExecutionMap:") || text.startsWith("Exit code ")) {
|
|
1234
|
+
return null;
|
|
1235
|
+
}
|
|
1236
|
+
if (level !== undefined && level >= 2) {
|
|
1237
|
+
return null;
|
|
1238
|
+
}
|
|
1239
|
+
return text;
|
|
1240
|
+
} catch {
|
|
1241
|
+
return cleaned;
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
function requiresSshAuthSockPermissionFix(sshAuthSockSource) {
|
|
1245
|
+
return sshAuthSockSource === DOCKER_DESKTOP_SSH_AUTH_SOCK_SOURCE;
|
|
1246
|
+
}
|
|
1247
|
+
function buildEnsureSshAuthSockAccessibleScript() {
|
|
1248
|
+
return `if [ -S ${quoteShell(SSH_AUTH_SOCK_TARGET)} ]; then chmod 666 ${quoteShell(SSH_AUTH_SOCK_TARGET)}; fi`;
|
|
1249
|
+
}
|
|
1250
|
+
function getRunnerHostKeysDir(remoteWorkspaceFolder) {
|
|
1251
|
+
const trimmed = remoteWorkspaceFolder.endsWith("/") ? remoteWorkspaceFolder.slice(0, -1) : remoteWorkspaceFolder;
|
|
1252
|
+
return `${trimmed}/${RUNNER_HOST_KEYS_DIRNAME}`;
|
|
1253
|
+
}
|
|
1254
|
+
function buildRestoreRunnerHostKeysScript(remoteWorkspaceFolder) {
|
|
1255
|
+
const hostKeysDir = getRunnerHostKeysDir(remoteWorkspaceFolder);
|
|
1256
|
+
return [
|
|
1257
|
+
`if [ -d ${quoteShell(hostKeysDir)} ]; then`,
|
|
1258
|
+
" mkdir -p /etc/ssh",
|
|
1259
|
+
` find ${quoteShell(hostKeysDir)} -maxdepth 1 -type f -name 'ssh_host_*' -exec cp {} /etc/ssh/ \\;`,
|
|
1260
|
+
" chmod 600 /etc/ssh/ssh_host_*_key 2>/dev/null || true",
|
|
1261
|
+
" chmod 644 /etc/ssh/ssh_host_*_key.pub 2>/dev/null || true",
|
|
1262
|
+
"fi"
|
|
1263
|
+
].join(`
|
|
1264
|
+
`);
|
|
1265
|
+
}
|
|
1266
|
+
function buildPersistRunnerHostKeysScript(remoteWorkspaceFolder) {
|
|
1267
|
+
const hostKeysDir = getRunnerHostKeysDir(remoteWorkspaceFolder);
|
|
1268
|
+
return [
|
|
1269
|
+
`mkdir -p ${quoteShell(hostKeysDir)}`,
|
|
1270
|
+
`find /etc/ssh -maxdepth 1 -type f -name 'ssh_host_*' -exec cp {} ${quoteShell(hostKeysDir)}/ \\;`,
|
|
1271
|
+
`chmod 700 ${quoteShell(hostKeysDir)}`,
|
|
1272
|
+
`chmod 600 ${quoteShell(hostKeysDir)}/ssh_host_*_key 2>/dev/null || true`,
|
|
1273
|
+
`chmod 644 ${quoteShell(hostKeysDir)}/ssh_host_*_key.pub 2>/dev/null || true`
|
|
1274
|
+
].join(`
|
|
1275
|
+
`);
|
|
1276
|
+
}
|
|
1277
|
+
function resolveSshAuthSockSource(input) {
|
|
1278
|
+
const hostEnvSshAuthSock = input.hostEnvSshAuthSock?.trim() || undefined;
|
|
1279
|
+
if (input.dockerDesktopHostServiceAvailable) {
|
|
1280
|
+
return { sshAuthSock: DOCKER_DESKTOP_SSH_AUTH_SOCK_SOURCE };
|
|
1281
|
+
}
|
|
1282
|
+
if (hostEnvSshAuthSock && input.hostEnvSockExists) {
|
|
1283
|
+
return { sshAuthSock: hostEnvSshAuthSock };
|
|
1284
|
+
}
|
|
1285
|
+
const detail = hostEnvSshAuthSock ? `SSH_AUTH_SOCK does not exist: ${hostEnvSshAuthSock}.` : "No usable SSH agent socket was found. Set SSH_AUTH_SOCK or use Docker Desktop host services.";
|
|
1286
|
+
if (input.allowMissingSsh) {
|
|
1287
|
+
return {
|
|
1288
|
+
sshAuthSock: null,
|
|
1289
|
+
warning: `${detail} Continuing without SSH agent sharing.`
|
|
1290
|
+
};
|
|
1291
|
+
}
|
|
1292
|
+
throw new UserError(`${detail} Pass --allow-missing-ssh to continue without SSH agent sharing.`);
|
|
1293
|
+
}
|
|
1294
|
+
async function ensureHostEnvironment(options) {
|
|
1295
|
+
if (process.platform !== "darwin" && process.platform !== "linux") {
|
|
1296
|
+
throw new UserError(`Unsupported platform: ${process.platform}. macOS and Linux are supported in v1.`);
|
|
1297
|
+
}
|
|
1298
|
+
if (!isExecutableAvailable("docker")) {
|
|
1299
|
+
throw new UserError("Docker is required but was not found in PATH.");
|
|
1300
|
+
}
|
|
1301
|
+
if (!isExecutableAvailable("devcontainer")) {
|
|
1302
|
+
throw new UserError("Dev Container CLI is required but was not found in PATH.");
|
|
1303
|
+
}
|
|
1304
|
+
const hostEnvSshAuthSock = process.env.SSH_AUTH_SOCK?.trim() || undefined;
|
|
1305
|
+
const hostEnvSockExists = hostEnvSshAuthSock ? await pathExists(hostEnvSshAuthSock) : false;
|
|
1306
|
+
const dockerDesktopHostServiceAvailable = await hasDockerDesktopHostService();
|
|
1307
|
+
return resolveSshAuthSockSource({
|
|
1308
|
+
hostEnvSshAuthSock,
|
|
1309
|
+
hostEnvSockExists,
|
|
1310
|
+
dockerDesktopHostServiceAvailable,
|
|
1311
|
+
allowMissingSsh: options.allowMissingSsh
|
|
1312
|
+
});
|
|
1313
|
+
}
|
|
1314
|
+
async function ensureGeneratedConfigIgnored(workspacePath, generatedConfigPath) {
|
|
1315
|
+
await ensurePathIgnored(workspacePath, generatedConfigPath);
|
|
1316
|
+
}
|
|
1317
|
+
async function ensurePathIgnored(workspacePath, absolutePath) {
|
|
1318
|
+
const gitTopLevel = await tryGetGitTopLevel(workspacePath);
|
|
1319
|
+
if (!gitTopLevel) {
|
|
1320
|
+
return;
|
|
1321
|
+
}
|
|
1322
|
+
const relative = path2.relative(gitTopLevel, absolutePath);
|
|
1323
|
+
if (!relative || relative.startsWith("..")) {
|
|
1324
|
+
return;
|
|
1325
|
+
}
|
|
1326
|
+
const normalized = `/${relative.split(path2.sep).join("/")}`;
|
|
1327
|
+
const excludePath = path2.join(gitTopLevel, ".git", "info", "exclude");
|
|
1328
|
+
await mkdir2(path2.dirname(excludePath), { recursive: true });
|
|
1329
|
+
let current = "";
|
|
1330
|
+
try {
|
|
1331
|
+
current = await readFile2(excludePath, "utf8");
|
|
1332
|
+
} catch {
|
|
1333
|
+
current = "";
|
|
1334
|
+
}
|
|
1335
|
+
const lines = new Set(current.split(/\r?\n/).filter(Boolean));
|
|
1336
|
+
if (lines.has(normalized)) {
|
|
1337
|
+
return;
|
|
1338
|
+
}
|
|
1339
|
+
const prefix = current.length > 0 && !current.endsWith(`
|
|
1340
|
+
`) ? `
|
|
1341
|
+
` : "";
|
|
1342
|
+
await appendFile(excludePath, `${prefix}${normalized}
|
|
1343
|
+
`, "utf8");
|
|
1344
|
+
}
|
|
1345
|
+
async function listManagedContainers(labels) {
|
|
1346
|
+
const args = ["ps", "-aq"];
|
|
1347
|
+
for (const [key, value] of Object.entries(labels)) {
|
|
1348
|
+
args.push("--filter", `label=${key}=${value}`);
|
|
1349
|
+
}
|
|
1350
|
+
const result = await execute(["docker", ...args], {
|
|
1351
|
+
stdoutMode: "capture",
|
|
1352
|
+
stderrMode: "capture"
|
|
1353
|
+
});
|
|
1354
|
+
return result.stdout.split(/\s+/).map((entry) => entry.trim()).filter(Boolean);
|
|
1355
|
+
}
|
|
1356
|
+
async function inspectContainers(containerIds) {
|
|
1357
|
+
if (containerIds.length === 0) {
|
|
1358
|
+
return [];
|
|
1359
|
+
}
|
|
1360
|
+
const result = await execute(["docker", "inspect", ...containerIds], {
|
|
1361
|
+
stdoutMode: "capture",
|
|
1362
|
+
stderrMode: "capture"
|
|
1363
|
+
});
|
|
1364
|
+
return JSON.parse(result.stdout);
|
|
1365
|
+
}
|
|
1366
|
+
async function assertPortAvailable(port, allowIfManagedContainerOwnsPort) {
|
|
1367
|
+
const pids = await findListeningPids(port);
|
|
1368
|
+
if (pids.length === 0) {
|
|
1369
|
+
return;
|
|
1370
|
+
}
|
|
1371
|
+
if (allowIfManagedContainerOwnsPort) {
|
|
1372
|
+
return;
|
|
1373
|
+
}
|
|
1374
|
+
throw new UserError(`Host port ${port} is already in use by PID(s): ${pids.join(", ")}.`);
|
|
1375
|
+
}
|
|
1376
|
+
async function removeContainers(containerIds) {
|
|
1377
|
+
if (containerIds.length === 0) {
|
|
1378
|
+
return;
|
|
1379
|
+
}
|
|
1380
|
+
await execute(["docker", "rm", "--force", ...containerIds], {
|
|
1381
|
+
stdoutMode: "raw",
|
|
1382
|
+
stderrMode: "raw"
|
|
1383
|
+
});
|
|
1384
|
+
}
|
|
1385
|
+
async function devcontainerUp(input) {
|
|
1386
|
+
await mkdir2(input.userDataDir, { recursive: true });
|
|
1387
|
+
const args = [
|
|
1388
|
+
"devcontainer",
|
|
1389
|
+
"up",
|
|
1390
|
+
"--workspace-folder",
|
|
1391
|
+
input.workspacePath,
|
|
1392
|
+
"--mount-workspace-git-root",
|
|
1393
|
+
"false",
|
|
1394
|
+
"--config",
|
|
1395
|
+
input.generatedConfigPath,
|
|
1396
|
+
"--user-data-folder",
|
|
1397
|
+
input.userDataDir,
|
|
1398
|
+
"--log-format",
|
|
1399
|
+
"json"
|
|
1400
|
+
];
|
|
1401
|
+
for (const [key, value] of Object.entries(input.labels)) {
|
|
1402
|
+
args.push("--id-label", `${key}=${value}`);
|
|
1403
|
+
}
|
|
1404
|
+
const result = await execute(args, {
|
|
1405
|
+
stdoutMode: "devcontainer-json",
|
|
1406
|
+
stderrMode: "devcontainer-json"
|
|
1407
|
+
});
|
|
1408
|
+
const outcome = parseDevcontainerOutcome(result.stdout);
|
|
1409
|
+
if (!outcome || outcome.outcome !== "success" || typeof outcome.containerId !== "string") {
|
|
1410
|
+
throw new UserError("devcontainer up did not return a success outcome.");
|
|
1411
|
+
}
|
|
1412
|
+
return outcome;
|
|
1413
|
+
}
|
|
1414
|
+
async function copyKnownHosts(containerId) {
|
|
1415
|
+
const script = `if [ -f ${quoteShell(KNOWN_HOSTS_TARGET)} ]; then umask 077 && mkdir -p ~/.ssh && cp ${quoteShell(KNOWN_HOSTS_TARGET)} ~/.ssh/known_hosts && chmod 600 ~/.ssh/known_hosts; fi`;
|
|
1416
|
+
await devcontainerExec(containerId, script, { quiet: true });
|
|
1417
|
+
}
|
|
1418
|
+
async function stopManagedSshd(containerId) {
|
|
1419
|
+
await devcontainerExec(containerId, buildStopManagedSshdScript(), { quiet: true });
|
|
1420
|
+
}
|
|
1421
|
+
async function ensureSshAuthSockAccessible(containerId) {
|
|
1422
|
+
await dockerExec(containerId, buildEnsureSshAuthSockAccessibleScript(), {
|
|
1423
|
+
quiet: true,
|
|
1424
|
+
user: "root"
|
|
1425
|
+
});
|
|
1426
|
+
}
|
|
1427
|
+
async function restoreRunnerHostKeys(containerId, remoteWorkspaceFolder) {
|
|
1428
|
+
await dockerExec(containerId, buildRestoreRunnerHostKeysScript(remoteWorkspaceFolder), {
|
|
1429
|
+
quiet: true,
|
|
1430
|
+
user: "root"
|
|
1431
|
+
});
|
|
1432
|
+
}
|
|
1433
|
+
async function startRunner(containerId, port, remoteWorkspaceFolder) {
|
|
1434
|
+
const script = `curl -fsSL ${quoteShell(RUNNER_URL)} | env SSH_PORT=${quoteShell(String(port))} CRED_FILE=${quoteShell(getRunnerCredFile(remoteWorkspaceFolder))} bash`;
|
|
1435
|
+
const result = await devcontainerExec(containerId, script, { quiet: true });
|
|
1436
|
+
const summaryLines = getRunnerSummaryLines(result.stdout);
|
|
1437
|
+
if (summaryLines.length > 0) {
|
|
1438
|
+
console.log(`
|
|
1439
|
+
SSH server:`);
|
|
1440
|
+
for (const line of summaryLines) {
|
|
1441
|
+
console.log(` ${line}`);
|
|
1442
|
+
}
|
|
1443
|
+
} else {
|
|
1444
|
+
const output = result.stdout.trim();
|
|
1445
|
+
if (output) {
|
|
1446
|
+
console.log(output);
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
async function persistRunnerHostKeys(containerId, remoteWorkspaceFolder) {
|
|
1451
|
+
await dockerExec(containerId, buildPersistRunnerHostKeysScript(remoteWorkspaceFolder), {
|
|
1452
|
+
quiet: true,
|
|
1453
|
+
user: "root"
|
|
1454
|
+
});
|
|
1455
|
+
}
|
|
1456
|
+
async function hasDockerDesktopHostService() {
|
|
1457
|
+
const result = await execute(["docker", "info", "--format", "{{.OperatingSystem}}"], {
|
|
1458
|
+
stdoutMode: "capture",
|
|
1459
|
+
stderrMode: "capture",
|
|
1460
|
+
allowFailure: true
|
|
1461
|
+
});
|
|
1462
|
+
if (result.exitCode !== 0) {
|
|
1463
|
+
return false;
|
|
1464
|
+
}
|
|
1465
|
+
return result.stdout.toLowerCase().includes("docker desktop");
|
|
1466
|
+
}
|
|
1467
|
+
async function devcontainerExec(containerId, script, options) {
|
|
1468
|
+
const args = ["devcontainer", "exec", "--container-id", containerId, "sh", "-lc", script];
|
|
1469
|
+
return execute(args, {
|
|
1470
|
+
stdoutMode: options.quiet ? "capture" : "raw",
|
|
1471
|
+
stderrMode: options.quiet ? "capture" : "raw"
|
|
1472
|
+
});
|
|
1473
|
+
}
|
|
1474
|
+
async function dockerExec(containerId, script, options) {
|
|
1475
|
+
const args = ["docker", "exec"];
|
|
1476
|
+
if (options.user) {
|
|
1477
|
+
args.push("--user", options.user);
|
|
1478
|
+
}
|
|
1479
|
+
args.push(containerId, "sh", "-lc", script);
|
|
1480
|
+
return execute(args, {
|
|
1481
|
+
stdoutMode: options.quiet ? "capture" : "raw",
|
|
1482
|
+
stderrMode: options.quiet ? "capture" : "raw"
|
|
1483
|
+
});
|
|
1484
|
+
}
|
|
1485
|
+
async function tryGetGitTopLevel(workspacePath) {
|
|
1486
|
+
try {
|
|
1487
|
+
const result = await execute(["git", "-C", workspacePath, "rev-parse", "--show-toplevel"], {
|
|
1488
|
+
stdoutMode: "capture",
|
|
1489
|
+
stderrMode: "capture",
|
|
1490
|
+
allowFailure: true
|
|
1491
|
+
});
|
|
1492
|
+
if (result.exitCode !== 0) {
|
|
1493
|
+
return null;
|
|
1494
|
+
}
|
|
1495
|
+
const trimmed = result.stdout.trim();
|
|
1496
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
1497
|
+
} catch {
|
|
1498
|
+
return null;
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
async function findListeningPids(port) {
|
|
1502
|
+
if (isExecutableAvailable("lsof")) {
|
|
1503
|
+
const result = await execute(["lsof", "-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], {
|
|
1504
|
+
stdoutMode: "capture",
|
|
1505
|
+
stderrMode: "capture",
|
|
1506
|
+
allowFailure: true
|
|
1507
|
+
});
|
|
1508
|
+
if (result.exitCode === 0) {
|
|
1509
|
+
return result.stdout.split(/\s+/).map((entry) => entry.trim()).filter(Boolean);
|
|
1510
|
+
}
|
|
1511
|
+
if (result.exitCode === 1) {
|
|
1512
|
+
return [];
|
|
1513
|
+
}
|
|
1514
|
+
throw new UserError(`Could not determine whether port ${port} is free because lsof failed with exit code ${result.exitCode}.`);
|
|
1515
|
+
}
|
|
1516
|
+
return [];
|
|
1517
|
+
}
|
|
1518
|
+
async function execute(command, options) {
|
|
1519
|
+
const env = { ...process.env, ...options.env ?? {} };
|
|
1520
|
+
for (const [key, value] of Object.entries(env)) {
|
|
1521
|
+
if (value === undefined) {
|
|
1522
|
+
delete env[key];
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
const subprocess = spawn(command[0], command.slice(1), {
|
|
1526
|
+
cwd: options.cwd,
|
|
1527
|
+
env,
|
|
1528
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1529
|
+
});
|
|
1530
|
+
const stdoutPromise = consumeStream(subprocess.stdout, options.stdoutMode ?? "capture", false);
|
|
1531
|
+
const stderrPromise = consumeStream(subprocess.stderr, options.stderrMode ?? "capture", true);
|
|
1532
|
+
const exitPromise = new Promise((resolve, reject) => {
|
|
1533
|
+
subprocess.once("error", reject);
|
|
1534
|
+
subprocess.once("close", (exitCode2) => {
|
|
1535
|
+
resolve(exitCode2 ?? 0);
|
|
1536
|
+
});
|
|
1537
|
+
});
|
|
1538
|
+
const [stdout, stderr, exitCode] = await Promise.all([stdoutPromise, stderrPromise, exitPromise]);
|
|
1539
|
+
const result = { stdout, stderr, exitCode };
|
|
1540
|
+
if (exitCode !== 0 && !options.allowFailure) {
|
|
1541
|
+
throw new CommandError(command, result);
|
|
1542
|
+
}
|
|
1543
|
+
return result;
|
|
1544
|
+
}
|
|
1545
|
+
async function consumeStream(stream, mode, useStderr) {
|
|
1546
|
+
if (!stream) {
|
|
1547
|
+
return "";
|
|
1548
|
+
}
|
|
1549
|
+
const writer = useStderr ? process.stderr : process.stdout;
|
|
1550
|
+
stream.setEncoding("utf8");
|
|
1551
|
+
let captured = "";
|
|
1552
|
+
let buffered = "";
|
|
1553
|
+
for await (const chunk of stream) {
|
|
1554
|
+
const text = typeof chunk === "string" ? chunk : String(chunk);
|
|
1555
|
+
captured += text;
|
|
1556
|
+
if (mode === "raw") {
|
|
1557
|
+
writer.write(text);
|
|
1558
|
+
continue;
|
|
1559
|
+
}
|
|
1560
|
+
if (mode === "capture") {
|
|
1561
|
+
continue;
|
|
1562
|
+
}
|
|
1563
|
+
buffered += text;
|
|
1564
|
+
let newlineIndex = buffered.indexOf(`
|
|
1565
|
+
`);
|
|
1566
|
+
while (newlineIndex >= 0) {
|
|
1567
|
+
const line = buffered.slice(0, newlineIndex);
|
|
1568
|
+
buffered = buffered.slice(newlineIndex + 1);
|
|
1569
|
+
renderDevcontainerJsonLine(line, writer);
|
|
1570
|
+
newlineIndex = buffered.indexOf(`
|
|
1571
|
+
`);
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
if (mode === "devcontainer-json" && buffered.length > 0) {
|
|
1575
|
+
renderDevcontainerJsonLine(buffered, writer);
|
|
1576
|
+
}
|
|
1577
|
+
return captured;
|
|
1578
|
+
}
|
|
1579
|
+
async function pathExists(targetPath) {
|
|
1580
|
+
try {
|
|
1581
|
+
await access2(targetPath);
|
|
1582
|
+
return true;
|
|
1583
|
+
} catch {
|
|
1584
|
+
return false;
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
function findExecutableOnPath(command) {
|
|
1588
|
+
if (command.includes(path2.sep)) {
|
|
1589
|
+
return isExecutablePath(command) ? command : null;
|
|
1590
|
+
}
|
|
1591
|
+
const pathValue = process.env.PATH ?? "";
|
|
1592
|
+
for (const directory of pathValue.split(path2.delimiter)) {
|
|
1593
|
+
if (!directory) {
|
|
1594
|
+
continue;
|
|
1595
|
+
}
|
|
1596
|
+
const candidate = path2.join(directory, command);
|
|
1597
|
+
if (isExecutablePath(candidate)) {
|
|
1598
|
+
return candidate;
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
return null;
|
|
1602
|
+
}
|
|
1603
|
+
function isExecutablePath(candidate) {
|
|
1604
|
+
try {
|
|
1605
|
+
accessSync(candidate, fsConstants.X_OK);
|
|
1606
|
+
return true;
|
|
1607
|
+
} catch {
|
|
1608
|
+
return false;
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
function renderDevcontainerJsonLine(line, writer) {
|
|
1612
|
+
const formatted = formatDevcontainerProgressLine(line);
|
|
1613
|
+
if (formatted) {
|
|
1614
|
+
writer.write(`${formatted}
|
|
1615
|
+
`);
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
function parseDevcontainerOutcome(stdout) {
|
|
1619
|
+
const lines = stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
1620
|
+
for (let index = lines.length - 1;index >= 0; index -= 1) {
|
|
1621
|
+
try {
|
|
1622
|
+
const parsed = JSON.parse(lines[index]);
|
|
1623
|
+
if (typeof parsed.outcome === "string") {
|
|
1624
|
+
return parsed;
|
|
1625
|
+
}
|
|
1626
|
+
} catch {}
|
|
1627
|
+
}
|
|
1628
|
+
return null;
|
|
1629
|
+
}
|
|
1630
|
+
function isCommandError(error) {
|
|
1631
|
+
return error instanceof CommandError;
|
|
1632
|
+
}
|
|
1633
|
+
function formatCommandError(error) {
|
|
1634
|
+
const stderr = error.result.stderr.trim();
|
|
1635
|
+
const stdout = error.result.stdout.trim();
|
|
1636
|
+
const details = stderr || stdout;
|
|
1637
|
+
if (!details) {
|
|
1638
|
+
return `${error.message} (exit ${error.result.exitCode})`;
|
|
1639
|
+
}
|
|
1640
|
+
return `${error.message}
|
|
1641
|
+
${details}`;
|
|
1642
|
+
}
|
|
1643
|
+
function stripAnsi(text) {
|
|
1644
|
+
return text.replace(/\u001B\[[0-9;]*m/g, "");
|
|
1645
|
+
}
|
|
1646
|
+
function labelsForWorkspaceHash(workspaceHash) {
|
|
1647
|
+
return {
|
|
1648
|
+
[MANAGED_LABEL_KEY]: "true",
|
|
1649
|
+
[WORKSPACE_LABEL_KEY]: workspaceHash
|
|
1650
|
+
};
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
// src/cli.ts
|
|
1654
|
+
async function main() {
|
|
1655
|
+
const parsed = parseArgs(process.argv.slice(2));
|
|
1656
|
+
if (parsed.command === "help") {
|
|
1657
|
+
console.log(helpText());
|
|
1658
|
+
return;
|
|
1659
|
+
}
|
|
1660
|
+
const workspacePath = await realpath(process.cwd());
|
|
1661
|
+
const state = await loadWorkspaceState(workspacePath);
|
|
1662
|
+
if (parsed.command === "down") {
|
|
1663
|
+
await handleDown(workspacePath, state);
|
|
1664
|
+
return;
|
|
1665
|
+
}
|
|
1666
|
+
await handleUpLike(parsed.command, workspacePath, state, parsed.port, parsed.allowMissingSsh);
|
|
1667
|
+
}
|
|
1668
|
+
async function handleUpLike(command, workspacePath, state, explicitPort, allowMissingSsh) {
|
|
1669
|
+
const environment = await ensureHostEnvironment({ allowMissingSsh });
|
|
1670
|
+
const port = resolvePort(command, explicitPort, state);
|
|
1671
|
+
const knownHostsPath = await getKnownHostsPath();
|
|
1672
|
+
const discovered = await discoverDevcontainerConfig(workspacePath);
|
|
1673
|
+
const generatedConfigPath = getGeneratedConfigPath(discovered.path);
|
|
1674
|
+
const legacyGeneratedConfigPath = getLegacyGeneratedConfigPath(discovered.path);
|
|
1675
|
+
const workspaceHash = hashWorkspacePath(workspacePath);
|
|
1676
|
+
const labels = getManagedLabels(workspaceHash);
|
|
1677
|
+
const userDataDir = getWorkspaceUserDataDir(workspacePath);
|
|
1678
|
+
const containerName = getManagedContainerName(workspacePath, port);
|
|
1679
|
+
const managedConfig = buildManagedConfig(discovered.config, {
|
|
1680
|
+
port,
|
|
1681
|
+
containerName,
|
|
1682
|
+
sshAuthSock: environment.sshAuthSock,
|
|
1683
|
+
knownHostsPath
|
|
1684
|
+
});
|
|
1685
|
+
if (environment.warning) {
|
|
1686
|
+
console.warn(`Warning: ${environment.warning}`);
|
|
1687
|
+
}
|
|
1688
|
+
await ensureGeneratedConfigIgnored(workspacePath, generatedConfigPath);
|
|
1689
|
+
await removeGeneratedConfig(legacyGeneratedConfigPath);
|
|
1690
|
+
await writeManagedConfig(generatedConfigPath, managedConfig);
|
|
1691
|
+
const existingContainerIds = await listManagedContainers(labels);
|
|
1692
|
+
if (command === "up" && existingContainerIds.length > 1) {
|
|
1693
|
+
throw new UserError("More than one managed container was found for this workspace. Run `devbox down` first.");
|
|
1694
|
+
}
|
|
1695
|
+
let existingInspects = [];
|
|
1696
|
+
if (existingContainerIds.length > 0) {
|
|
1697
|
+
existingInspects = await inspectContainers(existingContainerIds);
|
|
1698
|
+
}
|
|
1699
|
+
if (command === "rebuild") {
|
|
1700
|
+
await removeContainers(existingContainerIds);
|
|
1701
|
+
existingInspects = [];
|
|
1702
|
+
} else if (existingInspects[0]) {
|
|
1703
|
+
const publishedPorts = getPublishedHostPorts(existingInspects[0]);
|
|
1704
|
+
if (publishedPorts.length > 0 && !publishedPorts.includes(port)) {
|
|
1705
|
+
throw new UserError(`This workspace already has a managed container publishing port(s) ${publishedPorts.join(", ")}. Use \`devbox rebuild ${port}\` to change the port.`);
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
const allowCurrentPort = existingInspects.some((container) => container.State?.Running && getPublishedHostPorts(container).includes(port));
|
|
1709
|
+
await assertPortAvailable(port, allowCurrentPort);
|
|
1710
|
+
console.log(`Starting workspace on port ${port}...`);
|
|
1711
|
+
const upResult = await devcontainerUp({
|
|
1712
|
+
workspacePath,
|
|
1713
|
+
generatedConfigPath,
|
|
1714
|
+
userDataDir,
|
|
1715
|
+
labels
|
|
1716
|
+
});
|
|
1717
|
+
const remoteWorkspaceFolder = upResult.remoteWorkspaceFolder ?? getDefaultRemoteWorkspaceFolder(workspacePath);
|
|
1718
|
+
console.log("Configuring SSH access inside the devcontainer...");
|
|
1719
|
+
await ensurePathIgnored(workspacePath, path3.join(workspacePath, RUNNER_HOST_KEYS_DIRNAME));
|
|
1720
|
+
if (requiresSshAuthSockPermissionFix(environment.sshAuthSock)) {
|
|
1721
|
+
console.log("Making the forwarded SSH agent socket accessible to the container user...");
|
|
1722
|
+
await ensureSshAuthSockAccessible(upResult.containerId);
|
|
1723
|
+
}
|
|
1724
|
+
await copyKnownHosts(upResult.containerId);
|
|
1725
|
+
await stopManagedSshd(upResult.containerId);
|
|
1726
|
+
await restoreRunnerHostKeys(upResult.containerId, remoteWorkspaceFolder);
|
|
1727
|
+
console.log("Installing and starting the SSH server inside the container (first run can take a bit)...");
|
|
1728
|
+
await startRunner(upResult.containerId, port, remoteWorkspaceFolder);
|
|
1729
|
+
console.log("Saving SSH server state for future runs...");
|
|
1730
|
+
await persistRunnerHostKeys(upResult.containerId, remoteWorkspaceFolder);
|
|
1731
|
+
await saveWorkspaceState(createWorkspaceState({
|
|
1732
|
+
workspacePath,
|
|
1733
|
+
port,
|
|
1734
|
+
sourceConfigPath: discovered.path,
|
|
1735
|
+
generatedConfigPath,
|
|
1736
|
+
userDataDir,
|
|
1737
|
+
labels,
|
|
1738
|
+
containerId: upResult.containerId
|
|
1739
|
+
}));
|
|
1740
|
+
console.log(`
|
|
1741
|
+
Ready. ${upResult.containerId.slice(0, 12)} is available on port ${port}.`);
|
|
1742
|
+
if (!knownHostsPath) {
|
|
1743
|
+
console.log("Host known_hosts was not found, so only SSH agent sharing was configured.");
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
async function handleDown(workspacePath, state) {
|
|
1747
|
+
if (!isExecutableAvailable("docker")) {
|
|
1748
|
+
throw new UserError("Docker is required but was not found in PATH.");
|
|
1749
|
+
}
|
|
1750
|
+
const labels = labelsForWorkspaceHash(hashWorkspacePath(workspacePath));
|
|
1751
|
+
const containerIds = await listManagedContainers(labels);
|
|
1752
|
+
await removeContainers(containerIds);
|
|
1753
|
+
const generatedConfigPaths = new Set;
|
|
1754
|
+
if (state?.generatedConfigPath) {
|
|
1755
|
+
generatedConfigPaths.add(state.generatedConfigPath);
|
|
1756
|
+
}
|
|
1757
|
+
try {
|
|
1758
|
+
const discovered = await discoverDevcontainerConfig(workspacePath);
|
|
1759
|
+
generatedConfigPaths.add(getGeneratedConfigPath(discovered.path));
|
|
1760
|
+
generatedConfigPaths.add(getLegacyGeneratedConfigPath(discovered.path));
|
|
1761
|
+
} catch {}
|
|
1762
|
+
for (const generatedConfigPath of generatedConfigPaths) {
|
|
1763
|
+
await removeGeneratedConfig(generatedConfigPath);
|
|
1764
|
+
}
|
|
1765
|
+
await deleteWorkspaceState(workspacePath);
|
|
1766
|
+
if (containerIds.length === 0) {
|
|
1767
|
+
console.log("No managed container was running for this workspace.");
|
|
1768
|
+
return;
|
|
1769
|
+
}
|
|
1770
|
+
console.log(`Removed ${containerIds.length} managed container(s). Workspace-mounted SSH credentials and host keys were preserved.`);
|
|
1771
|
+
}
|
|
1772
|
+
function getPublishedHostPorts(container) {
|
|
1773
|
+
const ports = container.NetworkSettings?.Ports ?? {};
|
|
1774
|
+
const values = new Set;
|
|
1775
|
+
for (const bindings of Object.values(ports)) {
|
|
1776
|
+
if (!bindings) {
|
|
1777
|
+
continue;
|
|
1778
|
+
}
|
|
1779
|
+
for (const binding of bindings) {
|
|
1780
|
+
if (!binding?.HostPort) {
|
|
1781
|
+
continue;
|
|
1782
|
+
}
|
|
1783
|
+
const parsed = Number(binding.HostPort);
|
|
1784
|
+
if (Number.isInteger(parsed)) {
|
|
1785
|
+
values.add(parsed);
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
return [...values];
|
|
1790
|
+
}
|
|
1791
|
+
main().catch((error) => {
|
|
1792
|
+
if (error instanceof UserError) {
|
|
1793
|
+
console.error(`Error: ${error.message}`);
|
|
1794
|
+
process.exit(1);
|
|
1795
|
+
}
|
|
1796
|
+
if (isCommandError(error)) {
|
|
1797
|
+
console.error(`Error: ${formatCommandError(error)}`);
|
|
1798
|
+
process.exit(error.result.exitCode || 1);
|
|
1799
|
+
}
|
|
1800
|
+
console.error(error);
|
|
1801
|
+
process.exit(1);
|
|
1802
|
+
});
|