@shendeguize/remote-dsh-center 0.4.0

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.
Files changed (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.en.md +197 -0
  3. package/README.md +174 -0
  4. package/package.json +48 -0
  5. package/scripts/install.mjs +208 -0
  6. package/src/api.js +725 -0
  7. package/src/cli.js +1445 -0
  8. package/src/config-sync.js +157 -0
  9. package/src/daemon.js +362 -0
  10. package/src/defaults.js +89 -0
  11. package/src/dsh-workspace.js +467 -0
  12. package/src/launcher.js +627 -0
  13. package/src/lib/bundle.js +82 -0
  14. package/src/lib/bus.js +109 -0
  15. package/src/lib/capture.js +53 -0
  16. package/src/lib/clock.js +18 -0
  17. package/src/lib/entry.js +27 -0
  18. package/src/lib/errors.js +88 -0
  19. package/src/lib/logfile.js +65 -0
  20. package/src/lib/machine.js +63 -0
  21. package/src/lib/origin-guard.js +64 -0
  22. package/src/lib/pool.js +88 -0
  23. package/src/lib/proto.js +457 -0
  24. package/src/lib/semver.js +103 -0
  25. package/src/lib/shq.js +112 -0
  26. package/src/lib/ssh.js +647 -0
  27. package/src/lib/validate.js +363 -0
  28. package/src/monitor.js +145 -0
  29. package/src/patchsync.js +310 -0
  30. package/src/ports.js +93 -0
  31. package/src/prober.js +185 -0
  32. package/src/server.js +449 -0
  33. package/src/settings-file.js +550 -0
  34. package/src/ssh-config.js +152 -0
  35. package/src/store.js +772 -0
  36. package/src/tunnel.js +589 -0
  37. package/src/updater.js +450 -0
  38. package/src/web/actions.js +409 -0
  39. package/src/web/api.js +262 -0
  40. package/src/web/app.js +347 -0
  41. package/src/web/components/config-sync-dialog.js +469 -0
  42. package/src/web/components/confirm-dialog.js +61 -0
  43. package/src/web/components/defaults-card.js +216 -0
  44. package/src/web/components/event-panel.js +98 -0
  45. package/src/web/components/host-drawer.js +1039 -0
  46. package/src/web/components/host-table.js +317 -0
  47. package/src/web/components/hub.js +143 -0
  48. package/src/web/components/iframe-pane.js +377 -0
  49. package/src/web/components/manager-card.js +65 -0
  50. package/src/web/components/setup-wizard.js +726 -0
  51. package/src/web/components/tabbar.js +577 -0
  52. package/src/web/components/toast-region.js +107 -0
  53. package/src/web/favicon.svg +7 -0
  54. package/src/web/form.js +220 -0
  55. package/src/web/host-presentation.js +73 -0
  56. package/src/web/host-rules.js +76 -0
  57. package/src/web/index.html +17 -0
  58. package/src/web/router.js +118 -0
  59. package/src/web/setup-schema.js +203 -0
  60. package/src/web/sse.js +118 -0
  61. package/src/web/store.js +405 -0
  62. package/src/web/style.css +813 -0
  63. package/src/web/utils.js +210 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Yang Jingyu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.en.md ADDED
@@ -0,0 +1,197 @@
1
+ # DSH Center
2
+
3
+ [中文](README.md) · **English**
4
+
5
+ [![CI](https://github.com/shendeguize/Remote_DSH_Center/actions/workflows/ci.yml/badge.svg)](https://github.com/shendeguize/Remote_DSH_Center/actions/workflows/ci.yml)
6
+ [![Pages](https://github.com/shendeguize/Remote_DSH_Center/actions/workflows/pages.yml/badge.svg)](https://shendeguize.github.io/Remote_DSH_Center/)
7
+ [![Node](https://img.shields.io/badge/node-%E2%89%A522-3c873a)](https://nodejs.org/)
8
+ [![deps](https://img.shields.io/badge/npm%20deps-0-blue)](package.json)
9
+ [![License](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
10
+
11
+ Open local and remote `dsh web` instances from one Hub on your machine. Local instances connect
12
+ directly; remote instances are mapped onto local loopback through `ssh -L`. Nothing is installed
13
+ or kept resident on remote hosts: probe, start, and stop are all one-shot SSH commands.
14
+
15
+ **[▶ Live demo](https://shendeguize.github.io/Remote_DSH_Center/demo/)** — the real frontend
16
+ against an in-browser mock manager; try start, disconnect, and recovery ·
17
+ [Project page](https://shendeguize.github.io/Remote_DSH_Center/)
18
+
19
+ ![DSH Center Hub: one entry point for local and remote dsh web](site/assets/shots/dashboard.png)
20
+
21
+ *Hub — every available host, current state, and entry point on one page.*
22
+
23
+ ## Five-minute quick start
24
+
25
+ Minimum prerequisites: run the manager on macOS or Linux; source / git installs need Node ≥ 22;
26
+ managed targets need a configured `dsh` web profile, and remotes also need key-based SSH with TCP
27
+ forwarding. See [Requirements](#requirements) for details.
28
+
29
+ ```bash
30
+ curl -fsSL https://raw.githubusercontent.com/shendeguize/Remote_DSH_Center/main/install.sh | bash
31
+ export PATH="$HOME/.local/bin:$PATH" # default prefix; add a custom prefix as the installer says
32
+ dshc init # four steps: ports → remote convention → choose local/SSH hosts → confirm
33
+ dshc up # start the local manager in the background
34
+ dshc open # open the Hub in your browser
35
+ ```
36
+
37
+ By default, installation links `dshc` under `~/.local/bin`; the `export` makes it available in
38
+ the current shell immediately. If you passed `--prefix`, use the directory printed by the
39
+ installer instead. The installer chooses the git or macOS standalone channel automatically; the wizard probes a
40
+ local candidate plus remotes from `~/.ssh/config`. See the [handbook](HANDBOOK.en.md) for every
41
+ install option and first-start detail.
42
+
43
+ ## The problem
44
+
45
+ - **One local + remote entry point.** The browser uses the actual local web port or an
46
+ `ssh -L` mapped port for a remote.
47
+ - **Nothing resident or installed remotely.** Control actions use one-shot SSH. Center-managed
48
+ logs, patches, and temporary files stay under `~/.dsh_center_remote/`; the one explicit
49
+ exception is a user saving dsh configuration to `${DSH_HOME:-$HOME/.dsh}/settings.yaml`.
50
+ - **Hub plus persistent tabs.** Start and enter a `ready` host in one click; iframe sessions
51
+ survive view switches without reloads.
52
+ - **Self-healing tunnels.** A remote disconnect becomes `degraded` and retries with backoff;
53
+ only a genuinely dead process becomes `crashed`.
54
+ - **Never kills the wrong process.** Stop compares the `ps` command-line fingerprint verbatim;
55
+ manual instances are read-only, and a mismatch refuses the kill.
56
+ - **Zero npm dependencies.** Runtime and tests use Node ≥ 22 built-ins; the frontend is native
57
+ ESM with no build step.
58
+
59
+ ## Requirements
60
+
61
+ - Source / git installs need **Node ≥ 22**; macOS standalone bundles carry official Node.
62
+ - Every managed local or remote target needs DeepSeek Harness (`dsh`) and a configured web
63
+ profile. Center probes for them; it does not install `dsh`.
64
+ - Put remote hosts in `~/.ssh/config`, enable key-based login, and allow TCP forwarding.
65
+ Managing the local machine is optional.
66
+
67
+ ## Support matrix
68
+
69
+ | Platform | Install and release commitment | Verification |
70
+ |---|---|---|
71
+ | macOS arm64 (Apple Silicon) | First-class source / git install and standalone Release bundle | Matching bundle built and verified on real hardware |
72
+ | macOS x64 (Intel) | First-class source / git install and standalone Release bundle | Matching bundle built and verified on real hardware |
73
+ | Linux | Best-effort source / git installation and foreground operation; no bundle or `dshc service` | Project gates run in Ubuntu CI |
74
+
75
+ **Node 22** is the tested minimum for source / git installs. Any increase is announced in the
76
+ [CHANGELOG](CHANGELOG.md) and corresponding release notes.
77
+
78
+ ## Install
79
+
80
+ The one-command installer is at the top of this page. See the
81
+ [installation handbook](HANDBOOK.en.md#installation) for forced channels, pre-releases,
82
+ install prefixes, launchd autostart, manual installation, and bundle provenance verification
83
+ with `SHA256SUMS` plus `gh attestation verify`.
84
+
85
+ ## A look around
86
+
87
+ Center UI screenshots come from one headless-browser capture flow. The iframe image uses an
88
+ independent mock dsh web only to show the shape of the integration; it is not a screenshot of
89
+ the target product's real page.
90
+
91
+ | | |
92
+ |---|---|
93
+ | ![First-run wizard probing local and remote hosts](site/assets/shots/setup.png) | ![Host details with configuration and management actions](site/assets/shots/drawer.png) |
94
+ | *First-run wizard — probe candidates and choose managed hosts.* | *Host details — configuration, logs, and actions in one place.* |
95
+ | ![A mock dsh web in a persistent tab](site/assets/shots/iframe.png) | ![Recovery overlay after a remote tunnel disconnects](site/assets/shots/degraded.png) |
96
+ | *Workspace tab — an independent mock shows the integration shape; switches do not reload.* | *Disconnect recovery — content stays while the tunnel reconnects.* |
97
+
98
+ ## Architecture and data flow
99
+
100
+ ```mermaid
101
+ flowchart LR
102
+ B[Browser<br/>Hub + iframes] -->|REST + SSE| M[Local manager<br/>127.0.0.1]
103
+ C[dshc CLI] -->|REST| M
104
+ M -->|one-shot local shell| L[Local dsh web]
105
+ B -.->|actual loopback port| L
106
+ M -->|one-shot SSH control| R[Remote dsh web]
107
+ M --> T[ssh -L tunnel]
108
+ B -.->|local mapped port| T
109
+ T ==>|encrypted forwarding| R
110
+ ```
111
+
112
+ - The manager is the single source of truth for runtime state and ports; the frontend only uses
113
+ the `mappedUrl` returned by the backend.
114
+ - The control plane is REST/SSE followed by local shell or one-shot SSH. Pages, assets, and
115
+ WebSockets connect directly from the browser to the actual local port or remote mapped port;
116
+ the manager does not proxy the data plane.
117
+ - The UI never advances state optimistically; it waits for SSE events. See
118
+ [architecture details](HANDBOOK.en.md#architecture-details) for the state machine and recovery.
119
+
120
+ ## Everyday entry points
121
+
122
+ - `#/hub` is the default entry; a `ready` host starts and opens in one step, and iframe view
123
+ switches do not reload it.
124
+ - `#/manage` provides probe-all, configuration reload, global defaults, events, and host details.
125
+
126
+ ## States and self-healing
127
+
128
+ A remote disconnect becomes `degraded` and retries with 1/2/4/8/16/30-second backoff. A
129
+ 30-second sweep requires actual HTTP response bytes before deep SSH verification. A manager
130
+ restart adopts fingerprint-matched survivors instead of relaunching them. See the
131
+ [handbook](HANDBOOK.en.md#states-and-self-healing) for the full state machine.
132
+
133
+ ## Configuration and data
134
+
135
+ The only runtime configuration is `~/.dsh_center/config.json` (`DSHC_HOME` relocates it). See
136
+ [the handbook](HANDBOOK.en.md#configuration-and-data) for launch directories, dsh Workspace
137
+ registration, concurrency-safe editing of `${DSH_HOME:-$HOME/.dsh}/settings.yaml`, and artifact
138
+ boundaries.
139
+
140
+ ## Commands
141
+
142
+ ```text
143
+ Lifecycle: dshc init / up / down / restart / status / logs / service install|uninstall|status
144
+ Itself: dshc version / update
145
+ Hosts: dshc ls / probe / start / stop / reconnect / log / open / config
146
+ Exit codes: 0 success | 1 operation failed | 2 timeout/communication failure | 3 usage error | 130 Ctrl-C interrupted the wait (operation continues)
147
+ ```
148
+
149
+ ## Security boundary
150
+
151
+ > The manager listens on `127.0.0.1` and has **no authentication**. Never expose it on
152
+ > `0.0.0.0` or forward it to the public internet.
153
+
154
+ Stop verifies the process fingerprint verbatim. Injected environment variables and arguments
155
+ appear in `ps`, so never put secrets there. See the
156
+ [handbook](HANDBOOK.en.md#security-boundary) for cross-site defenses, configuration credential
157
+ handling, and artifact path restrictions.
158
+
159
+ ## FAQ
160
+
161
+ **Does it work on Linux?** Source / git installation and foreground operation are best-effort,
162
+ with project gates running in Ubuntu CI. There is no Linux bundle, and `dshc service` is
163
+ unavailable because it requires macOS launchd.
164
+
165
+ **Does closing the browser stop an instance?** No. `dshc down` also stops only the manager and
166
+ transport resources; use `dshc stop <host>` to stop a managed instance. See the
167
+ [handbook FAQ](HANDBOOK.en.md#faq) for more.
168
+
169
+ ## Full uninstall
170
+
171
+ Follow the [handbook procedure](HANDBOOK.en.md#full-uninstallation) to stop instances, remove
172
+ the service and CLI, and then delete manager data. Do not replace `dshc stop` with the
173
+ unfingerprinted `pkill -f "dsh web"`; it may kill other users' matching processes.
174
+
175
+ ## Documentation
176
+
177
+ - [English handbook](HANDBOOK.en.md) · [中文使用手册](HANDBOOK.md)
178
+ - [Changelog](CHANGELOG.md)
179
+ - [Contributing and PR rules](CONTRIBUTING.md)
180
+ - [Hard constraints before code changes](AGENTS.md)
181
+
182
+ ## Development
183
+
184
+ This repository has zero dependencies, so there is no `npm install` step:
185
+
186
+ ```bash
187
+ npm run check # complete quality gate
188
+ npm run site:check # site, demo, bilingual README links and commands
189
+ ```
190
+
191
+ See [development and real-machine acceptance](HANDBOOK.en.md#development-and-real-machine-acceptance)
192
+ for test layers, site development, the code map, and acceptance commands. Branching, commits,
193
+ PRs, releases, and review follow [CONTRIBUTING.md](CONTRIBUTING.md).
194
+
195
+ ## License
196
+
197
+ [MIT](LICENSE)
package/README.md ADDED
@@ -0,0 +1,174 @@
1
+ # DSH Center
2
+
3
+ **中文** · [English](README.en.md)
4
+
5
+ [![CI](https://github.com/shendeguize/Remote_DSH_Center/actions/workflows/ci.yml/badge.svg)](https://github.com/shendeguize/Remote_DSH_Center/actions/workflows/ci.yml)
6
+ [![Pages](https://github.com/shendeguize/Remote_DSH_Center/actions/workflows/pages.yml/badge.svg)](https://shendeguize.github.io/Remote_DSH_Center/)
7
+ [![Node](https://img.shields.io/badge/node-%E2%89%A522-3c873a)](https://nodejs.org/)
8
+ [![deps](https://img.shields.io/badge/npm%20deps-0-blue)](package.json)
9
+ [![License](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
10
+
11
+ 在本机用一个 Hub 打开本机与多台远端主机上的 `dsh web`:本机实例直接连接,远端实例经
12
+ `ssh -L` 安全映射到本机环回地址。远端无需安装 agent、无需常驻守护进程,探测、拉起与关停
13
+ 都由一次性 SSH 命令完成。
14
+
15
+ **[▶ Live demo](https://shendeguize.github.io/Remote_DSH_Center/demo/)** — 真实前端 +
16
+ 浏览器内模拟 manager,可直接体验拉起、断联与恢复 ·
17
+ [项目主页](https://shendeguize.github.io/Remote_DSH_Center/)
18
+
19
+ ![DSH Center Hub:统一进入本机与远端 dsh web](site/assets/shots/dashboard.png)
20
+
21
+ *Hub — 所有可用主机、运行状态与入口集中在一页。*
22
+
23
+ ## 5 分钟上手
24
+
25
+ 最小前提:manager 运行在 macOS 或 Linux;源码 / git 通道需要 Node ≥ 22;受管目标已配置
26
+ `dsh` web profile,远端还需免密 SSH 与 TCP 转发。细节见[前提](#前提)。
27
+
28
+ ```bash
29
+ curl -fsSL https://raw.githubusercontent.com/shendeguize/Remote_DSH_Center/main/install.sh | bash
30
+ export PATH="$HOME/.local/bin:$PATH" # 默认 prefix;自定义 prefix 请按安装器提示加入 PATH
31
+ dshc init # 四步向导:端口 → 远端约定 → 选择本机/SSH 主机 → 确认
32
+ dshc up # 后台启动本机 manager
33
+ dshc open # 在浏览器打开 Hub
34
+ ```
35
+
36
+ 默认安装把 `dshc` 链接到 `~/.local/bin`,上面的 `export` 让当前 shell 立即可用;若指定了
37
+ `--prefix`,请改为安装器提示的目录。安装器会自动选择 git 或 macOS standalone 通道;向导会探测本机候选和
38
+ `~/.ssh/config` 中的远端候选。完整安装选项与首次启动说明见[使用手册](HANDBOOK.md)。
39
+
40
+ ## 它解决什么问题
41
+
42
+ - **本机 + 远端统一入口**:本机浏览器直连实际 web 端口;远端页面走 `ssh -L` 映射端口。
43
+ - **远端零常驻、零安装**:控制动作使用单条一次性 SSH;Center 管理的日志、patch 与临时文件在
44
+ `~/.dsh_center_remote/`。唯一显式例外是用户保存 dsh 配置时写入
45
+ `${DSH_HOME:-$HOME/.dsh}/settings.yaml`。
46
+ - **Hub + 保活标签页**:`ready` 主机一步拉起并进入;切页不重载 iframe,会话状态保留。
47
+ - **隧道自愈**:远端断联进入 `degraded` 并退避重连;进程真死才标为 `crashed`。
48
+ - **不误杀**:关停前逐字核对 `ps` 命令行指纹;手动实例只读,指纹不符就拒杀。
49
+ - **零 npm 依赖**:运行时与测试仅使用 Node ≥ 22 内置能力,前端是无需构建的原生 ESM。
50
+
51
+ ## 前提
52
+
53
+ - 源码 / git / npm 安装需要 **Node ≥ 22**;macOS standalone 发布包自带官方 Node。
54
+ - 要纳管的本机或远端已安装 DeepSeek Harness(`dsh`)并配置 web profile;Center 只探测,
55
+ 不代装 `dsh`。
56
+ - 远端主机已写入 `~/.ssh/config`、可免密登录,且允许 TCP 转发。本机纳管可选。
57
+
58
+ ## 支持矩阵
59
+
60
+ | 平台 | 安装与发布承诺 | 验证 |
61
+ |---|---|---|
62
+ | macOS arm64(Apple Silicon) | 一等支持:源码 / git 安装与 standalone Release 包 | 对应架构发布包会构建并实机验包 |
63
+ | macOS x64(Intel) | 一等支持:源码 / git 安装与 standalone Release 包 | 对应架构发布包会构建并实机验包 |
64
+ | Linux | 源码 / git 安装与前台运行按 best-effort 支持;无发布包和 `dshc service` | Ubuntu CI 跑通项目闸门 |
65
+
66
+ 源码 / git 安装以 **Node 22** 为经过测试的最低版本;若提高,会在
67
+ [CHANGELOG](CHANGELOG.md) 与对应 Release notes 中提前说明。
68
+
69
+ ## 安装
70
+
71
+ 一键安装命令见页首。有 Node ≥ 22 的机器也可以 `npm i -g @shendeguize/remote-dsh-center`。强制通道、
72
+ 预发布版本、安装目录、launchd 自启、手动安装,以及 `SHA256SUMS` +
73
+ `gh attestation verify` 发布包溯源验证见[安装手册](HANDBOOK.md#安装)。
74
+
75
+ ## 界面速览
76
+
77
+ Center 界面截图来自同一套无头浏览器生成流程。iframe 图使用独立的 mock dsh web,仅用于呈现
78
+ Center 的嵌入集成轮廓,不是目标产品真实页面截图。
79
+
80
+ | | |
81
+ |---|---|
82
+ | ![首启向导:探测并选择本机与远端主机](site/assets/shots/setup.png) | ![主机详情:配置与管理动作](site/assets/shots/drawer.png) |
83
+ | *首启向导 — 探测候选并选择纳管主机。* | *主机详情 — 配置、日志与管理动作集中呈现。* |
84
+ | ![保活标签页中的 mock dsh web](site/assets/shots/iframe.png) | ![远端隧道断联后的重连遮罩](site/assets/shots/degraded.png) |
85
+ | *工作标签 — 独立 mock 展示嵌入轮廓;切换不重载。* | *断联恢复 — 内容保留,隧道重连后继续。* |
86
+
87
+ ## 架构与数据流
88
+
89
+ ```mermaid
90
+ flowchart LR
91
+ B[浏览器<br/>Hub + iframe] -->|REST + SSE| M[本机 manager<br/>127.0.0.1]
92
+ C[dshc CLI] -->|REST| M
93
+ M -->|一次性本机 shell| L[本机 dsh web]
94
+ B -.->|实际环回端口| L
95
+ M -->|一次性 SSH 控制| R[远端 dsh web]
96
+ M --> T[ssh -L 隧道]
97
+ B -.->|本机映射端口| T
98
+ T ==>|加密转发| R
99
+ ```
100
+
101
+ - manager 是运行状态与端口的单一真相源;前端只使用后端下发的 `mappedUrl`。
102
+ - 控制面走 REST/SSE,再落到本机 shell 或一次性 SSH;页面、资源与 WebSocket 由浏览器直连
103
+ 本机实际端口或远端映射端口,不经过 manager 代理。
104
+ - UI 不乐观修改状态,只根据 SSE 事件推进。架构、状态机与恢复细节见
105
+ [手册的架构章节](HANDBOOK.md#架构细节)。
106
+
107
+ ## 日常入口
108
+
109
+ - `#/hub` 是默认入口;`ready` 主机可一步拉起并进入,iframe 切页不重载。
110
+ - `#/manage` 提供全量探测、配置重载、全局默认、事件和主机详情。
111
+
112
+ ## 状态与自愈
113
+
114
+ 远端断联进入 `degraded`,按 1/2/4/8/16/30 秒退避重连;30 秒巡检要求 HTTP 真正返回字节,
115
+ 失败后再经 SSH 深复核。manager 重启只接管指纹匹配的存活实例,不会重新拉起进程。完整状态机
116
+ 见[手册](HANDBOOK.md#状态与自愈)。
117
+
118
+ ## 配置与数据
119
+
120
+ 唯一运行配置是 `~/.dsh_center/config.json`(`DSHC_HOME` 可更换目录)。主机启动目录、
121
+ dsh Workspace 登记、`${DSH_HOME:-$HOME/.dsh}/settings.yaml` 的并发安全编辑与落地物边界见
122
+ [手册](HANDBOOK.md#配置与数据)。
123
+
124
+ ## 命令一览
125
+
126
+ ```text
127
+ 生命周期:dshc init / up / down / restart / status / logs / service install|uninstall|status
128
+ 自身管理:dshc version / update
129
+ 主机操作:dshc ls / probe / start / stop / reconnect / log / open / config
130
+ 退出码:0 成功|1 操作失败|2 超时/通信失败|3 用法错误|130 Ctrl-C 中断等待(操作仍继续)
131
+ ```
132
+
133
+ ## 安全边界
134
+
135
+ > manager 只监听 `127.0.0.1` 且**没有鉴权**。不要暴露到 `0.0.0.0` 或转发到公网。
136
+
137
+ 停止前会逐字核对进程指纹;注入的环境变量与参数会出现在 `ps`,不要放密钥。完整跨站防护、
138
+ 配置文件凭据处理与落地路径限制见[手册](HANDBOOK.md#安全边界)。
139
+
140
+ ## FAQ
141
+
142
+ **Linux 能用吗?** 源码 / git 安装与前台运行按 best-effort 支持,Ubuntu CI 跑项目闸门;
143
+ 不提供 Linux 发布包,依赖 macOS launchd 的 `dshc service` 也不可用。
144
+
145
+ **关闭浏览器会停止实例吗?** 不会。`dshc down` 也只停止 manager 与运输资源;请用
146
+ `dshc stop <主机>` 显式停止受管实例。更多问题见[手册 FAQ](HANDBOOK.md#faq)。
147
+
148
+ ## 彻底卸载
149
+
150
+ 按[手册的卸载顺序](HANDBOOK.md#彻底卸载)先停实例、卸载服务与 CLI,再删除 manager 数据。
151
+ 不要用未经指纹校验的 `pkill -f "dsh web"` 代替 `dshc stop`,它可能误杀其他人的实例。
152
+
153
+ ## 文档
154
+
155
+ - [中文使用手册](HANDBOOK.md) · [English handbook](HANDBOOK.en.md)
156
+ - [版本变化](CHANGELOG.md)
157
+ - [参与开发与 PR 规则](CONTRIBUTING.md)
158
+ - [改代码前的硬约束](AGENTS.md)
159
+
160
+ ## 开发
161
+
162
+ 零依赖仓库,无需 `npm install`:
163
+
164
+ ```bash
165
+ npm run check # 完整质量闸门
166
+ npm run site:check # 站点、demo、双语 README 链接与命令检查
167
+ ```
168
+
169
+ 测试分层、站点开发、代码地图与真机验收命令见[手册的开发章节](HANDBOOK.md#开发与真机验收)。
170
+ 分支、提交、PR、发布与 review 以 [CONTRIBUTING.md](CONTRIBUTING.md) 为准。
171
+
172
+ ## License
173
+
174
+ [MIT](LICENSE)
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@shendeguize/remote-dsh-center",
3
+ "version": "0.4.0",
4
+ "description": "Local manager for remote dsh web instances: ssh tunnels, single-entry iframe shell, CLI lifecycle control",
5
+ "type": "module",
6
+ "engines": {
7
+ "node": ">=22"
8
+ },
9
+ "bin": {
10
+ "dshc": "src/cli.js"
11
+ },
12
+ "files": [
13
+ "src",
14
+ "scripts/install.mjs"
15
+ ],
16
+ "scripts": {
17
+ "start": "node src/cli.js up --foreground",
18
+ "check": "node scripts/check.mjs",
19
+ "build:bundle": "node scripts/build-bundle.mjs",
20
+ "test": "node --test \"tests/**/*.test.js\"",
21
+ "coverage": "node --test --experimental-test-coverage --test-coverage-include=\"src/**\" \"tests/**/*.test.js\"",
22
+ "coverage:gate": "node scripts/coverage-gate.mjs",
23
+ "ui:smoke": "node scripts/ui-smoke.mjs",
24
+ "acceptance:real": "node scripts/real-acceptance.mjs",
25
+ "install:cli": "node scripts/install.mjs",
26
+ "uninstall:cli": "node scripts/install.mjs --uninstall",
27
+ "site:build": "node scripts/build-site.mjs",
28
+ "site:dev": "node scripts/build-site.mjs --serve --port 4321",
29
+ "site:shots": "node scripts/site-shots.mjs",
30
+ "site:check": "node scripts/site-check.mjs"
31
+ },
32
+ "keywords": [
33
+ "dsh",
34
+ "ssh-tunnel",
35
+ "manager"
36
+ ],
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "git+https://github.com/shendeguize/Remote_DSH_Center.git"
40
+ },
41
+ "bugs": {
42
+ "url": "https://github.com/shendeguize/Remote_DSH_Center/issues"
43
+ },
44
+ "publishConfig": {
45
+ "access": "public"
46
+ },
47
+ "license": "MIT"
48
+ }
@@ -0,0 +1,208 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 把 dshc 装进 PATH(软链到本仓库/发布包,不复制)。
4
+ *
5
+ * 用软链而不是拷贝,是因为 launchd plist 里写的是入口的绝对路径
6
+ * (见 src/daemon.js buildPlist)——两份代码会立刻对不上。软链意味着
7
+ * `git pull` 即升级,不需要重装。
8
+ *
9
+ * 两种安装通道,链接指向不同:
10
+ * git 仓库 → `src/cli.js`(shebang 找系统 node)
11
+ * 发布包 → `<包根>/bin/dshc` 启动器(exec 随包自带的 node)
12
+ * 发布包必须指启动器而不是 `app/src/cli.js`:装的人可能压根没装 node,
13
+ * 直接跑 cli.js 会找不到解释器。通道判据与 `dshc update` 共用一份
14
+ * (`src/updater.js` 的 resolveInstall),不在这里另写一遍。
15
+ *
16
+ * 用法:
17
+ * node scripts/install.mjs # 装到 ~/.local/bin(不存在会建)
18
+ * node scripts/install.mjs --prefix /usr/local/bin
19
+ * node scripts/install.mjs --service # 顺带 dshc service install(launchd 自启)
20
+ * node scripts/install.mjs --uninstall # 只摘链接,不动 ~/.dsh_center 的配置
21
+ * node scripts/install.mjs --no-next-steps # 收尾提示交给上游(install.sh 用)
22
+ */
23
+
24
+ import fs from 'node:fs';
25
+ import os from 'node:os';
26
+ import path from 'node:path';
27
+ import { spawn } from 'node:child_process';
28
+ import { fileURLToPath } from 'node:url';
29
+
30
+ import { isMainEntry } from '../src/lib/entry.js';
31
+ import { resolveInstall } from '../src/updater.js';
32
+
33
+ const REPO = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
34
+ const DEFAULT_PREFIX = path.join(os.homedir(), '.local', 'bin');
35
+
36
+ /**
37
+ * 该把 dshc 软链到哪个文件。
38
+ * @returns {{target:string, channel:string, viaShim:boolean}}
39
+ */
40
+ export function linkTarget(repoRoot = REPO, deps = {}) {
41
+ const install = resolveInstall(repoRoot, deps);
42
+ if (install.channel === 'bundle') {
43
+ return { target: path.join(install.root, 'bin', 'dshc'), channel: 'bundle', viaShim: true };
44
+ }
45
+ return { target: path.join(repoRoot, 'src', 'cli.js'), channel: install.channel, viaShim: false };
46
+ }
47
+
48
+ /**
49
+ * 装之前先把链接位置的现状分类,避免把别人的 dshc 覆盖掉。
50
+ * @returns {{action:'create'|'relink'|'noop'|'conflict', current:string|null}}
51
+ */
52
+ export function linkPlan(linkPath, target, { lstat = fs.lstatSync, readlink = fs.readlinkSync } = {}) {
53
+ let st;
54
+ try {
55
+ st = lstat(linkPath);
56
+ } catch {
57
+ return { action: 'create', current: null };
58
+ }
59
+ if (!st.isSymbolicLink()) return { action: 'conflict', current: '(不是软链,是真实文件)' };
60
+ const current = readlink(linkPath);
61
+ if (path.resolve(path.dirname(linkPath), current) === target) return { action: 'noop', current };
62
+ return { action: 'relink', current };
63
+ }
64
+
65
+ /** PATH 里有没有这个前缀——没有的话装了也调不到,得当场说清楚。 */
66
+ export function prefixInPath(prefix, pathEnv = process.env.PATH ?? '') {
67
+ const norm = (p) => path.resolve(p.replace(/^~(?=$|\/)/, os.homedir()));
68
+ return pathEnv.split(path.delimiter).filter(Boolean).some((p) => norm(p) === path.resolve(prefix));
69
+ }
70
+
71
+ /** 给用户抄的那行(按当前 shell 猜 rc 文件)。 */
72
+ export function pathHint(prefix, shell = process.env.SHELL ?? '') {
73
+ const rc = shell.endsWith('zsh') ? '~/.zshrc' : (shell.endsWith('bash') ? '~/.bash_profile' : '你的 shell rc');
74
+ return `echo 'export PATH="${prefix}:$PATH"' >> ${rc} && exec $SHELL -l`;
75
+ }
76
+
77
+ /**
78
+ * 下游不看了(`| head` 之类)属于哪种错。
79
+ *
80
+ * 管道被提前关掉时 Node 默认把 EPIPE 抛成未捕获异常,刷一屏栈——但那不是安装失败,
81
+ * 而且软链早在报错前就建好了,看到栈的人会误以为装坏了(issue #16)。
82
+ * 流被销毁后的后续写入报的是另外两个码,同源同因,一起吞。
83
+ */
84
+ export function isBrokenPipe(err) {
85
+ return ['EPIPE', 'ERR_STREAM_DESTROYED', 'ERR_STREAM_WRITE_AFTER_END'].includes(err?.code);
86
+ }
87
+
88
+ /** 吞掉管道断裂,其余照抛(真正的写失败不该被藏起来)。 */
89
+ export function silenceBrokenPipe(streams = [process.stdout, process.stderr]) {
90
+ for (const s of streams) {
91
+ s.on('error', (err) => {
92
+ if (!isBrokenPipe(err)) throw err;
93
+ });
94
+ }
95
+ }
96
+
97
+ function run(cmd, args) {
98
+ return new Promise((resolve) => {
99
+ const child = spawn(cmd, args, { cwd: REPO, stdio: ['ignore', 'pipe', 'pipe'] });
100
+ let stdout = '';
101
+ let stderr = '';
102
+ child.stdout.setEncoding('utf8');
103
+ child.stderr.setEncoding('utf8');
104
+ child.stdout.on('data', (c) => { stdout += c; });
105
+ child.stderr.on('data', (c) => { stderr += c; });
106
+ child.on('error', (err) => resolve({ code: 127, stdout, stderr: String(err.message) }));
107
+ child.on('close', (code) => resolve({ code: code ?? 1, stdout, stderr }));
108
+ });
109
+ }
110
+
111
+ async function main() {
112
+ silenceBrokenPipe();
113
+ const argv = process.argv.slice(2);
114
+ const flag = (name) => argv.includes(`--${name}`);
115
+
116
+ // 不认识的旗标当用法错误。只挑认识的、其余不响,等于「按另一套意思照做」:
117
+ // 把 --service 拼成 --srevice 的人以为装了自启,实际什么都没装(issue #55)。
118
+ const KNOWN = new Set(['--prefix', '--service', '--uninstall', '--no-next-steps']);
119
+ const unknown = argv.filter((a, i) => a.startsWith('--') && !KNOWN.has(a)
120
+ // --prefix 的值不是旗标
121
+ && argv[i - 1] !== '--prefix');
122
+ if (unknown.length > 0) {
123
+ process.stderr.write(`不认识的参数:${unknown.join(' ')}\n可用:${[...KNOWN].join(' | ')}\n`);
124
+ process.exitCode = 3;
125
+ return;
126
+ }
127
+
128
+ const prefixArg = argv.indexOf('--prefix');
129
+ const prefix = path.resolve(prefixArg === -1 ? DEFAULT_PREFIX : argv[prefixArg + 1]);
130
+ const linkPath = path.join(prefix, 'dshc');
131
+ const { target: entry, viaShim } = linkTarget();
132
+
133
+ const major = Number(process.versions.node.split('.')[0]);
134
+ if (major < 22) {
135
+ process.stderr.write(`需要 Node ≥ 22(当前 ${process.versions.node}):本项目直接用了内置测试运行器与 fetch。\n`);
136
+ process.exitCode = 1;
137
+ return;
138
+ }
139
+
140
+ if (flag('uninstall')) {
141
+ const plan = linkPlan(linkPath, entry);
142
+ if (plan.action === 'create') {
143
+ process.stdout.write(`${linkPath} 本来就不存在,无事可做。\n`);
144
+ return;
145
+ }
146
+ if (plan.action === 'conflict') {
147
+ process.stderr.write(`${linkPath} 不是本仓库装的软链,没敢删。\n`);
148
+ process.exitCode = 1;
149
+ return;
150
+ }
151
+ fs.rmSync(linkPath);
152
+ process.stdout.write(`已摘除 ${linkPath}(配置与状态仍在 ~/.dsh_center,未动)。\n`);
153
+ process.stdout.write('如果之前装过 launchd 服务,用 dshc service uninstall 单独摘。\n');
154
+ return;
155
+ }
156
+
157
+ if (!fs.existsSync(entry)) {
158
+ process.stderr.write(`找不到入口:${entry}\n`);
159
+ process.exitCode = 1;
160
+ return;
161
+ }
162
+ fs.chmodSync(entry, 0o755);
163
+ fs.mkdirSync(prefix, { recursive: true });
164
+
165
+ const plan = linkPlan(linkPath, entry);
166
+ if (plan.action === 'conflict') {
167
+ process.stderr.write(`${linkPath} 已存在且不是软链,请先自行处理再装。\n`);
168
+ process.exitCode = 1;
169
+ return;
170
+ }
171
+ if (plan.action === 'relink') {
172
+ process.stdout.write(`覆盖旧链接(原指向 ${plan.current})。\n`);
173
+ fs.rmSync(linkPath);
174
+ }
175
+ if (plan.action !== 'noop') fs.symlinkSync(entry, linkPath);
176
+
177
+ const probe = await run(linkPath, ['--help']);
178
+ if (probe.code !== 0 || !probe.stdout.includes('dshc')) {
179
+ process.stderr.write(`装好了但跑不通:${linkPath}(退出码 ${probe.code})${probe.stderr}\n`);
180
+ process.exitCode = 1;
181
+ return;
182
+ }
183
+ process.stdout.write(`dshc → ${entry}\n已链接到 ${linkPath}\n`);
184
+ if (viaShim) process.stdout.write('(发布包安装:启动器会用随包自带的 Node,与系统 node 无关)\n');
185
+
186
+ if (!prefixInPath(prefix)) {
187
+ process.stdout.write(`\n注意:${prefix} 不在 PATH 里,现在敲 dshc 还找不到。加一行:\n ${pathHint(prefix)}\n`);
188
+ }
189
+
190
+ if (flag('service')) {
191
+ process.stdout.write('\n装 launchd 服务(开机自启 + 被杀拉回)…\n');
192
+ // plist 里的解释器路径来自跑这条命令的进程的 process.execPath(daemon.buildPlist)。
193
+ // 发布包安装必须经启动器跑,否则 plist 会写上系统 node——而那台机器可能根本没有。
194
+ const res = viaShim
195
+ ? await run(linkPath, ['service', 'install'])
196
+ : await run(process.execPath, [entry, 'service', 'install']);
197
+ process.stdout.write(res.stdout || res.stderr);
198
+ if (res.code !== 0) process.exitCode = res.code;
199
+ return;
200
+ }
201
+
202
+ // install.sh 调用时它自己会印一份更详细的收尾,这里就别再来一遍(issue #17)
203
+ if (flag('no-next-steps')) return;
204
+ process.stdout.write('\n下一步:dshc init(首次配置)→ dshc up → dshc open\n');
205
+ process.stdout.write('想开机自启:dshc service install(或重跑本脚本加 --service)\n');
206
+ }
207
+
208
+ if (isMainEntry(import.meta.url)) await main();