@dadigua/web-terminal 0.0.0-stage → 0.1.4
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/API.md +19 -0
- package/README.md +125 -2
- package/bin/web-terminal.mjs +2 -0
- package/config.example.yaml +9 -0
- package/dist/assets/index-Bv4iPZiS.js +27 -0
- package/dist/assets/index-DAKKO3a9.css +1 -0
- package/dist/favicon.svg +1 -0
- package/dist/index.html +4 -0
- package/dist-server/app-3OE4ER7S.js +772 -0
- package/dist-server/chunk-TD3NJESY.js +90 -0
- package/dist-server/cli.js +363 -0
- package/dist-server/vscodeInstaller-WIM5VHDI.js +210 -0
- package/package.json +76 -4
- package/release/web-terminal.vsix +0 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// package.json
|
|
2
|
+
var package_default = {
|
|
3
|
+
name: "@dadigua/web-terminal",
|
|
4
|
+
version: "0.1.4",
|
|
5
|
+
type: "module",
|
|
6
|
+
scripts: {
|
|
7
|
+
dev: "tsx src/server/index.ts --dev",
|
|
8
|
+
build: "tsc --noEmit && vite build && tsx scripts/build-server.ts",
|
|
9
|
+
start: "tsx src/server/index.ts",
|
|
10
|
+
test: "tsx --test tests/*.test.ts",
|
|
11
|
+
check: "tsc --noEmit",
|
|
12
|
+
"build:clients": "tsx scripts/build-clients.ts",
|
|
13
|
+
"package:vscode": "pnpm build:clients && tsx scripts/package-vscode.ts",
|
|
14
|
+
"install:vscode": "pnpm package:vscode && tsx src/cli.ts install-vscode",
|
|
15
|
+
electron: "pnpm build:clients && electron dist-clients/electron",
|
|
16
|
+
"package:electron:win": "pnpm build:clients && tsx scripts/package-electron.ts --win",
|
|
17
|
+
"package:electron:linux": "pnpm build:clients && tsx scripts/package-electron.ts --linux",
|
|
18
|
+
prepack: "pnpm build && pnpm package:vscode"
|
|
19
|
+
},
|
|
20
|
+
dependencies: {
|
|
21
|
+
"@xterm/addon-fit": "^0.11.0",
|
|
22
|
+
"@xterm/addon-serialize": "0.14.0",
|
|
23
|
+
"@xterm/headless": "6.0.0",
|
|
24
|
+
"@xterm/xterm": "^6.0.0",
|
|
25
|
+
express: "^5.1.0",
|
|
26
|
+
multer: "^2.0.2",
|
|
27
|
+
"node-pty": "^1.1.0",
|
|
28
|
+
react: "^19.2.0",
|
|
29
|
+
"react-dom": "^19.2.0",
|
|
30
|
+
sharp: "^0.34.5",
|
|
31
|
+
ws: "^8.18.3",
|
|
32
|
+
yaml: "^2.8.2"
|
|
33
|
+
},
|
|
34
|
+
devDependencies: {
|
|
35
|
+
"@types/vscode": "^1.96.0",
|
|
36
|
+
"@vscode/vsce": "^3.9.2",
|
|
37
|
+
electron: "^42.3.3",
|
|
38
|
+
"electron-builder": "^26.15.3",
|
|
39
|
+
esbuild: "^0.28.0",
|
|
40
|
+
"@types/express": "^5.0.0",
|
|
41
|
+
"@types/multer": "^2.0.0",
|
|
42
|
+
"@types/node": "^24.0.0",
|
|
43
|
+
"@types/react": "^19.2.0",
|
|
44
|
+
"@types/react-dom": "^19.2.0",
|
|
45
|
+
"@types/ws": "^8.18.1",
|
|
46
|
+
"@vitejs/plugin-react": "^5.1.0",
|
|
47
|
+
tsx: "^4.20.6",
|
|
48
|
+
typescript: "^5.9.3",
|
|
49
|
+
vite: "^7.2.0"
|
|
50
|
+
},
|
|
51
|
+
engines: {
|
|
52
|
+
node: ">=22"
|
|
53
|
+
},
|
|
54
|
+
packageManager: "pnpm@11.28.2",
|
|
55
|
+
bin: {
|
|
56
|
+
"web-terminal": "bin/web-terminal.mjs"
|
|
57
|
+
},
|
|
58
|
+
files: [
|
|
59
|
+
"bin/",
|
|
60
|
+
"dist-server/",
|
|
61
|
+
"dist/",
|
|
62
|
+
"release/web-terminal.vsix",
|
|
63
|
+
"config.example.yaml",
|
|
64
|
+
"README.md",
|
|
65
|
+
"API.md"
|
|
66
|
+
],
|
|
67
|
+
description: "Web Terminal\uFF1A\u5171\u4EAB PTY\u3001\u56FE\u7247\u4E0E\u6587\u672C\u9644\u4EF6\u3001\u6587\u4EF6\u9884\u89C8\u53CA VS Code \u5BA2\u6237\u7AEF",
|
|
68
|
+
repository: {
|
|
69
|
+
type: "git",
|
|
70
|
+
url: "git+https://github.com/laopo001/web-terminal.git"
|
|
71
|
+
},
|
|
72
|
+
homepage: "https://github.com/laopo001/web-terminal#readme",
|
|
73
|
+
bugs: {
|
|
74
|
+
url: "https://github.com/laopo001/web-terminal/issues"
|
|
75
|
+
},
|
|
76
|
+
publishConfig: {
|
|
77
|
+
access: "public"
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// src/shared/service.ts
|
|
82
|
+
var serviceName = "@dadigua/web-terminal";
|
|
83
|
+
var serviceProtocol = 1;
|
|
84
|
+
var serviceVersion = package_default.version;
|
|
85
|
+
|
|
86
|
+
export {
|
|
87
|
+
serviceName,
|
|
88
|
+
serviceProtocol,
|
|
89
|
+
serviceVersion
|
|
90
|
+
};
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import {
|
|
2
|
+
serviceName,
|
|
3
|
+
serviceProtocol,
|
|
4
|
+
serviceVersion
|
|
5
|
+
} from "./chunk-TD3NJESY.js";
|
|
6
|
+
|
|
7
|
+
// src/cli.ts
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { resolve as resolve3, join as join2, dirname } from "node:path";
|
|
10
|
+
import { mkdir as mkdir2, rm as rm2 } from "node:fs/promises";
|
|
11
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
12
|
+
import { parseArgs } from "node:util";
|
|
13
|
+
|
|
14
|
+
// src/server/config.ts
|
|
15
|
+
import { mkdirSync, readFileSync, existsSync, realpathSync, writeFileSync, chmodSync } from "node:fs";
|
|
16
|
+
import { homedir, hostname } from "node:os";
|
|
17
|
+
import { resolve } from "node:path";
|
|
18
|
+
import { randomBytes, createHash } from "node:crypto";
|
|
19
|
+
import { parse } from "yaml";
|
|
20
|
+
function loadConfig(defaults = {}) {
|
|
21
|
+
const configFile = resolve(process.env.WEB_TERMINAL_CONFIG || defaults.configFile || "config.yaml");
|
|
22
|
+
const raw = existsSync(configFile) ? parse(readFileSync(configFile, "utf8")) : {};
|
|
23
|
+
if (raw !== null && (typeof raw !== "object" || Array.isArray(raw))) throw new Error("config.yaml \u5FC5\u987B\u662F\u914D\u7F6E\u5BF9\u8C61");
|
|
24
|
+
const input = raw || {};
|
|
25
|
+
const dataDir = resolve(process.env.WEB_TERMINAL_DATA_DIR || input.dataDir || defaults.dataDir || ".data");
|
|
26
|
+
mkdirSync(dataDir, { recursive: true, mode: 448 });
|
|
27
|
+
const tokenFile = resolve(dataDir, "token");
|
|
28
|
+
let token = process.env.WEB_TERMINAL_TOKEN?.trim();
|
|
29
|
+
if (!token) {
|
|
30
|
+
if (!existsSync(tokenFile)) writeFileSync(tokenFile, randomBytes(32).toString("base64url") + "\n", { mode: 384, flag: "wx" });
|
|
31
|
+
chmodSync(tokenFile, 384);
|
|
32
|
+
token = readFileSync(tokenFile, "utf8").trim();
|
|
33
|
+
}
|
|
34
|
+
if (!token || token.length > 1024) throw new Error("token \u4E0D\u80FD\u4E3A\u7A7A\uFF0C\u4E14\u6700\u591A 1024 \u4E2A\u5B57\u7B26");
|
|
35
|
+
const port = Number(process.env.PORT || input.port || 3840);
|
|
36
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error("port \u65E0\u6548");
|
|
37
|
+
const uploadHome = resolve(process.env.WEB_TERMINAL_HOME || resolve(homedir(), ".web-terminal"));
|
|
38
|
+
return {
|
|
39
|
+
host: String(process.env.HOST || input.host || "127.0.0.1"),
|
|
40
|
+
port,
|
|
41
|
+
dataDir,
|
|
42
|
+
token,
|
|
43
|
+
uploadDir: resolve(input.uploadDir || defaults.uploadDir || resolve(uploadHome, "uploads")),
|
|
44
|
+
defaultCwd: realpathSync(resolve(input.defaultCwd || homedir())),
|
|
45
|
+
shell: String(input.shell || process.env.SHELL || (process.platform === "win32" ? "powershell.exe" : "/bin/bash")),
|
|
46
|
+
machineId: `${hostname()}-${createHash("sha256").update(dataDir).digest("hex").slice(0, 8)}`,
|
|
47
|
+
maxUploadBytes: 20 * 1024 * 1024
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// src/shared/clientUrl.ts
|
|
52
|
+
var defaultServerUrl = "http://localhost:3840";
|
|
53
|
+
function normalizeServerUrl(input) {
|
|
54
|
+
let url;
|
|
55
|
+
try {
|
|
56
|
+
url = new URL(input.trim());
|
|
57
|
+
} catch {
|
|
58
|
+
throw new Error("\u8BF7\u8F93\u5165\u5B8C\u6574\u7684 http:// \u6216 https:// \u670D\u52A1\u5668\u5730\u5740");
|
|
59
|
+
}
|
|
60
|
+
if (!["http:", "https:"].includes(url.protocol) || !url.hostname) throw new Error("\u670D\u52A1\u5668\u5730\u5740\u53EA\u652F\u6301 HTTP \u6216 HTTPS");
|
|
61
|
+
if (url.username || url.password || url.search || url.hash) throw new Error("\u670D\u52A1\u5668\u5730\u5740\u4E0D\u80FD\u5305\u542B\u8D26\u53F7\u3001\u5BC6\u7801\u3001\u67E5\u8BE2\u53C2\u6570\u6216\u951A\u70B9\uFF1Btoken \u8BF7\u5728 Web \u767B\u5F55\u9875\u8F93\u5165");
|
|
62
|
+
if (url.pathname !== "/") throw new Error("\u8BF7\u586B\u5199\u670D\u52A1\u5668\u6839\u5730\u5740\uFF0C\u4F8B\u5982 http://localhost:3840");
|
|
63
|
+
return url.origin;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// src/service/manager.ts
|
|
67
|
+
import { mkdir, readFile as readFile2, writeFile, rename, rm, open } from "node:fs/promises";
|
|
68
|
+
import { homedir as homedir2 } from "node:os";
|
|
69
|
+
import { join, resolve as resolve2 } from "node:path";
|
|
70
|
+
import { spawn } from "node:child_process";
|
|
71
|
+
import { randomUUID } from "node:crypto";
|
|
72
|
+
import { parse as parse2, stringify } from "yaml";
|
|
73
|
+
|
|
74
|
+
// src/service/probe.ts
|
|
75
|
+
import { connect } from "node:net";
|
|
76
|
+
function isLocalServer(input) {
|
|
77
|
+
const url = new URL(normalizeServerUrl(input));
|
|
78
|
+
return url.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
|
|
79
|
+
}
|
|
80
|
+
async function portState(url, timeout) {
|
|
81
|
+
return new Promise((resolve4) => {
|
|
82
|
+
const socket = connect({ host: url.hostname.replace(/^\[|\]$/g, ""), port: Number(url.port || (url.protocol === "https:" ? 443 : 80)) });
|
|
83
|
+
const finish = (state) => {
|
|
84
|
+
socket.destroy();
|
|
85
|
+
resolve4(state);
|
|
86
|
+
};
|
|
87
|
+
socket.setTimeout(timeout, () => finish("unknown"));
|
|
88
|
+
socket.once("connect", () => finish("open"));
|
|
89
|
+
socket.once("error", (error) => finish(error.code === "ECONNREFUSED" ? "closed" : "unknown"));
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
async function probeService(input, timeout = 1500) {
|
|
93
|
+
const root = normalizeServerUrl(input);
|
|
94
|
+
const url = new URL(root);
|
|
95
|
+
try {
|
|
96
|
+
const response = await fetch(`${root}/health`, { signal: AbortSignal.timeout(timeout), redirect: "manual" });
|
|
97
|
+
if (!response.ok || !response.headers.get("content-type")?.includes("application/json")) {
|
|
98
|
+
await response.body?.cancel();
|
|
99
|
+
return { kind: "conflict", message: `${root} \u7AEF\u53E3\u5DF2\u6709\u670D\u52A1\uFF0C\u4F46\u6CA1\u6709\u8FD4\u56DE Web Terminal \u8EAB\u4EFD\uFF08HTTP ${response.status}\uFF09\u3002\u8BF7\u68C0\u67E5\u7AEF\u53E3\u5360\u7528\u6216\u5347\u7EA7\u65E7\u670D\u52A1\u3002` };
|
|
100
|
+
}
|
|
101
|
+
const reader = response.body.getReader();
|
|
102
|
+
let body = "";
|
|
103
|
+
try {
|
|
104
|
+
const decoder = new TextDecoder();
|
|
105
|
+
for (; ; ) {
|
|
106
|
+
const { done, value } = await reader.read();
|
|
107
|
+
if (done) break;
|
|
108
|
+
body += decoder.decode(value, { stream: true });
|
|
109
|
+
if (body.length > 8192) throw new Error("\u8EAB\u4EFD\u54CD\u5E94\u8FC7\u5927");
|
|
110
|
+
}
|
|
111
|
+
body += decoder.decode();
|
|
112
|
+
} finally {
|
|
113
|
+
await reader.cancel();
|
|
114
|
+
}
|
|
115
|
+
let identity;
|
|
116
|
+
try {
|
|
117
|
+
identity = JSON.parse(body);
|
|
118
|
+
} catch {
|
|
119
|
+
return { kind: "conflict", message: `${root} \u8FD4\u56DE\u7684\u670D\u52A1\u8EAB\u4EFD\u65E0\u6548\u3002` };
|
|
120
|
+
}
|
|
121
|
+
if (identity?.service !== serviceName || typeof identity.version !== "string" || typeof identity.instanceId !== "string" || !Number.isInteger(identity.pid) || typeof identity.managed !== "boolean") {
|
|
122
|
+
return { kind: "conflict", message: `${root} \u7AEF\u53E3\u88AB\u5176\u4ED6\u670D\u52A1\u5360\u7528\uFF0C\u672A\u542F\u52A8\u6216\u505C\u6B62\u4EFB\u4F55\u8FDB\u7A0B\u3002` };
|
|
123
|
+
}
|
|
124
|
+
if (identity.protocol !== serviceProtocol) return { kind: "incompatible", message: `Web Terminal ${identity.version} \u7684\u670D\u52A1\u534F\u8BAE\u4E0D\u517C\u5BB9\uFF0C\u8BF7\u5347\u7EA7\u670D\u52A1\u6216\u5BA2\u6237\u7AEF\u3002` };
|
|
125
|
+
return { kind: "ready", identity };
|
|
126
|
+
} catch {
|
|
127
|
+
const state = await portState(url, timeout);
|
|
128
|
+
if (state === "closed") return { kind: "stopped" };
|
|
129
|
+
if (state === "open") return { kind: "unreachable", message: `${root} \u7AEF\u53E3\u5DF2\u5360\u7528\u4F46\u8EAB\u4EFD\u68C0\u67E5\u65E0\u54CD\u5E94\uFF1B\u670D\u52A1\u53EF\u80FD\u6302\u8D77\u3002\u672A\u542F\u52A8\u5176\u4ED6\u670D\u52A1\u3002` };
|
|
130
|
+
return { kind: "unreachable", message: `\u65E0\u6CD5\u8BBF\u95EE ${root}\uFF0C\u8BF7\u68C0\u67E5\u7F51\u7EDC\u3001\u5730\u5740\u6216\u8F6C\u53D1\u914D\u7F6E\u3002` };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// src/service/process.ts
|
|
135
|
+
import { readFile } from "node:fs/promises";
|
|
136
|
+
import { execFile } from "node:child_process";
|
|
137
|
+
import { promisify } from "node:util";
|
|
138
|
+
var execute = promisify(execFile);
|
|
139
|
+
async function processStamp(pid) {
|
|
140
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) return void 0;
|
|
141
|
+
try {
|
|
142
|
+
if (process.platform === "linux") {
|
|
143
|
+
const stat = await readFile(`/proc/${pid}/stat`, "utf8");
|
|
144
|
+
const fields = stat.slice(stat.lastIndexOf(")") + 2).split(" ");
|
|
145
|
+
if (fields[0] === "Z") return void 0;
|
|
146
|
+
return `${(await readFile("/proc/sys/kernel/random/boot_id", "utf8")).trim()}:${fields[19]}`;
|
|
147
|
+
}
|
|
148
|
+
if (process.platform === "win32") {
|
|
149
|
+
const { stdout: stdout2 } = await execute("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", `(Get-CimInstance Win32_Process -Filter 'ProcessId = ${pid}').CreationDate.ToUniversalTime().Ticks`], { timeout: 5e3, windowsHide: true });
|
|
150
|
+
return stdout2.trim() || void 0;
|
|
151
|
+
}
|
|
152
|
+
const { stdout } = await execute("ps", ["-p", String(pid), "-o", "lstart="], { timeout: 5e3 });
|
|
153
|
+
return stdout.trim() || void 0;
|
|
154
|
+
} catch {
|
|
155
|
+
return void 0;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
var delay = (ms) => new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
159
|
+
async function stopProcess(pid, stamp) {
|
|
160
|
+
if (await processStamp(pid) !== stamp) return;
|
|
161
|
+
process.kill(pid, "SIGTERM");
|
|
162
|
+
const deadline = Date.now() + 5e3;
|
|
163
|
+
while (Date.now() < deadline) {
|
|
164
|
+
if (await processStamp(pid) !== stamp) return;
|
|
165
|
+
await delay(100);
|
|
166
|
+
}
|
|
167
|
+
if (await processStamp(pid) === stamp) process.kill(pid, "SIGKILL");
|
|
168
|
+
const forcedDeadline = Date.now() + 3e3;
|
|
169
|
+
while (Date.now() < forcedDeadline) {
|
|
170
|
+
if (await processStamp(pid) !== stamp) return;
|
|
171
|
+
await delay(100);
|
|
172
|
+
}
|
|
173
|
+
throw new Error("\u540E\u53F0\u8FDB\u7A0B\u672A\u80FD\u9000\u51FA\uFF0C\u8BF7\u67E5\u770B\u670D\u52A1\u65E5\u5FD7");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// src/service/manager.ts
|
|
177
|
+
function servicePaths(url, home = process.env.WEB_TERMINAL_HOME || join(homedir2(), ".web-terminal")) {
|
|
178
|
+
if (!isLocalServer(url)) throw new Error("\u53EA\u80FD\u7BA1\u7406\u672C\u673A HTTP \u670D\u52A1\uFF1B\u8FDC\u7A0B\u5730\u5740\u8BF7\u5728\u670D\u52A1\u5668\u6240\u5728\u673A\u5668\u8FD0\u884C web-terminal");
|
|
179
|
+
const base = resolve2(home);
|
|
180
|
+
const directory = join(base, "servers", new URL(url).port || "80");
|
|
181
|
+
return { base, directory, state: join(directory, "server.yaml"), log: join(directory, "server.log"), config: join(base, "config.yaml"), lock: join(directory, "manager.lock") };
|
|
182
|
+
}
|
|
183
|
+
async function readState(file) {
|
|
184
|
+
try {
|
|
185
|
+
const state = parse2(await readFile2(file, "utf8"));
|
|
186
|
+
if (Number.isSafeInteger(state?.pid) && state.pid > 0 && typeof state.stamp === "string" && typeof state.instanceId === "string" && typeof state.url === "string") return state;
|
|
187
|
+
} catch (error) {
|
|
188
|
+
if (error.code !== "ENOENT") throw error;
|
|
189
|
+
}
|
|
190
|
+
return void 0;
|
|
191
|
+
}
|
|
192
|
+
async function writeState(file, state) {
|
|
193
|
+
const temporary = `${file}.${randomUUID()}.tmp`;
|
|
194
|
+
await writeFile(temporary, stringify(state), { mode: 384, flag: "wx" });
|
|
195
|
+
await rename(temporary, file);
|
|
196
|
+
}
|
|
197
|
+
async function manageService(action, options) {
|
|
198
|
+
const paths = servicePaths(options.url, options.home);
|
|
199
|
+
await mkdir(paths.directory, { recursive: true, mode: 448 });
|
|
200
|
+
const owner = { pid: process.pid, stamp: await processStamp(process.pid) };
|
|
201
|
+
if (!owner.stamp) throw new Error("\u65E0\u6CD5\u6821\u9A8C\u5F53\u524D\u8FDB\u7A0B\uFF0C\u4E0D\u80FD\u5B89\u5168\u7BA1\u7406\u540E\u53F0\u670D\u52A1");
|
|
202
|
+
const deadline = Date.now() + 2e4;
|
|
203
|
+
for (; ; ) {
|
|
204
|
+
try {
|
|
205
|
+
await writeFile(paths.lock, stringify(owner), { flag: "wx", mode: 384 });
|
|
206
|
+
break;
|
|
207
|
+
} catch (error) {
|
|
208
|
+
if (error.code !== "EEXIST") throw error;
|
|
209
|
+
try {
|
|
210
|
+
const previous = parse2(await readFile2(paths.lock, "utf8"));
|
|
211
|
+
if (previous?.stamp && await processStamp(previous.pid) !== previous.stamp) {
|
|
212
|
+
await rm(paths.lock, { force: true });
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
} catch (error2) {
|
|
216
|
+
if (error2.code === "ENOENT") continue;
|
|
217
|
+
}
|
|
218
|
+
if (Date.now() >= deadline) throw new Error("\u53E6\u4E00\u4E2A\u5BA2\u6237\u7AEF\u6B63\u5728\u7BA1\u7406\u670D\u52A1\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5");
|
|
219
|
+
await delay(150);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
try {
|
|
223
|
+
let status = await probeService(options.url);
|
|
224
|
+
const state = await readState(paths.state);
|
|
225
|
+
const owned = state && await processStamp(state.pid) === state.stamp;
|
|
226
|
+
if (status.kind === "conflict" || status.kind === "incompatible") throw new Error(status.message);
|
|
227
|
+
if (action === "start" && status.kind === "ready") return status.identity;
|
|
228
|
+
if (status.kind === "ready" && (!owned || status.identity.instanceId !== state.instanceId || status.identity.pid !== state.pid)) {
|
|
229
|
+
throw new Error("\u8BE5 Web Terminal \u7531\u5176\u4ED6\u65B9\u5F0F\u542F\u52A8\uFF0C\u672A\u505C\u6B62\u5B83\u3002\u8BF7\u7528\u539F\u6765\u7684\u670D\u52A1\u7BA1\u7406\u5668\u91CD\u542F\uFF08\u4F8B\u5982 systemctl --user restart web-terminal.service\uFF09\u3002");
|
|
230
|
+
}
|
|
231
|
+
if (owned) await stopProcess(state.pid, state.stamp);
|
|
232
|
+
else if (status.kind === "unreachable") throw new Error(status.message);
|
|
233
|
+
await rm(paths.state, { force: true });
|
|
234
|
+
if (action === "stop") return void 0;
|
|
235
|
+
status = await probeService(options.url);
|
|
236
|
+
if (status.kind === "ready") return status.identity;
|
|
237
|
+
if (status.kind !== "stopped") throw new Error(status.message);
|
|
238
|
+
const log = await open(paths.log, "a", 384);
|
|
239
|
+
let child;
|
|
240
|
+
let failure;
|
|
241
|
+
let exited = false;
|
|
242
|
+
try {
|
|
243
|
+
const config = options.config ? resolve2(options.config) : state?.config;
|
|
244
|
+
child = spawn(process.execPath, [options.entry, "serve", "--url", options.url, "--home", paths.base, ...config ? ["--config", config] : []], {
|
|
245
|
+
detached: true,
|
|
246
|
+
stdio: ["ignore", log.fd, log.fd],
|
|
247
|
+
windowsHide: true,
|
|
248
|
+
env: { ...process.env, ELECTRON_RUN_AS_NODE: void 0 }
|
|
249
|
+
});
|
|
250
|
+
child.once("error", (error) => {
|
|
251
|
+
failure = error;
|
|
252
|
+
});
|
|
253
|
+
child.once("exit", () => {
|
|
254
|
+
exited = true;
|
|
255
|
+
});
|
|
256
|
+
} finally {
|
|
257
|
+
await log.close();
|
|
258
|
+
}
|
|
259
|
+
child.unref();
|
|
260
|
+
const started = Date.now();
|
|
261
|
+
while (Date.now() - started < 15e3) {
|
|
262
|
+
if (failure || exited) break;
|
|
263
|
+
const current = await probeService(options.url, 500);
|
|
264
|
+
const launched = await readState(paths.state);
|
|
265
|
+
if (current.kind === "ready" && launched && launched.pid === child.pid && current.identity.instanceId === launched.instanceId) return current.identity;
|
|
266
|
+
if (current.kind === "conflict" || current.kind === "incompatible") break;
|
|
267
|
+
await delay(100);
|
|
268
|
+
}
|
|
269
|
+
if (child.pid && !exited) {
|
|
270
|
+
const stamp = await processStamp(child.pid);
|
|
271
|
+
if (stamp) await stopProcess(child.pid, stamp);
|
|
272
|
+
}
|
|
273
|
+
const logReader = await open(paths.log, "r");
|
|
274
|
+
let output;
|
|
275
|
+
try {
|
|
276
|
+
const size = (await logReader.stat()).size;
|
|
277
|
+
const tail = Buffer.alloc(Math.min(size, 4e3));
|
|
278
|
+
const { bytesRead } = await logReader.read(tail, 0, tail.length, Math.max(0, size - tail.length));
|
|
279
|
+
output = tail.subarray(0, bytesRead).toString("utf8");
|
|
280
|
+
} finally {
|
|
281
|
+
await logReader.close();
|
|
282
|
+
}
|
|
283
|
+
throw new Error(`\u540E\u53F0\u542F\u52A8\u5931\u8D25\uFF1A${failure?.message || output || "\u7B49\u5F85\u670D\u52A1\u5C31\u7EEA\u8D85\u65F6"}
|
|
284
|
+
\u65E5\u5FD7\uFF1A${paths.log}`);
|
|
285
|
+
} finally {
|
|
286
|
+
await rm(paths.lock, { force: true });
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// src/cli.ts
|
|
291
|
+
async function main() {
|
|
292
|
+
const { values, positionals } = parseArgs({ allowPositionals: true, options: {
|
|
293
|
+
url: { type: "string", default: defaultServerUrl },
|
|
294
|
+
home: { type: "string" },
|
|
295
|
+
config: { type: "string" },
|
|
296
|
+
version: { type: "boolean" },
|
|
297
|
+
help: { type: "boolean" },
|
|
298
|
+
vsix: { type: "string" }
|
|
299
|
+
} });
|
|
300
|
+
if (values.version) {
|
|
301
|
+
console.log(JSON.stringify({ service: serviceName, version: serviceVersion }));
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
const action = positionals[0] || "start";
|
|
305
|
+
if (values.help) {
|
|
306
|
+
console.log("web-terminal [start|restart|stop|status] [--url http://localhost:3840] [--config /path/config.yaml] [--home /path/state]\nweb-terminal install-vscode [--vsix /path/web-terminal.vsix]\n\u5B89\u88C5\u81EA\u5E26\u63D2\u4EF6\u5230\u5DF2\u68C0\u6D4B\u5230\u7684 VS Code Stable/Insiders\uFF1BWSL \u4E0B\u540C\u65F6\u5B89\u88C5 Windows \u5BBF\u4E3B\u3002\n\u914D\u7F6E\u3001token\u3001\u8FDB\u7A0B\u72B6\u6001\u548C\u65E5\u5FD7\u9ED8\u8BA4\u4F4D\u4E8E ~/.web-terminal/\u3002\u91CD\u542F\u4F1A\u7ED3\u675F\u666E\u901A Shell \u4F1A\u8BDD\u3002");
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
if (action === "install-vscode") {
|
|
310
|
+
const { installVSCodeExtension } = await import("./vscodeInstaller-WIM5VHDI.js");
|
|
311
|
+
await installVSCodeExtension({ vsixPath: values.vsix, onInstalled: (result) => console.log(`${result.target} \u5DF2\u5B89\u88C5 ${result.extension}`) });
|
|
312
|
+
console.log("\u8BF7\u5728\u5DF2\u6253\u5F00\u7684 VS Code \u7A97\u53E3\u8FD0\u884C Developer: Reload Window \u4EE5\u52A0\u8F7D\u63D2\u4EF6\u3002");
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
if (values.vsix) throw new Error("--vsix \u4EC5\u7528\u4E8E install-vscode");
|
|
316
|
+
const url = normalizeServerUrl(values.url);
|
|
317
|
+
if (action === "status") {
|
|
318
|
+
console.log(JSON.stringify(await probeService(url)));
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
if (action !== "serve") {
|
|
322
|
+
if (!["start", "restart", "stop"].includes(action)) throw new Error(`\u672A\u77E5\u547D\u4EE4\uFF1A${action}`);
|
|
323
|
+
const identity = await manageService(action, { url, entry: fileURLToPath(import.meta.url), home: values.home, config: values.config });
|
|
324
|
+
console.log(JSON.stringify({ ok: true, url, identity }));
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
const paths = servicePaths(url, values.home);
|
|
328
|
+
await mkdir2(paths.directory, { recursive: true, mode: 448 });
|
|
329
|
+
const configFile = resolve3(values.config || process.env.WEB_TERMINAL_CONFIG || paths.config);
|
|
330
|
+
process.env.WEB_TERMINAL_CONFIG = configFile;
|
|
331
|
+
process.chdir(dirname(configFile));
|
|
332
|
+
const config = loadConfig({ dataDir: join2(paths.directory, "data"), uploadDir: join2(paths.base, "uploads") });
|
|
333
|
+
config.host = new URL(url).hostname === "localhost" ? "127.0.0.1" : new URL(url).hostname.replace(/^\[|\]$/g, "");
|
|
334
|
+
config.port = Number(new URL(url).port || 80);
|
|
335
|
+
const instanceId = randomUUID2();
|
|
336
|
+
const { createApp } = await import("./app-3OE4ER7S.js");
|
|
337
|
+
const runtime = await createApp(config, { managed: true, instanceId, staticDir: fileURLToPath(new URL("../dist/", import.meta.url)) });
|
|
338
|
+
const stamp = await processStamp(process.pid);
|
|
339
|
+
if (!stamp) throw new Error("\u65E0\u6CD5\u53D6\u5F97\u8FDB\u7A0B\u542F\u52A8\u6807\u8BC6\uFF0C\u62D2\u7EDD\u542F\u52A8\u4E0D\u53EF\u5B89\u5168\u7BA1\u7406\u7684\u540E\u53F0");
|
|
340
|
+
await new Promise((done, reject) => {
|
|
341
|
+
runtime.server.once("error", reject);
|
|
342
|
+
runtime.server.listen(config.port, config.host, done);
|
|
343
|
+
});
|
|
344
|
+
await writeState(paths.state, { pid: process.pid, stamp, instanceId, url, config: configFile });
|
|
345
|
+
console.log(`Web Terminal ${serviceVersion}: ${url}
|
|
346
|
+
token \u6587\u4EF6\uFF1A${join2(config.dataDir, "token")}`);
|
|
347
|
+
let closing = false;
|
|
348
|
+
for (const signal of ["SIGINT", "SIGTERM"]) process.on(signal, () => {
|
|
349
|
+
if (closing) return;
|
|
350
|
+
closing = true;
|
|
351
|
+
void (async () => {
|
|
352
|
+
await runtime.close();
|
|
353
|
+
if ((await readState(paths.state))?.instanceId === instanceId) await rm2(paths.state, { force: true });
|
|
354
|
+
})().then(() => process.exit(0), (error) => {
|
|
355
|
+
console.error(error);
|
|
356
|
+
process.exit(1);
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
main().catch((error) => {
|
|
361
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
362
|
+
process.exit(1);
|
|
363
|
+
});
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import {
|
|
2
|
+
serviceVersion
|
|
3
|
+
} from "./chunk-TD3NJESY.js";
|
|
4
|
+
|
|
5
|
+
// src/vscodeInstaller.ts
|
|
6
|
+
import { execFile } from "node:child_process";
|
|
7
|
+
import { access, readFile, readdir, stat } from "node:fs/promises";
|
|
8
|
+
import { constants } from "node:fs";
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
import { delimiter, join, resolve } from "node:path";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
import { promisify } from "node:util";
|
|
13
|
+
var execute = promisify(execFile);
|
|
14
|
+
var extensionId = "dadigua.web-terminal";
|
|
15
|
+
var psQuote = (value) => `'${value.replaceAll("'", "''")}'`;
|
|
16
|
+
async function exists(path) {
|
|
17
|
+
try {
|
|
18
|
+
await access(path);
|
|
19
|
+
return true;
|
|
20
|
+
} catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
async function findServerLauncher(root) {
|
|
25
|
+
const cliRoot = join(root, "cli/servers");
|
|
26
|
+
let recent = [];
|
|
27
|
+
try {
|
|
28
|
+
const value = JSON.parse(await readFile(join(cliRoot, "lru.json"), "utf8"));
|
|
29
|
+
if (Array.isArray(value)) recent = value.filter((item) => typeof item === "string" && !item.includes("/") && !item.includes("\\"));
|
|
30
|
+
} catch {
|
|
31
|
+
}
|
|
32
|
+
const candidates = [
|
|
33
|
+
...(await readdir(cliRoot).catch(() => [])).map((name) => ({ name, path: join(cliRoot, name, "server") })),
|
|
34
|
+
...(await readdir(join(root, "bin")).catch(() => [])).map((name) => ({ name, path: join(root, "bin", name) }))
|
|
35
|
+
];
|
|
36
|
+
const available = [];
|
|
37
|
+
for (const candidate of candidates) {
|
|
38
|
+
const entry = join(candidate.path, "out/server-main.js");
|
|
39
|
+
if (!await exists(entry)) continue;
|
|
40
|
+
available.push({ ...candidate, entry, mtime: (await stat(candidate.path)).mtimeMs });
|
|
41
|
+
}
|
|
42
|
+
available.sort((a, b) => {
|
|
43
|
+
const rank = (name) => recent.includes(name) ? recent.indexOf(name) : Infinity;
|
|
44
|
+
return rank(a.name) - rank(b.name) || b.mtime - a.mtime;
|
|
45
|
+
});
|
|
46
|
+
const server = available[0];
|
|
47
|
+
if (!server) return void 0;
|
|
48
|
+
const node = join(server.path, "node");
|
|
49
|
+
return { file: await exists(node) ? node : process.execPath, args: [server.entry, "--extensions-dir", join(root, "extensions")] };
|
|
50
|
+
}
|
|
51
|
+
async function findCommand(command) {
|
|
52
|
+
for (const directory of (process.env.PATH || "").split(delimiter)) {
|
|
53
|
+
const candidate = resolve(directory, command);
|
|
54
|
+
try {
|
|
55
|
+
await access(candidate, constants.X_OK);
|
|
56
|
+
return candidate;
|
|
57
|
+
} catch {
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return void 0;
|
|
61
|
+
}
|
|
62
|
+
async function isWsl() {
|
|
63
|
+
if (process.platform !== "linux") return false;
|
|
64
|
+
if (process.env.WSL_DISTRO_NAME) return true;
|
|
65
|
+
return (await readFile("/proc/version", "utf8").catch(() => "")).toLowerCase().includes("microsoft");
|
|
66
|
+
}
|
|
67
|
+
async function localLaunchers(options, wsl) {
|
|
68
|
+
const launchers = [];
|
|
69
|
+
for (const [channel, command, override, folder] of [
|
|
70
|
+
["Stable", "code", options.codeCommand, ".vscode-server"],
|
|
71
|
+
["Insiders", "code-insiders", options.codeInsidersCommand, ".vscode-server-insiders"]
|
|
72
|
+
]) {
|
|
73
|
+
if (override) {
|
|
74
|
+
launchers.push({ label: `\u672C\u673A ${channel}`, file: override, args: [] });
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (process.platform === "linux") {
|
|
78
|
+
const defaultRoot = join(homedir(), folder);
|
|
79
|
+
const agentRoot = process.env.VSCODE_AGENT_FOLDER;
|
|
80
|
+
const root = agentRoot && agentRoot.includes("insiders") === (channel === "Insiders") ? agentRoot : defaultRoot;
|
|
81
|
+
const server = await findServerLauncher(root);
|
|
82
|
+
if (server) {
|
|
83
|
+
launchers.push({ label: `${wsl ? "WSL" : "Remote"} ${channel}`, ...server });
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const file = await findCommand(command);
|
|
88
|
+
if (file && !(wsl && (file.startsWith("/mnt/") || file.includes("/remote-cli/")))) {
|
|
89
|
+
launchers.push({ label: `\u672C\u673A ${channel}`, file, args: [] });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return launchers;
|
|
93
|
+
}
|
|
94
|
+
async function invoke(launcher, args) {
|
|
95
|
+
const { stdout } = await execute(launcher.file, [...launcher.args, ...args], {
|
|
96
|
+
timeout: 6e4,
|
|
97
|
+
windowsHide: true,
|
|
98
|
+
maxBuffer: 4 * 1024 * 1024
|
|
99
|
+
});
|
|
100
|
+
if (stdout.includes("Command is only available in WSL or inside a Visual Studio Code terminal")) {
|
|
101
|
+
throw new Error(stdout.trim());
|
|
102
|
+
}
|
|
103
|
+
return stdout;
|
|
104
|
+
}
|
|
105
|
+
function verifyExtension(output, version) {
|
|
106
|
+
const extension = output.split(/\r?\n/).map((line) => line.trim()).find((line) => line.toLowerCase().startsWith(`${extensionId}@`));
|
|
107
|
+
if (!extension || version && extension !== `${extensionId}@${version}`) {
|
|
108
|
+
throw new Error(`\u5B89\u88C5\u540E\u672A\u627E\u5230 ${extensionId}${version ? `@${version}` : ""}\uFF0C\u6269\u5C55\u5217\u8868\uFF1A
|
|
109
|
+
${output.trim()}`);
|
|
110
|
+
}
|
|
111
|
+
return extension;
|
|
112
|
+
}
|
|
113
|
+
async function installWindows(vsix, wsl, version) {
|
|
114
|
+
const source = wsl ? (await execute("wslpath", ["-w", vsix])).stdout.trim() : vsix;
|
|
115
|
+
const script = `
|
|
116
|
+
$ErrorActionPreference = 'Stop'
|
|
117
|
+
$temporary = $null
|
|
118
|
+
try {
|
|
119
|
+
Set-Location $env:SystemDrive\\
|
|
120
|
+
$vsix = ${psQuote(source)}
|
|
121
|
+
if (${wsl ? "$true" : "$false"}) {
|
|
122
|
+
$temporary = Join-Path $env:TEMP ('web-terminal-' + [guid]::NewGuid() + '.vsix')
|
|
123
|
+
Copy-Item -LiteralPath $vsix -Destination $temporary
|
|
124
|
+
$vsix = $temporary
|
|
125
|
+
}
|
|
126
|
+
$results = @()
|
|
127
|
+
$failures = @()
|
|
128
|
+
foreach ($channel in @(
|
|
129
|
+
@{ Name = 'Stable'; Command = 'code.cmd'; Folder = 'Microsoft VS Code' },
|
|
130
|
+
@{ Name = 'Insiders'; Command = 'code-insiders.cmd'; Folder = 'Microsoft VS Code Insiders' }
|
|
131
|
+
)) {
|
|
132
|
+
$command = (Get-Command $channel.Command -ErrorAction SilentlyContinue | Select-Object -First 1).Source
|
|
133
|
+
if (-not $command) {
|
|
134
|
+
foreach ($base in @((Join-Path $env:LOCALAPPDATA 'Programs'), $env:ProgramFiles, [Environment]::GetEnvironmentVariable('ProgramFiles(x86)'))) {
|
|
135
|
+
if (-not $base) { continue }
|
|
136
|
+
$candidate = Join-Path $base ($channel.Folder + '\\bin\\' + $channel.Command)
|
|
137
|
+
if (Test-Path -LiteralPath $candidate) { $command = $candidate; break }
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (-not $command) { continue }
|
|
141
|
+
try {
|
|
142
|
+
& $command --install-extension $vsix --force | Out-Host
|
|
143
|
+
if ($LASTEXITCODE -ne 0) { throw ('\u5B89\u88C5\u547D\u4EE4\u9000\u51FA\u7801\uFF1A' + $LASTEXITCODE) }
|
|
144
|
+
$extensions = & $command --list-extensions --show-versions
|
|
145
|
+
if ($LASTEXITCODE -ne 0) { throw ('\u6269\u5C55\u5217\u8868\u547D\u4EE4\u9000\u51FA\u7801\uFF1A' + $LASTEXITCODE) }
|
|
146
|
+
$match = $extensions | Where-Object { $_ -like '${extensionId}@*' } | Select-Object -First 1
|
|
147
|
+
if (-not $match${version ? ` -or $match -ne ${psQuote(`${extensionId}@${version}`)}` : ""}) { throw '\u5B89\u88C5\u540E\u672A\u627E\u5230\u6307\u5B9A\u7248\u672C\u7684 Web Terminal \u6269\u5C55' }
|
|
148
|
+
$results += @{ target = ('Windows ' + $channel.Name); extension = $match }
|
|
149
|
+
} catch { $failures += ('Windows ' + $channel.Name + ': ' + $_.Exception.Message) }
|
|
150
|
+
}
|
|
151
|
+
Write-Output ('WEB_TERMINAL_INSTALL::' + (ConvertTo-Json -Compress -Depth 4 -InputObject @{ installed = @($results); failures = @($failures) }))
|
|
152
|
+
} catch { Write-Error $_ -ErrorAction Continue; exit 1 }
|
|
153
|
+
finally { if ($temporary) { Remove-Item -LiteralPath $temporary -Force -ErrorAction SilentlyContinue } }
|
|
154
|
+
`;
|
|
155
|
+
const powershell = wsl ? "/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe" : "powershell.exe";
|
|
156
|
+
const { stdout } = await execute(powershell, ["-NoProfile", "-NonInteractive", "-EncodedCommand", Buffer.from(script, "utf16le").toString("base64")], {
|
|
157
|
+
timeout: 18e4,
|
|
158
|
+
windowsHide: true,
|
|
159
|
+
maxBuffer: 4 * 1024 * 1024
|
|
160
|
+
});
|
|
161
|
+
const marker = stdout.split(/\r?\n/).find((line) => line.startsWith("WEB_TERMINAL_INSTALL::"));
|
|
162
|
+
if (!marker) throw new Error(`Windows \u5B89\u88C5\u6CA1\u6709\u8FD4\u56DE\u9A8C\u8BC1\u7ED3\u679C\uFF1A
|
|
163
|
+
${stdout.trim()}`);
|
|
164
|
+
const result = JSON.parse(marker.slice("WEB_TERMINAL_INSTALL::".length));
|
|
165
|
+
for (const installed of result.installed) verifyExtension(installed.extension, version);
|
|
166
|
+
if (result.failures.length) throw new Error([...result.installed.map((item) => `${item.target} \u5DF2\u5B89\u88C5 ${item.extension}`), ...result.failures].join("\n"));
|
|
167
|
+
return result.installed;
|
|
168
|
+
}
|
|
169
|
+
async function installVSCodeExtension(options = {}) {
|
|
170
|
+
const path = options.vsixPath ? resolve(options.vsixPath) : fileURLToPath(new URL("../release/web-terminal.vsix", import.meta.url));
|
|
171
|
+
if (!await exists(path)) throw new Error(`\u672A\u627E\u5230 VSIX\uFF1A${path}\u3002\u672C\u5730\u5F00\u53D1\u8BF7\u8FD0\u884C pnpm package:vscode\uFF0C\u6216\u7528 --vsix \u6307\u5B9A\u6587\u4EF6\u3002`);
|
|
172
|
+
const version = options.vsixPath ? void 0 : serviceVersion;
|
|
173
|
+
const wsl = await isWsl();
|
|
174
|
+
const installed = [];
|
|
175
|
+
const failures = [];
|
|
176
|
+
const record = (result) => {
|
|
177
|
+
installed.push(result);
|
|
178
|
+
options.onInstalled?.(result);
|
|
179
|
+
};
|
|
180
|
+
if (process.platform !== "win32") {
|
|
181
|
+
for (const launcher of await localLaunchers(options, wsl)) {
|
|
182
|
+
try {
|
|
183
|
+
await invoke(launcher, ["--install-extension", path, "--force"]);
|
|
184
|
+
const extension = verifyExtension(await invoke(launcher, ["--list-extensions", "--show-versions"]), version);
|
|
185
|
+
record({ target: launcher.label, extension });
|
|
186
|
+
} catch (error) {
|
|
187
|
+
failures.push(`${launcher.label}\uFF1A${errorDetail(error)}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (process.platform === "win32" || (options.installWindowsHost ?? wsl)) {
|
|
192
|
+
try {
|
|
193
|
+
for (const result of await installWindows(path, wsl, version)) record(result);
|
|
194
|
+
} catch (error) {
|
|
195
|
+
failures.push(errorDetail(error));
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (failures.length) throw new Error(`\u63D2\u4EF6\u5B89\u88C5\u672A\u5168\u90E8\u5B8C\u6210\uFF1A
|
|
199
|
+
${failures.join("\n")}`);
|
|
200
|
+
if (!installed.length) throw new Error("\u672A\u627E\u5230\u53EF\u7528\u7684 VS Code\u3002\u8BF7\u5B89\u88C5 VS Code \u5E76\u5C06 code \u52A0\u5165 PATH\uFF1BWSL \u6269\u5C55\u5BBF\u4E3B\u9700\u5148\u7528 Remote WSL \u8FDE\u63A5\u4E00\u6B21\u3002");
|
|
201
|
+
return installed;
|
|
202
|
+
}
|
|
203
|
+
function errorDetail(error) {
|
|
204
|
+
const failure = error;
|
|
205
|
+
return [error instanceof Error ? error.message : String(error), failure.stderr?.trim(), failure.stdout?.trim()].filter(Boolean).join("\n");
|
|
206
|
+
}
|
|
207
|
+
export {
|
|
208
|
+
findServerLauncher,
|
|
209
|
+
installVSCodeExtension
|
|
210
|
+
};
|