@dadigua/web-terminal 0.1.3 → 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/README.md +18 -4
- package/config.example.yaml +2 -0
- package/dist-server/{app-TCUYGJ4K.js → app-3OE4ER7S.js} +45 -17
- package/dist-server/{chunk-R33XRLBM.js → chunk-TD3NJESY.js} +5 -3
- package/dist-server/cli.js +15 -5
- package/dist-server/vscodeInstaller-WIM5VHDI.js +210 -0
- package/package.json +5 -3
- package/release/web-terminal.vsix +0 -0
package/README.md
CHANGED
|
@@ -48,11 +48,15 @@ CLI 默认使用 `~/.web-terminal/config.yaml`,相对路径以配置文件所
|
|
|
48
48
|
|
|
49
49
|
CLI 只停止自己登记且启动标识匹配的进程,不接管 `pnpm start` 或 systemd 启动的外部服务。外部服务可以连接,重启时会提示使用原管理器。
|
|
50
50
|
|
|
51
|
+
## 发布
|
|
52
|
+
|
|
53
|
+
推送与 `package.json` 版本一致的 `v*` 标签后,`.github/workflows/publish.yaml` 自动检查、构建、打包,并通过 npm Trusted Publishing 发布,再创建 GitHub Release 和上传附件。npm 仅信任 `laopo001/web-terminal` 仓库中的此工作流,不需要发布 token。手动运行工作流只验证和生成产物。
|
|
54
|
+
|
|
51
55
|
## 使用
|
|
52
56
|
|
|
53
57
|
1. 首次打开时选择已有会话,或点击“新建 Shell”进入普通命令行;不会自动接入第一条历史会话。通过“+”选择工作目录;VS Code 中可快捷选择工作区目录。
|
|
54
58
|
2. 在终端里运行 `codex`,照常操作 CLI。 底部输入框点击“发送”或按 Enter 会写入文字并提交一次;中文输入法选词时不会提交。
|
|
55
|
-
3.
|
|
59
|
+
3. 粘贴截图、拖入图片,或点击工具栏的附件按钮。图片默认保存到服务器的 `~/.web-terminal/uploads/<12位短文件名>.png`(扩展名随图片格式变化,可通过 `uploadDir` 配置目录),附件区显示预览、上传状态与移除按钮;点击“发送”时,将文字与附件路径一起送入当前终端。支持多图和全部清除。
|
|
56
60
|
4. 悬停图片路径查看缩略图;在浏览器中点击文件路径,弹出图片或文本预览对话框,可复制路径、下载文件,按 Esc 或点击遮罩关闭。Markdown、代码和 YAML 等文本按原文显示,最多 1 MiB。VS Code 内点击路径会交给编辑器打开文件。
|
|
57
61
|
5. VS Code、Electron 和浏览器连接同一服务器并选择同一会话,就会共用一个终端;两端输入会进入同一个 Shell,当前操作的窗口控制尺寸,其他窗口按相同行列数显示。关闭浏览器不会结束会话。默认直接运行 Shell,重启 Web 服务会结束普通 Shell;点击会话的结束按钮才会终止 Shell 并删除该会话上传的图片。
|
|
58
62
|
|
|
@@ -90,11 +94,21 @@ ssh -N -L 3840:127.0.0.1:3840 user@server
|
|
|
90
94
|
|
|
91
95
|
### VS Code
|
|
92
96
|
|
|
97
|
+
安装全局 CLI 后,用一条命令安装 npm 包自带的插件:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
web-terminal install-vscode
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
自动安装到检测到的 VS Code Stable 和 Insiders;WSL 下同时安装 WSL 扩展宿主和 Windows 宿主。已打开的窗口需运行 `Developer: Reload Window` 加载插件。可用 `web-terminal install-vscode --vsix /path/web-terminal.vsix` 安装指定文件。
|
|
104
|
+
|
|
105
|
+
本地开发打包并安装:
|
|
106
|
+
|
|
93
107
|
```bash
|
|
94
|
-
pnpm
|
|
108
|
+
pnpm install:vscode
|
|
95
109
|
```
|
|
96
110
|
|
|
97
|
-
|
|
111
|
+
也可运行 `pnpm package:vscode`,在 VS Code 的扩展菜单选择“从 VSIX 安装”,打开 `release/web-terminal.vsix`。点击活动栏终端图标或运行 `Web Terminal: Show Sidebar` 打开侧边窗口;`Web Terminal: Open` 在编辑器标签页打开。两处均加载现有 Web 页面。默认地址为 `http://localhost:3840`;通过 `Web Terminal: Set Server URL` 或设置 `webTerminal.serverUrl` 修改,`Web Terminal: Reload` 重载;侧栏及编辑器标题栏的重启按钮重启后台并等待就绪。Remote WSL/SSH 在扩展宿主所在机器检测和启动,再使用 VS Code 的端口转发能力。Windows 本机默认先查本机全局 CLI,再查默认 WSL;可通过 `webTerminal.runtime` 和 `webTerminal.cliPath` 调整。
|
|
98
112
|
|
|
99
113
|
选中编辑器中的文本后,右键选择“发送选中文本到 Web Terminal”,以附件形式加入当前会话,带有文件路径、语言与行号。点击发送后送入终端。
|
|
100
114
|
|
|
@@ -105,7 +119,7 @@ pnpm electron
|
|
|
105
119
|
pnpm package:electron:win
|
|
106
120
|
```
|
|
107
121
|
|
|
108
|
-
开发命令打开桌面客户端;Windows 命令生成 `release/Web-Terminal-0.1.
|
|
122
|
+
开发命令打开桌面客户端;Windows 命令生成 `release/Web-Terminal-0.1.4-win-x64.zip`,解压后运行 `Web Terminal.exe`。菜单“连接 → 打开连接配置”打开 `client.yaml`,只需修改 `serverUrl`,再选择“重新加载服务器”;“重启后台服务”通过全局 CLI 重启。`client.yaml` 的 `runtime` 可选 auto、native、wsl,`cliPath` 可指定本机 CLI 路径。也可用 `WEB_TERMINAL_URL` 临时覆盖地址。Linux 目录包通过 `pnpm package:electron:linux` 生成。
|
|
109
123
|
|
|
110
124
|
浏览器、VS Code、Electron 各自首次在同一 Web 登录页输入 token,验证后保存在各自 Webview 的持久 localStorage 中。客户端配置不保存 token。Electron 远程页面开启 sandbox/contextIsolation、关闭 Node 集成;VS Code 通过专用 `/?embed=vscode` 入口加载,普通入口仍禁止 iframe 嵌入。
|
|
111
125
|
|
package/config.example.yaml
CHANGED
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
serviceName,
|
|
3
3
|
serviceProtocol,
|
|
4
4
|
serviceVersion
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-TD3NJESY.js";
|
|
6
6
|
|
|
7
7
|
// src/server/app.ts
|
|
8
8
|
import express from "express";
|
|
@@ -44,9 +44,10 @@ function authenticate(token) {
|
|
|
44
44
|
|
|
45
45
|
// src/server/files.ts
|
|
46
46
|
import { constants } from "node:fs";
|
|
47
|
-
import { mkdir, readdir, open, realpath,
|
|
47
|
+
import { mkdir, readdir, open, realpath, unlink, writeFile, appendFile, readFile, rm } from "node:fs/promises";
|
|
48
48
|
import { resolve, basename, dirname, join } from "node:path";
|
|
49
|
-
import {
|
|
49
|
+
import { randomBytes } from "node:crypto";
|
|
50
|
+
import { parse, stringify } from "yaml";
|
|
50
51
|
import sharp from "sharp";
|
|
51
52
|
var HttpError = class extends Error {
|
|
52
53
|
constructor(status, message) {
|
|
@@ -129,15 +130,43 @@ var Files = class {
|
|
|
129
130
|
if (!meta.width || !meta.height) throw new HttpError(415, "\u56FE\u7247\u65E0\u6CD5\u89E3\u7801");
|
|
130
131
|
const folder = join(this.config.dataDir, "uploads", sessionId);
|
|
131
132
|
await mkdir(folder, { recursive: true, mode: 448 });
|
|
133
|
+
await mkdir(this.config.uploadDir, { recursive: true, mode: 448 });
|
|
132
134
|
const ext = { "image/png": ".png", "image/jpeg": ".jpg", "image/webp": ".webp", "image/gif": ".gif" }[mime];
|
|
133
|
-
|
|
134
|
-
|
|
135
|
+
let path;
|
|
136
|
+
for (; ; ) {
|
|
137
|
+
path = join(this.config.uploadDir, `${randomBytes(6).toString("hex")}${ext}`);
|
|
138
|
+
try {
|
|
139
|
+
await writeFile(path, bytes, { flag: "wx", mode: 384 });
|
|
140
|
+
break;
|
|
141
|
+
} catch (error) {
|
|
142
|
+
if (error.code !== "EEXIST") throw error;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
await appendFile(join(folder, "files.yaml"), stringify([path]), { mode: 384 });
|
|
147
|
+
} catch (error) {
|
|
148
|
+
await unlink(path);
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
135
151
|
return { path, name: basename(originalName).replace(/[\x00-\x1f\x7f]/g, "").slice(0, 160) || basename(path), size: bytes.length, mime, isImage: true, width: meta.width, height: meta.height };
|
|
136
152
|
} finally {
|
|
137
153
|
await unlink(temporary).catch(() => {
|
|
138
154
|
});
|
|
139
155
|
}
|
|
140
156
|
}
|
|
157
|
+
async removeUploads(sessionId) {
|
|
158
|
+
const folder = join(this.config.dataDir, "uploads", sessionId);
|
|
159
|
+
let paths;
|
|
160
|
+
try {
|
|
161
|
+
paths = parse(await readFile(join(folder, "files.yaml"), "utf8"));
|
|
162
|
+
} catch (error) {
|
|
163
|
+
if (error.code !== "ENOENT") throw error;
|
|
164
|
+
}
|
|
165
|
+
if (Array.isArray(paths)) {
|
|
166
|
+
await Promise.all(paths.filter((path) => typeof path === "string" && /^[a-f0-9]{12}\.(?:png|jpg|webp|gif)$/.test(basename(path))).map((path) => rm(path, { force: true })));
|
|
167
|
+
}
|
|
168
|
+
await rm(folder, { recursive: true, force: true });
|
|
169
|
+
}
|
|
141
170
|
};
|
|
142
171
|
function imageMime(bytes) {
|
|
143
172
|
if (bytes.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]))) return "image/png";
|
|
@@ -148,10 +177,9 @@ function imageMime(bytes) {
|
|
|
148
177
|
|
|
149
178
|
// src/server/sessions.ts
|
|
150
179
|
import { readFileSync, writeFileSync, renameSync, existsSync } from "node:fs";
|
|
151
|
-
import { rm } from "node:fs/promises";
|
|
152
180
|
import { join as join2 } from "node:path";
|
|
153
|
-
import { randomUUID
|
|
154
|
-
import { parse, stringify } from "yaml";
|
|
181
|
+
import { randomUUID } from "node:crypto";
|
|
182
|
+
import { parse as parse2, stringify as stringify2 } from "yaml";
|
|
155
183
|
import * as pty from "node-pty";
|
|
156
184
|
|
|
157
185
|
// src/shared/terminalInput.ts
|
|
@@ -161,12 +189,12 @@ function pasteText(text, bracketedPaste) {
|
|
|
161
189
|
}
|
|
162
190
|
|
|
163
191
|
// src/server/processInfo.ts
|
|
164
|
-
import { readFile, readlink } from "node:fs/promises";
|
|
192
|
+
import { readFile as readFile2, readlink } from "node:fs/promises";
|
|
165
193
|
import { basename as basename2 } from "node:path";
|
|
166
194
|
async function foregroundInfo(pid, fallback) {
|
|
167
195
|
let foreground = pid;
|
|
168
196
|
try {
|
|
169
|
-
const stat = await
|
|
197
|
+
const stat = await readFile2(`/proc/${pid}/stat`, "utf8");
|
|
170
198
|
const group = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[5]);
|
|
171
199
|
if (Number.isInteger(group) && group > 0) foreground = group;
|
|
172
200
|
} catch {
|
|
@@ -182,7 +210,7 @@ async function foregroundInfo(pid, fallback) {
|
|
|
182
210
|
if (/\/claude\/versions\//.test(executable)) processName = "claude";
|
|
183
211
|
else if (basename2(executable) === "codex") processName = "codex";
|
|
184
212
|
else if (/^(node|nodejs|bun)$/.test(basename2(executable))) {
|
|
185
|
-
const argv = (await
|
|
213
|
+
const argv = (await readFile2(`/proc/${foreground}/cmdline`, "utf8")).split("\0", 2);
|
|
186
214
|
if (/[/\\](?:@anthropic-ai[/\\]claude-code|claude-code)[/\\]/.test(argv[1] || "")) processName = "claude";
|
|
187
215
|
else if (/[/\\]@openai[/\\]codex[/\\]/.test(argv[1] || "")) processName = "codex";
|
|
188
216
|
}
|
|
@@ -344,7 +372,7 @@ var Sessions = class {
|
|
|
344
372
|
this.files = files;
|
|
345
373
|
const saved = join2(config.dataDir, "sessions.yaml");
|
|
346
374
|
if (existsSync(saved)) {
|
|
347
|
-
const records =
|
|
375
|
+
const records = parse2(readFileSync(saved, "utf8"));
|
|
348
376
|
if (!Array.isArray(records)) throw new Error("sessions.yaml \u683C\u5F0F\u65E0\u6548");
|
|
349
377
|
for (const record of records) {
|
|
350
378
|
if (!/^[a-f0-9-]{36}$/.test(record.id)) continue;
|
|
@@ -369,7 +397,7 @@ var Sessions = class {
|
|
|
369
397
|
}
|
|
370
398
|
save() {
|
|
371
399
|
const file = join2(this.config.dataDir, "sessions.yaml");
|
|
372
|
-
writeFileSync(`${file}.tmp`,
|
|
400
|
+
writeFileSync(`${file}.tmp`, stringify2([...this.entries.values()].map((s) => s.info)), { mode: 384 });
|
|
373
401
|
renameSync(`${file}.tmp`, file);
|
|
374
402
|
}
|
|
375
403
|
get(id) {
|
|
@@ -397,7 +425,7 @@ var Sessions = class {
|
|
|
397
425
|
if (name !== void 0 && (typeof name !== "string" || name.length > 80)) throw new HttpError(400, "\u4F1A\u8BDD\u540D\u79F0\u6700\u591A 80 \u5B57\u7B26");
|
|
398
426
|
if (cwd !== void 0 && typeof cwd !== "string") throw new HttpError(400, "\u5DE5\u4F5C\u76EE\u5F55\u65E0\u6548");
|
|
399
427
|
const dir = await this.files.directory(cwd || this.config.defaultCwd);
|
|
400
|
-
const id =
|
|
428
|
+
const id = randomUUID();
|
|
401
429
|
const info = { id, name: name?.trim() || `\u7EC8\u7AEF ${this.entries.size + 1}`, cwd: dir, createdAt: (/* @__PURE__ */ new Date()).toISOString(), running: true };
|
|
402
430
|
const session = this.makeSession(info);
|
|
403
431
|
try {
|
|
@@ -526,7 +554,7 @@ var Sessions = class {
|
|
|
526
554
|
this.entries.delete(id);
|
|
527
555
|
this.save();
|
|
528
556
|
await session.screen.dispose();
|
|
529
|
-
await
|
|
557
|
+
await this.files.removeUploads(id);
|
|
530
558
|
}
|
|
531
559
|
async shutdown() {
|
|
532
560
|
this.shuttingDown = true;
|
|
@@ -570,12 +598,12 @@ function embeddingHeaders(path, embed, dev) {
|
|
|
570
598
|
}
|
|
571
599
|
|
|
572
600
|
// src/server/app.ts
|
|
573
|
-
import { randomUUID as
|
|
601
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
574
602
|
import { fileURLToPath } from "node:url";
|
|
575
603
|
async function createApp(config, options = {}) {
|
|
576
604
|
mkdirSync(config.dataDir, { recursive: true, mode: 448 });
|
|
577
605
|
const app = express();
|
|
578
|
-
const instanceId =
|
|
606
|
+
const instanceId = randomUUID2();
|
|
579
607
|
app.disable("x-powered-by");
|
|
580
608
|
const server = createServer(app);
|
|
581
609
|
const files = new Files(config);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// package.json
|
|
2
2
|
var package_default = {
|
|
3
3
|
name: "@dadigua/web-terminal",
|
|
4
|
-
version: "0.1.
|
|
4
|
+
version: "0.1.4",
|
|
5
5
|
type: "module",
|
|
6
6
|
scripts: {
|
|
7
7
|
dev: "tsx src/server/index.ts --dev",
|
|
@@ -11,10 +11,11 @@ var package_default = {
|
|
|
11
11
|
check: "tsc --noEmit",
|
|
12
12
|
"build:clients": "tsx scripts/build-clients.ts",
|
|
13
13
|
"package:vscode": "pnpm build:clients && tsx scripts/package-vscode.ts",
|
|
14
|
+
"install:vscode": "pnpm package:vscode && tsx src/cli.ts install-vscode",
|
|
14
15
|
electron: "pnpm build:clients && electron dist-clients/electron",
|
|
15
16
|
"package:electron:win": "pnpm build:clients && tsx scripts/package-electron.ts --win",
|
|
16
17
|
"package:electron:linux": "pnpm build:clients && tsx scripts/package-electron.ts --linux",
|
|
17
|
-
prepack: "pnpm build"
|
|
18
|
+
prepack: "pnpm build && pnpm package:vscode"
|
|
18
19
|
},
|
|
19
20
|
dependencies: {
|
|
20
21
|
"@xterm/addon-fit": "^0.11.0",
|
|
@@ -50,7 +51,7 @@ var package_default = {
|
|
|
50
51
|
engines: {
|
|
51
52
|
node: ">=22"
|
|
52
53
|
},
|
|
53
|
-
packageManager: "pnpm@11.
|
|
54
|
+
packageManager: "pnpm@11.28.2",
|
|
54
55
|
bin: {
|
|
55
56
|
"web-terminal": "bin/web-terminal.mjs"
|
|
56
57
|
},
|
|
@@ -58,6 +59,7 @@ var package_default = {
|
|
|
58
59
|
"bin/",
|
|
59
60
|
"dist-server/",
|
|
60
61
|
"dist/",
|
|
62
|
+
"release/web-terminal.vsix",
|
|
61
63
|
"config.example.yaml",
|
|
62
64
|
"README.md",
|
|
63
65
|
"API.md"
|
package/dist-server/cli.js
CHANGED
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
serviceName,
|
|
3
3
|
serviceProtocol,
|
|
4
4
|
serviceVersion
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-TD3NJESY.js";
|
|
6
6
|
|
|
7
7
|
// src/cli.ts
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
@@ -34,11 +34,13 @@ function loadConfig(defaults = {}) {
|
|
|
34
34
|
if (!token || token.length > 1024) throw new Error("token \u4E0D\u80FD\u4E3A\u7A7A\uFF0C\u4E14\u6700\u591A 1024 \u4E2A\u5B57\u7B26");
|
|
35
35
|
const port = Number(process.env.PORT || input.port || 3840);
|
|
36
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"));
|
|
37
38
|
return {
|
|
38
39
|
host: String(process.env.HOST || input.host || "127.0.0.1"),
|
|
39
40
|
port,
|
|
40
41
|
dataDir,
|
|
41
42
|
token,
|
|
43
|
+
uploadDir: resolve(input.uploadDir || defaults.uploadDir || resolve(uploadHome, "uploads")),
|
|
42
44
|
defaultCwd: realpathSync(resolve(input.defaultCwd || homedir())),
|
|
43
45
|
shell: String(input.shell || process.env.SHELL || (process.platform === "win32" ? "powershell.exe" : "/bin/bash")),
|
|
44
46
|
machineId: `${hostname()}-${createHash("sha256").update(dataDir).digest("hex").slice(0, 8)}`,
|
|
@@ -292,7 +294,8 @@ async function main() {
|
|
|
292
294
|
home: { type: "string" },
|
|
293
295
|
config: { type: "string" },
|
|
294
296
|
version: { type: "boolean" },
|
|
295
|
-
help: { type: "boolean" }
|
|
297
|
+
help: { type: "boolean" },
|
|
298
|
+
vsix: { type: "string" }
|
|
296
299
|
} });
|
|
297
300
|
if (values.version) {
|
|
298
301
|
console.log(JSON.stringify({ service: serviceName, version: serviceVersion }));
|
|
@@ -300,9 +303,16 @@ async function main() {
|
|
|
300
303
|
}
|
|
301
304
|
const action = positionals[0] || "start";
|
|
302
305
|
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");
|
|
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");
|
|
304
307
|
return;
|
|
305
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");
|
|
306
316
|
const url = normalizeServerUrl(values.url);
|
|
307
317
|
if (action === "status") {
|
|
308
318
|
console.log(JSON.stringify(await probeService(url)));
|
|
@@ -319,11 +329,11 @@ async function main() {
|
|
|
319
329
|
const configFile = resolve3(values.config || process.env.WEB_TERMINAL_CONFIG || paths.config);
|
|
320
330
|
process.env.WEB_TERMINAL_CONFIG = configFile;
|
|
321
331
|
process.chdir(dirname(configFile));
|
|
322
|
-
const config = loadConfig({ dataDir: join2(paths.directory, "data") });
|
|
332
|
+
const config = loadConfig({ dataDir: join2(paths.directory, "data"), uploadDir: join2(paths.base, "uploads") });
|
|
323
333
|
config.host = new URL(url).hostname === "localhost" ? "127.0.0.1" : new URL(url).hostname.replace(/^\[|\]$/g, "");
|
|
324
334
|
config.port = Number(new URL(url).port || 80);
|
|
325
335
|
const instanceId = randomUUID2();
|
|
326
|
-
const { createApp } = await import("./app-
|
|
336
|
+
const { createApp } = await import("./app-3OE4ER7S.js");
|
|
327
337
|
const runtime = await createApp(config, { managed: true, instanceId, staticDir: fileURLToPath(new URL("../dist/", import.meta.url)) });
|
|
328
338
|
const stamp = await processStamp(process.pid);
|
|
329
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");
|
|
@@ -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
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dadigua/web-terminal",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"dev": "tsx src/server/index.ts --dev",
|
|
@@ -10,10 +10,11 @@
|
|
|
10
10
|
"check": "tsc --noEmit",
|
|
11
11
|
"build:clients": "tsx scripts/build-clients.ts",
|
|
12
12
|
"package:vscode": "pnpm build:clients && tsx scripts/package-vscode.ts",
|
|
13
|
+
"install:vscode": "pnpm package:vscode && tsx src/cli.ts install-vscode",
|
|
13
14
|
"electron": "pnpm build:clients && electron dist-clients/electron",
|
|
14
15
|
"package:electron:win": "pnpm build:clients && tsx scripts/package-electron.ts --win",
|
|
15
16
|
"package:electron:linux": "pnpm build:clients && tsx scripts/package-electron.ts --linux",
|
|
16
|
-
"prepack": "pnpm build"
|
|
17
|
+
"prepack": "pnpm build && pnpm package:vscode"
|
|
17
18
|
},
|
|
18
19
|
"dependencies": {
|
|
19
20
|
"@xterm/addon-fit": "^0.11.0",
|
|
@@ -49,7 +50,7 @@
|
|
|
49
50
|
"engines": {
|
|
50
51
|
"node": ">=22"
|
|
51
52
|
},
|
|
52
|
-
"packageManager": "pnpm@11.
|
|
53
|
+
"packageManager": "pnpm@11.28.2",
|
|
53
54
|
"bin": {
|
|
54
55
|
"web-terminal": "bin/web-terminal.mjs"
|
|
55
56
|
},
|
|
@@ -57,6 +58,7 @@
|
|
|
57
58
|
"bin/",
|
|
58
59
|
"dist-server/",
|
|
59
60
|
"dist/",
|
|
61
|
+
"release/web-terminal.vsix",
|
|
60
62
|
"config.example.yaml",
|
|
61
63
|
"README.md",
|
|
62
64
|
"API.md"
|
|
Binary file
|