source-code-mgmt 1.14.0 → 1.15.1
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.en.md +13 -4
- package/README.md +35 -5
- package/lib/client.js +144 -10
- package/lib/index.js +533 -42
- package/package.json +2 -2
package/README.en.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# source-code-mgmt — DSH Source Code Management Plugin
|
|
2
2
|
|
|
3
|
-
> Version: **v1.
|
|
3
|
+
> Version: **v1.15.1** | 中文版见 [README.md](README.md)
|
|
4
4
|
|
|
5
5
|
> A source-code management plugin for the DSH Web GUI: it bundles「environment check → SSH setup → commit/push/upload code」into one「Code Management」panel with GitHub / Gitee dual-platform support.
|
|
6
6
|
|
|
@@ -25,7 +25,14 @@ The panel has five steps (①②③ are the core three; ④⑤ are the newer ext
|
|
|
25
25
|
- Shows the **OS** (friendly names `Windows` / `macOS` / `Linux`, from the underlying `win32` / `darwin` / `linux` platform ids)
|
|
26
26
|
- Detects **Git** and **GitHub CLI** presence and versions (e.g. `git version 2.55.0`, `gh version 2.97.0`)
|
|
27
27
|
- Detects whether an **SSH** client is available
|
|
28
|
-
- **Missing tools → install guidance + one-click install**: when a tool is missing, the row shows「❌ 未安装」+「复制安装命令」(copy install command) +「安装」(install)
|
|
28
|
+
- **Missing tools → adaptive install guidance + one-click install (mostly sudo-free)**: when a tool is missing, the row shows「❌ 未安装」+「复制安装命令」(copy install command) +「安装」(install):
|
|
29
|
+
- **GitHub CLI (Linux / macOS)**: **user-level install without sudo** — downloads the official binary for the current platform/architecture from GitHub Releases, extracts it and installs to `~/.local/bin/gh` (the directory is created if missing; no admin rights needed); falls back to the system package manager on failure.
|
|
30
|
+
- **Git (Linux)**: apt / dnf / pacman (auto `sudo -n`; **passwordless sudo is probed first** — when a password is required the panel does not run a doomed command, it shows "run this manually in a terminal" guidance with the exact command). **Git (macOS)**: brew, or **Xcode Command Line Tools** (`xcode-select --install`, the OS-provided installer that also ships git/ssh) when brew is absent.
|
|
31
|
+
- **SSH (Windows)**: built-in optional feature (admin); **SSH (Linux)**: openssh-client via apt / dnf / pacman (same sudo probe); **macOS**: bundled with the OS.
|
|
32
|
+
- **Copy install command** and the **Install** button share the same platform-adaptive logic (no more hardcoded Windows winget commands on Linux/macOS).
|
|
33
|
+
- **Live install-progress dialog**: clicking Install opens a progress window showing each step ("fetch latest version → download → extract → install → clean up") with streaming output; success/failure reasons are shown when done.
|
|
34
|
+
- **Restart prompt + one-click restart when needed**: when the running host cannot pick up the tool right away (e.g. gh installed to `~/.local/bin` while it is not on PATH), the dialog shows a「**重启 DSH**」(Restart DSH) button — the host hands off to a detached helper that replays the original launch command (same mechanism as dsh-update), the page briefly disconnects and comes back; when the host already resolves the tool (e.g. `DSH_SCM_GH` set or `~/.local/bin` on PATH), it finishes without interruption.
|
|
35
|
+
- Re-detects automatically after install.
|
|
29
36
|
|
|
30
37
|
### ② SSH Key & Connectivity
|
|
31
38
|
- **Platform selector**: GitHub (default) / Gitee — decides the SSH config target and connectivity test below
|
|
@@ -61,6 +68,8 @@ The panel has five steps (①②③ are the core three; ④⑤ are the newer ext
|
|
|
61
68
|
|
|
62
69
|
### ⑤ Publish npm package
|
|
63
70
|
- Five-step wizard (run in the target directory): **① check registry** `npm config get registry` → **② view auth config** `npm config list` (auto-redacts token/auth/password) → **③ verify identity** `npm whoami` → **④ preview packed files** `npm pack --dry-run` (see exactly what would be published, nothing is packed or uploaded) → **⑤ publish** `npm publish`
|
|
71
|
+
- **Login and publish always use the official registry `https://registry.npmjs.org`**: even when your global npm config points at a mirror (e.g. `registry.npmmirror.com` — mirrors only sync, they do not accept publishes), `whoami` / `npm login` / `npm publish` all carry `--registry=https://registry.npmjs.org`; the status area shows a yellow "publishing uses the official registry" hint when a mirror is configured.
|
|
72
|
+
- **"Open terminal to run npm login" now spawns a terminal safely**: it first probes for a terminal that actually exists on PATH (`konsole` / `gnome-terminal` / xterm-family, etc.), attaches an `error` listener to every child, and uses `konsole --separate` on KDE — fixing the old bug where spawning a missing binary (e.g. no `x-terminal-emulator` on SteamOS) raised an uncaught async ENOENT that **crashed the dsh host process** (which looked like your `pnpm dsh web` terminal dying). When no terminal exists it returns explicit "run manually: <command>" guidance instead of pretending success.
|
|
64
73
|
- Publishing requires ticking「I've reviewed the above — confirm publishing to the npm registry」first, preventing accidental publishes
|
|
65
74
|
|
|
66
75
|
### Data loading timing (fetch on open, "refreshing…" indicator)
|
|
@@ -190,7 +199,7 @@ All routes are **loopback-only** (`sec-fetch-site` + Origin checks) — only the
|
|
|
190
199
|
- Optional explicit binary paths via env vars: `DSH_SCM_GIT` / `DSH_SCM_GH` / `DSH_SCM_SSH` / `DSH_SCM_SSH_KEYGEN`
|
|
191
200
|
- **SSH transport fix**: Git for Windows' bundled MSYS `ssh.exe` (`usr\bin\ssh.exe`) can fail with `couldn't create signal pipe, Win32 error 5` when spawned from a detached/agent process, breaking `git push`/`git pull`. The plugin injects `GIT_SSH` pointing at a working `ssh` (usually the system OpenSSH `C:\Windows\System32\OpenSSH\ssh.exe`) for git remote operations
|
|
192
201
|
- **Non-ASCII filename compatibility**: git's default `core.quotepath` prints paths with non-ASCII bytes as octal-escaped quoted strings (which display as garbled text). The plugin injects `-c core.quotepath=false` into every git invocation (raw UTF-8 output) and additionally unescapes any still-quoted paths via `parseGitPath()` — Chinese filenames display correctly in the changes list and in diff headers
|
|
193
|
-
- **One-click missing-tool install across platforms**:
|
|
202
|
+
- **One-click missing-tool install across platforms (since v1.15.0, adaptive & mostly sudo-free)**: GitHub CLI installs user-level on Linux/macOS (official binary → `~/.local/bin/gh`, no sudo); git/ssh on Linux use apt-get / dnf / pacman (passwordless sudo probed first, manual guidance otherwise); macOS uses brew or the Xcode Command Line Tools; Windows uses winget / built-in features (fallback choco/scoop). The install runs in a live progress dialog, and a one-click DSH restart is offered when the running host needs it. The native folder picker is Windows-only; on macOS/Linux paste the path into the input box instead
|
|
194
203
|
|
|
195
204
|
## Development
|
|
196
205
|
|
|
@@ -273,7 +282,7 @@ This release focuses on the changed-files preview experience and Chinese-filenam
|
|
|
273
282
|
- **Profile Bundle distribution — install = activate**: `dsh.bundle` changed from the bare string `"./lib/index.js"` to the object form `{ "patch": "./cordis.patch.yml" }`, with a new `cordis.patch.yml` (inserts the `source-code-mgmt` row). `dsh plugin --profile web add source-code-mgmt` now appends the package to `dsh.profile.bundles` and registers it into the Cordis loader tree automatically — **no manual `cordis.patch.yml` editing**. The「install ≠ activate」warning and the PowerShell activation script were removed from the README. Behavior is otherwise unchanged (same `lib/index.js` host half + `lib/client.js` browser half).
|
|
274
283
|
|
|
275
284
|
### v1.8.0 and earlier (history)
|
|
276
|
-
See the full Chinese changelog in [README.md](README.md#版本历史). Highlights of recent releases: push-staged button (v1.8.0), fetch-on-open with refreshing indicator (v1.7.0), header button + right panel when better-sidebar is absent (v1.6.0), one-click missing-tool install (v1.5.0), SSH key auto-detection (v1.4.0), local Git workflow — stage/unstage, custom commit message, branch switch, history with revert/cherry-pick, side-by-side diff (v1.3.0), adaptive entry + Gitee support (v1.1–1.2), first release (v1.0.0).
|
|
285
|
+
See the full Chinese changelog in [README.md](README.md#版本历史). Highlights of recent releases: npm-login terminal fix (no host crash) + official-registry publish (v1.15.1), adaptive sudo-free installs with live progress dialog and one-click DSH restart (v1.15.0), PR removal (v1.14.0), push-staged button (v1.8.0), fetch-on-open with refreshing indicator (v1.7.0), header button + right panel when better-sidebar is absent (v1.6.0), one-click missing-tool install (v1.5.0), SSH key auto-detection (v1.4.0), local Git workflow — stage/unstage, custom commit message, branch switch, history with revert/cherry-pick, side-by-side diff (v1.3.0), adaptive entry + Gitee support (v1.1–1.2), first release (v1.0.0).
|
|
277
286
|
|
|
278
287
|
## License
|
|
279
288
|
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> [English](README.en.md) | 中文
|
|
4
4
|
|
|
5
|
-
> 版本:**v1.
|
|
5
|
+
> 版本:**v1.15.1** | 更新日志见文末「[版本历史](#版本历史)」
|
|
6
6
|
|
|
7
7
|
> **界面语言跟随 DSH 设置实时切换**:面板与 host 端消息自动使用 DSH 的语言(设置 → 通用 → 语言),中文 ↔ 英文即时生效,无需重启。
|
|
8
8
|
|
|
@@ -25,7 +25,14 @@
|
|
|
25
25
|
- 显示**操作系统**(美化名:`Windows` / `macOS` / `Linux`,对应底层 Node 平台标识 `win32` / `darwin` / `linux`)
|
|
26
26
|
- 自动检测 **Git**、**GitHub CLI** 是否安装及版本(如 `git version 2.55.0`、`gh version 2.97.0`)
|
|
27
27
|
- 检测 **SSH** 客户端是否可用(解析到可用 `ssh` 即显示「已找到」)
|
|
28
|
-
- **缺工具时给安装指引 +
|
|
28
|
+
- **缺工具时给安装指引 + 一键安装(按平台自适应、尽量免 sudo)**:某工具未找到时,该行显示「❌ 未安装」+「**复制安装命令**」+「**安装**」按钮:
|
|
29
|
+
- **GitHub CLI(Linux / macOS)**:**免 sudo 用户级安装**——从官方 GitHub Release 下载对应平台/架构的二进制包,解压后装到 `~/.local/bin/gh`(目录不存在会自动创建,全程不需要管理员权限);失败时自动回退系统包管理器。
|
|
30
|
+
- **Git(Linux)**:apt / dnf / pacman(自动带 `sudo -n`;**先探测 sudo 是否免密**,需要密码时不再盲目执行,而是给出「请在终端手动执行」的指引 + 完整命令)。**Git(macOS)**:brew,无 brew 时走 **Xcode 命令行工具**(`xcode-select --install`,系统自带安装,含 git/ssh)。
|
|
31
|
+
- **SSH(Windows)**:内置可选功能(需管理员);**SSH(Linux)**:apt / dnf / pacman 的 openssh-client(同上 sudo 探测);**macOS** 系统自带。
|
|
32
|
+
- **复制安装命令**与「安装」按钮**走同一套平台自适应逻辑**(不再固定显示 Windows 的 winget 命令)。
|
|
33
|
+
- **安装过程实时进度弹窗**:点「安装」后弹出进度窗口,逐步显示「查询最新版本 → 下载 → 解压 → 安装 → 清理」及实时输出;完成后显示成功/失败原因。
|
|
34
|
+
- **需要重启时提示并一键重启**:检测到安装后当前 host 进程无法立即识别(如 gh 装到了不在 PATH 的 `~/.local/bin`)时,弹窗提示并显示「**重启 DSH**」按钮——点击后 host 通过 detached 辅助进程按原启动命令自动拉起新进程(机制同 dsh-update),页面短暂断开后刷新即可;若 host 已能识别(如设置了 `DSH_SCM_GH` 或 `~/.local/bin` 在 PATH),则不打扰直接完成。
|
|
35
|
+
- 安装完成后自动重新检测。
|
|
29
36
|
|
|
30
37
|
### ② SSH 密钥与连接
|
|
31
38
|
- **平台选择**:下拉选择代码托管平台 **GitHub(默认)** / **Gitee**,决定下面的 SSH 配置写入与连接测试目标
|
|
@@ -67,6 +74,8 @@
|
|
|
67
74
|
|
|
68
75
|
### ⑤ 发布 npm 包(npm publish)
|
|
69
76
|
- 五步向导(在目标目录执行):**① 确认源** `npm config get registry` → **② 查看认证配置** `npm config list`(自动脱敏 token/auth/password)→ **③ 验证身份** `npm whoami` → **④ 预览打包** `npm pack --dry-run`(先看会发布哪些文件,不真正打包发布)→ **⑤ 发布** `npm publish`
|
|
77
|
+
- **登录与发布固定走官方源 `https://registry.npmjs.org`**:即使你的全局 npm 配置是镜像源(如 `registry.npmmirror.com`——镜像只同步、不接受发布),`whoami` / `npm login` / `npm publish` 也统一带 `--registry=https://registry.npmjs.org`;状态区在检测到镜像配置时会黄色提示「发布将使用官方源」。
|
|
78
|
+
- **「打开终端执行 npm login」安全弹终端**:先探测系统真实存在的终端程序(`konsole` / `gnome-terminal` / `xterm` 系等),再 spawn(并挂 error 监听兜底)——修复了旧版 `spawn` 不存在二进制时异步 ENOENT 未捕获、**直接把 dsh host 进程打崩**的问题;KDE 下用 `konsole --separate` 开独立窗口,不与宿主所在终端实例纠缠。找不到终端时明确返回手动命令指引,不再假装成功。
|
|
70
79
|
- 发布前必须勾选「我已核对以上内容,确认发布到 npm registry」才可点「发布」,防止误操作
|
|
71
80
|
|
|
72
81
|
### 数据加载时机(打开时联网、显示刷新中)
|
|
@@ -144,7 +153,8 @@ dsh web
|
|
|
144
153
|
| 路由 | 方法 | 说明 |
|
|
145
154
|
|------|------|------|
|
|
146
155
|
| `/api/source-code-mgmt/env` | GET | 环境检查(git/gh 版本) |
|
|
147
|
-
| `/api/source-code-mgmt/install-tool` | POST | 一键安装缺失工具(body `tool`: `git`/`gh`/`ssh
|
|
156
|
+
| `/api/source-code-mgmt/install-tool` | POST | 一键安装缺失工具(body `tool`: `git`/`gh`/`ssh`,按平台自适应:gh 在 Linux/macOS 免 sudo 装到 `~/.local/bin`;git/ssh 走系统包管理器 / brew / Xcode CLT / winget;响应为 **NDJSON 事件流**:`step`/`out`/`result`,供进度弹窗实时展示) |
|
|
157
|
+
| `/api/source-code-mgmt/restart` | POST | 一键重启 dsh(detached helper 按原启动命令拉起新进程;严格 loopback + origin 同源校验;supervisor 托管时禁用) |
|
|
148
158
|
| `/api/source-code-mgmt/ssh` | GET | SSH 密钥 / config / gh 登录状态 |
|
|
149
159
|
| `/api/source-code-mgmt/gen-key` | POST | 生成 ed25519 密钥 |
|
|
150
160
|
| `/api/source-code-mgmt/write-config` | POST | 写入 SSH config(body `provider`: `github` 默认 / `gitee`) |
|
|
@@ -191,7 +201,7 @@ dsh web
|
|
|
191
201
|
- 如需手动指定二进制路径,可用环境变量覆盖:`DSH_SCM_GIT` / `DSH_SCM_GH` / `DSH_SCM_SSH` / `DSH_SCM_SSH_KEYGEN`
|
|
192
202
|
- **SSH 传输修复**:Git for Windows 自带的 MSYS `ssh.exe`(`usr\bin\ssh.exe`)在被 detached/agent 进程调用时可能报 `couldn't create signal pipe, Win32 error 5`,导致 `git push`/`git pull` 失败。插件执行 git 远程命令时会自动注入 `GIT_SSH` 指向解析到的可用 `ssh`(通常为系统 OpenSSH `C:\Windows\System32\OpenSSH\ssh.exe`),避免该问题。
|
|
193
203
|
- **中文文件名兼容**:git 默认 `core.quotepath` 会把含非 ASCII 字节的路径输出成八进制转义的引号串(显示为乱码)。插件对所有 git 调用统一注入 `-c core.quotepath=false`(直接输出原始 UTF-8 路径),并对仍带引号转义的路径做 `parseGitPath()` 反转义兜底——改动列表 / diff 里的中文文件名显示正常
|
|
194
|
-
-
|
|
204
|
+
- **缺工具一键安装跨平台(v1.15.0 起按平台自适应、尽量免 sudo)**:GitHub CLI 在 Linux/macOS 直接**免 sudo 用户级安装**(官方二进制 → `~/.local/bin/gh`,失败回退系统包管理器);git/ssh 在 Linux 走 apt-get / dnf / pacman(**先探测 sudo 免密**,需要密码时给出手动命令指引而非静默失败),macOS 走 brew / Xcode 命令行工具,Windows 走 winget / 内置功能(回退 choco/scoop)。安装过程有**实时进度弹窗**,需要重启时提供**一键重启 dsh**。SSH 密钥探测、本地 Git 工作流、并排 diff 等在三个平台行为一致;「浏览目录」的原生选择器仅 Windows 可用,macOS/Linux 上请在输入框直接填路径(可手动输入粘贴)。
|
|
195
205
|
|
|
196
206
|
## 开发
|
|
197
207
|
|
|
@@ -207,7 +217,27 @@ dsh plugin --profile web add link:$(pwd)
|
|
|
207
217
|
|
|
208
218
|
## 版本历史
|
|
209
219
|
|
|
210
|
-
### v1.
|
|
220
|
+
### v1.15.1(当前)
|
|
221
|
+
⑤ 发布 npm 包功能修复(v1.15.0 的补丁):
|
|
222
|
+
|
|
223
|
+
- **修复「打开终端执行 npm login」打崩 host**:旧实现 `spawn` 不存在的终端二进制(如 SteamOS 上没有 `x-terminal-emulator`)时,异步 ENOENT 错误无监听器被抛成未捕获异常,**直接把 dsh host 进程打崩**(表现:运行 `pnpm dsh web` 的终端跟着结束)。现改为先 `findTerminal()` 探测 PATH 中真实存在的终端(konsole / gnome-terminal / xterm 系等),并对每个 child 挂 `error` 监听器兜底——绝不再崩宿主。
|
|
224
|
+
- **KDE konsole 用 `--separate` 开独立窗口**:不与宿主所在的 konsole 实例纠缠;找不到任何终端时明确返回「请手动运行:<命令>」指引,不再假装打开成功。
|
|
225
|
+
- **npm 登录/发布固定走官方源**:`npm login` / `npm whoami` / `npm publish` 统一带 `--registry=https://registry.npmjs.org`(镜像源只同步、不接受发布);`npmStatus` 新增 `publishRegistry` / `mirrorConfigured`,面板在检测到镜像配置(如 npmmirror)时黄色提示「发布将使用官方源」;「复制登录命令」也同步为官方源版本。
|
|
226
|
+
- 变更范围:`lib/index.js`(需重启 dsh web)、`lib/client.js`(刷新即生效)。版本号 1.15.0 → 1.15.1。
|
|
227
|
+
|
|
228
|
+
### v1.15.0(历史)
|
|
229
|
+
一键安装全面升级:**平台自适应 + 尽量免 sudo + 实时进度 + 一键重启**。
|
|
230
|
+
|
|
231
|
+
- **修复「复制安装命令」在 Linux/macOS 上错误显示 winget**:客户端写死的 Windows 提示改为取 host `/env` 返回的 `installHints`(与「安装」按钮同一套平台自适应逻辑);非 Windows 且无可用方式时留空,不再误导。
|
|
232
|
+
- **GitHub CLI(Linux/macOS)改免 sudo 用户级安装**:`installCommand()` 对 gh 优先返回官方二进制下载脚本(查最新版本 → 按平台/架构下载 `gh_<ver>_linux_amd64` / `macOS_arm64` 等 → 解压 → 装到 `~/.local/bin/gh`),一键安装失败自动回退系统包管理器。`resolveBin` 新增 `~/.local/bin` 探测——**插件加载时即使 PATH 里没有该目录也能找到用户级 gh**(不再依赖改 PATH / 设环境变量)。
|
|
233
|
+
- **sudo 免密探测(Linux)**:`canSudo()` 用 `sudo -n true` 探测;需要密码时**不再盲目执行**(`-n` 必然失败),返回「请在终端手动执行:<命令>」指引 + 原因,配合复制按钮即可完成。
|
|
234
|
+
- **macOS 兜底**:git/ssh 无 brew 时走 **Xcode 命令行工具**(`xcode-select --install`,系统自带安装、含 git/ssh),弹窗以「⏳ 已触发,按系统提示完成后点重新检查」呈现。
|
|
235
|
+
- **安装后实测复查**:`toolInstalled()` 改为按当前 PATH/落点直接探测(git/ssh 走 `--version`、gh 走 `~/.local/bin` 优先),**不再依赖模块加载时缓存的历史解析结果**——装完立即识别,修掉「装好了还报失败」的隐患。
|
|
236
|
+
- **安装进度弹窗**:`/install-tool` 改为 **NDJSON 事件流**(`step`/`out`/`result`,`runLive()` spawn 流式转发),浏览器端 `jpostStream()` 逐行消费;弹窗逐步显示「▶ 查询最新版本 → 下载 → 解压 → 安装 → 清理」与实时输出尾部。
|
|
237
|
+
- **需要重启时提示 + 一键重启**:`restartNeededFor()` 智能判定(`DSH_SCM_GH` 显式指定或 `~/.local/bin` 在 PATH 时无需重启);需要时弹窗显示「**重启 DSH**」按钮——新增 `POST /restart` 路由(机制同 dsh-update:detached helper 等端口释放后按原启动命令拉起新进程,POSIX detached spawn / Windows PowerShell 隐藏窗口;严格 loopback + origin 同源校验;systemd supervisor 托管或 `DSH_SCM_RESTART=0` 时禁用),点击后页面短暂断开、新进程自动起来。
|
|
238
|
+
- 变更范围:`lib/index.js`(需重启 dsh web)、`lib/client.js`(刷新即生效)。版本号 1.14.0 → 1.15.0。
|
|
239
|
+
|
|
240
|
+
### v1.14.0(历史)
|
|
211
241
|
按你的要求**移除了 ⑤「提交 PR」功能**:
|
|
212
242
|
|
|
213
243
|
- **前端**:`lib/client.js` 删除整个 `PrSection`(含 ⑤ 区块的渲染与面板导语中的「⑤提交 PR」文案),一并清理了对应的 i18n 键(`EN_DICT`)。
|
package/lib/client.js
CHANGED
|
@@ -74,6 +74,39 @@ window.__ModuleLoader__.load({
|
|
|
74
74
|
if (!res.ok) throw new Error("HTTP " + String(res.status));
|
|
75
75
|
return await res.json();
|
|
76
76
|
}
|
|
77
|
+
/** POST 并逐行消费 NDJSON 事件流({type:'step'|'out'|'result'},用于一键安装进度)。返回 result 对象。 */
|
|
78
|
+
async function jpostStream(path, body, onEvent) {
|
|
79
|
+
const res = await fetch(API + path + (path.indexOf("?") >= 0 ? "&" : "?") + "lang=" + currentLang(), {
|
|
80
|
+
method: "POST",
|
|
81
|
+
headers: { "content-type": "application/json" },
|
|
82
|
+
body: JSON.stringify(body || {}),
|
|
83
|
+
});
|
|
84
|
+
if (!res.ok || !res.body) {
|
|
85
|
+
const text = await res.text().catch(() => "");
|
|
86
|
+
throw new Error("HTTP " + String(res.status) + (text ? ": " + text : ""));
|
|
87
|
+
}
|
|
88
|
+
const reader = res.body.getReader();
|
|
89
|
+
const decoder = new TextDecoder();
|
|
90
|
+
let buf = "";
|
|
91
|
+
let result = null;
|
|
92
|
+
for (;;) {
|
|
93
|
+
const { done, value } = await reader.read();
|
|
94
|
+
if (done) break;
|
|
95
|
+
buf += decoder.decode(value, { stream: true });
|
|
96
|
+
let nl;
|
|
97
|
+
while ((nl = buf.indexOf("\n")) >= 0) {
|
|
98
|
+
const line = buf.slice(0, nl).trim();
|
|
99
|
+
buf = buf.slice(nl + 1);
|
|
100
|
+
if (!line) continue;
|
|
101
|
+
let evt;
|
|
102
|
+
try { evt = JSON.parse(line); } catch { continue }
|
|
103
|
+
if (evt && evt.type === "result") result = evt.result;
|
|
104
|
+
else if (onEvent) { try { onEvent(evt); } catch {} }
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (result === null) throw new Error("no result");
|
|
108
|
+
return result;
|
|
109
|
+
}
|
|
77
110
|
|
|
78
111
|
// ---------- preload cache ----------
|
|
79
112
|
// 只在打开插件时按需拉取仓库状态;DSH 打开(插件激活)时只预取静态的 env / ssh /
|
|
@@ -179,7 +212,7 @@ window.__ModuleLoader__.load({
|
|
|
179
212
|
const h = React.createElement;
|
|
180
213
|
|
|
181
214
|
// ---------- i18n: follows DSH’s language setting (Settings → General → Language), live ----------
|
|
182
|
-
const EN_DICT = {"展开":"Expand","折叠":"Collapse","展开该部分":"Expand this section","折叠该部分":"Collapse this section","已安装":"Installed","❌ 未安装":"❌ Not installed","复制安装命令":"Copy install command","一键安装(会自动选包管理器)":"Install automatically (picks a package manager)","安装中…":"Installing…","安装":"Install"," 执行成功":" executed successfully","(如未生效需以管理员身份重试)":" (retry as administrator if it did not take effect)","安装失败":"Install failed","① 环境检查":"① Environment","✅ 均存在":"✅ All present","操作系统":"OS","已找到":"Found","检测中":"Checking","失败: ":"Failed: ","未找到的工具可用上方「安装」按钮一键安装(":"Missing tools can be installed with the \"Install\" button above (","Windows 用 winget / 内置功能":"winget / built-in features on Windows","用系统包管理器":"your system package manager",",可能需管理员权限);也可手动复制安装命令执行,安装后点「重新检查」。":"; admin rights may be required). You can also copy the install command and run it manually, then click \"Re-check\".","重新检查":"Re-check",":已存在,无需重复操作":": already exists, no need to repeat","成功":" succeeded","失败":" failed","失败:":"Failed: ","平台":"Platform","选择代码托管平台:GitHub 或 Gitee(默认 GitHub),③代码管理会跟随切换":"Choose the code hosting platform: GitHub or Gitee (GitHub default); step ③ follows this switch","GitHub(默认)":"GitHub (default)","② SSH 密钥与连接":"② SSH Key & Connection","密钥":"Key"," 已生成":" generated","⚠️ 未生成":"⚠️ Not generated","GH 登录":"GH login","已登录":"Logged in","⚠️ 未登录":"⚠️ Not logged in","SSH 配置":"SSH config"," 已配置(443)":" configured (443)","⚠️ 未配置/限制 22 端口需配置":"⚠️ Not configured / port 22 blocked — use 443","生成密钥":"Generate key","配置 SSH(config)":"Configure SSH config","配置 SSH":"Configure SSH","测试连接":"Test connection","SSH 连接成功(":"SSH connection succeeded (","已认证":"authenticated","连接失败,请确认密钥已上传到 ":"Connection failed. Make sure the key is uploaded to "," 或已登录 gh":" or that gh is logged in","测试失败:":"Test failed: ","公钥(复制上传到 ":"Public key (copy and upload to ","Gitee → 设置 → SSH 公钥":"Gitee → Settings → SSH keys",",或运行 gh auth login 自动上传):":", or run gh auth login to upload it automatically):","该目录不是 git 仓库":"This folder is not a git repository","读取令牌状态失败:":"Failed to read token status: ","已保存 Gitee 令牌(账号:":"Gitee token saved (account: ","保存令牌失败":"Failed to save token","保存令牌失败:":"Failed to save token: ","已清除 Gitee 令牌":"Gitee token cleared","清除失败":"Clear failed","清除令牌失败:":"Failed to clear token: ","已推送":"Pushed","推送失败":"Push failed","推送失败:":"Push failed: ","已拉取远程更新(本地已对齐到远程最新状态)":"Pulled remote updates (local is now aligned with the latest remote state)","拉取失败":"Pull failed","拉取失败:":"Pull failed: ","已拉取远程更新并推送更改":"Pulled remote updates and pushed changes","合并推送失败":"Merge-push failed","合并推送失败:":"Merge-push failed: ","强制推送会用本地版本覆盖远程仓库,远程上非本地的更改将被丢弃。确定继续?":"Force-push overwrites the remote repo with your local version; remote-only changes will be lost. Continue?","已强制推送,远程已更新为本地状态":"Force-pushed; the remote is now your local state","强制推送失败":"Force-push failed","强制推送失败:":"Force-push failed: ","强制拉取会把远程更新并入本地。如果本地有不想保留的内容,将按冲突处理或遗失。确定继续?":"Force-pull merges remote updates into local; anything you don't want kept may conflict or be lost. Continue?","已强制拉取远程更新":"Force-pulled remote updates","强制拉取失败":"Force-pull failed","强制拉取失败:":"Force-pull failed: ","请先加载一个有效的文件夹":"Load a valid folder first","确定把仓库 ":"Change repo "," 改为「":" visibility to「","公开":"Public","私有":"Private","」?修改可见性可能影响 Star、关注者等。":"」? Changing visibility may affect stars, watchers, etc.","已把仓库设置为「":"Repo set to「","修改可见性失败":"Failed to change visibility","修改可见性失败:":"Failed to change visibility: ","仓库已创建并推送:":"Repo created and pushed: ","创建失败":"Create failed","创建失败:":"Create failed: ","未选择目录":"No folder selected","选择失败:":"Selection failed: ","请输入或选择目录路径":"Enter or pick a folder path","添加目录失败":"Failed to add folder","添加目录失败:":"Failed to add folder: ","删除失败":"Delete failed","删除失败:":"Delete failed: ","强制对齐会用远程分支覆盖本地(丢弃本地未推送的更改与提交),确定继续?":"Force-align overwrites local with the remote branch (drops unpushed local changes and commits). Continue?","已强制对齐到远程分支":"Force-aligned to the remote branch","强制对齐失败":"Force-align failed","强制对齐失败:":"Force-align failed: ","已是 git 仓库":"Already a git repository","已创建 git 仓库,请自行拉取或推送":"Git repository created — pull or push as you wish","创建 git 失败":"Failed to init git","创建 git 失败:":"Failed to init git: ","暂存失败":"Stage failed","暂存失败:":"Stage failed: ","取消暂存失败":"Unstage failed","取消暂存失败:":"Unstage failed: ","已提交 ":"Committed "," 个文件":" file(s)","提交失败":"Commit failed","提交失败:":"Commit failed: ","已提交「":"Committed「","(自动生成)":" (auto-generated)","」并推送":"」 and pushed","已推送本地已有的提交":"Pushed existing local commits","读取分支失败":"Failed to load branches","读取分支失败:":"Failed to load branches: ","切换到分支「":"Switch to branch「","」?(若当前有未提交改动,git 会拒绝切换)":"」? (git will refuse if you have uncommitted changes)","已切换到分支 ":"Switched to branch ","切换分支失败":"Failed to switch branch","切换分支失败:":"Failed to switch branch: ","读取历史失败":"Failed to load history","读取历史失败:":"Failed to load history: ","revert 提交「":"Revert commit「","」——会生成一个反向提交,期间可能产生冲突,确定继续?":"」 — this creates a reverse commit and may cause conflicts. Continue?","已 revert 提交 ":"Reverted commit ","revert 失败":"Revert failed","revert 失败:":"Revert failed: ","cherry-pick 提交「":"Cherry-pick commit「","」到当前分支——期间可能产生冲突,确定继续?":"」 onto the current branch — this may cause conflicts. Continue?","已 cherry-pick 提交 ":"Cherry-picked commit ","cherry-pick 失败":"Cherry-pick failed","cherry-pick 失败:":"Cherry-pick failed: ","读取失败":"Failed to load","已复制":"Copied","短哈希":"short hash","完整哈希":"full hash","提交信息":"commit message","复制失败:请手动复制":"Copy failed — copy it manually","③ 代码管理":"③ Code Management","Gitee 私人令牌(OpenAPI,需 projects 权限)":"Gitee personal token (OpenAPI, requires projects permission)","✅ 已配置":"✅ Configured","(账号 ":" (account ","清除令牌":"Clear token","粘贴 Gitee 私人令牌(https://gitee.com/personal_access_tokens)":"Paste the Gitee personal token (https://gitee.com/personal_access_tokens)","保存令牌":"Save token","令牌只保存在本机 ~/.dsh/storages(0600),不会写进插件目录;需勾选个人令牌的 projects 权限。公钥需已上传到 Gitee(② 里检查)。":"The token is stored only on this machine at ~/.dsh/storages (0600), never in the plugin directory; the personal token must have projects permission. Your public key must be uploaded to Gitee (checked in ②).","选择工作区":"Workspace","选择 DSH 已登记的工作区文件夹":"Choose a workspace folder registered in DSH","(暂无可选工作区)":"(no workspaces available)","— 请选择 —":"— Select —","选择目录":"Choose folder","自定义目录:":"Custom folder: ","删除该目录记录":"Remove this folder entry","删除该目录记录(不删除实际文件夹)":"Remove this folder entry (does not delete the actual folder)","目录":"Folder","分支":"Branch","(无)":"(none)","切换分支":"Switch branch","切换":"Switch","查看提交历史":"View commit history","历史":"History","远程":"Remote","改动":"Changes","无":"None","查看改动的文件列表":"View the list of changed files","查看":"View","同步":"Sync","本地领先 ":"Ahead by "," 提交":" commit(s)","、落后 ":", behind by ","落后 ":"behind by ","与远程一致":"Up to date with remote","查看本地与远程的提交差异":"View commit differences between local and remote","⚠️ 以下 ":"⚠️ The following "," 项 >100MB(超过 GitHub 限制,推送时将自动忽略不上传):":" item(s) >100MB (over GitHub's limit — auto-ignored on push):","/(整个文件夹)":"/ (entire folder)","创建 Git":"Init Git","仅初始化 git 仓库,不拉取不推送,由你决定下一步":"Initializes a git repo only — no pull/push; you decide the next step","本地有改动且远程有更新,可直接用「强制对齐」将本地重置为远程状态":"You have local changes AND remote updates — use \"Force align\" to reset local to the remote state","强制对齐":"Force align","本地完全重置为远程分支(丢弃本地差异),解决“文件相同仍显示同步差异”的情况":"Fully resets local to the remote branch (drops local differences); fixes \"files identical but sync still shows a difference\"","推送更改":"Push changes","拉取更新":"Pull updates","✓ 已是最新":"✓ Up to date","⟳ 正在刷新状态,联网同步 GitHub/Gitee 最新数据…":"⟳ Refreshing status — syncing the latest GitHub/Gitee data…","⟳ 切换平台,正在重新检测「":"⟳ Switching platform, re-checking「","」仓库状态…":"」 repo status…","推送中…":"Pushing…","推送暂存":"Push staged","把已暂存的内容用填写的(或自动生成的)信息提交后推送到远程;不会自动暂存其他未暂存的改动":"Commits only what is staged (with your message, or auto-generated) and pushes it; does not auto-stage other changes","刷新中…":"Refreshing…","刷新状态":"Refresh status","提交信息(留空则用默认)":"Commit message (default if empty)","填写提交信息后点「提交」;留空则自动生成(chore: update <文件夹名>)":"Type a message then click \"Commit\"; leave empty to auto-generate (chore: update <folder>)","提交":"Commit","仓库名称自动取文件夹名(不可修改)":"Repo name is taken from the folder name (read-only)","(未加载文件夹)":"(no folder loaded)","仓库可见性:私有 / 公开":"Repo visibility: private / public","(修改当前仓库的可见性)":" (changes the current repo's visibility)","新建仓库并推送":"Create repo & push","修改仓库状态":"Change repo status","✓ 仓库已是「":"✓ Repo is already「","」状态,如需修改请调整左侧可见性选择。":"」 — to change it, adjust the visibility dropdown.","将把仓库从「":"Will change the repo from「","」改为「":"」 to「","」,点击「修改仓库状态」执行。":"」 — click \"Change repo status\" to apply.","⚠️ 同名仓库已经创建(无法读取当前可见性,可能未":"⚠️ A repo with this name already exists (couldn't read its visibility — maybe not ","配置 Gitee 令牌":"Gitee token configured","登录 gh":"logged into gh","✓ 同名仓库 ":"✓ No repo named "," 不存在,可在 ":" exists — you can create it on ","「新建仓库并推送」创建(可选私有/公开)。":" with \"Create repo & push\" (private or public).","已忽略未上传(":"Skipped uploads ("," 项 >100MB):":" item(s) >100MB):","已提交":"Committed"," 并推送":" and pushed","(推送省略)":" (push skipped)","查看详情":"View details","改动文件":"Changed files","提交历史":"Commit history","与远程同步差异":"Sync differences vs remote","关闭":"Close","已安装 dsh-better-sidebar,此处只列改动文件列表;具体改动内容请到 dsh-better-sidebar 的「源代码管理面板」查看。":"dsh-better-sidebar is installed — this lists changed files only; see the actual changes in its \"Source Control panel\".","点击收起":"Click to collapse","点击查看改动内容":"Click to view the diff","新文件(untracked)暂无内容 diff":"New (untracked) file — no diff yet","已暂存":"Staged","未暂存":"Unstaged","取消暂存":"Unstage","暂存":"Stage","加载中…":"Loading…","▾ 收起":"▾ Collapse","▸ 查看":"▸ View","正在加载改动内容…":"Loading changes…","(无内容差异)":"(no diff)","当前没有改动。":"No changes.","正在读取分支…":"Loading branches…","(无分支)":"(no branches)","(当前)":"(current)","正在读取历史…":"Loading history…","(暂无提交)":"(no commits)","更多操作(右键也可打开)":"More actions (right-click also works)","查看提交差异":"View commit diff","复制短哈希":"Copy short hash","复制完整哈希":"Copy full hash","复制提交信息":"Copy commit message","还原此提交":"Revert this commit","拾取此提交":"Cherry-pick this commit","正在加载提交 diff…":"Loading commit diff…","本地落后 ":"Behind by "," 个提交(远程有而本地没有):":" commit(s) (on remote, not local):"," 个提交(本地有而远程没有):":" commit(s) (on local, not remote):","✓ 已与远程同步,无差异。":"✓ In sync with remote — no differences.","选择代码目录":"Choose a code folder","可手动输入/粘贴目录绝对路径,或点击「浏览…」弹出本地文件夹选择器。确认后将添加到下方下拉并记住,下次打开无需重新选择。":"Paste an absolute folder path, or click \"Browse…\" for the native picker. Confirmed folders are added to the dropdown and remembered.","C:\\Users\\你的用户名\\项目目录":"C:\\Users\\your-name\\project","浏览…":"Browse…","取消":"Cancel","确定":"OK","新增":"Added","删除":"Deleted","重命名":"Renamed","修改":"Modified","旧版本":"Old","新版本":"New","源代码管理":"Source Control","按顺序完成:①环境检查 → ②SSH 密钥与连接 → ③代码管理。推送会自动忽略 >100MB 的文件(":"Work through in order: ① Environment → ② SSH Key & Connection → ③ Code Management. Files over 100MB are auto-ignored on push ("," 限制)并说明原因。":" limit) with the reason shown.","代码管理":"Code Management","拖动调整面板宽度":"Drag to resize the panel","二进制文件,无法查看文本内容":"Binary file — text preview unavailable","(空文件)":"(empty file)"};
|
|
215
|
+
const EN_DICT = {"展开":"Expand","折叠":"Collapse","展开该部分":"Expand this section","折叠该部分":"Collapse this section","已安装":"Installed","❌ 未安装":"❌ Not installed","复制安装命令":"Copy install command","一键安装(会自动选包管理器)":"Install automatically (picks a package manager)","安装中…":"Installing…","安装中,请稍候…":"Installing, please wait…","安装完成":"Installation complete","安装完成,需要重启 DSH 生效。点击「重启 DSH」立即重启,页面会短暂断开,恢复后请刷新。":"Installation complete — restart DSH to apply. Click \"Restart DSH\" to restart now; the page will briefly disconnect, refresh once it is back.","重启 DSH":"Restart DSH","正在重启 DSH,页面将短暂断开;恢复后请刷新网页。":"Restarting DSH — the page will briefly disconnect; refresh once it is back.","重启失败:":"Restart failed: ","重启似乎未生效,请手动重启 dsh。":"The restart does not seem to have taken effect — please restart dsh manually.",",需要重启 DSH 生效":" — restart DSH to apply","安装":"Install"," 执行成功":" executed successfully","(已装到 ~/.local/bin,免 sudo)":" (installed to ~/.local/bin, no sudo needed)","(如未生效需以管理员身份重试)":" (retry as administrator if it did not take effect)","安装失败":"Install failed","① 环境检查":"① Environment","✅ 均存在":"✅ All present","操作系统":"OS","已找到":"Found","检测中":"Checking","失败: ":"Failed: ","未找到的工具可用上方「安装」按钮一键安装(":"Missing tools can be installed with the \"Install\" button above (","Windows 用 winget / 内置功能":"winget / built-in features on Windows","用系统包管理器":"your system package manager",",可能需管理员权限);也可手动复制安装命令执行,安装后点「重新检查」。":"; admin rights may be required). You can also copy the install command and run it manually, then click \"Re-check\".","重新检查":"Re-check",":已存在,无需重复操作":": already exists, no need to repeat","成功":" succeeded","失败":" failed","失败:":"Failed: ","平台":"Platform","选择代码托管平台:GitHub 或 Gitee(默认 GitHub),③代码管理会跟随切换":"Choose the code hosting platform: GitHub or Gitee (GitHub default); step ③ follows this switch","GitHub(默认)":"GitHub (default)","② SSH 密钥与连接":"② SSH Key & Connection","密钥":"Key"," 已生成":" generated","⚠️ 未生成":"⚠️ Not generated","GH 登录":"GH login","已登录":"Logged in","⚠️ 未登录":"⚠️ Not logged in","SSH 配置":"SSH config"," 已配置(443)":" configured (443)","⚠️ 未配置/限制 22 端口需配置":"⚠️ Not configured / port 22 blocked — use 443","生成密钥":"Generate key","配置 SSH(config)":"Configure SSH config","配置 SSH":"Configure SSH","测试连接":"Test connection","SSH 连接成功(":"SSH connection succeeded (","已认证":"authenticated","连接失败,请确认密钥已上传到 ":"Connection failed. Make sure the key is uploaded to "," 或已登录 gh":" or that gh is logged in","测试失败:":"Test failed: ","公钥(复制上传到 ":"Public key (copy and upload to ","Gitee → 设置 → SSH 公钥":"Gitee → Settings → SSH keys",",或运行 gh auth login 自动上传):":", or run gh auth login to upload it automatically):","该目录不是 git 仓库":"This folder is not a git repository","读取令牌状态失败:":"Failed to read token status: ","已保存 Gitee 令牌(账号:":"Gitee token saved (account: ","保存令牌失败":"Failed to save token","保存令牌失败:":"Failed to save token: ","已清除 Gitee 令牌":"Gitee token cleared","清除失败":"Clear failed","清除令牌失败:":"Failed to clear token: ","已推送":"Pushed","推送失败":"Push failed","推送失败:":"Push failed: ","已拉取远程更新(本地已对齐到远程最新状态)":"Pulled remote updates (local is now aligned with the latest remote state)","拉取失败":"Pull failed","拉取失败:":"Pull failed: ","已拉取远程更新并推送更改":"Pulled remote updates and pushed changes","合并推送失败":"Merge-push failed","合并推送失败:":"Merge-push failed: ","强制推送会用本地版本覆盖远程仓库,远程上非本地的更改将被丢弃。确定继续?":"Force-push overwrites the remote repo with your local version; remote-only changes will be lost. Continue?","已强制推送,远程已更新为本地状态":"Force-pushed; the remote is now your local state","强制推送失败":"Force-push failed","强制推送失败:":"Force-push failed: ","强制拉取会把远程更新并入本地。如果本地有不想保留的内容,将按冲突处理或遗失。确定继续?":"Force-pull merges remote updates into local; anything you don't want kept may conflict or be lost. Continue?","已强制拉取远程更新":"Force-pulled remote updates","强制拉取失败":"Force-pull failed","强制拉取失败:":"Force-pull failed: ","请先加载一个有效的文件夹":"Load a valid folder first","确定把仓库 ":"Change repo "," 改为「":" visibility to「","公开":"Public","私有":"Private","」?修改可见性可能影响 Star、关注者等。":"」? Changing visibility may affect stars, watchers, etc.","已把仓库设置为「":"Repo set to「","修改可见性失败":"Failed to change visibility","修改可见性失败:":"Failed to change visibility: ","仓库已创建并推送:":"Repo created and pushed: ","创建失败":"Create failed","创建失败:":"Create failed: ","未选择目录":"No folder selected","选择失败:":"Selection failed: ","请输入或选择目录路径":"Enter or pick a folder path","添加目录失败":"Failed to add folder","添加目录失败:":"Failed to add folder: ","删除失败":"Delete failed","删除失败:":"Delete failed: ","强制对齐会用远程分支覆盖本地(丢弃本地未推送的更改与提交),确定继续?":"Force-align overwrites local with the remote branch (drops unpushed local changes and commits). Continue?","已强制对齐到远程分支":"Force-aligned to the remote branch","强制对齐失败":"Force-align failed","强制对齐失败:":"Force-align failed: ","已是 git 仓库":"Already a git repository","已创建 git 仓库,请自行拉取或推送":"Git repository created — pull or push as you wish","创建 git 失败":"Failed to init git","创建 git 失败:":"Failed to init git: ","暂存失败":"Stage failed","暂存失败:":"Stage failed: ","取消暂存失败":"Unstage failed","取消暂存失败:":"Unstage failed: ","已提交 ":"Committed "," 个文件":" file(s)","提交失败":"Commit failed","提交失败:":"Commit failed: ","已提交「":"Committed「","(自动生成)":" (auto-generated)","」并推送":"」 and pushed","已推送本地已有的提交":"Pushed existing local commits","读取分支失败":"Failed to load branches","读取分支失败:":"Failed to load branches: ","切换到分支「":"Switch to branch「","」?(若当前有未提交改动,git 会拒绝切换)":"」? (git will refuse if you have uncommitted changes)","已切换到分支 ":"Switched to branch ","切换分支失败":"Failed to switch branch","切换分支失败:":"Failed to switch branch: ","读取历史失败":"Failed to load history","读取历史失败:":"Failed to load history: ","revert 提交「":"Revert commit「","」——会生成一个反向提交,期间可能产生冲突,确定继续?":"」 — this creates a reverse commit and may cause conflicts. Continue?","已 revert 提交 ":"Reverted commit ","revert 失败":"Revert failed","revert 失败:":"Revert failed: ","cherry-pick 提交「":"Cherry-pick commit「","」到当前分支——期间可能产生冲突,确定继续?":"」 onto the current branch — this may cause conflicts. Continue?","已 cherry-pick 提交 ":"Cherry-picked commit ","cherry-pick 失败":"Cherry-pick failed","cherry-pick 失败:":"Cherry-pick failed: ","读取失败":"Failed to load","已复制":"Copied","短哈希":"short hash","完整哈希":"full hash","提交信息":"commit message","复制失败:请手动复制":"Copy failed — copy it manually","③ 代码管理":"③ Code Management","Gitee 私人令牌(OpenAPI,需 projects 权限)":"Gitee personal token (OpenAPI, requires projects permission)","✅ 已配置":"✅ Configured","(账号 ":" (account ","清除令牌":"Clear token","粘贴 Gitee 私人令牌(https://gitee.com/personal_access_tokens)":"Paste the Gitee personal token (https://gitee.com/personal_access_tokens)","保存令牌":"Save token","令牌只保存在本机 ~/.dsh/storages(0600),不会写进插件目录;需勾选个人令牌的 projects 权限。公钥需已上传到 Gitee(② 里检查)。":"The token is stored only on this machine at ~/.dsh/storages (0600), never in the plugin directory; the personal token must have projects permission. Your public key must be uploaded to Gitee (checked in ②).","选择工作区":"Workspace","选择 DSH 已登记的工作区文件夹":"Choose a workspace folder registered in DSH","(暂无可选工作区)":"(no workspaces available)","— 请选择 —":"— Select —","选择目录":"Choose folder","自定义目录:":"Custom folder: ","删除该目录记录":"Remove this folder entry","删除该目录记录(不删除实际文件夹)":"Remove this folder entry (does not delete the actual folder)","目录":"Folder","分支":"Branch","(无)":"(none)","切换分支":"Switch branch","切换":"Switch","查看提交历史":"View commit history","历史":"History","远程":"Remote","改动":"Changes","无":"None","查看改动的文件列表":"View the list of changed files","查看":"View","同步":"Sync","本地领先 ":"Ahead by "," 提交":" commit(s)","、落后 ":", behind by ","落后 ":"behind by ","与远程一致":"Up to date with remote","查看本地与远程的提交差异":"View commit differences between local and remote","⚠️ 以下 ":"⚠️ The following "," 项 >100MB(超过 GitHub 限制,推送时将自动忽略不上传):":" item(s) >100MB (over GitHub's limit — auto-ignored on push):","/(整个文件夹)":"/ (entire folder)","创建 Git":"Init Git","仅初始化 git 仓库,不拉取不推送,由你决定下一步":"Initializes a git repo only — no pull/push; you decide the next step","本地有改动且远程有更新,可直接用「强制对齐」将本地重置为远程状态":"You have local changes AND remote updates — use \"Force align\" to reset local to the remote state","强制对齐":"Force align","本地完全重置为远程分支(丢弃本地差异),解决“文件相同仍显示同步差异”的情况":"Fully resets local to the remote branch (drops local differences); fixes \"files identical but sync still shows a difference\"","推送更改":"Push changes","拉取更新":"Pull updates","✓ 已是最新":"✓ Up to date","⟳ 正在刷新状态,联网同步 GitHub/Gitee 最新数据…":"⟳ Refreshing status — syncing the latest GitHub/Gitee data…","⟳ 切换平台,正在重新检测「":"⟳ Switching platform, re-checking「","」仓库状态…":"」 repo status…","推送中…":"Pushing…","推送暂存":"Push staged","把已暂存的内容用填写的(或自动生成的)信息提交后推送到远程;不会自动暂存其他未暂存的改动":"Commits only what is staged (with your message, or auto-generated) and pushes it; does not auto-stage other changes","刷新中…":"Refreshing…","刷新状态":"Refresh status","提交信息(留空则用默认)":"Commit message (default if empty)","填写提交信息后点「提交」;留空则自动生成(chore: update <文件夹名>)":"Type a message then click \"Commit\"; leave empty to auto-generate (chore: update <folder>)","提交":"Commit","仓库名称自动取文件夹名(不可修改)":"Repo name is taken from the folder name (read-only)","(未加载文件夹)":"(no folder loaded)","仓库可见性:私有 / 公开":"Repo visibility: private / public","(修改当前仓库的可见性)":" (changes the current repo's visibility)","新建仓库并推送":"Create repo & push","修改仓库状态":"Change repo status","✓ 仓库已是「":"✓ Repo is already「","」状态,如需修改请调整左侧可见性选择。":"」 — to change it, adjust the visibility dropdown.","将把仓库从「":"Will change the repo from「","」改为「":"」 to「","」,点击「修改仓库状态」执行。":"」 — click \"Change repo status\" to apply.","⚠️ 同名仓库已经创建(无法读取当前可见性,可能未":"⚠️ A repo with this name already exists (couldn't read its visibility — maybe not ","配置 Gitee 令牌":"Gitee token configured","登录 gh":"logged into gh","✓ 同名仓库 ":"✓ No repo named "," 不存在,可在 ":" exists — you can create it on ","「新建仓库并推送」创建(可选私有/公开)。":" with \"Create repo & push\" (private or public).","已忽略未上传(":"Skipped uploads ("," 项 >100MB):":" item(s) >100MB):","已提交":"Committed"," 并推送":" and pushed","(推送省略)":" (push skipped)","查看详情":"View details","改动文件":"Changed files","提交历史":"Commit history","与远程同步差异":"Sync differences vs remote","关闭":"Close","已安装 dsh-better-sidebar,此处只列改动文件列表;具体改动内容请到 dsh-better-sidebar 的「源代码管理面板」查看。":"dsh-better-sidebar is installed — this lists changed files only; see the actual changes in its \"Source Control panel\".","点击收起":"Click to collapse","点击查看改动内容":"Click to view the diff","新文件(untracked)暂无内容 diff":"New (untracked) file — no diff yet","已暂存":"Staged","未暂存":"Unstaged","取消暂存":"Unstage","暂存":"Stage","加载中…":"Loading…","▾ 收起":"▾ Collapse","▸ 查看":"▸ View","正在加载改动内容…":"Loading changes…","(无内容差异)":"(no diff)","当前没有改动。":"No changes.","正在读取分支…":"Loading branches…","(无分支)":"(no branches)","(当前)":"(current)","正在读取历史…":"Loading history…","(暂无提交)":"(no commits)","更多操作(右键也可打开)":"More actions (right-click also works)","查看提交差异":"View commit diff","复制短哈希":"Copy short hash","复制完整哈希":"Copy full hash","复制提交信息":"Copy commit message","还原此提交":"Revert this commit","拾取此提交":"Cherry-pick this commit","正在加载提交 diff…":"Loading commit diff…","本地落后 ":"Behind by "," 个提交(远程有而本地没有):":" commit(s) (on remote, not local):"," 个提交(本地有而远程没有):":" commit(s) (on local, not remote):","✓ 已与远程同步,无差异。":"✓ In sync with remote — no differences.","选择代码目录":"Choose a code folder","可手动输入/粘贴目录绝对路径,或点击「浏览…」弹出本地文件夹选择器。确认后将添加到下方下拉并记住,下次打开无需重新选择。":"Paste an absolute folder path, or click \"Browse…\" for the native picker. Confirmed folders are added to the dropdown and remembered.","C:\\Users\\你的用户名\\项目目录":"C:\\Users\\your-name\\project","浏览…":"Browse…","取消":"Cancel","确定":"OK","新增":"Added","删除":"Deleted","重命名":"Renamed","修改":"Modified","旧版本":"Old","新版本":"New","源代码管理":"Source Control","按顺序完成:①环境检查 → ②SSH 密钥与连接 → ③代码管理。推送会自动忽略 >100MB 的文件(":"Work through in order: ① Environment → ② SSH Key & Connection → ③ Code Management. Files over 100MB are auto-ignored on push ("," 限制)并说明原因。":" limit) with the reason shown.","代码管理":"Code Management","拖动调整面板宽度":"Drag to resize the panel","二进制文件,无法查看文本内容":"Binary file — text preview unavailable","(空文件)":"(empty file)"};
|
|
183
216
|
Object.assign(EN_DICT, {
|
|
184
217
|
// ④ 克隆仓库
|
|
185
218
|
"④ 克隆仓库": "④ Clone repos",
|
|
@@ -223,6 +256,7 @@ Object.assign(EN_DICT, {
|
|
|
223
256
|
"未登录 npm": "Not logged in to npm",
|
|
224
257
|
"npm 状态": "npm status",
|
|
225
258
|
"registry:": "registry: ",
|
|
259
|
+
"发布将使用官方源:": "Publishing uses the official registry: ",
|
|
226
260
|
"我已核对以上内容,确认发布到 npm registry": "I've reviewed the above — confirm publishing to the npm registry",
|
|
227
261
|
"发布完成": "Publish complete",
|
|
228
262
|
"发布失败": "Publish failed",
|
|
@@ -379,11 +413,88 @@ function useSession() {
|
|
|
379
413
|
gh: "winget install --id GitHub.cli -e",
|
|
380
414
|
ssh: "Add-WindowsCapability -Online -Name OpenSSH.Client",
|
|
381
415
|
};
|
|
416
|
+
// 一键安装进度弹窗:实时展示分步进度 + 尾部输出 + 结果 / 重启提示。
|
|
417
|
+
function InstallDialog({ tool, logs, result, onClose }) {
|
|
418
|
+
const scrollRef = useRef(null);
|
|
419
|
+
const restartTimer = useRef(null);
|
|
420
|
+
const [restartState, setRestartState] = useState(null); // null | 'restarting' | 'error'
|
|
421
|
+
const [restartError, setRestartError] = useState(null);
|
|
422
|
+
useEffect(() => {
|
|
423
|
+
if (scrollRef.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
|
424
|
+
}, [logs]);
|
|
425
|
+
useEffect(() => () => { if (restartTimer.current) clearTimeout(restartTimer.current); }, []);
|
|
426
|
+
/** 一键重启 dsh:服务端 detached helper 按原命令拉起新进程(机制同 dsh-update)。 */
|
|
427
|
+
const doRestart = async () => {
|
|
428
|
+
setRestartState("restarting"); setRestartError(null);
|
|
429
|
+
try {
|
|
430
|
+
const res = await fetch(API + "/restart", { method: "POST" });
|
|
431
|
+
if (!res.ok) {
|
|
432
|
+
let detail = "";
|
|
433
|
+
try { detail = await res.text(); } catch {}
|
|
434
|
+
throw new Error("HTTP " + String(res.status) + (detail !== "" ? ": " + detail : ""));
|
|
435
|
+
}
|
|
436
|
+
// 成功后本进程约 500ms 退出、helper 接管——页面会短暂断开。
|
|
437
|
+
// 若 12 秒后组件仍存活(异常情况),提示手动重启。
|
|
438
|
+
restartTimer.current = window.setTimeout(() => {
|
|
439
|
+
setRestartState((s) => (s === "restarting" ? "error" : s));
|
|
440
|
+
}, 12000);
|
|
441
|
+
} catch (e) {
|
|
442
|
+
setRestartState("error");
|
|
443
|
+
setRestartError(String(e));
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
const done = result !== null;
|
|
447
|
+
const ok = !!(result && result.ok);
|
|
448
|
+
const toolName = tool === "gh" ? "GitHub CLI" : tool === "git" ? "Git" : "SSH";
|
|
449
|
+
const needRestart = done && !!(result && result.restartNeeded);
|
|
450
|
+
return h("div", { style: { position: "fixed", inset: 0, zIndex: 9999, background: T.mask, display: "flex", alignItems: "center", justifyContent: "center" } },
|
|
451
|
+
h("div", { style: { width: 460, maxWidth: "92vw", background: T.layer1, border: "1px solid " + T.border, borderRadius: 14, padding: 16, display: "flex", flexDirection: "column", gap: 10 } },
|
|
452
|
+
h("div", { style: { fontSize: 14, fontWeight: 600, color: T.label } },
|
|
453
|
+
(done ? (ok ? "✅ " : "❌ ") : "⏳ ") + toolName + " " + (done ? t("安装完成") : t("安装中…"))),
|
|
454
|
+
h("div", { ref: scrollRef, style: { overflowY: "auto", maxHeight: 240, fontFamily: "var(--ds-font-family-code, ui-monospace, monospace)", fontSize: 12, lineHeight: 1.7 } },
|
|
455
|
+
logs.length === 0
|
|
456
|
+
? h("div", { style: { color: T.secondary } }, "…")
|
|
457
|
+
: logs.map((log, i) =>
|
|
458
|
+
h("div", { key: i, style: { display: "flex", gap: 6, alignItems: "baseline" } },
|
|
459
|
+
h("span", { style: { color: log.state === "running" ? T.brand : T.success } },
|
|
460
|
+
log.state === "running" ? "▶" : "✓"),
|
|
461
|
+
h("span", { style: { color: T.label, whiteSpace: "pre-wrap", wordBreak: "break-all" } },
|
|
462
|
+
log.label + (log.tail ? " " + log.tail : "")),
|
|
463
|
+
)
|
|
464
|
+
)),
|
|
465
|
+
done
|
|
466
|
+
? needRestart
|
|
467
|
+
? h("div", { style: { fontSize: 12, color: T.warn, lineHeight: 1.6 } },
|
|
468
|
+
t("安装完成,需要重启 DSH 生效。点击「重启 DSH」立即重启,页面会短暂断开,恢复后请刷新。"))
|
|
469
|
+
: ok
|
|
470
|
+
? h("div", { style: { fontSize: 12, color: T.success } },
|
|
471
|
+
t("安装完成") + (result.userLevel ? t("(已装到 ~/.local/bin,免 sudo)") : ""))
|
|
472
|
+
: h("div", { style: { fontSize: 12, color: T.danger } },
|
|
473
|
+
"❌ " + (result.error || t("安装失败")) + (result.command && !result.manual ? ":" + result.command : ""))
|
|
474
|
+
: h("div", { style: { fontSize: 12, color: T.secondary } }, t("安装中,请稍候…")),
|
|
475
|
+
restartState === "restarting"
|
|
476
|
+
? h("div", { style: { fontSize: 12, color: T.brand, lineHeight: 1.6 } },
|
|
477
|
+
t("正在重启 DSH,页面将短暂断开;恢复后请刷新网页。"))
|
|
478
|
+
: restartState === "error"
|
|
479
|
+
? h("div", { style: { fontSize: 12, color: T.danger, lineHeight: 1.6 } },
|
|
480
|
+
(restartError ? t("重启失败:") + restartError + " " : "") + t("重启似乎未生效,请手动重启 dsh。"))
|
|
481
|
+
: null,
|
|
482
|
+
h("div", { style: { display: "flex", justifyContent: "flex-end", gap: 8 } },
|
|
483
|
+
needRestart
|
|
484
|
+
? h(Btn, { label: t("重启 DSH"), onClick: doRestart, tone: "primary", disabled: restartState !== null })
|
|
485
|
+
: null,
|
|
486
|
+
h(Btn, { label: t("关闭"), onClick: onClose, tone: "ghost", disabled: restartState === "restarting" })
|
|
487
|
+
)
|
|
488
|
+
)
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
|
|
382
492
|
function EnvSection() {
|
|
383
493
|
const [env, setEnv] = useState(() => cache.env);
|
|
384
494
|
const [err, setErr] = useState(null);
|
|
385
495
|
const [installing, setInstalling] = useState(null); // 'git'|'gh'|'ssh'|null
|
|
386
496
|
const [installResult, setInstallResult] = useState(null);
|
|
497
|
+
const [installLogs, setInstallLogs] = useState([]); // [{label, state:'running'|'done', tail}]
|
|
387
498
|
const load = useCallback(async () => {
|
|
388
499
|
setErr(null);
|
|
389
500
|
// 先读缓存(DSH 打开时已预取),再后台刷新保持最新
|
|
@@ -393,12 +504,28 @@ function useSession() {
|
|
|
393
504
|
}, []);
|
|
394
505
|
useEffect(() => { void preload().then(load); }, [load]);
|
|
395
506
|
|
|
396
|
-
//
|
|
507
|
+
// 一键安装:流式调用 host(NDJSON 进度事件),弹窗实时展示分步进度,成功后重新检测。
|
|
397
508
|
const doInstall = useCallback(async (tool) => {
|
|
398
|
-
setInstalling(tool); setInstallResult(null);
|
|
509
|
+
setInstalling(tool); setInstallResult(null); setInstallLogs([]);
|
|
399
510
|
try {
|
|
400
|
-
const r = await
|
|
511
|
+
const r = await jpostStream("/install-tool", { tool }, (evt) => {
|
|
512
|
+
if (evt.type === "step") {
|
|
513
|
+
setInstallLogs((logs) => [
|
|
514
|
+
...logs.map((l) => l.state === "running" ? { ...l, state: "done" } : l),
|
|
515
|
+
{ label: evt.label, state: "running", tail: "" },
|
|
516
|
+
]);
|
|
517
|
+
} else if (evt.type === "out" && typeof evt.text === "string") {
|
|
518
|
+
setInstallLogs((logs) => {
|
|
519
|
+
if (logs.length === 0) return logs;
|
|
520
|
+
const next = [...logs];
|
|
521
|
+
const last = next[next.length - 1];
|
|
522
|
+
next[next.length - 1] = { ...last, tail: (last.tail + evt.text).slice(-300) };
|
|
523
|
+
return next;
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
});
|
|
401
527
|
setInstallResult(r);
|
|
528
|
+
setInstallLogs((logs) => logs.map((l) => l.state === "running" ? { ...l, state: "done" } : l));
|
|
402
529
|
if (r.ok) void load();
|
|
403
530
|
} catch (e) { setInstallResult({ ok: false, tool, error: String(e) }); }
|
|
404
531
|
finally { setInstalling(null); }
|
|
@@ -413,7 +540,10 @@ function useSession() {
|
|
|
413
540
|
|
|
414
541
|
// 渲染单个工具行;缺失时带安装命令 + 安装按钮。
|
|
415
542
|
const toolRow = (tool, label, installed, version) => {
|
|
416
|
-
|
|
543
|
+
// 优先用 host 返回的平台自适应命令(与「一键安装」同源);
|
|
544
|
+
// Windows 兜底用内置提示;非 Windows 且 host 未返回时留空(不误导)。
|
|
545
|
+
const hint = (env && env.installHints && env.installHints[tool])
|
|
546
|
+
|| (winOs ? INSTALL_HINTS[tool] : "")
|
|
417
547
|
const status = installed
|
|
418
548
|
? h("span", { style: { color: T.label, fontSize: 13 } }, "✅ " + (version || t("已安装")))
|
|
419
549
|
: h("span", { style: { display: "inline-flex", alignItems: "center", gap: 8 } },
|
|
@@ -422,9 +552,11 @@ function useSession() {
|
|
|
422
552
|
h("button", { type: "button", title: t("一键安装(会自动选包管理器)"), disabled: installing !== null, style: { ...changeBtnStyle(), color: T.brand, fontWeight: 600 }, onClick: (e) => { e.stopPropagation(); void doInstall(tool); } }, installing === tool ? t("安装中…") : t("安装"))
|
|
423
553
|
)
|
|
424
554
|
const feedback = installResult && installResult.tool === tool
|
|
425
|
-
? (installResult.
|
|
426
|
-
? h("div", { style: { marginTop: 4, color: T.
|
|
427
|
-
:
|
|
555
|
+
? (installResult.started
|
|
556
|
+
? h("div", { style: { marginTop: 4, color: T.secondary, fontSize: 12 } }, "⏳ " + (installResult.error || ""))
|
|
557
|
+
: installResult.ok
|
|
558
|
+
? h("div", { style: { marginTop: 4, color: T.success, fontSize: 12 } }, "✅ " + (installResult.command || t("已安装")) + t(" 执行成功") + (installResult.userLevel ? t("(已装到 ~/.local/bin,免 sudo)") : "") + (installResult.needElevation ? t("(如未生效需以管理员身份重试)") : "") + (installResult.restartNeeded ? t(",需要重启 DSH 生效") : ""))
|
|
559
|
+
: h("div", { style: { marginTop: 4, color: T.danger, fontSize: 12 } }, "❌ " + (installResult.error || t("安装失败")) + (installResult.command && !installResult.manual ? ":" + installResult.command : "")))
|
|
428
560
|
: null
|
|
429
561
|
return h("div", { key: tool, style: { marginTop: 4 } },
|
|
430
562
|
h(Field, { label }, status),
|
|
@@ -441,7 +573,8 @@ function useSession() {
|
|
|
441
573
|
) : h(Field, { label: t("检测中"), value: err ? t("失败: ") + err : "…" }),
|
|
442
574
|
(!env || !env.git.installed || !env.gh.installed || !(env.ssh && env.ssh.installed)) ? h("div", { style: { marginTop: 8, fontSize: 12, color: T.secondary, lineHeight: 1.6 } },
|
|
443
575
|
t("未找到的工具可用上方「安装」按钮一键安装(") + (winOs ? t("Windows 用 winget / 内置功能") : t("用系统包管理器")) + t(",可能需管理员权限);也可手动复制安装命令执行,安装后点「重新检查」。")
|
|
444
|
-
) : h(Btn, { label: t("重新检查"), onClick: load, tone: "ghost" })
|
|
576
|
+
) : h(Btn, { label: t("重新检查"), onClick: load, tone: "ghost" }),
|
|
577
|
+
installing !== null ? h(InstallDialog, { tool: installing, logs: installLogs, result: installResult, onClose: () => { setInstalling(null); } }) : null
|
|
445
578
|
);
|
|
446
579
|
}
|
|
447
580
|
|
|
@@ -2053,6 +2186,7 @@ function useSession() {
|
|
|
2053
2186
|
: null,
|
|
2054
2187
|
h("div", null, (status.installed ? "" : t("npm 未安装(请先在 ① 环境检查安装)") + " ") + (status.npmPath || "npm")),
|
|
2055
2188
|
status.registry ? h("div", null, t("registry:") + status.registry) : null,
|
|
2189
|
+
status.publishRegistry ? h("div", { style: status.mirrorConfigured ? { color: T.warn } : { color: T.secondary } }, (status.mirrorConfigured ? "⚠️ " : "") + t("发布将使用官方源:") + status.publishRegistry) : null,
|
|
2056
2190
|
status.whoami
|
|
2057
2191
|
? h("div", { style: { color: T.success } }, t("已登录:") + status.whoami)
|
|
2058
2192
|
: h("div", { style: { color: T.warn } }, "⚠️ " + (status.whoamiError || t("未登录 npm"))),
|
|
@@ -2062,7 +2196,7 @@ function useSession() {
|
|
|
2062
2196
|
h("div", { style: { fontSize: 12, color: T.warn, marginBottom: 6 } }, t("未登录 npm,发布前需先登录")),
|
|
2063
2197
|
h("div", { style: { display: "flex", gap: 8, flexWrap: "wrap" } },
|
|
2064
2198
|
h(Btn, { label: loginBusy ? t("执行中…") : t("打开终端执行 npm login"), onClick: openLogin, disabled: loginBusy || !String(dir || "").trim(), tone: "primary", noBg: true }),
|
|
2065
|
-
h(Btn, { label: t("复制登录命令"), onClick: () => { void copyText("npm login"); setMsg(t("已复制:") + "npm login"); }, noBg: true }),
|
|
2199
|
+
h(Btn, { label: t("复制登录命令"), onClick: () => { void copyText("npm login --registry=https://registry.npmjs.org"); setMsg(t("已复制:") + "npm login --registry=https://registry.npmjs.org"); }, noBg: true }),
|
|
2066
2200
|
),
|
|
2067
2201
|
) : null,
|
|
2068
2202
|
steps.map((s) =>
|
package/lib/index.js
CHANGED
|
@@ -21,8 +21,8 @@ import {
|
|
|
21
21
|
existsSync, mkdirSync, writeFileSync, readFileSync, chmodSync,
|
|
22
22
|
readdirSync, statSync, openSync, readSync, closeSync,
|
|
23
23
|
} from 'node:fs'
|
|
24
|
-
import { join, dirname } from 'node:path'
|
|
25
|
-
import { homedir } from 'node:os'
|
|
24
|
+
import { join, dirname, isAbsolute, resolve } from 'node:path'
|
|
25
|
+
import { homedir, tmpdir } from 'node:os'
|
|
26
26
|
|
|
27
27
|
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
28
28
|
import { fileURLToPath } from 'node:url'
|
|
@@ -76,6 +76,17 @@ Object.assign(HOST_EN, {
|
|
|
76
76
|
"创建分支失败(上游分支可能不存在,请检查规则 baseBranch)": "Failed to create branch (upstream base may not exist — check the rule's baseBranch)",
|
|
77
77
|
"已打开终端窗口,请完成 npm login 后回到面板点「重新检查」": "A terminal window has been opened — complete npm login, then click \"Re-check\" in the panel",
|
|
78
78
|
"未找到可用终端,请手动在目标目录运行:": "No usable terminal found — run this manually in the target directory: ",
|
|
79
|
+
"需要管理员密码(当前 sudo 需要密码,无法在面板内自动执行),请在终端手动执行:": "Admin password required (sudo prompts for a password here, so the panel cannot run it) — run this manually in a terminal: ",
|
|
80
|
+
"已触发安装,请按系统提示完成,然后点「重新检查」": "Installation started — follow the system prompt, then click \"Re-check\"",
|
|
81
|
+
"查询最新版本…": "Fetching latest version…",
|
|
82
|
+
"下载 gh 安装包…": "Downloading gh…",
|
|
83
|
+
"解压…": "Extracting…",
|
|
84
|
+
"安装到 ~/.local/bin…": "Installing to ~/.local/bin…",
|
|
85
|
+
"清理临时文件…": "Cleaning up…",
|
|
86
|
+
"执行安装命令:": "Running install command: ",
|
|
87
|
+
"用户级安装失败,改用系统包管理器安装…": "User-level install failed — falling back to the system package manager…",
|
|
88
|
+
"需要管理员密码,改用以下手动命令:": "Admin password required — run this manually: ",
|
|
89
|
+
"配置源是镜像,发布将使用官方源:": "Configured registry is a mirror — publishing will use the official registry: ",
|
|
79
90
|
"规则文件获取失败:": "Failed to fetch rules file: ",
|
|
80
91
|
"规则文件读取失败:": "Failed to read rules file: ",
|
|
81
92
|
"找不到规则文件:": "Rules file not found: ",
|
|
@@ -238,6 +249,12 @@ function resolveBin(name, envVar) {
|
|
|
238
249
|
// 1) PATH hit is the most portable answer.
|
|
239
250
|
const onPath = findOnPath(name)
|
|
240
251
|
if (onPath) return onPath
|
|
252
|
+
// 1.5) Linux/macOS 用户级安装目录:免 sudo 安装的 gh(本插件的「一键安装」落点)
|
|
253
|
+
// 也会装到这里,即使 ~/.local/bin 不在 shell PATH 中,模块加载时也能解析到。
|
|
254
|
+
if (!IS_WIN) {
|
|
255
|
+
const userBin = probePrefix(join(homedir(), '.local', 'bin'), name)
|
|
256
|
+
if (userBin) return userBin
|
|
257
|
+
}
|
|
241
258
|
// 2) Windows: git bundles ssh / ssh-keygen.
|
|
242
259
|
if (IS_WIN) {
|
|
243
260
|
for (const prefix of windowsGitPrefixes()) {
|
|
@@ -382,6 +399,231 @@ function run(cmd, args, opts = {}) {
|
|
|
382
399
|
}
|
|
383
400
|
}
|
|
384
401
|
|
|
402
|
+
/**
|
|
403
|
+
* 流式执行一条命令(spawn):stdout/stderr 分块实时回调 onChunk,
|
|
404
|
+
* 进程退出后 resolve { ok, code, stdout, stderr }。用于一键安装的进度展示。
|
|
405
|
+
*/
|
|
406
|
+
function runLive(cmd, args, opts = {}) {
|
|
407
|
+
const { onChunk, timeout = 300_000 } = opts
|
|
408
|
+
const env = { ...process.env, ...(opts.env ?? {}) }
|
|
409
|
+
return new Promise((resolve) => {
|
|
410
|
+
let stdout = ''
|
|
411
|
+
let stderr = ''
|
|
412
|
+
let settled = false
|
|
413
|
+
let timer = null
|
|
414
|
+
let child
|
|
415
|
+
const finish = (value) => {
|
|
416
|
+
if (settled) return
|
|
417
|
+
settled = true
|
|
418
|
+
if (timer !== null) clearTimeout(timer)
|
|
419
|
+
resolve(value)
|
|
420
|
+
}
|
|
421
|
+
try {
|
|
422
|
+
child = spawn(cmd, args, { env, stdio: ['ignore', 'pipe', 'pipe'] })
|
|
423
|
+
} catch (error) {
|
|
424
|
+
finish({ ok: false, code: 1, stdout: '', stderr: String(error?.message ?? error) })
|
|
425
|
+
return
|
|
426
|
+
}
|
|
427
|
+
timer = timeout > 0 ? setTimeout(() => { try { child.kill('SIGKILL') } catch {} }, timeout) : null
|
|
428
|
+
const push = (buf, stream) => {
|
|
429
|
+
const text = buf.toString('utf8')
|
|
430
|
+
if (stream === 'out') stdout += text
|
|
431
|
+
else stderr += text
|
|
432
|
+
if (onChunk) { try { onChunk(text, stream) } catch {} }
|
|
433
|
+
}
|
|
434
|
+
child.stdout?.on('data', (b) => push(b, 'out'))
|
|
435
|
+
child.stderr?.on('data', (b) => push(b, 'err'))
|
|
436
|
+
child.on('error', (error) => {
|
|
437
|
+
finish({ ok: false, code: 1, stdout, stderr: stderr || String(error?.message ?? error) })
|
|
438
|
+
})
|
|
439
|
+
child.on('close', (code) => {
|
|
440
|
+
finish({ ok: code === 0, code: code ?? 1, stdout, stderr })
|
|
441
|
+
})
|
|
442
|
+
})
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// ---------------------------------------------------------------------------
|
|
446
|
+
// 一键重启 dsh(机制同 dsh-update / dsh-market):detached helper 接管后按原命令
|
|
447
|
+
// 拉起替换进程,本进程随后退出。Windows 走 PowerShell 隐藏窗口;POSIX 走 detached spawn。
|
|
448
|
+
// ---------------------------------------------------------------------------
|
|
449
|
+
|
|
450
|
+
/** The real Node executable (Android-safe argv0 preference), for spawning. */
|
|
451
|
+
function nodeExecutable(argv0 = process.argv0, execPath = process.execPath) {
|
|
452
|
+
if (argv0 !== undefined && argv0 !== '' && isAbsolute(argv0) && existsSync(argv0)) return argv0
|
|
453
|
+
return execPath
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/** Whether a bare `dsh` name must go through a shell (Windows .cmd shim). */
|
|
457
|
+
const winCmdShim = process.platform === 'win32'
|
|
458
|
+
|
|
459
|
+
/** The exact boot invocation the detached restart helper replays. */
|
|
460
|
+
function restartLaunch() {
|
|
461
|
+
const entry = process.argv[1]
|
|
462
|
+
if (entry !== undefined && entry !== '') {
|
|
463
|
+
// Absolute paths are required: source launches pass a relative entry,
|
|
464
|
+
// which the child resolves against its OWN cwd and dies with
|
|
465
|
+
// MODULE_NOT_FOUND. cwd near the entry keeps execArgv imports (tsx/esm)
|
|
466
|
+
// resolvable on source launches. Launching node + the very same entry is
|
|
467
|
+
// the most faithful restart and does NOT depend on `dsh` being on PATH
|
|
468
|
+
// (macOS/Linux bins often live inside node_modules behind pnpm/npx).
|
|
469
|
+
const abs = resolve(entry)
|
|
470
|
+
return { file: nodeExecutable(), args: [...process.execArgv, abs, ...process.argv.slice(2)], cwd: dirname(abs), viaShell: false }
|
|
471
|
+
}
|
|
472
|
+
return { file: 'dsh', args: process.argv.slice(2), cwd: undefined, viaShell: winCmdShim }
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/** The process supervisor running this host, when one can be identified.
|
|
476
|
+
* systemd only, and only when this process is the unit's OWN main process
|
|
477
|
+
* (ppid === 1): INVOCATION_ID alone is inherited by every descendant. */
|
|
478
|
+
function detectedSupervisor(env = process.env, ppid = process.ppid) {
|
|
479
|
+
const set = (name) => (env[name] ?? '') !== ''
|
|
480
|
+
if ((set('INVOCATION_ID') || set('JOURNAL_STREAM')) && ppid === 1) return 'systemd'
|
|
481
|
+
return null
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/** Self-restart is enabled by default, disabled by an explicit env or by a
|
|
485
|
+
* detected supervisor (which owns restarts). */
|
|
486
|
+
function restartAllowed() {
|
|
487
|
+
const flag = process.env.DSH_SCM_RESTART
|
|
488
|
+
if (flag !== undefined && flag !== '') return flag !== '0' && flag !== 'false'
|
|
489
|
+
return detectedSupervisor() === null
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/** The port this process serves, read off the request that asked to restart. */
|
|
493
|
+
function servingPort(request) {
|
|
494
|
+
const host = request.headers.host
|
|
495
|
+
if (host === undefined) return null
|
|
496
|
+
const match = /:(\d{1,5})$/u.exec(host)
|
|
497
|
+
if (match === null) return null
|
|
498
|
+
const port = Number(match[1])
|
|
499
|
+
return Number.isInteger(port) && port > 0 && port < 65536 ? port : null
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/** Whether a process-control request came directly from this host's loopback
|
|
503
|
+
* peer (stricter than isLoopbackRequest: also rejects proxied requests). */
|
|
504
|
+
function trustedRestartRequest(request) {
|
|
505
|
+
const address = request.socket && request.socket.remoteAddress
|
|
506
|
+
if (address !== '127.0.0.1' && address !== '::1' && address !== '::ffff:127.0.0.1') return false
|
|
507
|
+
if (request.headers.forwarded !== undefined
|
|
508
|
+
|| request.headers['x-forwarded-for'] !== undefined
|
|
509
|
+
|| request.headers['x-real-ip'] !== undefined) return false
|
|
510
|
+
const origin = request.headers.origin
|
|
511
|
+
const host = request.headers.host
|
|
512
|
+
if (origin === undefined || host === undefined) return false
|
|
513
|
+
try {
|
|
514
|
+
const parsed = new URL(origin)
|
|
515
|
+
return (parsed.protocol === 'http:' || parsed.protocol === 'https:') && parsed.host === host
|
|
516
|
+
} catch {
|
|
517
|
+
return false
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/** Platform-correct spawn for the replacement host. POSIX keeps the plain
|
|
522
|
+
* detached spawn; Windows relaunches through a hidden PowerShell window
|
|
523
|
+
* (detached:true would break the PowerShell -> node handle chain, Node #51018). */
|
|
524
|
+
function respawnInvocation(launch, platform = process.platform) {
|
|
525
|
+
if (platform !== 'win32') {
|
|
526
|
+
return { file: launch.file, args: launch.args, viaShell: launch.viaShell, detached: true, windowsHide: false }
|
|
527
|
+
}
|
|
528
|
+
const quote = (part) => `'${String(part).replace(/'/g, "''")}'`
|
|
529
|
+
const file = launch.viaShell && !/\.(?:cmd|bat)$/iu.test(launch.file) ? `${launch.file}.cmd` : launch.file
|
|
530
|
+
return {
|
|
531
|
+
file: 'powershell.exe',
|
|
532
|
+
args: ['-NoProfile', '-WindowStyle', 'Hidden', '-Command',
|
|
533
|
+
[`& ${quote(file)}`, ...launch.args.map(quote)].join(' ')],
|
|
534
|
+
viaShell: false,
|
|
535
|
+
detached: false,
|
|
536
|
+
windowsHide: true,
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/** Source for the detached helper that outlives this process and brings the
|
|
541
|
+
* replacement up: wait for the port to go quiet (checked by CONNECTING, so
|
|
542
|
+
* the probe never holds the port), start the replacement, then CHECK that
|
|
543
|
+
* something came up and write a diagnosis when it did not. */
|
|
544
|
+
function restartHelperSource(spawned, launch, logs, port) {
|
|
545
|
+
return [
|
|
546
|
+
"const { spawn } = require('node:child_process')",
|
|
547
|
+
"const fs = require('node:fs')",
|
|
548
|
+
"const net = require('node:net')",
|
|
549
|
+
`const file = ${JSON.stringify(spawned.file)}`,
|
|
550
|
+
`const args = ${JSON.stringify(spawned.args)}`,
|
|
551
|
+
`const cwd = ${JSON.stringify(launch.cwd)}`,
|
|
552
|
+
`const viaShell = ${JSON.stringify(spawned.viaShell)}`,
|
|
553
|
+
`const detached = ${JSON.stringify(spawned.detached)}`,
|
|
554
|
+
`const windowsHide = ${JSON.stringify(spawned.windowsHide ?? false)}`,
|
|
555
|
+
`const logOut = ${JSON.stringify(logs.out)}`,
|
|
556
|
+
`const logErr = ${JSON.stringify(logs.err)}`,
|
|
557
|
+
`const port = ${JSON.stringify(port)}`,
|
|
558
|
+
'const sleep = (ms) => new Promise(r => setTimeout(r, ms))',
|
|
559
|
+
'const note = (line) => { try { fs.appendFileSync(logErr, `[source-code-mgmt] ${line}\n`) } catch {} }',
|
|
560
|
+
'const listening = () => new Promise((resolve) => {',
|
|
561
|
+
' const probe = net.connect({ host: "127.0.0.1", port })',
|
|
562
|
+
' const done = (value) => { probe.destroy(); resolve(value) }',
|
|
563
|
+
' probe.on("connect", () => done(true))',
|
|
564
|
+
' probe.on("error", () => done(false))',
|
|
565
|
+
' setTimeout(() => done(false), 500)',
|
|
566
|
+
'})',
|
|
567
|
+
'const main = async () => {',
|
|
568
|
+
' if (port) {',
|
|
569
|
+
' const until = Date.now() + 30000',
|
|
570
|
+
' while (Date.now() < until && await listening()) await sleep(250)',
|
|
571
|
+
' if (await listening()) note(`port ${port} was still in use after 30s; starting anyway`)',
|
|
572
|
+
' } else {',
|
|
573
|
+
' await sleep(1500)',
|
|
574
|
+
' }',
|
|
575
|
+
' let child',
|
|
576
|
+
' try {',
|
|
577
|
+
' const out = fs.openSync(logOut, "a")',
|
|
578
|
+
' const err = fs.openSync(logErr, "a")',
|
|
579
|
+
' child = spawn(file, args, { cwd, detached, windowsHide, stdio: ["ignore", out, err], env: process.env, shell: viaShell })',
|
|
580
|
+
' child.on("error", (error) => note(`could not start the replacement: ${error && error.message ? error.message : error}`))',
|
|
581
|
+
' child.unref()',
|
|
582
|
+
' } catch (error) {',
|
|
583
|
+
' note(`could not start the replacement: ${error && error.message ? error.message : error}`)',
|
|
584
|
+
' return',
|
|
585
|
+
' }',
|
|
586
|
+
" if (!port) { await sleep(3000); return }",
|
|
587
|
+
' const upBy = Date.now() + 20000',
|
|
588
|
+
' while (Date.now() < upBy && !(await listening())) await sleep(500)',
|
|
589
|
+
' if (!(await listening())) note(`the replacement did not bind port ${port} within 20s — see the output log beside this one`)',
|
|
590
|
+
'}',
|
|
591
|
+
'main()',
|
|
592
|
+
].join('\n')
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/** One-shot restart latch: 防止手动点击与安装后重启重复调度 helper(两个 helper
|
|
596
|
+
* 会同抢端口)。 */
|
|
597
|
+
let restartScheduled = false
|
|
598
|
+
|
|
599
|
+
/** Relaunch this exact dsh entry after a detached handoff, then stop this
|
|
600
|
+
* process. The helper outlives us (detached + unref), waits for our port to
|
|
601
|
+
* be released before starting the replacement, and logs under tmpdir.
|
|
602
|
+
* Returns null when a restart is already scheduled. */
|
|
603
|
+
function scheduleRestart(port = null) {
|
|
604
|
+
if (restartScheduled) return null
|
|
605
|
+
restartScheduled = true
|
|
606
|
+
const launch = restartLaunch()
|
|
607
|
+
const spawned = respawnInvocation(launch)
|
|
608
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)
|
|
609
|
+
const logOut = join(tmpdir(), `dsh-scm-restart-${stamp}.out.log`)
|
|
610
|
+
const logErr = join(tmpdir(), `dsh-scm-restart-${stamp}.err.log`)
|
|
611
|
+
try {
|
|
612
|
+
const helper = spawn(nodeExecutable(), ['-e', restartHelperSource(spawned, launch, { out: logOut, err: logErr }, port)], {
|
|
613
|
+
detached: true,
|
|
614
|
+
stdio: 'ignore',
|
|
615
|
+
env: process.env,
|
|
616
|
+
})
|
|
617
|
+
helper.unref()
|
|
618
|
+
setTimeout(() => process.kill(process.pid, 'SIGTERM'), 500)
|
|
619
|
+
return { pid: process.pid, helperPid: helper.pid, logOut, logErr }
|
|
620
|
+
} catch (error) {
|
|
621
|
+
// helper 拉起失败(如 EMFILE):解除 latch 并重新抛出,避免重启能力永久失效。
|
|
622
|
+
restartScheduled = false
|
|
623
|
+
throw error
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
385
627
|
/**
|
|
386
628
|
* Decode a path as printed by git. With core.quotepath ON (the default) git
|
|
387
629
|
* prints paths containing non-ASCII bytes / special characters as a
|
|
@@ -456,6 +698,12 @@ function checkEnv() {
|
|
|
456
698
|
path: SSH,
|
|
457
699
|
sshKeygen: SSH_KEYGEN,
|
|
458
700
|
},
|
|
701
|
+
// 平台自适应的可复制安装命令(与「一键安装」走同一逻辑;无可用方式时为 null)。
|
|
702
|
+
installHints: {
|
|
703
|
+
git: installCommand('git')?.label ?? null,
|
|
704
|
+
gh: installCommand('gh')?.label ?? null,
|
|
705
|
+
ssh: installCommand('ssh')?.label ?? null,
|
|
706
|
+
},
|
|
459
707
|
}
|
|
460
708
|
}
|
|
461
709
|
|
|
@@ -1772,6 +2020,103 @@ function commitDiffFlow(dir, hash) {
|
|
|
1772
2020
|
// 工具安装(① 环境检查缺工具时的一键安装)——best-effort:按可用包管理器自动选取命令。
|
|
1773
2021
|
// ---------------------------------------------------------------------------
|
|
1774
2022
|
|
|
2023
|
+
/** GitHub CLI Release 资产名(Linux/macOS):gh_<版本>_<os>_<arch>.tar.gz */
|
|
2024
|
+
function ghReleaseAsset(platform, arch) {
|
|
2025
|
+
const osName = platform === 'darwin' ? 'macOS' : 'linux'
|
|
2026
|
+
const archName = arch === 'x64' ? 'amd64' : arch === 'arm64' ? 'arm64' : arch
|
|
2027
|
+
return { osName, archName }
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
/**
|
|
2031
|
+
* 免 sudo 用户级安装 GitHub CLI 的单行脚本(Linux / macOS):
|
|
2032
|
+
* 解析官方最新版本号 → 下载对应平台/架构 tarball → 解压 → 把 bin/gh 装到
|
|
2033
|
+
* ~/.local/bin/gh(目录不存在则创建)→ 清理临时文件。全程不要求 root。
|
|
2034
|
+
* @returns {string} 可直接 `sh -c` 执行 / 复制给用户的脚本。
|
|
2035
|
+
*/
|
|
2036
|
+
function ghUserLevelScript() {
|
|
2037
|
+
const { osName, archName } = ghReleaseAsset(process.platform, process.arch)
|
|
2038
|
+
const localBin = join(homedir(), '.local', 'bin')
|
|
2039
|
+
// 注意:VER 是脚本内的 shell 变量,用 \${VER} 转义避免被 JS 模板字面量展开。
|
|
2040
|
+
return [
|
|
2041
|
+
'VER=$(curl -fsSL https://api.github.com/repos/cli/cli/releases/latest | sed -n \'s/.*"tag_name": *"v\\([^"]*\\)".*/\\1/p\')',
|
|
2042
|
+
'VER=${VER#v}',
|
|
2043
|
+
`mkdir -p "${localBin}"`,
|
|
2044
|
+
`curl -fsSL "https://github.com/cli/cli/releases/download/v\${VER}/gh_\${VER}_${osName}_${archName}.tar.gz" -o /tmp/gh-scm.tgz`,
|
|
2045
|
+
'tar -xzf /tmp/gh-scm.tgz -C /tmp',
|
|
2046
|
+
`install -m 755 "/tmp/gh_\${VER}_${osName}_${archName}/bin/gh" "${localBin}/gh"`,
|
|
2047
|
+
'rm -rf /tmp/gh-scm.tgz "/tmp/gh_${VER}_' + osName + '_' + archName + '"',
|
|
2048
|
+
].join(' && ')
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
/**
|
|
2052
|
+
* gh 用户级安装的分步脚本(每步独立执行,供「一键安装」进度逐条展示)。
|
|
2053
|
+
* 与 ghUserLevelScript()(复制用单行版)保持同源逻辑;版本号经 /tmp/gh-scm-ver 传递。
|
|
2054
|
+
* @returns {Array<{label: string, cmd: string}>}
|
|
2055
|
+
*/
|
|
2056
|
+
function ghUserLevelSteps() {
|
|
2057
|
+
const { osName, archName } = ghReleaseAsset(process.platform, process.arch)
|
|
2058
|
+
const localBin = join(homedir(), '.local', 'bin')
|
|
2059
|
+
const verFile = '/tmp/gh-scm-ver'
|
|
2060
|
+
const fetchVer = 'VER=$(curl -fsSL https://api.github.com/repos/cli/cli/releases/latest | sed -n \'s/.*"tag_name": *"v\\([^"]*\\)".*/\\1/p\'); [ -n "$VER" ] || exit 1; printf \'%s\' "${VER#v}" > ' + verFile
|
|
2061
|
+
const readVer = 'VER=$(cat ' + verFile + ')'
|
|
2062
|
+
return [
|
|
2063
|
+
{ label: tr('查询最新版本…'), cmd: fetchVer },
|
|
2064
|
+
{ label: tr('下载 gh 安装包…'), cmd: readVer + ' && curl -fsSL "https://github.com/cli/cli/releases/download/v${VER}/gh_${VER}_' + osName + '_' + archName + '.tar.gz" -o /tmp/gh-scm.tgz' },
|
|
2065
|
+
{ label: tr('解压…'), cmd: 'tar -xzf /tmp/gh-scm.tgz -C /tmp' },
|
|
2066
|
+
{ label: tr('安装到 ~/.local/bin…'), cmd: readVer + ' && mkdir -p "' + localBin + '" && install -m 755 "/tmp/gh_${VER}_' + osName + '_' + archName + '/bin/gh" "' + localBin + '/gh"' },
|
|
2067
|
+
{ label: tr('清理临时文件…'), cmd: 'rm -rf /tmp/gh-scm.tgz ' + verFile + ' /tmp/gh_*_' + osName + '_' + archName },
|
|
2068
|
+
]
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
/** 安装后即时复查工具是否真的可用(不依赖模块加载时缓存的历史解析结果)。 */
|
|
2072
|
+
function toolInstalled(t) {
|
|
2073
|
+
if (t === 'gh') {
|
|
2074
|
+
// 用户级安装落点优先复查;不在 PATH 时也能发现。
|
|
2075
|
+
if (!IS_WIN) {
|
|
2076
|
+
const userGh = probePrefix(join(homedir(), '.local', 'bin'), 'gh')
|
|
2077
|
+
if (userGh && isFile(userGh)) return true
|
|
2078
|
+
}
|
|
2079
|
+
return run('gh', ['--version']).ok
|
|
2080
|
+
}
|
|
2081
|
+
// git / ssh:直接按当前 PATH 实测(系统包管理器安装后即可发现,无需重启 host)。
|
|
2082
|
+
if (t === 'git') return run('git', ['--version']).ok
|
|
2083
|
+
if (t === 'ssh') return run('ssh', ['-V']).ok
|
|
2084
|
+
return false
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
/**
|
|
2088
|
+
* sudo 是否免密可用(`sudo -n true` 非交互探测)。
|
|
2089
|
+
* 需要密码的 sudo 在面板内无法自动输入,返回 false 以走用户级安装或手动指引。
|
|
2090
|
+
* @returns {boolean} 已是 root 或 sudo 免密可用时为 true。
|
|
2091
|
+
*/
|
|
2092
|
+
function canSudo() {
|
|
2093
|
+
if (IS_WIN) return false
|
|
2094
|
+
if (typeof process.getuid === 'function' && process.getuid() === 0) return true
|
|
2095
|
+
return run('sudo', ['-n', 'true'], { timeout: 5_000 }).ok
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
/** 某个绝对目录是否就在 PATH 里(尾斜杠归一化比较)。 */
|
|
2099
|
+
function isInPath(dir) {
|
|
2100
|
+
const target = dir.length > 0 && dir[dir.length - 1] !== '/' ? dir + '/' : dir
|
|
2101
|
+
return pathDirs().some((p) => {
|
|
2102
|
+
const norm = p.length > 0 && p[p.length - 1] !== '/' ? p + '/' : p
|
|
2103
|
+
return norm === target
|
|
2104
|
+
})
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2107
|
+
/**
|
|
2108
|
+
* 工具装好后,当前 host 进程的检测是否要等重启才生效:
|
|
2109
|
+
* - 模块加载时已解析到具体路径(PATH 命中或 DSH_SCM_* 显式覆盖)→ 立即可用,无需重启;
|
|
2110
|
+
* - 模块加载时解析失败(bare name 兜底)且用户级落点 ~/.local/bin 又不在 PATH
|
|
2111
|
+
* → env 检测要等重启后经 ~/.local/bin 探测才翻新,此时提示用户重启。
|
|
2112
|
+
* @param {'git'|'gh'|'ssh'} t
|
|
2113
|
+
*/
|
|
2114
|
+
function restartNeededFor(t) {
|
|
2115
|
+
if (t !== 'gh') return false
|
|
2116
|
+
if (GH !== 'gh') return false
|
|
2117
|
+
return !isInPath(join(homedir(), '.local', 'bin'))
|
|
2118
|
+
}
|
|
2119
|
+
|
|
1775
2120
|
/**
|
|
1776
2121
|
* Whether a command exists (probe `--version`; for shell scripts use `command -v`).
|
|
1777
2122
|
* @param {string} bin - bare command name.
|
|
@@ -1784,11 +2129,10 @@ function binExists(bin) {
|
|
|
1784
2129
|
}
|
|
1785
2130
|
|
|
1786
2131
|
/**
|
|
1787
|
-
*
|
|
1788
|
-
* usable package manager is available. Returns arrays usable with `run`.
|
|
2132
|
+
* 仅按系统包管理器选择安装命令(不含 gh 的用户级路径)。
|
|
1789
2133
|
* @param {'git'|'gh'|'ssh'} tool
|
|
1790
2134
|
*/
|
|
1791
|
-
function
|
|
2135
|
+
function installCommandSystem(tool) {
|
|
1792
2136
|
if (IS_WIN) {
|
|
1793
2137
|
// SSH 客户端:Windows 内置可选功能(需管理员),走 powershell。
|
|
1794
2138
|
if (tool === 'ssh') {
|
|
@@ -1809,52 +2153,122 @@ function installCommand(tool) {
|
|
|
1809
2153
|
if (process.platform === 'darwin') {
|
|
1810
2154
|
const pkg = tool === 'git' ? 'git' : tool === 'gh' ? 'gh' : 'openssh'
|
|
1811
2155
|
if (binExists('brew')) return { bin: 'brew', args: ['install', pkg], label: 'brew install ' + pkg }
|
|
2156
|
+
// macOS 无 brew:git/ssh 走 Xcode 命令行工具(系统自带安装,含 git 与 ssh)。
|
|
2157
|
+
if (tool === 'git' || tool === 'ssh') {
|
|
2158
|
+
return { bin: 'sh', args: ['-c', 'xcode-select --install'], label: 'xcode-select --install', started: true }
|
|
2159
|
+
}
|
|
1812
2160
|
return null
|
|
1813
2161
|
}
|
|
1814
2162
|
// Linux:按可用包管理器选择。安装需 root,用 sudo(非交互 -n,避免挂起等待密码;已是 root 则直接跑)。
|
|
2163
|
+
// needsSudo 标记供上层探测:sudo 需要密码时改走手动指引,避免静默失败。
|
|
1815
2164
|
const pkg = tool === 'git' ? 'git' : tool === 'gh' ? 'gh' : 'openssh-client'
|
|
1816
2165
|
const isRoot = typeof process.getuid === 'function' && process.getuid() === 0
|
|
1817
2166
|
const sudo = !isRoot && binExists('sudo') ? 'sudo -n ' : ''
|
|
2167
|
+
const tag = sudo === '' ? {} : { needsSudo: true }
|
|
1818
2168
|
if (binExists('apt-get')) {
|
|
1819
|
-
return { bin: 'sh', args: ['-c', sudo + 'apt-get install -y ' + pkg], label: 'sudo apt-get install -y ' + pkg }
|
|
2169
|
+
return { bin: 'sh', args: ['-c', sudo + 'apt-get install -y ' + pkg], label: 'sudo apt-get install -y ' + pkg, ...tag }
|
|
1820
2170
|
}
|
|
1821
2171
|
if (binExists('dnf')) {
|
|
1822
|
-
return { bin: 'sh', args: ['-c', sudo + 'dnf install -y ' + pkg], label: 'sudo dnf install -y ' + pkg }
|
|
2172
|
+
return { bin: 'sh', args: ['-c', sudo + 'dnf install -y ' + pkg], label: 'sudo dnf install -y ' + pkg, ...tag }
|
|
1823
2173
|
}
|
|
1824
2174
|
if (binExists('pacman')) {
|
|
1825
|
-
return { bin: 'sh', args: ['-c', sudo + 'pacman -S --noconfirm ' + pkg], label: 'sudo pacman -S ' + pkg }
|
|
2175
|
+
return { bin: 'sh', args: ['-c', sudo + 'pacman -S --noconfirm ' + pkg], label: 'sudo pacman -S ' + pkg, ...tag }
|
|
1826
2176
|
}
|
|
1827
2177
|
return null
|
|
1828
2178
|
}
|
|
1829
2179
|
|
|
1830
2180
|
/**
|
|
1831
|
-
*
|
|
1832
|
-
*
|
|
2181
|
+
* Build the install command to run for `tool` on this machine, or null when no
|
|
2182
|
+
* usable package manager is available. Returns arrays usable with `run`.
|
|
2183
|
+
* @param {'git'|'gh'|'ssh'} tool
|
|
2184
|
+
*/
|
|
2185
|
+
function installCommand(tool) {
|
|
2186
|
+
// Linux/macOS 的 GitHub CLI:优先免 sudo 用户级安装(官方二进制 → ~/.local/bin)。
|
|
2187
|
+
if (tool === 'gh' && !IS_WIN) {
|
|
2188
|
+
return { bin: 'sh', args: ['-c', ghUserLevelScript()], label: ghUserLevelScript(), userLevel: true }
|
|
2189
|
+
}
|
|
2190
|
+
return installCommandSystem(tool)
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
/**
|
|
2194
|
+
* Install a missing tool (git / gh / ssh). GitHub CLI on Linux/macOS installs
|
|
2195
|
+
* user-level (no sudo, official binary → ~/.local/bin); git/ssh keep the
|
|
2196
|
+
* system package manager. Best-effort: reports the command it ran and output.
|
|
1833
2197
|
* @param {string} tool - 'git' | 'gh' | 'ssh'.
|
|
2198
|
+
* @param {(evt: object) => void} [emit] - 进度事件回调({type:'step',label} / {type:'out',text}),
|
|
2199
|
+
* 由流式路由逐行转发给浏览器端弹窗。
|
|
1834
2200
|
*/
|
|
1835
|
-
function installToolFlow(tool) {
|
|
2201
|
+
async function installToolFlow(tool, emit) {
|
|
1836
2202
|
const t = String(tool || '').trim().toLowerCase()
|
|
1837
2203
|
if (t !== 'git' && t !== 'gh' && t !== 'ssh') return { ok: false, error: tr('未知工具:') + tool }
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
2204
|
+
const say = (obj) => { if (emit) { try { emit(obj) } catch {} } }
|
|
2205
|
+
// 已安装就直接返回,不重复安装(实测复查,不依赖模块加载时的缓存)。
|
|
2206
|
+
if (toolInstalled(t)) return { ok: true, alreadyInstalled: true, tool: t }
|
|
2207
|
+
|
|
2208
|
+
// Linux/macOS 的 gh:免 sudo 用户级安装优先;失败再退回系统包管理器。
|
|
2209
|
+
if (t === 'gh' && !IS_WIN) {
|
|
2210
|
+
const steps = ghUserLevelSteps()
|
|
2211
|
+
let lastOut = ''
|
|
2212
|
+
let failed = false
|
|
2213
|
+
for (const step of steps) {
|
|
2214
|
+
say({ type: 'step', label: step.label })
|
|
2215
|
+
const r = await runLive('sh', ['-c', step.cmd], { timeout: 300_000, onChunk: (text) => say({ type: 'out', text }) })
|
|
2216
|
+
lastOut = (r.stdout + '\n' + r.stderr).trim()
|
|
2217
|
+
if (!r.ok) { failed = true; break }
|
|
2218
|
+
}
|
|
2219
|
+
if (!failed && toolInstalled('gh')) {
|
|
2220
|
+
return {
|
|
2221
|
+
ok: true,
|
|
2222
|
+
tool: t,
|
|
2223
|
+
command: 'GitHub CLI',
|
|
2224
|
+
output: lastOut || tr('安装成功'),
|
|
2225
|
+
userLevel: true,
|
|
2226
|
+
path: join(homedir(), '.local', 'bin', 'gh'),
|
|
2227
|
+
restartNeeded: restartNeededFor('gh'),
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
// 用户级安装失败 → 退回系统包管理器(并说明原因)。
|
|
2231
|
+
const fallback = installCommandSystem(t)
|
|
2232
|
+
if (fallback) {
|
|
2233
|
+
// 回退命令需要 sudo 但 sudo 要密码时,同样改走手动指引。
|
|
2234
|
+
if (fallback.needsSudo && !canSudo()) {
|
|
2235
|
+
say({ type: 'step', label: tr('需要管理员密码,改用以下手动命令:') })
|
|
2236
|
+
return { ok: false, tool: t, error: tr('需要管理员密码(当前 sudo 需要密码,无法在面板内自动执行),请在终端手动执行:') + fallback.label, command: fallback.label, manual: true }
|
|
2237
|
+
}
|
|
2238
|
+
say({ type: 'step', label: tr('用户级安装失败,改用系统包管理器安装…') })
|
|
2239
|
+
const fr = await runLive(fallback.bin, fallback.args, { timeout: 300_000, onChunk: (text) => say({ type: 'out', text }) })
|
|
2240
|
+
const fout = (fr.stdout + '\n' + fr.stderr).trim()
|
|
2241
|
+
const fok = toolInstalled('gh')
|
|
2242
|
+
return { ok: fok, tool: t, command: fallback.label, output: fout || (fok ? tr('安装成功') : tr('安装失败')), restartNeeded: restartNeededFor('gh') }
|
|
2243
|
+
}
|
|
2244
|
+
return { ok: false, error: tr('未找到可用的包管理器(') + 'apt-get / dnf / pacman' + tr(')。请手动安装。'), tool: t }
|
|
2245
|
+
}
|
|
1843
2246
|
|
|
1844
2247
|
const cmd = installCommand(t)
|
|
1845
2248
|
if (!cmd) {
|
|
1846
|
-
return { ok: false, error: tr('未找到可用的包管理器(') + (IS_WIN ? 'winget / choco / scoop' : process.platform === 'darwin' ? 'brew' : 'apt-get / dnf / pacman') + tr(')。请手动安装。'), tool: t }
|
|
2249
|
+
return { ok: false, error: tr('未找到可用的包管理器(') + (IS_WIN ? 'winget / choco / scoop' : process.platform === 'darwin' ? 'brew / Xcode CLT' : 'apt-get / dnf / pacman') + tr(')。请手动安装。'), tool: t }
|
|
2250
|
+
}
|
|
2251
|
+
// 需要 sudo 但 sudo 要密码:不执行(-n 必然失败),直接给出手动命令与原因。
|
|
2252
|
+
if (cmd.needsSudo && !canSudo()) {
|
|
2253
|
+
say({ type: 'step', label: tr('需要管理员密码,改用以下手动命令:') })
|
|
2254
|
+
return { ok: false, tool: t, error: tr('需要管理员密码(当前 sudo 需要密码,无法在面板内自动执行),请在终端手动执行:') + cmd.label, command: cmd.label, manual: true }
|
|
1847
2255
|
}
|
|
1848
|
-
|
|
2256
|
+
say({ type: 'step', label: tr('执行安装命令:') + cmd.label })
|
|
2257
|
+
const r = await runLive(cmd.bin, cmd.args, { timeout: 300_000, onChunk: (text) => say({ type: 'out', text }) })
|
|
1849
2258
|
const output = (r.stdout + '\n' + r.stderr).trim()
|
|
2259
|
+
// 启动型安装(如 macOS xcode-select):安装是系统弹窗异步完成的,返回「已触发」指引。
|
|
2260
|
+
if (cmd.started) {
|
|
2261
|
+
return { ok: false, started: true, tool: t, command: cmd.label, error: tr('已触发安装,请按系统提示完成,然后点「重新检查」') }
|
|
2262
|
+
}
|
|
1850
2263
|
// 安装后复查是否已装上。
|
|
1851
|
-
const ok =
|
|
2264
|
+
const ok = toolInstalled(t)
|
|
1852
2265
|
return {
|
|
1853
2266
|
ok,
|
|
1854
2267
|
tool: t,
|
|
1855
2268
|
command: cmd.label,
|
|
1856
2269
|
output: output || (ok ? tr('安装成功') : tr('安装失败')),
|
|
1857
2270
|
needElevation: IS_WIN && t === 'ssh',
|
|
2271
|
+
restartNeeded: ok ? restartNeededFor(t) : false,
|
|
1858
2272
|
}
|
|
1859
2273
|
}
|
|
1860
2274
|
|
|
@@ -2074,13 +2488,18 @@ function cloneFlow(url, dest, name) {
|
|
|
2074
2488
|
|
|
2075
2489
|
// ---------- ⑤ 发布 npm 包(npm publish) ----------
|
|
2076
2490
|
|
|
2077
|
-
/** npm
|
|
2491
|
+
/** 发布/登录统一使用的官方 npm registry(镜像源只同步、不接受发布)。 */
|
|
2492
|
+
const NPM_OFFICIAL_REGISTRY = 'https://registry.npmjs.org'
|
|
2493
|
+
|
|
2494
|
+
/** npm 状态:是否安装、registry、whoami、目标目录是否存在 package.json。
|
|
2495
|
+
* whoami 与发布都固定走官方源 NPM_OFFICIAL_REGISTRY(用户全局可能是镜像源)。 */
|
|
2078
2496
|
function npmStatus(dir) {
|
|
2079
2497
|
const npm = NPM
|
|
2080
2498
|
const cwd = String(dir || '').trim() || cloneDefaultDir()
|
|
2081
2499
|
const installed = npm !== 'npm' && npm !== 'npm.cmd'
|
|
2082
2500
|
const registry = run(npm, ['config', 'get', 'registry'], { shell: IS_WIN })
|
|
2083
|
-
const whoami = run(npm, ['whoami'], { shell: IS_WIN })
|
|
2501
|
+
const whoami = run(npm, ['whoami', '--registry=' + NPM_OFFICIAL_REGISTRY], { shell: IS_WIN })
|
|
2502
|
+
const configReg = registry.ok ? registry.stdout.trim() : undefined
|
|
2084
2503
|
return {
|
|
2085
2504
|
ok: true,
|
|
2086
2505
|
installed,
|
|
@@ -2088,7 +2507,10 @@ function npmStatus(dir) {
|
|
|
2088
2507
|
dir: cwd,
|
|
2089
2508
|
dirExists: existsSync(cwd),
|
|
2090
2509
|
hasPackageJson: existsSync(join(cwd, 'package.json')),
|
|
2091
|
-
registry:
|
|
2510
|
+
registry: configReg,
|
|
2511
|
+
// 发布实际使用的官方源;配置源是镜像时置 warning 供面板提示。
|
|
2512
|
+
publishRegistry: NPM_OFFICIAL_REGISTRY,
|
|
2513
|
+
mirrorConfigured: configReg !== undefined && !configReg.startsWith('https://registry.npmjs.org'),
|
|
2092
2514
|
whoami: whoami.ok ? whoami.stdout.trim() : undefined,
|
|
2093
2515
|
whoamiError: whoami.ok ? undefined : ((whoami.stderr || whoami.stdout || '').trim() || tr('未登录 npm')),
|
|
2094
2516
|
}
|
|
@@ -2096,13 +2518,19 @@ function npmStatus(dir) {
|
|
|
2096
2518
|
|
|
2097
2519
|
/**
|
|
2098
2520
|
* 打开一个终端窗口,在目标目录运行 `npm login`(交互式登录无法非交互执行,
|
|
2099
|
-
*
|
|
2521
|
+
* 需要用户在弹出的终端里完成)。登录固定走官方源。
|
|
2522
|
+
*
|
|
2523
|
+
* 安全要点:
|
|
2524
|
+
* - 先探测终端二进制真实存在(findTerminal),绝不 spawn 不存在的程序——
|
|
2525
|
+
* spawn 对 ENOENT 是异步 error,无监听器会抛未捕获异常把 host 进程打崩;
|
|
2526
|
+
* - 每个 child 都挂 'error' 监听器兜底;
|
|
2527
|
+
* - KDE 的 konsole 用 --separate 开独立窗口,避免与宿主所在的 konsole 实例纠缠。
|
|
2100
2528
|
* @param {string} [dir] - 目标目录。
|
|
2101
2529
|
* @returns {{ok:boolean, opened:boolean, command:string, detail:string}}
|
|
2102
2530
|
*/
|
|
2103
2531
|
function npmLoginTerminal(dir) {
|
|
2104
2532
|
const cwd = String(dir || '').trim() || cloneDefaultDir()
|
|
2105
|
-
const command = 'npm login'
|
|
2533
|
+
const command = 'npm login --registry=' + NPM_OFFICIAL_REGISTRY
|
|
2106
2534
|
const cdCmd = IS_WIN
|
|
2107
2535
|
? `cd /d "${cwd}" && ${command}`
|
|
2108
2536
|
: `cd "${cwd}" && ${command}`
|
|
@@ -2110,28 +2538,40 @@ function npmLoginTerminal(dir) {
|
|
|
2110
2538
|
if (IS_WIN) {
|
|
2111
2539
|
// start cmd /k 打开独立窗口(第一引号段是窗口标题);detached 不阻塞宿主。
|
|
2112
2540
|
const child = spawn('cmd', ['/c', 'start', '"npm-login"', 'cmd', '/k', cdCmd], { detached: true, stdio: 'ignore', windowsHide: false })
|
|
2541
|
+
child.on('error', () => { /* 防御:错误已由返回文案覆盖 */ })
|
|
2113
2542
|
child.unref()
|
|
2114
2543
|
return { ok: true, opened: true, command, detail: tr('已打开终端窗口,请完成 npm login 后回到面板点「重新检查」') }
|
|
2115
2544
|
}
|
|
2116
|
-
const
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
['konsole', ['-e', 'sh', '-c', cdCmd]],
|
|
2120
|
-
['xterm', ['-e', 'sh', '-c', cdCmd]],
|
|
2121
|
-
]
|
|
2122
|
-
for (const [bin, args] of terms) {
|
|
2123
|
-
try {
|
|
2124
|
-
const child = spawn(bin, args, { detached: true, stdio: 'ignore' })
|
|
2125
|
-
child.unref()
|
|
2126
|
-
return { ok: true, opened: true, command, detail: tr('已打开终端窗口,请完成 npm login 后回到面板点「重新检查」') }
|
|
2127
|
-
} catch { /* try next terminal */ }
|
|
2545
|
+
const bin = findTerminal()
|
|
2546
|
+
if (bin === null) {
|
|
2547
|
+
return { ok: false, opened: false, command, detail: tr('未找到可用终端,请手动在目标目录运行:') + command }
|
|
2128
2548
|
}
|
|
2129
|
-
|
|
2549
|
+
const args = (bin === 'konsole')
|
|
2550
|
+
? ['--separate', '-e', 'sh', '-c', cdCmd] // KDE:独立窗口,不并入宿主实例
|
|
2551
|
+
: (bin === 'gnome-terminal')
|
|
2552
|
+
? ['--', 'sh', '-c', cdCmd]
|
|
2553
|
+
: ['-e', 'sh', '-c', cdCmd] // xterm 系 / x-terminal-emulator 等
|
|
2554
|
+
const child = spawn(bin, args, { detached: true, stdio: 'ignore' })
|
|
2555
|
+
child.on('error', () => { /* 防御:二进制被删等竞态也不会崩宿主 */ })
|
|
2556
|
+
child.unref()
|
|
2557
|
+
return { ok: true, opened: true, command, detail: tr('已打开终端窗口,请完成 npm login 后回到面板点「重新检查」') }
|
|
2130
2558
|
} catch (e) {
|
|
2131
2559
|
return { ok: false, opened: false, command, detail: String((e && e.message) || e) }
|
|
2132
2560
|
}
|
|
2133
2561
|
}
|
|
2134
2562
|
|
|
2563
|
+
/** 在 PATH 中找可用的终端程序(找不到返回 null)。 */
|
|
2564
|
+
function findTerminal() {
|
|
2565
|
+
const candidates = [
|
|
2566
|
+
'x-terminal-emulator', 'konsole', 'gnome-terminal', 'xfce4-terminal',
|
|
2567
|
+
'mate-terminal', 'lxterminal', 'xterm', 'kitty', 'alacritty', 'wezterm',
|
|
2568
|
+
]
|
|
2569
|
+
for (const bin of candidates) {
|
|
2570
|
+
if (findOnPath(bin)) return bin
|
|
2571
|
+
}
|
|
2572
|
+
return null
|
|
2573
|
+
}
|
|
2574
|
+
|
|
2135
2575
|
/** 脱敏 npm config list 输出:隐藏 token / auth 等敏感值。 */
|
|
2136
2576
|
function redactNpmList(output) {
|
|
2137
2577
|
return String(output || '').split(/\r?\n/).map((line) => {
|
|
@@ -2154,14 +2594,19 @@ function npmRunStep(dir, step) {
|
|
|
2154
2594
|
switch (step) {
|
|
2155
2595
|
case 'registry': {
|
|
2156
2596
|
const r = runNpm(['config', 'get', 'registry'])
|
|
2157
|
-
|
|
2597
|
+
const cfg = r.ok ? r.stdout.trim() : ((r.stderr || '') + (r.stdout || '')).trim()
|
|
2598
|
+
// 配置源与发布源分离展示:发布固定走官方源,避免镜像源导致发布失败。
|
|
2599
|
+
const extra = cfg && !cfg.startsWith('https://registry.npmjs.org')
|
|
2600
|
+
? '\n' + tr('配置源是镜像,发布将使用官方源:') + NPM_OFFICIAL_REGISTRY
|
|
2601
|
+
: ''
|
|
2602
|
+
return { ok: true, step, output: cfg + extra }
|
|
2158
2603
|
}
|
|
2159
2604
|
case 'list': {
|
|
2160
2605
|
const r = runNpm(['config', 'list'])
|
|
2161
2606
|
return { ok: true, step, output: redactNpmList((r.stdout || '') + (r.stderr || '')) }
|
|
2162
2607
|
}
|
|
2163
2608
|
case 'whoami': {
|
|
2164
|
-
const r = runNpm(['whoami'])
|
|
2609
|
+
const r = runNpm(['whoami', '--registry=' + NPM_OFFICIAL_REGISTRY])
|
|
2165
2610
|
return { ok: true, step, output: r.ok ? r.stdout.trim() : ((r.stderr || r.stdout || '').trim() || tr('未登录 npm')), loggedIn: r.ok }
|
|
2166
2611
|
}
|
|
2167
2612
|
case 'pack': {
|
|
@@ -2169,7 +2614,8 @@ function npmRunStep(dir, step) {
|
|
|
2169
2614
|
return { ok: true, step, output: (r.stdout || '') + (r.stderr || '') }
|
|
2170
2615
|
}
|
|
2171
2616
|
case 'publish': {
|
|
2172
|
-
|
|
2617
|
+
// 发布固定走官方源(用户全局可能是 npmmirror 等镜像,镜像不接受发布)。
|
|
2618
|
+
const r = runNpm(['publish', '--registry=' + NPM_OFFICIAL_REGISTRY], 180_000)
|
|
2173
2619
|
const blob = ((r.stdout || '') + (r.stderr || '')).trim()
|
|
2174
2620
|
return { ok: r.ok, step, output: blob || tr('npm publish 已执行'), error: r.ok ? undefined : (blob || tr('npm publish 失败')) }
|
|
2175
2621
|
}
|
|
@@ -2499,15 +2945,57 @@ export function apply(ctx) {
|
|
|
2499
2945
|
json(res, 200, payload)
|
|
2500
2946
|
})
|
|
2501
2947
|
|
|
2948
|
+
// 流式变体:逐行 NDJSON 事件({type:'step'|'out'|'result'}),供一键安装弹窗展示实时进度。
|
|
2949
|
+
const handleStream = async (req, res, fn) => requestLangStore.run(langOf(req), async () => {
|
|
2950
|
+
if (!isLoopbackRequest(req)) return forbidden(res)
|
|
2951
|
+
res.writeHead(200, { 'Content-Type': 'application/x-ndjson; charset=utf-8', 'Cache-Control': 'no-cache' })
|
|
2952
|
+
const emit = (obj) => { try { res.write(JSON.stringify(obj) + '\n') } catch { /* client gone */ } }
|
|
2953
|
+
try {
|
|
2954
|
+
const result = await fn(emit)
|
|
2955
|
+
emit({ type: 'result', result })
|
|
2956
|
+
} catch (error) {
|
|
2957
|
+
emit({ type: 'result', result: { ok: false, error: String((error && error.message) || error) } })
|
|
2958
|
+
}
|
|
2959
|
+
try { res.end() } catch { /* ignore */ }
|
|
2960
|
+
})
|
|
2961
|
+
|
|
2502
2962
|
const routes = {
|
|
2503
2963
|
'/env': (req, res) => handle(req, res, async () => ({ ok: true, ...checkEnv() })),
|
|
2504
2964
|
'/install-tool': async (req, res) => {
|
|
2505
2965
|
if (req.method !== 'POST') return methodNotAllowed(res, req.method)
|
|
2506
|
-
await
|
|
2966
|
+
await handleStream(req, res, async (emit) => {
|
|
2507
2967
|
const body = JSON.parse((await readBody(req)) || '{}')
|
|
2508
|
-
return installToolFlow(body.tool)
|
|
2968
|
+
return installToolFlow(body.tool, emit)
|
|
2509
2969
|
})
|
|
2510
2970
|
},
|
|
2971
|
+
'/restart': async (req, res) => {
|
|
2972
|
+
if (req.method !== 'POST') {
|
|
2973
|
+
res.writeHead(405, { 'content-type': 'text/plain; charset=utf-8' })
|
|
2974
|
+
res.end('method not allowed')
|
|
2975
|
+
return
|
|
2976
|
+
}
|
|
2977
|
+
if (!trustedRestartRequest(req)) {
|
|
2978
|
+
res.writeHead(403, { 'content-type': 'application/json; charset=utf-8' })
|
|
2979
|
+
res.end(JSON.stringify({ ok: false, code: 'forbidden' }))
|
|
2980
|
+
return
|
|
2981
|
+
}
|
|
2982
|
+
if (!restartAllowed()) {
|
|
2983
|
+
res.writeHead(403, { 'content-type': 'application/json; charset=utf-8' })
|
|
2984
|
+
res.end(JSON.stringify({ ok: false, code: 'restart-disabled', message: '重启已被禁用(supervisor 托管或 DSH_SCM_RESTART=0)' }))
|
|
2985
|
+
return
|
|
2986
|
+
}
|
|
2987
|
+
// 先应答,再让 helper 接手:500ms 后本进程退出,helper 等端口释放后按原命令拉起新进程。
|
|
2988
|
+
let result
|
|
2989
|
+
try {
|
|
2990
|
+
result = scheduleRestart(servingPort(req))
|
|
2991
|
+
} catch (error) {
|
|
2992
|
+
res.writeHead(500, { 'content-type': 'application/json; charset=utf-8' })
|
|
2993
|
+
res.end(JSON.stringify({ ok: false, error: `无法启动重启进程:${error instanceof Error ? error.message : String(error)}` }))
|
|
2994
|
+
return
|
|
2995
|
+
}
|
|
2996
|
+
res.writeHead(200, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-cache' })
|
|
2997
|
+
res.end(JSON.stringify({ ok: true, ...(result ?? { already: true }) }))
|
|
2998
|
+
},
|
|
2511
2999
|
'/ssh': (req, res) => handle(req, res, async () => ({ ok: true, ...checkSsh() })),
|
|
2512
3000
|
'/gen-key': async (req, res) => {
|
|
2513
3001
|
if (req.method !== 'POST') return methodNotAllowed(res, req.method)
|
|
@@ -2820,3 +3308,6 @@ export function apply(ctx) {
|
|
|
2820
3308
|
}
|
|
2821
3309
|
|
|
2822
3310
|
export default { name, inject, apply }
|
|
3311
|
+
|
|
3312
|
+
// 供插件作者 / 测试直接调用(不影响插件加载)。
|
|
3313
|
+
export { checkEnv, installCommand, installCommandSystem, ghUserLevelScript, ghUserLevelSteps, ghReleaseAsset, toolInstalled, canSudo, restartNeededFor, installToolFlow, restartAllowed, servingPort, trustedRestartRequest, restartLaunch, scheduleRestart, npmStatus, npmLoginTerminal, npmRunStep, findTerminal }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "source-code-mgmt",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.15.1",
|
|
4
4
|
"description": "DSH plugin: source code management. Registers「代码管理」as a dsh-better-sidebar sidebar Tab when installed, otherwise a button in the DSH session header's right-aligned utilities (beside \"Session log\") opening a right-side integrated panel that pushes the main content — env checks (git/gh), SSH ed25519 key setup, GitHub/Gitee dual-platform (SSH config 443 + Gitee OpenAPI), connectivity test, push / new repo, auto-ignore files over 100MB with per-file skipped reasons, a clone section (remote repos from your account with local-exists detection, plus clone any repo by URL into any location), and a five-step npm publish wizard. Cross-platform (Windows / Linux / macOS).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -38,4 +38,4 @@
|
|
|
38
38
|
"cordis.patch.yml"
|
|
39
39
|
],
|
|
40
40
|
"license": "MIT"
|
|
41
|
-
}
|
|
41
|
+
}
|