@ycodium-ai/plugin-browser-workbench 0.2.2098
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 +99 -0
- package/dist/index.js +1327 -0
- package/locales/en.json +30 -0
- package/package.json +50 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1327 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { definePlugin } from "@ycodium-ai/plugin-api";
|
|
3
|
+
|
|
4
|
+
// src/catalog.ts
|
|
5
|
+
var catalog = {
|
|
6
|
+
settings: {
|
|
7
|
+
card: {
|
|
8
|
+
title: "External workbench",
|
|
9
|
+
description: "Point this machine at a code-server you already run. Ycodium only forwards the port; it does not start or stop that process."
|
|
10
|
+
},
|
|
11
|
+
attachPort: {
|
|
12
|
+
label: "code-server port",
|
|
13
|
+
description: "Loopback port of a code-server already listening on this machine. Leave empty to use the official VS Code CLI instead."
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
failure: {
|
|
17
|
+
"license-required": "Confirm the VS Code Server license terms to continue.",
|
|
18
|
+
"download-failed": "The VS Code CLI could not be downloaded.",
|
|
19
|
+
"disk-full": "This machine does not have enough disk space to install the VS Code CLI.",
|
|
20
|
+
"launch-failed": "The VS Code CLI started but did not publish a local address in time.",
|
|
21
|
+
"port-in-use": "The local port for the workbench is already in use.",
|
|
22
|
+
"server-download-failed": "The VS Code server did not become ready in time.",
|
|
23
|
+
"grant-expired": "The desktop connection grant expired.",
|
|
24
|
+
"host-key-mismatch": "This machine's key does not match the pinned host key.",
|
|
25
|
+
"channel-failed": "The desktop could not open a channel to the workbench.",
|
|
26
|
+
unknown: "The workbench could not be opened."
|
|
27
|
+
},
|
|
28
|
+
attach: {
|
|
29
|
+
noListener: "Nothing is listening on 127.0.0.1:{{port}}."
|
|
30
|
+
},
|
|
31
|
+
running: {
|
|
32
|
+
stopFirst: "Stop the workbench before updating the CLI."
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
var failureMessage = (kind) => catalog.failure[kind];
|
|
36
|
+
var attachNoListenerMessage = (port) => catalog.attach.noListener.replace("{{port}}", String(port));
|
|
37
|
+
|
|
38
|
+
// src/progress.ts
|
|
39
|
+
var PROGRESS_BUFFER_LIMIT = 64;
|
|
40
|
+
var computeTransfer = (input) => {
|
|
41
|
+
const elapsedSec = (input.nowMs - input.startedAtMs) / 1e3;
|
|
42
|
+
return {
|
|
43
|
+
transferredBytes: input.transferredBytes,
|
|
44
|
+
totalBytes: input.totalBytes,
|
|
45
|
+
bytesPerSecond: elapsedSec > 0 ? input.transferredBytes / elapsedSec : null
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
var progressEvent = (input) => ({
|
|
49
|
+
stage: input.stage,
|
|
50
|
+
status: input.status,
|
|
51
|
+
at: input.at ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
52
|
+
...input.detail === void 0 ? {} : { detail: input.detail },
|
|
53
|
+
...input.skipReason === void 0 ? {} : { skipReason: input.skipReason },
|
|
54
|
+
...input.transfer === void 0 ? {} : { transfer: input.transfer },
|
|
55
|
+
...input.failureKind === void 0 ? {} : { failureKind: input.failureKind }
|
|
56
|
+
});
|
|
57
|
+
var ProgressBus = class {
|
|
58
|
+
listeners = /* @__PURE__ */ new Set();
|
|
59
|
+
latest = [];
|
|
60
|
+
emit(event) {
|
|
61
|
+
this.latest = [...this.latest, event].slice(-PROGRESS_BUFFER_LIMIT);
|
|
62
|
+
for (const listener of this.listeners) listener(event);
|
|
63
|
+
}
|
|
64
|
+
subscribe(listener) {
|
|
65
|
+
for (const event of this.latest) listener(event);
|
|
66
|
+
this.listeners.add(listener);
|
|
67
|
+
return () => {
|
|
68
|
+
this.listeners.delete(listener);
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// src/runtime.ts
|
|
74
|
+
import * as NodeFSP3 from "node:fs/promises";
|
|
75
|
+
|
|
76
|
+
// src/attach.ts
|
|
77
|
+
import * as NodeNet from "node:net";
|
|
78
|
+
var ATTACH_PROBE_TIMEOUT_MS = 1e3;
|
|
79
|
+
var probeLoopback = (port, timeoutMs = ATTACH_PROBE_TIMEOUT_MS) => new Promise((resolve) => {
|
|
80
|
+
const socket = NodeNet.connect({ host: "127.0.0.1", port });
|
|
81
|
+
const finish = (ok) => {
|
|
82
|
+
socket.removeAllListeners();
|
|
83
|
+
socket.destroy();
|
|
84
|
+
resolve(ok);
|
|
85
|
+
};
|
|
86
|
+
const timer = setTimeout(() => finish(false), timeoutMs);
|
|
87
|
+
socket.once("connect", () => {
|
|
88
|
+
clearTimeout(timer);
|
|
89
|
+
finish(true);
|
|
90
|
+
});
|
|
91
|
+
socket.once("error", () => {
|
|
92
|
+
clearTimeout(timer);
|
|
93
|
+
finish(false);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// src/cli.ts
|
|
98
|
+
import * as NodeFS from "node:fs";
|
|
99
|
+
import * as NodeFSP2 from "node:fs/promises";
|
|
100
|
+
import * as NodeProcess2 from "node:process";
|
|
101
|
+
import * as NodeStream from "node:stream";
|
|
102
|
+
import * as NodeStreamPromises from "node:stream/promises";
|
|
103
|
+
|
|
104
|
+
// src/archive.ts
|
|
105
|
+
import * as NodeZlib from "node:zlib";
|
|
106
|
+
|
|
107
|
+
// src/types.ts
|
|
108
|
+
var WORKBENCH_PROGRESS_STAGES = [
|
|
109
|
+
"license",
|
|
110
|
+
"check-cli",
|
|
111
|
+
"download-cli",
|
|
112
|
+
"start-server",
|
|
113
|
+
"download-server",
|
|
114
|
+
"register-port"
|
|
115
|
+
];
|
|
116
|
+
var WORKBENCH_PROGRESS_STATUSES = [
|
|
117
|
+
"started",
|
|
118
|
+
"progress",
|
|
119
|
+
"output",
|
|
120
|
+
"succeeded",
|
|
121
|
+
"skipped",
|
|
122
|
+
"failed"
|
|
123
|
+
];
|
|
124
|
+
var WORKBENCH_FAILURE_KINDS = [
|
|
125
|
+
"license-required",
|
|
126
|
+
"download-failed",
|
|
127
|
+
"disk-full",
|
|
128
|
+
"launch-failed",
|
|
129
|
+
"port-in-use",
|
|
130
|
+
"server-download-failed",
|
|
131
|
+
"grant-expired",
|
|
132
|
+
"host-key-mismatch",
|
|
133
|
+
"channel-failed",
|
|
134
|
+
"unknown"
|
|
135
|
+
];
|
|
136
|
+
var WorkbenchFailure = class extends Error {
|
|
137
|
+
code;
|
|
138
|
+
constructor(code, message) {
|
|
139
|
+
super(message);
|
|
140
|
+
this.name = "WorkbenchFailure";
|
|
141
|
+
this.code = code;
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// src/archive.ts
|
|
146
|
+
var TAR_BLOCK = 512;
|
|
147
|
+
var ZIP_LOCAL = 67324752;
|
|
148
|
+
var ZIP_CENTRAL = 33639248;
|
|
149
|
+
var ZIP_EOCD = 101010256;
|
|
150
|
+
var isCliBinaryName = (name) => {
|
|
151
|
+
const base = name.split("/").pop() ?? name;
|
|
152
|
+
return base === "code" || base === "code.exe";
|
|
153
|
+
};
|
|
154
|
+
var gunzip2 = (input) => new Promise((resolve, reject) => {
|
|
155
|
+
NodeZlib.gunzip(input, (error, result) => {
|
|
156
|
+
if (error) {
|
|
157
|
+
reject(error);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
resolve(result);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
var readOctal = (bytes, start, length) => {
|
|
164
|
+
const raw = bytes.subarray(start, start + length).toString("utf8").replaceAll("\0", "").trim();
|
|
165
|
+
if (raw.length === 0) return 0;
|
|
166
|
+
const parsed = Number.parseInt(raw, 8);
|
|
167
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
168
|
+
};
|
|
169
|
+
var tarChecksumOk = (header) => {
|
|
170
|
+
const stored = readOctal(header, 148, 8);
|
|
171
|
+
let sum = 0;
|
|
172
|
+
for (let i = 0; i < TAR_BLOCK; i += 1) {
|
|
173
|
+
sum += i >= 148 && i < 156 ? 32 : header[i] ?? 0;
|
|
174
|
+
}
|
|
175
|
+
return sum === stored;
|
|
176
|
+
};
|
|
177
|
+
var extractTar = (archive) => {
|
|
178
|
+
const buf = Buffer.from(archive);
|
|
179
|
+
const files = [];
|
|
180
|
+
let offset = 0;
|
|
181
|
+
while (offset + TAR_BLOCK <= buf.length) {
|
|
182
|
+
const header = buf.subarray(offset, offset + TAR_BLOCK);
|
|
183
|
+
if (header.every((byte) => byte === 0)) break;
|
|
184
|
+
if (!tarChecksumOk(header)) {
|
|
185
|
+
throw new WorkbenchFailure("download-failed", "The CLI archive is not a valid tar.");
|
|
186
|
+
}
|
|
187
|
+
const name = header.subarray(0, 100).toString("utf8").replaceAll("\0", "").trim();
|
|
188
|
+
const prefix = header.subarray(345, 345 + 155).toString("utf8").replaceAll("\0", "").trim();
|
|
189
|
+
const fullName = prefix.length > 0 ? `${prefix}/${name}` : name;
|
|
190
|
+
const size = readOctal(header, 124, 12);
|
|
191
|
+
const type = header[156] === 0 ? 48 : header[156];
|
|
192
|
+
offset += TAR_BLOCK;
|
|
193
|
+
const content = buf.subarray(offset, offset + size);
|
|
194
|
+
offset += Math.ceil(size / TAR_BLOCK) * TAR_BLOCK;
|
|
195
|
+
if ((type === 48 || type === 0) && fullName.length > 0 && size > 0) {
|
|
196
|
+
files.push({ name: fullName, content: new Uint8Array(content) });
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return files;
|
|
200
|
+
};
|
|
201
|
+
var readU32 = (buf, offset) => buf.readUInt32LE(offset);
|
|
202
|
+
var readU16 = (buf, offset) => buf.readUInt16LE(offset);
|
|
203
|
+
var inflateRaw2 = (input) => new Promise((resolve, reject) => {
|
|
204
|
+
NodeZlib.inflateRaw(input, (error, result) => {
|
|
205
|
+
if (error) {
|
|
206
|
+
reject(error);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
resolve(result);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
var extractZip = async (archive) => {
|
|
213
|
+
const buf = Buffer.from(archive);
|
|
214
|
+
const files = [];
|
|
215
|
+
let offset = 0;
|
|
216
|
+
while (offset + 30 <= buf.length) {
|
|
217
|
+
const sig = readU32(buf, offset);
|
|
218
|
+
if (sig === ZIP_CENTRAL || sig === ZIP_EOCD) break;
|
|
219
|
+
if (sig !== ZIP_LOCAL) {
|
|
220
|
+
throw new WorkbenchFailure("download-failed", "The CLI archive is not a valid zip.");
|
|
221
|
+
}
|
|
222
|
+
const flags = readU16(buf, offset + 6);
|
|
223
|
+
const method = readU16(buf, offset + 8);
|
|
224
|
+
const compressedSize = readU32(buf, offset + 18);
|
|
225
|
+
const nameLength = readU16(buf, offset + 26);
|
|
226
|
+
const extraLength = readU16(buf, offset + 28);
|
|
227
|
+
const nameStart = offset + 30;
|
|
228
|
+
const name = buf.subarray(nameStart, nameStart + nameLength).toString("utf8");
|
|
229
|
+
if ((flags & 1) !== 0) {
|
|
230
|
+
throw new WorkbenchFailure("download-failed", "The CLI zip is encrypted.");
|
|
231
|
+
}
|
|
232
|
+
if ((flags & 8) !== 0) {
|
|
233
|
+
throw new WorkbenchFailure("download-failed", "The CLI zip uses a data descriptor.");
|
|
234
|
+
}
|
|
235
|
+
const dataStart = nameStart + nameLength + extraLength;
|
|
236
|
+
const compressed = buf.subarray(dataStart, dataStart + compressedSize);
|
|
237
|
+
offset = dataStart + compressedSize;
|
|
238
|
+
if (name.endsWith("/")) continue;
|
|
239
|
+
let content;
|
|
240
|
+
if (method === 0) content = new Uint8Array(compressed);
|
|
241
|
+
else if (method === 8) content = await inflateRaw2(compressed);
|
|
242
|
+
else {
|
|
243
|
+
throw new WorkbenchFailure(
|
|
244
|
+
"download-failed",
|
|
245
|
+
`The CLI zip uses compression ${String(method)}.`
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
files.push({ name, content });
|
|
249
|
+
}
|
|
250
|
+
return files;
|
|
251
|
+
};
|
|
252
|
+
var pickCliBinary = (files) => {
|
|
253
|
+
const named = files.find((file) => isCliBinaryName(file.name));
|
|
254
|
+
if (named !== void 0) return named.content;
|
|
255
|
+
if (files.length === 1 && files[0] !== void 0) return files[0].content;
|
|
256
|
+
throw new WorkbenchFailure("download-failed", "The CLI archive did not contain a code binary.");
|
|
257
|
+
};
|
|
258
|
+
var extractCliBinary = async (archive, kind) => {
|
|
259
|
+
if (kind === "tar.gz") {
|
|
260
|
+
const tar = await gunzip2(archive);
|
|
261
|
+
return pickCliBinary(extractTar(tar));
|
|
262
|
+
}
|
|
263
|
+
return pickCliBinary(await extractZip(archive));
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
// src/platform.ts
|
|
267
|
+
import * as NodeProcess from "node:process";
|
|
268
|
+
var CLI_DOWNLOAD_BASE_URL = "https://update.code.visualstudio.com/latest";
|
|
269
|
+
var CLI_PROGRAM_SELECTOR = "data:bin/code";
|
|
270
|
+
var CLI_RELATIVE_PATH = "bin/code";
|
|
271
|
+
var detectCliPlatform = (platform3 = NodeProcess.platform, arch3 = NodeProcess.arch) => {
|
|
272
|
+
if (platform3 === "linux" && arch3 === "x64") return "linux-x64";
|
|
273
|
+
if (platform3 === "linux" && arch3 === "arm64") return "linux-arm64";
|
|
274
|
+
if (platform3 === "darwin" && arch3 === "x64") return "darwin-x64";
|
|
275
|
+
if (platform3 === "darwin" && arch3 === "arm64") return "darwin-arm64";
|
|
276
|
+
if (platform3 === "win32" && arch3 === "x64") return "win32-x64";
|
|
277
|
+
return null;
|
|
278
|
+
};
|
|
279
|
+
var cliDownloadUrl = (cliPlatform) => `${CLI_DOWNLOAD_BASE_URL}/cli-${cliPlatform}/stable`;
|
|
280
|
+
var cliArchiveKind = (cliPlatform) => cliPlatform.startsWith("win32") ? "zip" : "tar.gz";
|
|
281
|
+
|
|
282
|
+
// src/paths.ts
|
|
283
|
+
import * as NodeCrypto from "node:crypto";
|
|
284
|
+
import * as NodeFSP from "node:fs/promises";
|
|
285
|
+
import * as NodePath from "node:path";
|
|
286
|
+
var workbenchPaths = (dataDir) => ({
|
|
287
|
+
dataDir,
|
|
288
|
+
binDir: NodePath.join(dataDir, "bin"),
|
|
289
|
+
cliPath: NodePath.join(dataDir, CLI_RELATIVE_PATH),
|
|
290
|
+
cliDataDir: NodePath.join(dataDir, "cli-data"),
|
|
291
|
+
serverDataDir: NodePath.join(dataDir, "server-data"),
|
|
292
|
+
tokenPath: NodePath.join(dataDir, "serve-web-token"),
|
|
293
|
+
instancePath: NodePath.join(dataDir, "instance.json"),
|
|
294
|
+
licensePath: NodePath.join(dataDir, "license-accepted.json"),
|
|
295
|
+
configPath: NodePath.join(dataDir, "config.json")
|
|
296
|
+
});
|
|
297
|
+
var writeAtomic = async (filePath, contents, mode) => {
|
|
298
|
+
await NodeFSP.mkdir(NodePath.dirname(filePath), { recursive: true });
|
|
299
|
+
const tmp = `${filePath}.${NodeCrypto.randomBytes(8).toString("hex")}.tmp`;
|
|
300
|
+
const payload = typeof contents === "string" ? contents : Buffer.from(contents);
|
|
301
|
+
try {
|
|
302
|
+
await NodeFSP.writeFile(tmp, payload, mode === void 0 ? {} : { mode });
|
|
303
|
+
await NodeFSP.rename(tmp, filePath);
|
|
304
|
+
if (mode !== void 0) await NodeFSP.chmod(filePath, mode);
|
|
305
|
+
} catch (error) {
|
|
306
|
+
await NodeFSP.unlink(tmp).catch(() => void 0);
|
|
307
|
+
throw error;
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
var fileExists = async (filePath) => {
|
|
311
|
+
try {
|
|
312
|
+
await NodeFSP.access(filePath);
|
|
313
|
+
return true;
|
|
314
|
+
} catch {
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
var readJsonFile = async (filePath) => {
|
|
319
|
+
try {
|
|
320
|
+
const text = await NodeFSP.readFile(filePath, "utf8");
|
|
321
|
+
return JSON.parse(text);
|
|
322
|
+
} catch {
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
var writeJsonFile = async (filePath, value, mode) => {
|
|
327
|
+
await writeAtomic(filePath, `${JSON.stringify(value)}
|
|
328
|
+
`, mode);
|
|
329
|
+
};
|
|
330
|
+
var removeFile = async (filePath) => {
|
|
331
|
+
await NodeFSP.unlink(filePath).catch(() => void 0);
|
|
332
|
+
};
|
|
333
|
+
var readInstance = (paths) => readJsonFile(paths.instancePath);
|
|
334
|
+
var writeInstance = (paths, record) => writeJsonFile(paths.instancePath, record);
|
|
335
|
+
var readConfigFile = (paths) => readJsonFile(paths.configPath);
|
|
336
|
+
var writeConfigFile = (paths, value) => writeJsonFile(paths.configPath, value);
|
|
337
|
+
var writeLicenseAccepted = async (paths) => {
|
|
338
|
+
await writeJsonFile(paths.licensePath, { acceptedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
339
|
+
};
|
|
340
|
+
var TOKEN_BYTES = 32;
|
|
341
|
+
var readServeWebToken = async (paths) => {
|
|
342
|
+
try {
|
|
343
|
+
const text = (await NodeFSP.readFile(paths.tokenPath, "utf8")).trim();
|
|
344
|
+
return text.length > 0 ? text : null;
|
|
345
|
+
} catch {
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
var ensureServeWebToken = async (paths) => {
|
|
350
|
+
const existing = await readServeWebToken(paths);
|
|
351
|
+
if (existing !== null) return existing;
|
|
352
|
+
const token = NodeCrypto.randomBytes(TOKEN_BYTES).toString("base64url");
|
|
353
|
+
await writeAtomic(paths.tokenPath, `${token}
|
|
354
|
+
`, 384);
|
|
355
|
+
return token;
|
|
356
|
+
};
|
|
357
|
+
var ensureDataDirs = async (paths) => {
|
|
358
|
+
await NodeFSP.mkdir(paths.binDir, { recursive: true });
|
|
359
|
+
await NodeFSP.mkdir(paths.cliDataDir, { recursive: true });
|
|
360
|
+
await NodeFSP.mkdir(paths.serverDataDir, { recursive: true });
|
|
361
|
+
};
|
|
362
|
+
var directorySize = async (dir) => {
|
|
363
|
+
let total = 0;
|
|
364
|
+
const walk = async (current) => {
|
|
365
|
+
let entries;
|
|
366
|
+
try {
|
|
367
|
+
entries = await NodeFSP.readdir(current, { withFileTypes: true });
|
|
368
|
+
} catch {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
for (const entry of entries) {
|
|
372
|
+
const child = NodePath.join(current, entry.name);
|
|
373
|
+
if (entry.isDirectory()) {
|
|
374
|
+
await walk(child);
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
try {
|
|
378
|
+
total += (await NodeFSP.stat(child)).size;
|
|
379
|
+
} catch {
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
await walk(dir);
|
|
384
|
+
return total;
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
// src/util.ts
|
|
388
|
+
var messageOf = (error) => error instanceof Error ? error.message : String(error);
|
|
389
|
+
var isDiskFull = (error) => {
|
|
390
|
+
if (typeof error !== "object" || error === null || !("code" in error)) return false;
|
|
391
|
+
return error.code === "ENOSPC";
|
|
392
|
+
};
|
|
393
|
+
var parsePort = (value) => {
|
|
394
|
+
const asNumber = (input) => Number.isInteger(input) && input >= 1 && input <= 65535 ? input : null;
|
|
395
|
+
if (typeof value === "number") return asNumber(value);
|
|
396
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
397
|
+
const parsed = Number(value);
|
|
398
|
+
return Number.isFinite(parsed) ? asNumber(parsed) : null;
|
|
399
|
+
}
|
|
400
|
+
return null;
|
|
401
|
+
};
|
|
402
|
+
var redactWorkbenchSecrets = (line) => line.replaceAll(/([?&]tkn=)[^&\s]+/gi, "$1<redacted>");
|
|
403
|
+
var isProcessAlive = (pid) => {
|
|
404
|
+
try {
|
|
405
|
+
process.kill(pid, 0);
|
|
406
|
+
return true;
|
|
407
|
+
} catch {
|
|
408
|
+
return false;
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
var sleep = (ms, signal) => new Promise((resolve, reject) => {
|
|
412
|
+
if (signal?.aborted) {
|
|
413
|
+
reject(signal.reason instanceof Error ? signal.reason : new Error("aborted"));
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
const timer = setTimeout(() => {
|
|
417
|
+
signal?.removeEventListener("abort", onAbort);
|
|
418
|
+
resolve();
|
|
419
|
+
}, ms);
|
|
420
|
+
const onAbort = () => {
|
|
421
|
+
clearTimeout(timer);
|
|
422
|
+
reject(signal?.reason instanceof Error ? signal.reason : new Error("aborted"));
|
|
423
|
+
};
|
|
424
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
// src/cli.ts
|
|
428
|
+
var CLI_DOWNLOAD_SUFFIX = ".download";
|
|
429
|
+
var CLI_NEW_SUFFIX = ".new";
|
|
430
|
+
var parseCliVersion = (stdout) => {
|
|
431
|
+
const match = /(?:^|\n)code\s+(\S+)/.exec(stdout);
|
|
432
|
+
const version = match?.[1];
|
|
433
|
+
return version === void 0 || version.length === 0 ? null : version;
|
|
434
|
+
};
|
|
435
|
+
var cliIsInstalled = (paths) => fileExists(paths.cliPath);
|
|
436
|
+
var readInstalledCliVersion = async (processApi) => {
|
|
437
|
+
const result = await processApi.run({
|
|
438
|
+
program: CLI_PROGRAM_SELECTOR,
|
|
439
|
+
args: ["--version"],
|
|
440
|
+
timeoutMs: 15e3
|
|
441
|
+
});
|
|
442
|
+
if (result.exitCode !== 0) {
|
|
443
|
+
throw new WorkbenchFailure("unknown", "The VS Code CLI did not report a version.");
|
|
444
|
+
}
|
|
445
|
+
return parseCliVersion(result.stdout) ?? parseCliVersion(result.stderr) ?? "unknown";
|
|
446
|
+
};
|
|
447
|
+
var contentLengthOf = (response) => {
|
|
448
|
+
const raw = response.headers.get("content-length");
|
|
449
|
+
if (raw === null || raw.trim() === "") return null;
|
|
450
|
+
const parsed = Number(raw);
|
|
451
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : null;
|
|
452
|
+
};
|
|
453
|
+
var streamResponseToFile = async (input) => {
|
|
454
|
+
if (input.response.body === null) {
|
|
455
|
+
throw new WorkbenchFailure("download-failed", "The CLI download had no body.");
|
|
456
|
+
}
|
|
457
|
+
const totalBytes = contentLengthOf(input.response);
|
|
458
|
+
const startedAtMs = Date.now();
|
|
459
|
+
let transferredBytes = 0;
|
|
460
|
+
const readable = NodeStream.Readable.fromWeb(
|
|
461
|
+
input.response.body
|
|
462
|
+
);
|
|
463
|
+
readable.on("data", (chunk) => {
|
|
464
|
+
transferredBytes += chunk.length;
|
|
465
|
+
input.onTransfer(
|
|
466
|
+
computeTransfer({
|
|
467
|
+
transferredBytes,
|
|
468
|
+
totalBytes,
|
|
469
|
+
startedAtMs,
|
|
470
|
+
nowMs: Date.now()
|
|
471
|
+
})
|
|
472
|
+
);
|
|
473
|
+
});
|
|
474
|
+
try {
|
|
475
|
+
await NodeStreamPromises.pipeline(readable, NodeFS.createWriteStream(input.filePath));
|
|
476
|
+
} catch (error) {
|
|
477
|
+
await NodeFSP2.unlink(input.filePath).catch(() => void 0);
|
|
478
|
+
if (isDiskFull(error)) {
|
|
479
|
+
throw new WorkbenchFailure("disk-full", messageOf(error));
|
|
480
|
+
}
|
|
481
|
+
throw error;
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
var downloadCliBinary = async (input) => {
|
|
485
|
+
const platform3 = input.platform === void 0 ? detectCliPlatform() : input.platform;
|
|
486
|
+
if (platform3 === null) {
|
|
487
|
+
throw new WorkbenchFailure(
|
|
488
|
+
"download-failed",
|
|
489
|
+
`This machine's platform (${NodeProcess2.platform}/${NodeProcess2.arch}) has no VS Code CLI build.`
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
const url = input.url ?? cliDownloadUrl(platform3);
|
|
493
|
+
let response;
|
|
494
|
+
try {
|
|
495
|
+
response = await input.http(url, input.signal === void 0 ? {} : { signal: input.signal });
|
|
496
|
+
} catch (error) {
|
|
497
|
+
if (isDiskFull(error)) throw new WorkbenchFailure("disk-full", messageOf(error));
|
|
498
|
+
throw new WorkbenchFailure("download-failed", messageOf(error));
|
|
499
|
+
}
|
|
500
|
+
if (!response.ok) {
|
|
501
|
+
throw new WorkbenchFailure(
|
|
502
|
+
"download-failed",
|
|
503
|
+
`The CLI download returned HTTP ${String(response.status)}.`
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
await NodeFSP2.mkdir(input.paths.binDir, { recursive: true });
|
|
507
|
+
const archivePath = `${input.destPath}${CLI_DOWNLOAD_SUFFIX}`;
|
|
508
|
+
try {
|
|
509
|
+
await streamResponseToFile({
|
|
510
|
+
response,
|
|
511
|
+
filePath: archivePath,
|
|
512
|
+
onTransfer: input.onTransfer
|
|
513
|
+
});
|
|
514
|
+
const archive = await NodeFSP2.readFile(archivePath);
|
|
515
|
+
const binary = await extractCliBinary(archive, cliArchiveKind(platform3));
|
|
516
|
+
await writeAtomic(input.destPath, binary, 493);
|
|
517
|
+
} catch (error) {
|
|
518
|
+
if (error instanceof WorkbenchFailure) throw error;
|
|
519
|
+
if (isDiskFull(error)) throw new WorkbenchFailure("disk-full", messageOf(error));
|
|
520
|
+
throw new WorkbenchFailure("download-failed", messageOf(error));
|
|
521
|
+
} finally {
|
|
522
|
+
await NodeFSP2.unlink(archivePath).catch(() => void 0);
|
|
523
|
+
}
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
// src/serveWeb.ts
|
|
527
|
+
var START_SERVER_TIMEOUT_MS = 3e4;
|
|
528
|
+
var DOWNLOAD_SERVER_TIMEOUT_MS = 10 * 6e4;
|
|
529
|
+
var DOWNLOAD_SERVER_POLL_MS = 1e3;
|
|
530
|
+
var WEB_UI_AVAILABLE_PATTERN = /Web UI available at http:\/\/127\.0\.0\.1:(\d+)/;
|
|
531
|
+
var SERVER_DOWNLOADING_PATTERN = /Downloading server\b/;
|
|
532
|
+
var SERVER_STARTING_PATTERN = /Starting server\b/;
|
|
533
|
+
var parseWebUiPort = (line) => {
|
|
534
|
+
const match = WEB_UI_AVAILABLE_PATTERN.exec(line);
|
|
535
|
+
if (match === null) return null;
|
|
536
|
+
const port = Number(match[1]);
|
|
537
|
+
return Number.isInteger(port) && port >= 1 && port <= 65535 ? port : null;
|
|
538
|
+
};
|
|
539
|
+
var isServerLogLine = (line) => SERVER_DOWNLOADING_PATTERN.test(line) || SERVER_STARTING_PATTERN.test(line);
|
|
540
|
+
var serveWebArgs = (paths) => [
|
|
541
|
+
"serve-web",
|
|
542
|
+
"--host",
|
|
543
|
+
"127.0.0.1",
|
|
544
|
+
"--port",
|
|
545
|
+
"0",
|
|
546
|
+
"--connection-token-file",
|
|
547
|
+
paths.tokenPath,
|
|
548
|
+
"--accept-server-license-terms",
|
|
549
|
+
"--server-data-dir",
|
|
550
|
+
paths.serverDataDir,
|
|
551
|
+
"--cli-data-dir",
|
|
552
|
+
paths.cliDataDir,
|
|
553
|
+
"--disable-telemetry"
|
|
554
|
+
];
|
|
555
|
+
var splitLines = (text) => {
|
|
556
|
+
const parts = text.split(/\r?\n/);
|
|
557
|
+
const rest = parts.pop() ?? "";
|
|
558
|
+
return { complete: parts, rest };
|
|
559
|
+
};
|
|
560
|
+
var consumeProcessLines = (stream, onLine) => {
|
|
561
|
+
const decoder = new TextDecoder();
|
|
562
|
+
let pending = "";
|
|
563
|
+
const run = async () => {
|
|
564
|
+
for await (const chunk of stream) {
|
|
565
|
+
pending += decoder.decode(chunk, { stream: true });
|
|
566
|
+
const split = splitLines(pending);
|
|
567
|
+
pending = split.rest;
|
|
568
|
+
for (const line of split.complete) onLine(redactWorkbenchSecrets(line));
|
|
569
|
+
}
|
|
570
|
+
pending += decoder.decode();
|
|
571
|
+
if (pending.length > 0) onLine(redactWorkbenchSecrets(pending));
|
|
572
|
+
};
|
|
573
|
+
return run();
|
|
574
|
+
};
|
|
575
|
+
var waitForServeWebPort = (input) => {
|
|
576
|
+
const timeoutMs = input.timeoutMs ?? START_SERVER_TIMEOUT_MS;
|
|
577
|
+
const stderrChunks = [];
|
|
578
|
+
let settled = false;
|
|
579
|
+
let resolvePort = () => void 0;
|
|
580
|
+
let rejectPort = () => void 0;
|
|
581
|
+
const portPromise = new Promise((resolve, reject) => {
|
|
582
|
+
resolvePort = resolve;
|
|
583
|
+
rejectPort = reject;
|
|
584
|
+
});
|
|
585
|
+
const finishError = (error) => {
|
|
586
|
+
if (settled) return;
|
|
587
|
+
settled = true;
|
|
588
|
+
rejectPort(error);
|
|
589
|
+
};
|
|
590
|
+
const finishPort = (port) => {
|
|
591
|
+
if (settled) return;
|
|
592
|
+
settled = true;
|
|
593
|
+
resolvePort(port);
|
|
594
|
+
};
|
|
595
|
+
const timer = setTimeout(() => {
|
|
596
|
+
finishError(
|
|
597
|
+
new WorkbenchFailure(
|
|
598
|
+
"launch-failed",
|
|
599
|
+
"The VS Code CLI started but did not publish a local address in time."
|
|
600
|
+
)
|
|
601
|
+
);
|
|
602
|
+
}, timeoutMs);
|
|
603
|
+
void consumeProcessLines(input.handle.stdout, (line) => {
|
|
604
|
+
input.onStdoutLine(line);
|
|
605
|
+
const port = parseWebUiPort(line);
|
|
606
|
+
if (port !== null) finishPort(port);
|
|
607
|
+
}).catch((error) => {
|
|
608
|
+
finishError(error instanceof Error ? error : new Error(String(error)));
|
|
609
|
+
});
|
|
610
|
+
void consumeProcessLines(input.handle.stderr, (line) => {
|
|
611
|
+
stderrChunks.push(line);
|
|
612
|
+
input.onStderrLine(line);
|
|
613
|
+
}).catch(() => void 0);
|
|
614
|
+
void input.handle.exited.then((code) => {
|
|
615
|
+
if (settled) return;
|
|
616
|
+
const tail = stderrChunks.slice(-20).join("\n");
|
|
617
|
+
finishError(
|
|
618
|
+
new WorkbenchFailure(
|
|
619
|
+
"launch-failed",
|
|
620
|
+
tail.length > 0 ? `The VS Code CLI exited before it published a local address.
|
|
621
|
+
${tail}` : `The VS Code CLI exited before it published a local address (code ${String(code)}).`
|
|
622
|
+
)
|
|
623
|
+
);
|
|
624
|
+
});
|
|
625
|
+
return portPromise.then(
|
|
626
|
+
(port) => {
|
|
627
|
+
clearTimeout(timer);
|
|
628
|
+
return { port, stderr: stderrChunks.join("\n") };
|
|
629
|
+
},
|
|
630
|
+
(error) => {
|
|
631
|
+
clearTimeout(timer);
|
|
632
|
+
throw error;
|
|
633
|
+
}
|
|
634
|
+
);
|
|
635
|
+
};
|
|
636
|
+
var serverReadyStatus = (status) => {
|
|
637
|
+
if (status === 202) return "downloading";
|
|
638
|
+
if (status === 200 || status === 301 || status === 302 || status === 307 || status === 308) {
|
|
639
|
+
return "ready";
|
|
640
|
+
}
|
|
641
|
+
if (status === 403) return "forbidden";
|
|
642
|
+
return "other";
|
|
643
|
+
};
|
|
644
|
+
var waitForServerDownload = async (input) => {
|
|
645
|
+
const timeoutMs = input.timeoutMs ?? DOWNLOAD_SERVER_TIMEOUT_MS;
|
|
646
|
+
const pollMs = input.pollMs ?? DOWNLOAD_SERVER_POLL_MS;
|
|
647
|
+
const startedAtMs = Date.now();
|
|
648
|
+
const deadline = startedAtMs + timeoutMs;
|
|
649
|
+
const url = `http://127.0.0.1:${String(input.port)}/?tkn=${input.token}`;
|
|
650
|
+
const serveWebDir = `${input.cliDataDir}/serve-web`;
|
|
651
|
+
while (Date.now() < deadline) {
|
|
652
|
+
if (input.signal?.aborted) {
|
|
653
|
+
throw new WorkbenchFailure("server-download-failed", "The server download was cancelled.");
|
|
654
|
+
}
|
|
655
|
+
try {
|
|
656
|
+
const response = await input.http(
|
|
657
|
+
url,
|
|
658
|
+
input.signal === void 0 ? { redirect: "manual" } : { redirect: "manual", signal: input.signal }
|
|
659
|
+
);
|
|
660
|
+
const kind = serverReadyStatus(response.status);
|
|
661
|
+
if (kind === "ready") return;
|
|
662
|
+
if (kind === "forbidden") {
|
|
663
|
+
throw new WorkbenchFailure(
|
|
664
|
+
"server-download-failed",
|
|
665
|
+
"The VS Code server rejected the connection token."
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
} catch (error) {
|
|
669
|
+
if (error instanceof WorkbenchFailure) throw error;
|
|
670
|
+
}
|
|
671
|
+
const transferredBytes = await directorySize(serveWebDir);
|
|
672
|
+
input.onTransfer(
|
|
673
|
+
computeTransfer({
|
|
674
|
+
transferredBytes,
|
|
675
|
+
totalBytes: null,
|
|
676
|
+
startedAtMs,
|
|
677
|
+
nowMs: Date.now()
|
|
678
|
+
})
|
|
679
|
+
);
|
|
680
|
+
await sleep(pollMs, input.signal);
|
|
681
|
+
}
|
|
682
|
+
throw new WorkbenchFailure(
|
|
683
|
+
"server-download-failed",
|
|
684
|
+
"The VS Code server did not become ready in time."
|
|
685
|
+
);
|
|
686
|
+
};
|
|
687
|
+
|
|
688
|
+
// src/instance.ts
|
|
689
|
+
var instanceIsLive = async (record) => isProcessAlive(record.pid) && await probeLoopback(record.port);
|
|
690
|
+
var reapOrphanInstance = async (paths) => {
|
|
691
|
+
const record = await readInstance(paths);
|
|
692
|
+
if (record === null) return;
|
|
693
|
+
if (isProcessAlive(record.pid)) {
|
|
694
|
+
try {
|
|
695
|
+
process.kill(record.pid, "SIGTERM");
|
|
696
|
+
} catch {
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
await removeFile(paths.instancePath);
|
|
700
|
+
};
|
|
701
|
+
var startServeWeb = async (input) => {
|
|
702
|
+
await ensureDataDirs(input.paths);
|
|
703
|
+
await ensureServeWebToken(input.paths);
|
|
704
|
+
const handle = await input.processApi.spawn({
|
|
705
|
+
program: CLI_PROGRAM_SELECTOR,
|
|
706
|
+
args: [...serveWebArgs(input.paths)]
|
|
707
|
+
});
|
|
708
|
+
try {
|
|
709
|
+
await handle.closeStdin();
|
|
710
|
+
} catch {
|
|
711
|
+
}
|
|
712
|
+
const waitForPort = input.waitForPort ?? waitForServeWebPort;
|
|
713
|
+
try {
|
|
714
|
+
const { port } = await waitForPort({
|
|
715
|
+
handle,
|
|
716
|
+
onStdoutLine: input.onStdoutLine,
|
|
717
|
+
onStderrLine: input.onStderrLine
|
|
718
|
+
});
|
|
719
|
+
const record = {
|
|
720
|
+
pid: handle.pid,
|
|
721
|
+
port,
|
|
722
|
+
cliVersion: input.cliVersion,
|
|
723
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
724
|
+
};
|
|
725
|
+
await writeInstance(input.paths, record);
|
|
726
|
+
return { handle, record };
|
|
727
|
+
} catch (error) {
|
|
728
|
+
await handle.kill("SIGTERM").catch(() => void 0);
|
|
729
|
+
throw error;
|
|
730
|
+
}
|
|
731
|
+
};
|
|
732
|
+
var STOP_GRACE_MS = 5e3;
|
|
733
|
+
var stopManagedProcess = async (managed, paths) => {
|
|
734
|
+
if (managed !== null) {
|
|
735
|
+
await managed.handle.kill("SIGTERM").catch(() => void 0);
|
|
736
|
+
const timedOut = await Promise.race([
|
|
737
|
+
managed.handle.exited.then(() => false).catch(() => false),
|
|
738
|
+
sleep(STOP_GRACE_MS).then(() => true)
|
|
739
|
+
]);
|
|
740
|
+
if (timedOut) {
|
|
741
|
+
await managed.handle.kill("SIGKILL").catch(() => void 0);
|
|
742
|
+
await managed.handle.exited.catch(() => void 0);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
await removeFile(paths.instancePath);
|
|
746
|
+
};
|
|
747
|
+
|
|
748
|
+
// src/runtime.ts
|
|
749
|
+
var MANAGED_LABEL = "VS Code";
|
|
750
|
+
var EXTERNAL_LABEL = "code-server";
|
|
751
|
+
var WorkbenchRuntime = class {
|
|
752
|
+
bus = new ProgressBus();
|
|
753
|
+
ctx;
|
|
754
|
+
paths;
|
|
755
|
+
chain = Promise.resolve();
|
|
756
|
+
managed = null;
|
|
757
|
+
managedPort = null;
|
|
758
|
+
externalPort = null;
|
|
759
|
+
externalListenPort = null;
|
|
760
|
+
outputStage = "start-server";
|
|
761
|
+
constructor(ctx) {
|
|
762
|
+
this.ctx = ctx;
|
|
763
|
+
this.paths = workbenchPaths(ctx.paths.dataDir);
|
|
764
|
+
}
|
|
765
|
+
/** 激活时收孤儿、按设置接外接端口。不能让 activate 失败。 */
|
|
766
|
+
async start() {
|
|
767
|
+
await reapOrphanInstance(this.paths);
|
|
768
|
+
const desired = await this.desiredExternalPort();
|
|
769
|
+
if (desired === null) return;
|
|
770
|
+
try {
|
|
771
|
+
await this.attachPort(desired);
|
|
772
|
+
} catch (error) {
|
|
773
|
+
this.ctx.log.warn("external workbench is not listening", {
|
|
774
|
+
port: desired,
|
|
775
|
+
detail: messageOf(error)
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
async dispose() {
|
|
780
|
+
await this.enqueue(async () => {
|
|
781
|
+
await this.stopManaged();
|
|
782
|
+
await this.clearExternal();
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
status() {
|
|
786
|
+
return this.enqueue(() => this.readStatus());
|
|
787
|
+
}
|
|
788
|
+
open(payload) {
|
|
789
|
+
return this.enqueue(() => this.runOpen(payload));
|
|
790
|
+
}
|
|
791
|
+
stop() {
|
|
792
|
+
return this.enqueue(async () => {
|
|
793
|
+
await this.stopManaged();
|
|
794
|
+
return {};
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
update() {
|
|
798
|
+
return this.enqueue(() => this.runUpdate());
|
|
799
|
+
}
|
|
800
|
+
attach(port) {
|
|
801
|
+
return this.enqueue(async () => {
|
|
802
|
+
const registration = await this.attachPort(port);
|
|
803
|
+
await writeConfigFile(this.paths, { attachPort: port });
|
|
804
|
+
return { portId: registration.portId };
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
detach() {
|
|
808
|
+
return this.enqueue(async () => {
|
|
809
|
+
await this.clearExternal();
|
|
810
|
+
await writeConfigFile(this.paths, {});
|
|
811
|
+
return {};
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
enqueue(fn) {
|
|
815
|
+
const run = this.chain.then(fn, fn);
|
|
816
|
+
this.chain = run.then(
|
|
817
|
+
() => void 0,
|
|
818
|
+
() => void 0
|
|
819
|
+
);
|
|
820
|
+
return run;
|
|
821
|
+
}
|
|
822
|
+
emit(event) {
|
|
823
|
+
this.bus.emit(event);
|
|
824
|
+
}
|
|
825
|
+
stage(stage, status, extra = {}) {
|
|
826
|
+
this.emit(progressEvent({ stage, status, ...extra }));
|
|
827
|
+
}
|
|
828
|
+
fail(stage, kind, detail) {
|
|
829
|
+
this.stage(stage, "failed", {
|
|
830
|
+
failureKind: kind,
|
|
831
|
+
...detail === void 0 ? {} : { detail }
|
|
832
|
+
});
|
|
833
|
+
throw new WorkbenchFailure(kind, failureMessage(kind));
|
|
834
|
+
}
|
|
835
|
+
async desiredExternalPort() {
|
|
836
|
+
const fromSettings = parsePort(this.ctx.config.attachPort);
|
|
837
|
+
if (fromSettings !== null) return fromSettings;
|
|
838
|
+
const file = await readConfigFile(this.paths);
|
|
839
|
+
return parsePort(file?.attachPort);
|
|
840
|
+
}
|
|
841
|
+
async readStatus() {
|
|
842
|
+
const installed = await cliIsInstalled(this.paths);
|
|
843
|
+
const live = await this.managedIsLive();
|
|
844
|
+
const external = this.externalPort !== null && this.externalListenPort !== null ? { port: this.externalListenPort, portId: this.externalPort.portId } : void 0;
|
|
845
|
+
if (live && this.managed !== null) {
|
|
846
|
+
return {
|
|
847
|
+
mode: "managed",
|
|
848
|
+
installed,
|
|
849
|
+
running: true,
|
|
850
|
+
port: this.managed.record.port,
|
|
851
|
+
cliVersion: this.managed.record.cliVersion,
|
|
852
|
+
...this.managedPort === null ? {} : { portId: this.managedPort.portId },
|
|
853
|
+
...external === void 0 ? {} : { external }
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
if (external !== void 0) {
|
|
857
|
+
return {
|
|
858
|
+
mode: "external",
|
|
859
|
+
installed,
|
|
860
|
+
running: await probeLoopback(external.port),
|
|
861
|
+
external
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
let cliVersion;
|
|
865
|
+
if (installed) {
|
|
866
|
+
try {
|
|
867
|
+
cliVersion = await readInstalledCliVersion(this.ctx.process);
|
|
868
|
+
} catch {
|
|
869
|
+
cliVersion = void 0;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
return {
|
|
873
|
+
mode: "none",
|
|
874
|
+
installed,
|
|
875
|
+
running: false,
|
|
876
|
+
...cliVersion === void 0 ? {} : { cliVersion }
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
async managedIsLive() {
|
|
880
|
+
if (this.managed !== null) {
|
|
881
|
+
if (await instanceIsLive(this.managed.record)) return true;
|
|
882
|
+
this.managed = null;
|
|
883
|
+
return false;
|
|
884
|
+
}
|
|
885
|
+
const record = await readInstance(this.paths);
|
|
886
|
+
if (record === null) return false;
|
|
887
|
+
return instanceIsLive(record);
|
|
888
|
+
}
|
|
889
|
+
async runOpen(payload) {
|
|
890
|
+
const desiredExternal = await this.desiredExternalPort();
|
|
891
|
+
if (desiredExternal !== null && !await this.managedIsLive()) {
|
|
892
|
+
if (this.externalPort === null) await this.attachPort(desiredExternal);
|
|
893
|
+
this.stage("register-port", "skipped", { skipReason: "external" });
|
|
894
|
+
const portId2 = this.externalPort?.portId;
|
|
895
|
+
if (portId2 === void 0) this.fail("register-port", "unknown");
|
|
896
|
+
return { portId: portId2 };
|
|
897
|
+
}
|
|
898
|
+
await this.stepLicense(payload.acceptLicense === true);
|
|
899
|
+
const cliVersion = await this.stepCheckAndDownloadCli();
|
|
900
|
+
await this.stepStartServer(cliVersion);
|
|
901
|
+
const token = await ensureServeWebToken(this.paths);
|
|
902
|
+
await this.stepDownloadServer(token);
|
|
903
|
+
const portId = await this.stepRegisterManaged();
|
|
904
|
+
return { portId, token };
|
|
905
|
+
}
|
|
906
|
+
async stepLicense(acceptLicense) {
|
|
907
|
+
this.stage("license", "started");
|
|
908
|
+
if (await fileExists(this.paths.licensePath)) {
|
|
909
|
+
this.stage("license", "skipped", { skipReason: "already-accepted" });
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
if (!acceptLicense) this.fail("license", "license-required");
|
|
913
|
+
await writeLicenseAccepted(this.paths);
|
|
914
|
+
this.stage("license", "succeeded");
|
|
915
|
+
}
|
|
916
|
+
async stepCheckAndDownloadCli() {
|
|
917
|
+
this.stage("check-cli", "started");
|
|
918
|
+
if (await fileExists(this.paths.cliPath)) {
|
|
919
|
+
try {
|
|
920
|
+
const version = await readInstalledCliVersion(this.ctx.process);
|
|
921
|
+
this.stage("check-cli", "succeeded", { detail: version });
|
|
922
|
+
this.stage("download-cli", "skipped", { skipReason: "already-installed" });
|
|
923
|
+
return version;
|
|
924
|
+
} catch (error) {
|
|
925
|
+
this.fail("check-cli", "unknown", messageOf(error));
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
this.stage("check-cli", "succeeded");
|
|
929
|
+
return this.stepDownloadCli(this.paths.cliPath);
|
|
930
|
+
}
|
|
931
|
+
async stepDownloadCli(destPath) {
|
|
932
|
+
this.stage("download-cli", "started");
|
|
933
|
+
try {
|
|
934
|
+
await downloadCliBinary({
|
|
935
|
+
http: this.ctx.http,
|
|
936
|
+
paths: this.paths,
|
|
937
|
+
destPath,
|
|
938
|
+
onTransfer: (transfer) => this.stage("download-cli", "progress", { transfer })
|
|
939
|
+
});
|
|
940
|
+
} catch (error) {
|
|
941
|
+
this.rethrowAs(error, "download-cli");
|
|
942
|
+
}
|
|
943
|
+
try {
|
|
944
|
+
const version = await readInstalledCliVersion(this.ctx.process);
|
|
945
|
+
this.stage("download-cli", "succeeded", { detail: version });
|
|
946
|
+
return version;
|
|
947
|
+
} catch (error) {
|
|
948
|
+
this.fail("download-cli", "unknown", messageOf(error));
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
async stepStartServer(cliVersion) {
|
|
952
|
+
this.stage("start-server", "started");
|
|
953
|
+
this.outputStage = "start-server";
|
|
954
|
+
if (this.managed !== null && await instanceIsLive(this.managed.record)) {
|
|
955
|
+
this.stage("start-server", "skipped", { skipReason: "already-running" });
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
const record = await readInstance(this.paths);
|
|
959
|
+
if (record !== null && await instanceIsLive(record)) {
|
|
960
|
+
this.managed = { handle: orphanHandle(record.pid), record };
|
|
961
|
+
this.stage("start-server", "skipped", { skipReason: "already-running" });
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
try {
|
|
965
|
+
this.managed = await startServeWeb({
|
|
966
|
+
processApi: this.ctx.process,
|
|
967
|
+
paths: this.paths,
|
|
968
|
+
cliVersion,
|
|
969
|
+
onStdoutLine: (line) => this.onProcessLine(line),
|
|
970
|
+
onStderrLine: (line) => this.onProcessLine(line)
|
|
971
|
+
});
|
|
972
|
+
} catch (error) {
|
|
973
|
+
this.rethrowAs(error, "start-server", "launch-failed");
|
|
974
|
+
}
|
|
975
|
+
const started = this.managed;
|
|
976
|
+
if (started === null) this.fail("start-server", "launch-failed");
|
|
977
|
+
this.stage("start-server", "succeeded", {
|
|
978
|
+
detail: `127.0.0.1:${String(started.record.port)}`
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
onProcessLine(line) {
|
|
982
|
+
if (line.trim().length === 0) return;
|
|
983
|
+
const stage = isServerLogLine(line) ? "download-server" : this.outputStage;
|
|
984
|
+
this.stage(stage, "output", { detail: line });
|
|
985
|
+
}
|
|
986
|
+
async stepDownloadServer(token) {
|
|
987
|
+
this.stage("download-server", "started");
|
|
988
|
+
this.outputStage = "download-server";
|
|
989
|
+
const managed = this.managed;
|
|
990
|
+
if (managed === null) this.fail("download-server", "launch-failed");
|
|
991
|
+
try {
|
|
992
|
+
await waitForServerDownload({
|
|
993
|
+
http: this.ctx.http,
|
|
994
|
+
port: managed.record.port,
|
|
995
|
+
token,
|
|
996
|
+
cliDataDir: this.paths.cliDataDir,
|
|
997
|
+
onTransfer: (transfer) => this.stage("download-server", "progress", { transfer })
|
|
998
|
+
});
|
|
999
|
+
} catch (error) {
|
|
1000
|
+
this.rethrowAs(error, "download-server", "server-download-failed");
|
|
1001
|
+
}
|
|
1002
|
+
this.stage("download-server", "succeeded");
|
|
1003
|
+
}
|
|
1004
|
+
async stepRegisterManaged() {
|
|
1005
|
+
this.stage("register-port", "started");
|
|
1006
|
+
const managed = this.managed;
|
|
1007
|
+
if (managed === null) this.fail("register-port", "launch-failed");
|
|
1008
|
+
if (this.managedPort !== null) {
|
|
1009
|
+
this.stage("register-port", "skipped", { skipReason: "already-registered" });
|
|
1010
|
+
return this.managedPort.portId;
|
|
1011
|
+
}
|
|
1012
|
+
try {
|
|
1013
|
+
this.managedPort = await this.ctx.ports.register({
|
|
1014
|
+
kind: "workbench",
|
|
1015
|
+
label: MANAGED_LABEL,
|
|
1016
|
+
port: managed.record.port
|
|
1017
|
+
});
|
|
1018
|
+
} catch (error) {
|
|
1019
|
+
const reason = capabilityReason(error);
|
|
1020
|
+
if (reason === "no-listener") this.fail("register-port", "launch-failed", messageOf(error));
|
|
1021
|
+
this.fail("register-port", "unknown", messageOf(error));
|
|
1022
|
+
}
|
|
1023
|
+
this.stage("register-port", "succeeded");
|
|
1024
|
+
return this.managedPort.portId;
|
|
1025
|
+
}
|
|
1026
|
+
async runUpdate() {
|
|
1027
|
+
if (await this.managedIsLive()) {
|
|
1028
|
+
throw new WorkbenchFailure("unknown", catalog.running.stopFirst);
|
|
1029
|
+
}
|
|
1030
|
+
this.stage("download-cli", "started");
|
|
1031
|
+
const destPath = `${this.paths.cliPath}${CLI_NEW_SUFFIX}`;
|
|
1032
|
+
try {
|
|
1033
|
+
await downloadCliBinary({
|
|
1034
|
+
http: this.ctx.http,
|
|
1035
|
+
paths: this.paths,
|
|
1036
|
+
destPath,
|
|
1037
|
+
onTransfer: (transfer) => this.stage("download-cli", "progress", { transfer })
|
|
1038
|
+
});
|
|
1039
|
+
await NodeFSP3.rename(destPath, this.paths.cliPath);
|
|
1040
|
+
} catch (error) {
|
|
1041
|
+
await NodeFSP3.unlink(destPath).catch(() => void 0);
|
|
1042
|
+
this.rethrowAs(error, "download-cli");
|
|
1043
|
+
}
|
|
1044
|
+
this.stage("download-cli", "succeeded");
|
|
1045
|
+
const version = await readInstalledCliVersion(this.ctx.process);
|
|
1046
|
+
return { cliVersion: version };
|
|
1047
|
+
}
|
|
1048
|
+
async attachPort(port) {
|
|
1049
|
+
if (!await probeLoopback(port)) {
|
|
1050
|
+
throw new WorkbenchFailure("unknown", attachNoListenerMessage(port));
|
|
1051
|
+
}
|
|
1052
|
+
if (this.externalPort !== null && this.externalListenPort === port) return this.externalPort;
|
|
1053
|
+
await this.clearExternal();
|
|
1054
|
+
const registration = await this.ctx.ports.register({
|
|
1055
|
+
kind: "workbench-external",
|
|
1056
|
+
label: EXTERNAL_LABEL,
|
|
1057
|
+
port
|
|
1058
|
+
});
|
|
1059
|
+
this.externalPort = registration;
|
|
1060
|
+
this.externalListenPort = port;
|
|
1061
|
+
return registration;
|
|
1062
|
+
}
|
|
1063
|
+
async clearExternal() {
|
|
1064
|
+
if (this.externalPort !== null) {
|
|
1065
|
+
await this.externalPort.unregister().catch(() => void 0);
|
|
1066
|
+
}
|
|
1067
|
+
this.externalPort = null;
|
|
1068
|
+
this.externalListenPort = null;
|
|
1069
|
+
}
|
|
1070
|
+
async stopManaged() {
|
|
1071
|
+
if (this.managedPort !== null) {
|
|
1072
|
+
await this.managedPort.unregister().catch(() => void 0);
|
|
1073
|
+
this.managedPort = null;
|
|
1074
|
+
}
|
|
1075
|
+
await stopManagedProcess(this.managed, this.paths);
|
|
1076
|
+
this.managed = null;
|
|
1077
|
+
const leftover = await readInstance(this.paths);
|
|
1078
|
+
if (leftover !== null && isProcessAlive(leftover.pid)) {
|
|
1079
|
+
try {
|
|
1080
|
+
process.kill(leftover.pid, "SIGTERM");
|
|
1081
|
+
} catch {
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
await removeFile(this.paths.instancePath);
|
|
1085
|
+
}
|
|
1086
|
+
rethrowAs(error, stage, fallback = "download-failed") {
|
|
1087
|
+
if (error instanceof WorkbenchFailure) {
|
|
1088
|
+
this.fail(stage, error.code, error.message);
|
|
1089
|
+
}
|
|
1090
|
+
this.fail(stage, fallback, messageOf(error));
|
|
1091
|
+
}
|
|
1092
|
+
};
|
|
1093
|
+
var capabilityReason = (error) => {
|
|
1094
|
+
if (typeof error !== "object" || error === null || !("reason" in error)) return null;
|
|
1095
|
+
const reason = error.reason;
|
|
1096
|
+
return typeof reason === "string" ? reason : null;
|
|
1097
|
+
};
|
|
1098
|
+
var orphanHandle = (pid) => ({
|
|
1099
|
+
pid,
|
|
1100
|
+
stdout: emptyBytes(),
|
|
1101
|
+
stderr: emptyBytes(),
|
|
1102
|
+
writeStdin: async () => void 0,
|
|
1103
|
+
closeStdin: async () => void 0,
|
|
1104
|
+
kill: async (signal) => {
|
|
1105
|
+
if (isProcessAlive(pid)) process.kill(pid, signal ?? "SIGTERM");
|
|
1106
|
+
},
|
|
1107
|
+
exited: Promise.resolve(null)
|
|
1108
|
+
});
|
|
1109
|
+
async function* emptyBytes() {
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
// src/schemas.ts
|
|
1113
|
+
var emptyObject = {
|
|
1114
|
+
type: "object",
|
|
1115
|
+
properties: {},
|
|
1116
|
+
additionalProperties: false
|
|
1117
|
+
};
|
|
1118
|
+
var emptyPayloadSchema = emptyObject;
|
|
1119
|
+
var openPayloadSchema = {
|
|
1120
|
+
type: "object",
|
|
1121
|
+
properties: {
|
|
1122
|
+
acceptLicense: { type: "boolean" }
|
|
1123
|
+
},
|
|
1124
|
+
additionalProperties: false
|
|
1125
|
+
};
|
|
1126
|
+
var attachPayloadSchema = {
|
|
1127
|
+
type: "object",
|
|
1128
|
+
additionalProperties: false,
|
|
1129
|
+
required: ["port"],
|
|
1130
|
+
properties: {
|
|
1131
|
+
port: { type: "integer", minimum: 1, maximum: 65535 }
|
|
1132
|
+
}
|
|
1133
|
+
};
|
|
1134
|
+
var portIdSchema = { type: "string", minLength: 1 };
|
|
1135
|
+
var openResultSchema = {
|
|
1136
|
+
oneOf: [
|
|
1137
|
+
{
|
|
1138
|
+
type: "object",
|
|
1139
|
+
additionalProperties: false,
|
|
1140
|
+
required: ["portId", "token"],
|
|
1141
|
+
properties: {
|
|
1142
|
+
portId: portIdSchema,
|
|
1143
|
+
token: { type: "string", minLength: 1 }
|
|
1144
|
+
}
|
|
1145
|
+
},
|
|
1146
|
+
{
|
|
1147
|
+
type: "object",
|
|
1148
|
+
additionalProperties: false,
|
|
1149
|
+
required: ["portId"],
|
|
1150
|
+
properties: {
|
|
1151
|
+
portId: portIdSchema
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
]
|
|
1155
|
+
};
|
|
1156
|
+
var statusResultSchema = {
|
|
1157
|
+
type: "object",
|
|
1158
|
+
additionalProperties: false,
|
|
1159
|
+
required: ["mode", "installed", "running"],
|
|
1160
|
+
properties: {
|
|
1161
|
+
mode: { type: "string", enum: ["managed", "external", "none"] },
|
|
1162
|
+
installed: { type: "boolean" },
|
|
1163
|
+
running: { type: "boolean" },
|
|
1164
|
+
port: { type: "integer", minimum: 1, maximum: 65535 },
|
|
1165
|
+
cliVersion: { type: "string", minLength: 1 },
|
|
1166
|
+
portId: portIdSchema,
|
|
1167
|
+
external: {
|
|
1168
|
+
type: "object",
|
|
1169
|
+
additionalProperties: false,
|
|
1170
|
+
required: ["port", "portId"],
|
|
1171
|
+
properties: {
|
|
1172
|
+
port: { type: "integer", minimum: 1, maximum: 65535 },
|
|
1173
|
+
portId: portIdSchema
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
};
|
|
1178
|
+
var updateResultSchema = {
|
|
1179
|
+
type: "object",
|
|
1180
|
+
additionalProperties: false,
|
|
1181
|
+
required: ["cliVersion"],
|
|
1182
|
+
properties: {
|
|
1183
|
+
cliVersion: { type: "string", minLength: 1 }
|
|
1184
|
+
}
|
|
1185
|
+
};
|
|
1186
|
+
var attachResultSchema = {
|
|
1187
|
+
type: "object",
|
|
1188
|
+
additionalProperties: false,
|
|
1189
|
+
required: ["portId"],
|
|
1190
|
+
properties: {
|
|
1191
|
+
portId: portIdSchema
|
|
1192
|
+
}
|
|
1193
|
+
};
|
|
1194
|
+
var emptyResultSchema = emptyObject;
|
|
1195
|
+
var progressItemSchema = {
|
|
1196
|
+
type: "object",
|
|
1197
|
+
additionalProperties: false,
|
|
1198
|
+
required: ["stage", "status", "at"],
|
|
1199
|
+
properties: {
|
|
1200
|
+
stage: { type: "string", enum: [...WORKBENCH_PROGRESS_STAGES] },
|
|
1201
|
+
status: { type: "string", enum: [...WORKBENCH_PROGRESS_STATUSES] },
|
|
1202
|
+
detail: { type: "string" },
|
|
1203
|
+
skipReason: { type: "string" },
|
|
1204
|
+
transfer: {
|
|
1205
|
+
type: "object",
|
|
1206
|
+
additionalProperties: false,
|
|
1207
|
+
required: ["transferredBytes", "totalBytes", "bytesPerSecond"],
|
|
1208
|
+
properties: {
|
|
1209
|
+
transferredBytes: { type: "number" },
|
|
1210
|
+
totalBytes: { type: ["number", "null"] },
|
|
1211
|
+
bytesPerSecond: { type: ["number", "null"] }
|
|
1212
|
+
}
|
|
1213
|
+
},
|
|
1214
|
+
failureKind: { type: "string", enum: [...WORKBENCH_FAILURE_KINDS] },
|
|
1215
|
+
at: { type: "string" }
|
|
1216
|
+
}
|
|
1217
|
+
};
|
|
1218
|
+
|
|
1219
|
+
// src/index.ts
|
|
1220
|
+
var index_default = definePlugin({
|
|
1221
|
+
id: "browser-workbench",
|
|
1222
|
+
apiVersion: 2,
|
|
1223
|
+
activate: (ctx) => {
|
|
1224
|
+
const runtime = new WorkbenchRuntime(ctx);
|
|
1225
|
+
void runtime.start().catch((error) => {
|
|
1226
|
+
ctx.log.warn("browser-workbench failed to recover previous state", {
|
|
1227
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
1228
|
+
});
|
|
1229
|
+
});
|
|
1230
|
+
ctx.stream({
|
|
1231
|
+
name: "progress",
|
|
1232
|
+
description: "Install and start progress for the browser VS Code workbench.",
|
|
1233
|
+
scope: "read",
|
|
1234
|
+
payloadSchema: emptyPayloadSchema,
|
|
1235
|
+
itemSchema: progressItemSchema,
|
|
1236
|
+
buffer: { latest: PROGRESS_BUFFER_LIMIT },
|
|
1237
|
+
handle: async (_payload, emit, signal) => {
|
|
1238
|
+
const unsubscribe = runtime.bus.subscribe((event) => {
|
|
1239
|
+
if (!signal.aborted) emit(event);
|
|
1240
|
+
});
|
|
1241
|
+
try {
|
|
1242
|
+
await new Promise((resolve) => {
|
|
1243
|
+
if (signal.aborted) {
|
|
1244
|
+
resolve();
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
signal.addEventListener("abort", () => resolve(), { once: true });
|
|
1248
|
+
});
|
|
1249
|
+
} finally {
|
|
1250
|
+
unsubscribe();
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
});
|
|
1254
|
+
ctx.command({
|
|
1255
|
+
name: "status",
|
|
1256
|
+
description: "Whether the browser VS Code workbench is installed, running, or attached.",
|
|
1257
|
+
scope: "read",
|
|
1258
|
+
payloadSchema: emptyPayloadSchema,
|
|
1259
|
+
resultSchema: statusResultSchema,
|
|
1260
|
+
handle: () => runtime.status()
|
|
1261
|
+
});
|
|
1262
|
+
ctx.command({
|
|
1263
|
+
name: "open",
|
|
1264
|
+
description: "Install, start, and register the browser VS Code workbench.",
|
|
1265
|
+
scope: "operate",
|
|
1266
|
+
payloadSchema: openPayloadSchema,
|
|
1267
|
+
resultSchema: openResultSchema,
|
|
1268
|
+
handle: (payload) => runtime.open({
|
|
1269
|
+
acceptLicense: payload["acceptLicense"] === true
|
|
1270
|
+
})
|
|
1271
|
+
});
|
|
1272
|
+
ctx.command({
|
|
1273
|
+
name: "stop",
|
|
1274
|
+
description: "Stop the managed VS Code CLI and unregister its port.",
|
|
1275
|
+
scope: "operate",
|
|
1276
|
+
payloadSchema: emptyPayloadSchema,
|
|
1277
|
+
resultSchema: emptyResultSchema,
|
|
1278
|
+
handle: () => runtime.stop()
|
|
1279
|
+
});
|
|
1280
|
+
ctx.command({
|
|
1281
|
+
name: "update",
|
|
1282
|
+
description: "Replace the VS Code CLI. The workbench must be stopped first.",
|
|
1283
|
+
scope: "operate",
|
|
1284
|
+
payloadSchema: emptyPayloadSchema,
|
|
1285
|
+
resultSchema: updateResultSchema,
|
|
1286
|
+
handle: () => runtime.update()
|
|
1287
|
+
});
|
|
1288
|
+
ctx.command({
|
|
1289
|
+
name: "attach",
|
|
1290
|
+
description: "Register a code-server already listening on this machine.",
|
|
1291
|
+
scope: "operate",
|
|
1292
|
+
payloadSchema: attachPayloadSchema,
|
|
1293
|
+
resultSchema: attachResultSchema,
|
|
1294
|
+
handle: (payload) => {
|
|
1295
|
+
const port = parsePort(payload["port"]);
|
|
1296
|
+
if (port === null) {
|
|
1297
|
+
throw new Error("attach requires a port between 1 and 65535.");
|
|
1298
|
+
}
|
|
1299
|
+
return runtime.attach(port);
|
|
1300
|
+
}
|
|
1301
|
+
});
|
|
1302
|
+
ctx.command({
|
|
1303
|
+
name: "detach",
|
|
1304
|
+
description: "Unregister the attached code-server port.",
|
|
1305
|
+
scope: "operate",
|
|
1306
|
+
payloadSchema: emptyPayloadSchema,
|
|
1307
|
+
resultSchema: emptyResultSchema,
|
|
1308
|
+
handle: () => runtime.detach()
|
|
1309
|
+
});
|
|
1310
|
+
ctx.settingsCard({
|
|
1311
|
+
title: catalog.settings.card.title,
|
|
1312
|
+
description: catalog.settings.card.description,
|
|
1313
|
+
fields: [
|
|
1314
|
+
{
|
|
1315
|
+
key: "attachPort",
|
|
1316
|
+
kind: "number",
|
|
1317
|
+
label: catalog.settings.attachPort.label,
|
|
1318
|
+
description: catalog.settings.attachPort.description
|
|
1319
|
+
}
|
|
1320
|
+
]
|
|
1321
|
+
});
|
|
1322
|
+
ctx.onDispose(() => runtime.dispose());
|
|
1323
|
+
}
|
|
1324
|
+
});
|
|
1325
|
+
export {
|
|
1326
|
+
index_default as default
|
|
1327
|
+
};
|