@dadigua/web-terminal 0.0.0-stage → 0.1.3

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.
@@ -0,0 +1,88 @@
1
+ // package.json
2
+ var package_default = {
3
+ name: "@dadigua/web-terminal",
4
+ version: "0.1.3",
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
+ electron: "pnpm build:clients && electron dist-clients/electron",
15
+ "package:electron:win": "pnpm build:clients && tsx scripts/package-electron.ts --win",
16
+ "package:electron:linux": "pnpm build:clients && tsx scripts/package-electron.ts --linux",
17
+ prepack: "pnpm build"
18
+ },
19
+ dependencies: {
20
+ "@xterm/addon-fit": "^0.11.0",
21
+ "@xterm/addon-serialize": "0.14.0",
22
+ "@xterm/headless": "6.0.0",
23
+ "@xterm/xterm": "^6.0.0",
24
+ express: "^5.1.0",
25
+ multer: "^2.0.2",
26
+ "node-pty": "^1.1.0",
27
+ react: "^19.2.0",
28
+ "react-dom": "^19.2.0",
29
+ sharp: "^0.34.5",
30
+ ws: "^8.18.3",
31
+ yaml: "^2.8.2"
32
+ },
33
+ devDependencies: {
34
+ "@types/vscode": "^1.96.0",
35
+ "@vscode/vsce": "^3.9.2",
36
+ electron: "^42.3.3",
37
+ "electron-builder": "^26.15.3",
38
+ esbuild: "^0.28.0",
39
+ "@types/express": "^5.0.0",
40
+ "@types/multer": "^2.0.0",
41
+ "@types/node": "^24.0.0",
42
+ "@types/react": "^19.2.0",
43
+ "@types/react-dom": "^19.2.0",
44
+ "@types/ws": "^8.18.1",
45
+ "@vitejs/plugin-react": "^5.1.0",
46
+ tsx: "^4.20.6",
47
+ typescript: "^5.9.3",
48
+ vite: "^7.2.0"
49
+ },
50
+ engines: {
51
+ node: ">=22"
52
+ },
53
+ packageManager: "pnpm@11.12.0",
54
+ bin: {
55
+ "web-terminal": "bin/web-terminal.mjs"
56
+ },
57
+ files: [
58
+ "bin/",
59
+ "dist-server/",
60
+ "dist/",
61
+ "config.example.yaml",
62
+ "README.md",
63
+ "API.md"
64
+ ],
65
+ 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",
66
+ repository: {
67
+ type: "git",
68
+ url: "git+https://github.com/laopo001/web-terminal.git"
69
+ },
70
+ homepage: "https://github.com/laopo001/web-terminal#readme",
71
+ bugs: {
72
+ url: "https://github.com/laopo001/web-terminal/issues"
73
+ },
74
+ publishConfig: {
75
+ access: "public"
76
+ }
77
+ };
78
+
79
+ // src/shared/service.ts
80
+ var serviceName = "@dadigua/web-terminal";
81
+ var serviceProtocol = 1;
82
+ var serviceVersion = package_default.version;
83
+
84
+ export {
85
+ serviceName,
86
+ serviceProtocol,
87
+ serviceVersion
88
+ };
@@ -0,0 +1,353 @@
1
+ import {
2
+ serviceName,
3
+ serviceProtocol,
4
+ serviceVersion
5
+ } from "./chunk-R33XRLBM.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
+ return {
38
+ host: String(process.env.HOST || input.host || "127.0.0.1"),
39
+ port,
40
+ dataDir,
41
+ token,
42
+ defaultCwd: realpathSync(resolve(input.defaultCwd || homedir())),
43
+ shell: String(input.shell || process.env.SHELL || (process.platform === "win32" ? "powershell.exe" : "/bin/bash")),
44
+ machineId: `${hostname()}-${createHash("sha256").update(dataDir).digest("hex").slice(0, 8)}`,
45
+ maxUploadBytes: 20 * 1024 * 1024
46
+ };
47
+ }
48
+
49
+ // src/shared/clientUrl.ts
50
+ var defaultServerUrl = "http://localhost:3840";
51
+ function normalizeServerUrl(input) {
52
+ let url;
53
+ try {
54
+ url = new URL(input.trim());
55
+ } catch {
56
+ throw new Error("\u8BF7\u8F93\u5165\u5B8C\u6574\u7684 http:// \u6216 https:// \u670D\u52A1\u5668\u5730\u5740");
57
+ }
58
+ if (!["http:", "https:"].includes(url.protocol) || !url.hostname) throw new Error("\u670D\u52A1\u5668\u5730\u5740\u53EA\u652F\u6301 HTTP \u6216 HTTPS");
59
+ 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");
60
+ if (url.pathname !== "/") throw new Error("\u8BF7\u586B\u5199\u670D\u52A1\u5668\u6839\u5730\u5740\uFF0C\u4F8B\u5982 http://localhost:3840");
61
+ return url.origin;
62
+ }
63
+
64
+ // src/service/manager.ts
65
+ import { mkdir, readFile as readFile2, writeFile, rename, rm, open } from "node:fs/promises";
66
+ import { homedir as homedir2 } from "node:os";
67
+ import { join, resolve as resolve2 } from "node:path";
68
+ import { spawn } from "node:child_process";
69
+ import { randomUUID } from "node:crypto";
70
+ import { parse as parse2, stringify } from "yaml";
71
+
72
+ // src/service/probe.ts
73
+ import { connect } from "node:net";
74
+ function isLocalServer(input) {
75
+ const url = new URL(normalizeServerUrl(input));
76
+ return url.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
77
+ }
78
+ async function portState(url, timeout) {
79
+ return new Promise((resolve4) => {
80
+ const socket = connect({ host: url.hostname.replace(/^\[|\]$/g, ""), port: Number(url.port || (url.protocol === "https:" ? 443 : 80)) });
81
+ const finish = (state) => {
82
+ socket.destroy();
83
+ resolve4(state);
84
+ };
85
+ socket.setTimeout(timeout, () => finish("unknown"));
86
+ socket.once("connect", () => finish("open"));
87
+ socket.once("error", (error) => finish(error.code === "ECONNREFUSED" ? "closed" : "unknown"));
88
+ });
89
+ }
90
+ async function probeService(input, timeout = 1500) {
91
+ const root = normalizeServerUrl(input);
92
+ const url = new URL(root);
93
+ try {
94
+ const response = await fetch(`${root}/health`, { signal: AbortSignal.timeout(timeout), redirect: "manual" });
95
+ if (!response.ok || !response.headers.get("content-type")?.includes("application/json")) {
96
+ await response.body?.cancel();
97
+ 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` };
98
+ }
99
+ const reader = response.body.getReader();
100
+ let body = "";
101
+ try {
102
+ const decoder = new TextDecoder();
103
+ for (; ; ) {
104
+ const { done, value } = await reader.read();
105
+ if (done) break;
106
+ body += decoder.decode(value, { stream: true });
107
+ if (body.length > 8192) throw new Error("\u8EAB\u4EFD\u54CD\u5E94\u8FC7\u5927");
108
+ }
109
+ body += decoder.decode();
110
+ } finally {
111
+ await reader.cancel();
112
+ }
113
+ let identity;
114
+ try {
115
+ identity = JSON.parse(body);
116
+ } catch {
117
+ return { kind: "conflict", message: `${root} \u8FD4\u56DE\u7684\u670D\u52A1\u8EAB\u4EFD\u65E0\u6548\u3002` };
118
+ }
119
+ if (identity?.service !== serviceName || typeof identity.version !== "string" || typeof identity.instanceId !== "string" || !Number.isInteger(identity.pid) || typeof identity.managed !== "boolean") {
120
+ 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` };
121
+ }
122
+ 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` };
123
+ return { kind: "ready", identity };
124
+ } catch {
125
+ const state = await portState(url, timeout);
126
+ if (state === "closed") return { kind: "stopped" };
127
+ 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` };
128
+ return { kind: "unreachable", message: `\u65E0\u6CD5\u8BBF\u95EE ${root}\uFF0C\u8BF7\u68C0\u67E5\u7F51\u7EDC\u3001\u5730\u5740\u6216\u8F6C\u53D1\u914D\u7F6E\u3002` };
129
+ }
130
+ }
131
+
132
+ // src/service/process.ts
133
+ import { readFile } from "node:fs/promises";
134
+ import { execFile } from "node:child_process";
135
+ import { promisify } from "node:util";
136
+ var execute = promisify(execFile);
137
+ async function processStamp(pid) {
138
+ if (!Number.isSafeInteger(pid) || pid <= 0) return void 0;
139
+ try {
140
+ if (process.platform === "linux") {
141
+ const stat = await readFile(`/proc/${pid}/stat`, "utf8");
142
+ const fields = stat.slice(stat.lastIndexOf(")") + 2).split(" ");
143
+ if (fields[0] === "Z") return void 0;
144
+ return `${(await readFile("/proc/sys/kernel/random/boot_id", "utf8")).trim()}:${fields[19]}`;
145
+ }
146
+ if (process.platform === "win32") {
147
+ const { stdout: stdout2 } = await execute("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", `(Get-CimInstance Win32_Process -Filter 'ProcessId = ${pid}').CreationDate.ToUniversalTime().Ticks`], { timeout: 5e3, windowsHide: true });
148
+ return stdout2.trim() || void 0;
149
+ }
150
+ const { stdout } = await execute("ps", ["-p", String(pid), "-o", "lstart="], { timeout: 5e3 });
151
+ return stdout.trim() || void 0;
152
+ } catch {
153
+ return void 0;
154
+ }
155
+ }
156
+ var delay = (ms) => new Promise((resolve4) => setTimeout(resolve4, ms));
157
+ async function stopProcess(pid, stamp) {
158
+ if (await processStamp(pid) !== stamp) return;
159
+ process.kill(pid, "SIGTERM");
160
+ const deadline = Date.now() + 5e3;
161
+ while (Date.now() < deadline) {
162
+ if (await processStamp(pid) !== stamp) return;
163
+ await delay(100);
164
+ }
165
+ if (await processStamp(pid) === stamp) process.kill(pid, "SIGKILL");
166
+ const forcedDeadline = Date.now() + 3e3;
167
+ while (Date.now() < forcedDeadline) {
168
+ if (await processStamp(pid) !== stamp) return;
169
+ await delay(100);
170
+ }
171
+ throw new Error("\u540E\u53F0\u8FDB\u7A0B\u672A\u80FD\u9000\u51FA\uFF0C\u8BF7\u67E5\u770B\u670D\u52A1\u65E5\u5FD7");
172
+ }
173
+
174
+ // src/service/manager.ts
175
+ function servicePaths(url, home = process.env.WEB_TERMINAL_HOME || join(homedir2(), ".web-terminal")) {
176
+ 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");
177
+ const base = resolve2(home);
178
+ const directory = join(base, "servers", new URL(url).port || "80");
179
+ return { base, directory, state: join(directory, "server.yaml"), log: join(directory, "server.log"), config: join(base, "config.yaml"), lock: join(directory, "manager.lock") };
180
+ }
181
+ async function readState(file) {
182
+ try {
183
+ const state = parse2(await readFile2(file, "utf8"));
184
+ if (Number.isSafeInteger(state?.pid) && state.pid > 0 && typeof state.stamp === "string" && typeof state.instanceId === "string" && typeof state.url === "string") return state;
185
+ } catch (error) {
186
+ if (error.code !== "ENOENT") throw error;
187
+ }
188
+ return void 0;
189
+ }
190
+ async function writeState(file, state) {
191
+ const temporary = `${file}.${randomUUID()}.tmp`;
192
+ await writeFile(temporary, stringify(state), { mode: 384, flag: "wx" });
193
+ await rename(temporary, file);
194
+ }
195
+ async function manageService(action, options) {
196
+ const paths = servicePaths(options.url, options.home);
197
+ await mkdir(paths.directory, { recursive: true, mode: 448 });
198
+ const owner = { pid: process.pid, stamp: await processStamp(process.pid) };
199
+ 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");
200
+ const deadline = Date.now() + 2e4;
201
+ for (; ; ) {
202
+ try {
203
+ await writeFile(paths.lock, stringify(owner), { flag: "wx", mode: 384 });
204
+ break;
205
+ } catch (error) {
206
+ if (error.code !== "EEXIST") throw error;
207
+ try {
208
+ const previous = parse2(await readFile2(paths.lock, "utf8"));
209
+ if (previous?.stamp && await processStamp(previous.pid) !== previous.stamp) {
210
+ await rm(paths.lock, { force: true });
211
+ continue;
212
+ }
213
+ } catch (error2) {
214
+ if (error2.code === "ENOENT") continue;
215
+ }
216
+ 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");
217
+ await delay(150);
218
+ }
219
+ }
220
+ try {
221
+ let status = await probeService(options.url);
222
+ const state = await readState(paths.state);
223
+ const owned = state && await processStamp(state.pid) === state.stamp;
224
+ if (status.kind === "conflict" || status.kind === "incompatible") throw new Error(status.message);
225
+ if (action === "start" && status.kind === "ready") return status.identity;
226
+ if (status.kind === "ready" && (!owned || status.identity.instanceId !== state.instanceId || status.identity.pid !== state.pid)) {
227
+ 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");
228
+ }
229
+ if (owned) await stopProcess(state.pid, state.stamp);
230
+ else if (status.kind === "unreachable") throw new Error(status.message);
231
+ await rm(paths.state, { force: true });
232
+ if (action === "stop") return void 0;
233
+ status = await probeService(options.url);
234
+ if (status.kind === "ready") return status.identity;
235
+ if (status.kind !== "stopped") throw new Error(status.message);
236
+ const log = await open(paths.log, "a", 384);
237
+ let child;
238
+ let failure;
239
+ let exited = false;
240
+ try {
241
+ const config = options.config ? resolve2(options.config) : state?.config;
242
+ child = spawn(process.execPath, [options.entry, "serve", "--url", options.url, "--home", paths.base, ...config ? ["--config", config] : []], {
243
+ detached: true,
244
+ stdio: ["ignore", log.fd, log.fd],
245
+ windowsHide: true,
246
+ env: { ...process.env, ELECTRON_RUN_AS_NODE: void 0 }
247
+ });
248
+ child.once("error", (error) => {
249
+ failure = error;
250
+ });
251
+ child.once("exit", () => {
252
+ exited = true;
253
+ });
254
+ } finally {
255
+ await log.close();
256
+ }
257
+ child.unref();
258
+ const started = Date.now();
259
+ while (Date.now() - started < 15e3) {
260
+ if (failure || exited) break;
261
+ const current = await probeService(options.url, 500);
262
+ const launched = await readState(paths.state);
263
+ if (current.kind === "ready" && launched && launched.pid === child.pid && current.identity.instanceId === launched.instanceId) return current.identity;
264
+ if (current.kind === "conflict" || current.kind === "incompatible") break;
265
+ await delay(100);
266
+ }
267
+ if (child.pid && !exited) {
268
+ const stamp = await processStamp(child.pid);
269
+ if (stamp) await stopProcess(child.pid, stamp);
270
+ }
271
+ const logReader = await open(paths.log, "r");
272
+ let output;
273
+ try {
274
+ const size = (await logReader.stat()).size;
275
+ const tail = Buffer.alloc(Math.min(size, 4e3));
276
+ const { bytesRead } = await logReader.read(tail, 0, tail.length, Math.max(0, size - tail.length));
277
+ output = tail.subarray(0, bytesRead).toString("utf8");
278
+ } finally {
279
+ await logReader.close();
280
+ }
281
+ throw new Error(`\u540E\u53F0\u542F\u52A8\u5931\u8D25\uFF1A${failure?.message || output || "\u7B49\u5F85\u670D\u52A1\u5C31\u7EEA\u8D85\u65F6"}
282
+ \u65E5\u5FD7\uFF1A${paths.log}`);
283
+ } finally {
284
+ await rm(paths.lock, { force: true });
285
+ }
286
+ }
287
+
288
+ // src/cli.ts
289
+ async function main() {
290
+ const { values, positionals } = parseArgs({ allowPositionals: true, options: {
291
+ url: { type: "string", default: defaultServerUrl },
292
+ home: { type: "string" },
293
+ config: { type: "string" },
294
+ version: { type: "boolean" },
295
+ help: { type: "boolean" }
296
+ } });
297
+ if (values.version) {
298
+ console.log(JSON.stringify({ service: serviceName, version: serviceVersion }));
299
+ return;
300
+ }
301
+ const action = positionals[0] || "start";
302
+ if (values.help) {
303
+ console.log("web-terminal [start|restart|stop|status] [--url http://localhost:3840] [--config /path/config.yaml] [--home /path/state]\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");
304
+ return;
305
+ }
306
+ const url = normalizeServerUrl(values.url);
307
+ if (action === "status") {
308
+ console.log(JSON.stringify(await probeService(url)));
309
+ return;
310
+ }
311
+ if (action !== "serve") {
312
+ if (!["start", "restart", "stop"].includes(action)) throw new Error(`\u672A\u77E5\u547D\u4EE4\uFF1A${action}`);
313
+ const identity = await manageService(action, { url, entry: fileURLToPath(import.meta.url), home: values.home, config: values.config });
314
+ console.log(JSON.stringify({ ok: true, url, identity }));
315
+ return;
316
+ }
317
+ const paths = servicePaths(url, values.home);
318
+ await mkdir2(paths.directory, { recursive: true, mode: 448 });
319
+ const configFile = resolve3(values.config || process.env.WEB_TERMINAL_CONFIG || paths.config);
320
+ process.env.WEB_TERMINAL_CONFIG = configFile;
321
+ process.chdir(dirname(configFile));
322
+ const config = loadConfig({ dataDir: join2(paths.directory, "data") });
323
+ config.host = new URL(url).hostname === "localhost" ? "127.0.0.1" : new URL(url).hostname.replace(/^\[|\]$/g, "");
324
+ config.port = Number(new URL(url).port || 80);
325
+ const instanceId = randomUUID2();
326
+ const { createApp } = await import("./app-TCUYGJ4K.js");
327
+ const runtime = await createApp(config, { managed: true, instanceId, staticDir: fileURLToPath(new URL("../dist/", import.meta.url)) });
328
+ const stamp = await processStamp(process.pid);
329
+ 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");
330
+ await new Promise((done, reject) => {
331
+ runtime.server.once("error", reject);
332
+ runtime.server.listen(config.port, config.host, done);
333
+ });
334
+ await writeState(paths.state, { pid: process.pid, stamp, instanceId, url, config: configFile });
335
+ console.log(`Web Terminal ${serviceVersion}: ${url}
336
+ token \u6587\u4EF6\uFF1A${join2(config.dataDir, "token")}`);
337
+ let closing = false;
338
+ for (const signal of ["SIGINT", "SIGTERM"]) process.on(signal, () => {
339
+ if (closing) return;
340
+ closing = true;
341
+ void (async () => {
342
+ await runtime.close();
343
+ if ((await readState(paths.state))?.instanceId === instanceId) await rm2(paths.state, { force: true });
344
+ })().then(() => process.exit(0), (error) => {
345
+ console.error(error);
346
+ process.exit(1);
347
+ });
348
+ });
349
+ }
350
+ main().catch((error) => {
351
+ console.error(error instanceof Error ? error.message : String(error));
352
+ process.exit(1);
353
+ });
package/package.json CHANGED
@@ -1,6 +1,76 @@
1
1
  {
2
2
  "name": "@dadigua/web-terminal",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.3",
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "tsx src/server/index.ts --dev",
7
+ "build": "tsc --noEmit && vite build && tsx scripts/build-server.ts",
8
+ "start": "tsx src/server/index.ts",
9
+ "test": "tsx --test tests/*.test.ts",
10
+ "check": "tsc --noEmit",
11
+ "build:clients": "tsx scripts/build-clients.ts",
12
+ "package:vscode": "pnpm build:clients && tsx scripts/package-vscode.ts",
13
+ "electron": "pnpm build:clients && electron dist-clients/electron",
14
+ "package:electron:win": "pnpm build:clients && tsx scripts/package-electron.ts --win",
15
+ "package:electron:linux": "pnpm build:clients && tsx scripts/package-electron.ts --linux",
16
+ "prepack": "pnpm build"
17
+ },
18
+ "dependencies": {
19
+ "@xterm/addon-fit": "^0.11.0",
20
+ "@xterm/addon-serialize": "0.14.0",
21
+ "@xterm/headless": "6.0.0",
22
+ "@xterm/xterm": "^6.0.0",
23
+ "express": "^5.1.0",
24
+ "multer": "^2.0.2",
25
+ "node-pty": "^1.1.0",
26
+ "react": "^19.2.0",
27
+ "react-dom": "^19.2.0",
28
+ "sharp": "^0.34.5",
29
+ "ws": "^8.18.3",
30
+ "yaml": "^2.8.2"
31
+ },
32
+ "devDependencies": {
33
+ "@types/vscode": "^1.96.0",
34
+ "@vscode/vsce": "^3.9.2",
35
+ "electron": "^42.3.3",
36
+ "electron-builder": "^26.15.3",
37
+ "esbuild": "^0.28.0",
38
+ "@types/express": "^5.0.0",
39
+ "@types/multer": "^2.0.0",
40
+ "@types/node": "^24.0.0",
41
+ "@types/react": "^19.2.0",
42
+ "@types/react-dom": "^19.2.0",
43
+ "@types/ws": "^8.18.1",
44
+ "@vitejs/plugin-react": "^5.1.0",
45
+ "tsx": "^4.20.6",
46
+ "typescript": "^5.9.3",
47
+ "vite": "^7.2.0"
48
+ },
49
+ "engines": {
50
+ "node": ">=22"
51
+ },
52
+ "packageManager": "pnpm@11.12.0",
53
+ "bin": {
54
+ "web-terminal": "bin/web-terminal.mjs"
55
+ },
56
+ "files": [
57
+ "bin/",
58
+ "dist-server/",
59
+ "dist/",
60
+ "config.example.yaml",
61
+ "README.md",
62
+ "API.md"
63
+ ],
64
+ "description": "Web Terminal:共享 PTY、图片与文本附件、文件预览及 VS Code 客户端",
65
+ "repository": {
66
+ "type": "git",
67
+ "url": "git+https://github.com/laopo001/web-terminal.git"
68
+ },
69
+ "homepage": "https://github.com/laopo001/web-terminal#readme",
70
+ "bugs": {
71
+ "url": "https://github.com/laopo001/web-terminal/issues"
72
+ },
73
+ "publishConfig": {
74
+ "access": "public"
75
+ }
76
+ }