dsh-remote 0.8.14 → 0.8.15

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/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 dsh-remote contributors
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.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 dsh-remote contributors
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.md CHANGED
@@ -1,235 +1,237 @@
1
- **English** · [中文](./README.zh.md)
2
-
3
- ---
4
-
5
- # dsh-remote
6
-
7
- [![npm version](https://img.shields.io/npm/v/dsh-remote)](https://www.npmjs.com/package/dsh-remote)
8
- [![license](https://img.shields.io/github/license/flymysql/dsh-remote)](LICENSE)
9
- [![dsh-plugin](https://img.shields.io/badge/topic-dsh--plugin-7a3ef3)](https://github.com/topics/dsh-plugin)
10
-
11
- **Remote-work assistant for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (DSH).**
12
-
13
- Manage several SSH machines, then pick a **remote workspace** (or a **local** one) and let the agent operate right there without leaving the harness — listing files, reading code, running builds & commands over the remote host, and keeping that remote directory mirrored into a real local workspace object.
14
-
15
- The harness Web UI intentionally binds `127.0.0.1` (the CLI rejects `--host 0.0.0.0` for safety). This plugin goes the other way: **you connect out** to the machines you maintain, pick a workspace, and work in it through the normal DSH workspace + agent fs flows — no changes to `dsh-workspace` or the harness core.
16
-
17
- ## Screen previews
18
-
19
- Settings → **远程工作区** — a multi-machine SSH registry (add / edit / delete / set-current, password stored locally):
20
-
21
- <img src="https://cdn.jsdelivr.net/gh/flymysql/dsh-remote@main/docs/ui-settings-panel.png" alt="dsh-remote settings — multi-machine registry (light theme, host scrubbed)" width="720"/>
22
-
23
- The native **"Add workspace" / "Select workspace"** flow — a centered modal, two tabs, opens on **本机 (local)**; switch to **远程 (remote)**:
24
-
25
- - **远程** — a **machine `<select>`**, a path field that **auto-prefills `/` and live-completes** directories (picking one immediately reveals its next level, OS/VSCode-style), plus a **浏览…** floating browser that fills the field without committing — you review, edit, then **设为远程工作区**.
26
-
27
- Real capture (host scrubbed to a placeholder):
28
-
29
- <img src="https://cdn.jsdelivr.net/gh/flymysql/dsh-remote@main/docs/ui-picker-panel.png" alt="dsh-remote workspace picker — real dialog; 本机 (local) tab; 远程 machine select + prefilled root path + autocomplete" width="720"/>
30
-
31
- ---
32
-
33
- ## Features
34
-
35
- - **Multi-machine SSH** — save any number of hosts (`host`/`port`/`user` + **private key** or **password**). Passwords are stored locally and never shown back in the UI. Switch with one click in Settings. Per-machine **passphrase / host-key mode / SSH agent / keyboard-interactive (OTP) / proxy jump (bastion)** and an **optional OS-keychain password** (`加密保存密码` — macOS Keychain / Windows DPAPI / Linux secret-tool).
36
- - **`~/.ssh/config` import** — the Settings page lists your `Host` aliases; one click fills the form (path reference only, the plugin never reads key material).
37
- - **Two-tab workspace picker** (fills the native "Add workspace" flow):
38
- - **本机 / Local** — opens the **native OS folder chooser** over the host (macOS `osascript` / Linux `zenity`→`kdialog` / **Windows `FolderBrowserDialog`**), or lets you type a local path → adopted directly as a normal DSH local workspace.
39
- - **远程 / Remote** — the picker is a **centered modal**. Pick a **machine** → on Windows hosts the root shows a **"This PC" drive view** (`C:\`, `D:\`, `E:\`… instead of the Git Bash MSYS root) and the path field live **autocompletes** directories (accepts `C:\Users\…` or `/c/Users/…` — Windows paths are rewritten to the Git Bash form underneath); selecting a directory immediately lists its next level. A **浏览…** floating browser (Windows-aware breadcrumb `此电脑 / C:\ / Users / dev`, drive rows, size + mtime, dirs first, follows symlinks) fills the field without committing; the **回上一级** button works at any depth (even when the browser was opened at the path bar's value). **最近 workspaces** quick-pick, **`~` 主目录** shortcut and **新建目录** are one click away. On confirm it creates a **real local mirror** under `$DSH_HOME/remote-workspaces/<host>-<user>-<port>/<base>` that passes `fs.realpath` → the harness adopts it as a real workspace while dsh-remote keeps it synced over SFTP.
40
- - **Git Bash default terminal (Windows remotes)** — the remote platform is auto-detected (`cmd /c ver`, plus an `uname -s` MINGW/MSYS probe as fallback); on Windows the plugin locates Git Bash (`config.shell` can pin a path or `native` disables wrapping) and pipes every command to `bash -s` over the exec channel, so quoting/backslash escaping is never an issue regardless of the SSH default shell. `rw_exec` runs with a Git Bash cwd (`/c/Users/…` form). `/dsh-remote/status`, `rw_info` and the 测试连接 button report the detected platform + shell.
41
- - **Windows path auto-conversion** — typing `C:\Users\dev\project` (or `C:/…`, `/c/…`, `/C:/…`) is normalized underneath to the Git Bash form `/c/Users/dev/project` for shell commands, while workspaces are stored and shown Windows-style (`C:\Users\dev\project`). All model tools accept and report both forms; SFTP access uses the Win32-OpenSSH `/D:/…` form (see `toSftpPath`).
42
- - **Bidirectional SFTP sync, conflict-aware** — `rw_sync` (remote → mirror) and `rw_push` (mirror → remote) are **three-way** (remote vs local vs last-synced snapshot): files changed on both sides are **reported as conflicts and never silently overwritten** (`force=true` overrides). Both support **dry-run**, **background tasks**, and honor **gitignore-style ignore rules** (`.dsh-remote-ignore` under `remote-workspaces`, defaults cover `.git/node_modules/target/dist/build/…`).
43
- - **Model tools** — 20 tools, all Windows/POSIX portable via SFTP: `rw_info`, `rw_connect` (with `save`), `rw_pick_workspace`, `rw_list_dir` (size+mtime), `rw_stat`, `rw_read_file` (encoding-aware: utf-8/gbk), `rw_write_file`, **`rw_edit`** (literal replace + mtime optimistic lock), `rw_append`, `rw_mkdir`, `rw_remove` (recursive, bounded), `rw_move`, `rw_exec` (pty/env), **`rw_search`** (SFTP tree walk — works on Windows too, honors ignore rules, context lines), `rw_download`/`rw_upload` (streaming fastGet/fastPut + size caps), **`rw_forward`** (SSH tunnels), `rw_sync`, `rw_push`, `rw_disconnect`.
44
- - **Port forwarding panel** — create/start/stop/remove **local** (`127.0.0.1:port → remote`) and **reverse** (`remote → local`) tunnels in the Settings page or via `rw_forward`; definitions persist, auto-restart on reconnect when enabled, all tunnels stop on disconnect.
45
- - **Sidebar remote editing** — the better-sidebar remote file tab is now **editable**: click **编辑** → edit → **保存到远程** with an mtime optimistic lock (409 + "重新读取" on concurrent change). The explorer rows show file sizes and have a **right-click menu** (下载到本地镜像 / 重命名 / 删除 / 新建目录).
46
- - **Command audit log** — every `rw_exec`/write/remove/move/forward is appended to `$DSH_HOME/remote-workspaces/audit.log` (time · user@host · op · exit code · command); the Settings page shows the last 30.
47
- - **Async long tasks** — `rw_sync`/`rw_push` with `async: true` return a `taskId`; progress/result/cancel via `/dsh-remote/task` (single-flight queue).
48
- - **Connection health** — a **「测试连接」** button validates host/user/key/password (with per-category error hints: auth / network / host key / timeout) before you save a machine; latency is cached on the machine record.
49
- - The active `user@host:/path` is injected into every system prompt (plus active forwards).
50
- - **No official `dsh-workspace` core is modified** — everything is delivered as a normal plugin (directory-flow holes filled by the client half at `priority -100`).
51
- - **Cross-platform remotes** — all file access is SFTP-protocol-level (no shell dependency), so Linux/macOS/Windows remotes all work for listing, reading, writing, searching and syncing.
52
- - **Host-key verification (TOFU)** — every SSH connect verifies the host key
53
- (`hostKeyMode: accept-new`): first connect records it, a later CHANGE is rejected
54
- as a possible man-in-the-middle. `verify` also refuses hosts never seen before;
55
- `off` disables it. Stored at `$DSH_HOME/remote-workspaces/known_hosts.json`; reset
56
- with `/remote forget-key`.
57
- - **Data lives under the harness home** — machines + mirrors follow `$DSH_HOME`; pre-0.6 data under `~/.dsh/remote-workspaces` is migrated automatically on first run.
58
-
59
- ## Install
60
-
61
- ```bash
62
- dsh plugin add dsh-remote # add the bundle
63
- ```
64
-
65
- One command installs everything: since **v0.7.2** the sidebar
66
- ([dsh-better-sidebar](https://www.npmjs.com/package/dsh-better-sidebar)) is a
67
- hard dependency and is mounted automatically — the 🌐 remote-file explorer and
68
- remote file viewer show up in the sidebar with no extra step. If you already
69
- have the sidebar installed on its own, the embedded copy backs off (no double
70
- mount) — regardless of whether the standalone bundle is listed **before or
71
- after** `dsh-remote` in `dsh.profile.bundles` (order-independent guard since
72
- 0.8.7; earlier versions crashed boot with `duplicate prefix route
73
- "/sidebar/api"` when the standalone bundle came after `dsh-remote`).
74
-
75
- > **Upgrading from ≤0.8.6 with a standalone sidebar?** You may keep the
76
- > standalone `dsh-better-sidebar` bundle (any order) — 0.8.7+ no longer
77
- > crashes. Or remove it from `bundles` and let dsh-remote mount the embedded
78
- > copy (version ^0.14.0).
79
-
80
- > **Requires the profile's pnpm linker to be `hoisted`** (the DSH profile
81
- > default, `nodeLinker: hoisted` in `pnpm-workspace.yaml`). The loader resolves
82
- > plugin packages from the profile root, so the sidebar must be reachable in
83
- > the top-level `node_modules`. If your `pnpm-workspace.yaml` was rewritten
84
- > without `nodeLinker: hoisted`, add it back (`nodeLinker: hoisted`) and run
85
- > `pnpm install` once — otherwise the embedded sidebar row fails with
86
- > `Cannot find package 'dsh-better-sidebar'`.
87
-
88
- (or `npm install dsh-remote` + add `- id: dsh-remote / name: dsh-remote` in `cordis.patch.yml`).
89
-
90
- ## Quick start
91
-
92
- 1. **Add a machine** — Settings → 远程工作区 → add host/port/user + key or password → (optional) set it current.
93
- 2. **Open a workspace** — click **Add workspace** in the sidebar / conversation:
94
- - **本机** → system folder chooser (or type a local path) → local workspace.
95
- On hosts without a usable OS dialog (DSH Desktop's browse backend, headless
96
- SSH hosts without zenity/kdialog) the in-app directory browser pops up
97
- instead — breadcrumbs, Windows drive switch, new-folder, pick-and-fill.
98
- - **远程** → choose the machine → browse to a remote directory (or type `/path`) → "设为远程工作区" ⇒ a local mirror workspace is created and adopted.
99
- 3. **Work with the agent** — treat it like any workspace:
100
- - `rw_list_dir(path?)`/`rw_read_file` — inspect remote files
101
- - `rw_write_file(path, content)` / `rw_edit(path, old, new)` — create / patch a remote file directly
102
- - `rw_stat(path)` / `rw_mkdir(path)` / `rw_remove(path, recursive?)` / `rw_move(path, dest)` — manage remote paths
103
- - `rw_search(pattern, path?)` — grep remote files (SFTP walk, Windows OK)
104
- - `rw_exec(command, cwd?, pty?)` — run remote shell commands (defaults to the workspace dir)
105
- - `rw_forward(listenPort, targetHost?, targetPort?)` — open an SSH tunnel
106
- - `rw_sync(dryRun?/force?/async?)` / `rw_push(dryRun?/force?/async?)` — conflict-aware mirror pull/push
107
-
108
- ## CLI defaults (optional)
109
-
110
- Provide a default machine in `cordis.patch.yml`:
111
-
112
- ```yaml
113
- # Example only — use values for your own machine.
114
- - id: dsh-remote
115
- name: dsh-remote
116
- config:
117
- host: 203.0.113.10 # or your real host / hostname
118
- port: 22
119
- username: dev
120
- privateKeyPath: ~/.ssh/id_rsa
121
- # or password: '…'
122
- workspace: ~/project
123
- ```
124
-
125
- If `host` is empty the plugin starts disconnected and you configure machines in the UI.
126
-
127
- ## CLI quick reference
128
-
129
- Installing and driving DSH may live in different shells, so both the `dsh` binary and the `npx` form are shown. Always tell DSH **which profile** to use with `--profile <name>` (usually `web`).
130
-
131
- ```bash
132
- # install the bundle into a profile (npm is pulled by pnpm; recommended)
133
- dsh plugin --profile web add dsh-remote
134
- # same but when `dsh` is not on PATH (e.g. Windows PowerShell inside a repo)
135
- npx --yes @deepseek-ai/dsh plugin --profile web add dsh-remote
136
-
137
- # confirm it is installed wire
138
- dsh plugin --profile web list
139
- npx --yes @deepseek-ai/dsh plugin --profile web list
140
-
141
- # start the web surface (reload profile; the plugin activates on boot)
142
- dsh --profile web
143
- npx --yes @deepseek-ai/dsh --profile web # http://127.0.0.1:3080
144
-
145
- # use a local checkout instead of the npm version (dev iteration)
146
- npx --yes @deepseek-ai/dsh plugin --profile web add /path/to/dsh-remote
147
- npx --yes @deepseek-ai/dsh plugin --profile web remove dsh-remote # back to release
148
- ```
149
-
150
- After a successful start, `Settings → 远程工作区` appears and the "Add workspace" flow gains the 本机 / 远程 tabs (screenshots above).
151
-
152
- ## Development (sandbox, not product)
153
-
154
- Iterate **in the sandbox**, never by hand-editing a product profile — the
155
- product profile is re-managed by the plugin manager and reverts hand-deployed
156
- files on reinstall. Use the helper script:
157
-
158
- ```bash
159
- scripts/dev-run.sh --restart # start / restart the isolated sandbox
160
- scripts/dev-run.sh --stop # stop it
161
- scripts/dev-run.sh --status # is it running?
162
- ```
163
-
164
- - Runs its own DSH instance (`dev-harness/harness` inside this repo) with the
165
- plugin copied in from `lib/` — it boots through the same `bin.js web --patch`
166
- path as the desktop app, so the sandbox reproduces the product boot behavior.
167
- - The sandbox web UI serves on `http://127.0.0.1:50599` and the plugin routes
168
- are live immediately (e.g. `GET /dsh-remote/machines`).
169
- - **Host-half changes** (`lib/index.js`) need a sandbox restart (`--restart`);
170
- **client-half changes** (`lib/client.js`) need a page refresh.
171
- - Node ESM resolves dependencies from the importing file's real path, so the
172
- script **copies** `lib/` (hardlink copy, `cp -al`) into the sandbox profile
173
- instead of symlinking — a symlink breaks `@deepseek-ai/*` resolution.
174
- - Run `scripts/check.mjs` (static framework-constraint gate: command-name
175
- regex, …) before every commit; `scripts/boot-smoke.sh` boots an isolated
176
- instance to prove the plugin still starts.
177
- - Full rules live in `scripts/dev-standards.md` (command names, cordis service
178
- access via `ctx.get()` only, optional framework services may never register,
179
- verify third-party callback contracts against the real runtime, …).
180
-
181
- Deploying to a product profile is a separate, explicit action (`./sync.sh`)
182
- and should be done only when you intend to release.
183
-
184
- ## Configuration
185
-
186
- | Key | Type | Default | Meaning |
187
- | --- | --- | --- | --- |
188
- | `host` | string | `''` | default SSH host (else start disconnected) |
189
- | `port` | int | `22` | default SSH port |
190
- | `username` | string | `''` | default SSH user |
191
- | `password` | string | `''` | default SSH password (non-empty overrides key) |
192
- | `privateKeyPath` | string | `''` | private key path (used only when explicitly provided) |
193
- | `passphrase` | string | `''` | passphrase for an encrypted private key |
194
- | `workspace` | string | `''` | default remote workspace path |
195
- | `shell` | string | `''` | remote command terminal strategy: `''`=auto-detect (Git Bash on Windows remotes), `'git-bash'`=prefer Git Bash, `'native'`=never wrap, anything else=explicit bash.exe path (e.g. `C:\Program Files\Git\bin\bash.exe`) |
196
- | `commandTimeoutMs` | int | 20000 | per remote command timeout |
197
- | `connectTimeoutMs` | int | 15000 | SSH connect timeout |
198
- | `maxFileBytes` | int | 52428800 | skip mirroring/reading files larger than this (0 = no cap) |
199
- | `hostKeyMode` | string | `accept-new` | host-key policy: `accept-new` (TOFU), `verify` (reject unknown hosts), `off` (skip) |
200
- | `useAgent` | bool | `false` | authenticate via the OpenSSH agent (`SSH_AUTH_SOCK`) |
201
- | `keyboardInteractive` | bool | `false` | allow keyboard-interactive auth (OTP/MFA) with the configured password |
202
- | `proxy` | object | — | jump host: `{ host, port?, username?, password?, privateKeyPath? }` |
203
- | `autoPush` | bool | `false` | auto-push edited mirror files back to the remote (watcher, debounced) |
204
- | `auditLog` | bool | `true` | append executed commands to `$DSH_HOME/remote-workspaces/audit.log` |
205
- | `encoding` | string | `utf-8` | text encoding for remote file reads/writes (e.g. `gbk`) |
206
-
207
- ## FAQ / troubleshooting
208
-
209
- **Host key 变了 / 提示可能中间人** — 主机重装过或密钥更换过:`/remote-forget-key`(或设置页 → 机器 → 重新信任),下次连接重新记录。
210
-
211
- **连接报"认证失败"** — 检查用户名/密码/私钥路径;私钥加密了要填 Passphrase;公司机器要求 OTP/动态码时勾选 keyboard-interactive。
212
-
213
- **连不上内网机器** — 走跳板机:机器表单里填「跳板机」主机(也可以先把它本身配成一台机器)。主机不可达类错误会给出分类提示。
214
-
215
- **rw_sync/rw_push 报冲突** — 远端和本地都改过同一个文件时会跳过并列出冲突(绝不静默覆盖)。处理:手动合并后重新同步,或用 `force=true` 以一边为准。
216
-
217
- **Windows 远程** — 列表/读写/搜索/同步全部走 SFTP 协议,不依赖 POSIX shell;中文文件用 `encoding=gbk` 读。
218
-
219
- **镜像里没有某个目录** — 默认 ignore 规则(`.git`、`node_modules`、`target` 等)会跳过;在 `$DSH_HOME/remote-workspaces/.dsh-remote-ignore` 加 `!` 之外的条目即可调整(gitignore 语法)。
220
-
221
- **侧边栏远程文件保存失败(409)** — 远端文件在你打开后已被改动,重新读取后再编辑(mtime 乐观锁保护)。
222
-
223
- **密码怎么加密保存** — 机器表单勾选「加密保存密码」:macOS 用系统钥匙串(security),Windows 用 DPAPI,Linux 需要 secret-tool(libsecret);后端不可用时自动回退明文。
224
-
225
- ## Safety
226
-
227
- Giving the plugin a machine's credentials lets the agent run **shell commands as your user** on that host. Only add machines you trust. Passwords are saved on the local machine file (or the OS keychain when enabled); treat it as sensitive (you may lock file ACLs). Every executed command is recorded in the audit log when `auditLog` is on — review it from the Settings page.
228
-
229
- ## License
230
-
231
- MIT
232
-
233
- ## Changelog
234
-
1
+ **English** · [中文](./README.zh.md)
2
+
3
+ ---
4
+
5
+ # dsh-remote
6
+
7
+ [![npm version](https://img.shields.io/npm/v/dsh-remote)](https://www.npmjs.com/package/dsh-remote)
8
+ [![license](https://img.shields.io/github/license/flymysql/dsh-remote)](LICENSE)
9
+ [![dsh-plugin](https://img.shields.io/badge/topic-dsh--plugin-7a3ef3)](https://github.com/topics/dsh-plugin)
10
+
11
+ **Remote-work assistant for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (DSH).**
12
+
13
+ Manage several SSH machines, then pick a **remote workspace** (or a **local** one) and let the agent operate right there without leaving the harness — listing files, reading code, running builds & commands over the remote host, and keeping that remote directory mirrored into a real local workspace object.
14
+
15
+ The harness Web UI intentionally binds `127.0.0.1` (the CLI rejects `--host 0.0.0.0` for safety). This plugin goes the other way: **you connect out** to the machines you maintain, pick a workspace, and work in it through the normal DSH workspace + agent fs flows — no changes to `dsh-workspace` or the harness core.
16
+
17
+ ## Screen previews
18
+
19
+ Settings → **远程工作区** — a multi-machine SSH registry (add / edit / delete / set-current, password stored locally):
20
+
21
+ <img src="https://cdn.jsdelivr.net/gh/flymysql/dsh-remote@main/docs/ui-settings-panel.png" alt="dsh-remote settings — multi-machine registry (light theme, host scrubbed)" width="720"/>
22
+
23
+ The native **"Add workspace" / "Select workspace"** flow — a centered modal, two tabs, opens on **本机 (local)**; switch to **远程 (remote)**:
24
+
25
+ - **远程** — a **machine `<select>`**, a path field that **auto-prefills `/` and live-completes** directories (picking one immediately reveals its next level, OS/VSCode-style), plus a **浏览…** floating browser that fills the field without committing — you review, edit, then **设为远程工作区**.
26
+
27
+ Real capture (host scrubbed to a placeholder):
28
+
29
+ <img src="https://cdn.jsdelivr.net/gh/flymysql/dsh-remote@main/docs/ui-picker-panel.png" alt="dsh-remote workspace picker — real dialog; 本机 (local) tab; 远程 machine select + prefilled root path + autocomplete" width="720"/>
30
+
31
+ ---
32
+
33
+ ## Features
34
+
35
+ - **Multi-machine SSH** — save any number of hosts (`host`/`port`/`user` + **private key** or **password**). Passwords are stored locally and never shown back in the UI. Switch with one click in Settings. Per-machine **passphrase / host-key mode / SSH agent / keyboard-interactive (OTP) / proxy jump (bastion)** and an **optional OS-keychain password** (`加密保存密码` — macOS Keychain / Windows DPAPI / Linux secret-tool).
36
+ - **`~/.ssh/config` import** — the Settings page lists your `Host` aliases; one click fills the form (path reference only, the plugin never reads key material).
37
+ - **Two-tab workspace picker** (fills the native "Add workspace" flow):
38
+ - **本机 / Local** — opens the **native OS folder chooser** over the host (macOS `osascript` / Linux `zenity`→`kdialog` / **Windows `FolderBrowserDialog`**), or lets you type a local path → adopted directly as a normal DSH local workspace.
39
+ - **远程 / Remote** — the picker is a **centered modal**. Pick a **machine** → on Windows hosts the root shows a **"This PC" drive view** (`C:\`, `D:\`, `E:\`… instead of the Git Bash MSYS root) and the path field live **autocompletes** directories (accepts `C:\Users\…` or `/c/Users/…` — Windows paths are rewritten to the Git Bash form underneath); selecting a directory immediately lists its next level. A **浏览…** floating browser (Windows-aware breadcrumb `此电脑 / C:\ / Users / dev`, drive rows, size + mtime, dirs first, follows symlinks) fills the field without committing; the **回上一级** button works at any depth (even when the browser was opened at the path bar's value). **最近 workspaces** quick-pick, **`~` 主目录** shortcut and **新建目录** are one click away. On confirm it creates a **real local mirror** under `$DSH_HOME/remote-workspaces/<host>-<user>-<port>/<base>` that passes `fs.realpath` → the harness adopts it as a real workspace while dsh-remote keeps it synced over SFTP.
40
+ - **Git Bash default terminal (Windows remotes)** — the remote platform is auto-detected (`cmd /c ver`, plus an `uname -s` MINGW/MSYS probe as fallback); on Windows the plugin locates Git Bash (`config.shell` can pin a path or `native` disables wrapping) and pipes every command to `bash -s` over the exec channel, so quoting/backslash escaping is never an issue regardless of the SSH default shell. `rw_exec` runs with a Git Bash cwd (`/c/Users/…` form). `/dsh-remote/status`, `rw_info` and the 测试连接 button report the detected platform + shell.
41
+ - **Windows path auto-conversion** — typing `C:\Users\dev\project` (or `C:/…`, `/c/…`, `/C:/…`) is normalized underneath to the Git Bash form `/c/Users/dev/project` for shell commands, while workspaces are stored and shown Windows-style (`C:\Users\dev\project`). All model tools accept and report both forms; SFTP access uses the Win32-OpenSSH `/D:/…` form (see `toSftpPath`).
42
+ - **Bidirectional SFTP sync, conflict-aware** — `rw_sync` (remote → mirror) and `rw_push` (mirror → remote) are **three-way** (remote vs local vs last-synced snapshot): files changed on both sides are **reported as conflicts and never silently overwritten** (`force=true` overrides). Both support **dry-run**, **background tasks**, and honor **gitignore-style ignore rules** (`.dsh-remote-ignore` under `remote-workspaces`, defaults cover `.git/node_modules/target/dist/build/…`).
43
+ - **Model tools** — 20 tools, all Windows/POSIX portable via SFTP: `rw_info`, `rw_connect` (with `save`), `rw_pick_workspace`, `rw_list_dir` (size+mtime), `rw_stat`, `rw_read_file` (encoding-aware: utf-8/gbk), `rw_write_file`, **`rw_edit`** (literal replace + mtime optimistic lock), `rw_append`, `rw_mkdir`, `rw_remove` (recursive, bounded), `rw_move`, `rw_exec` (pty/env), **`rw_search`** (SFTP tree walk — works on Windows too, honors ignore rules, context lines), `rw_download`/`rw_upload` (streaming fastGet/fastPut + size caps), **`rw_forward`** (SSH tunnels), `rw_sync`, `rw_push`, `rw_disconnect`.
44
+ - **Port forwarding panel** — create/start/stop/remove **local** (`127.0.0.1:port → remote`) and **reverse** (`remote → local`) tunnels in the Settings page or via `rw_forward`; definitions persist, auto-restart on reconnect when enabled, all tunnels stop on disconnect.
45
+ - **Sidebar remote editing** — the better-sidebar remote file tab is now **editable**: click **编辑** → edit → **保存到远程** with an mtime optimistic lock (409 + "重新读取" on concurrent change). The explorer rows show file sizes and have a **right-click menu** (下载到本地镜像 / 重命名 / 删除 / 新建目录).
46
+ - **Command audit log** — every `rw_exec`/write/remove/move/forward is appended to `$DSH_HOME/remote-workspaces/audit.log` (time · user@host · op · exit code · command); the Settings page shows the last 30.
47
+ - **Async long tasks** — `rw_sync`/`rw_push` with `async: true` return a `taskId`; progress/result/cancel via `/dsh-remote/task` (single-flight queue).
48
+ - **Connection health** — a **「测试连接」** button validates host/user/key/password (with per-category error hints: auth / network / host key / timeout) before you save a machine; latency is cached on the machine record.
49
+ - The active `user@host:/path` is injected into every system prompt (plus active forwards).
50
+ - **No official `dsh-workspace` core is modified** — everything is delivered as a normal plugin (directory-flow holes filled by the client half at `priority -100`).
51
+ - **Cross-platform remotes** — all file access is SFTP-protocol-level (no shell dependency), so Linux/macOS/Windows remotes all work for listing, reading, writing, searching and syncing.
52
+ - **Host-key verification (TOFU)** — every SSH connect verifies the host key
53
+ (`hostKeyMode: accept-new`): first connect records it, a later CHANGE is rejected
54
+ as a possible man-in-the-middle. `verify` also refuses hosts never seen before;
55
+ `off` disables it. Stored at `$DSH_HOME/remote-workspaces/known_hosts.json`; reset
56
+ with `/remote forget-key`.
57
+ - **Data lives under the harness home** — machines + mirrors follow `$DSH_HOME`; pre-0.6 data under `~/.dsh/remote-workspaces` is migrated automatically on first run.
58
+
59
+ ## Install
60
+
61
+ ```bash
62
+ dsh plugin add dsh-remote # add the bundle
63
+ ```
64
+
65
+ One command installs everything: since **v0.7.2** the sidebar
66
+ ([dsh-better-sidebar](https://www.npmjs.com/package/dsh-better-sidebar)) is a
67
+ hard dependency and is mounted automatically — the 🌐 remote-file explorer and
68
+ remote file viewer show up in the sidebar with no extra step. If you already
69
+ have the sidebar installed on its own, the embedded copy backs off (no double
70
+ mount) — regardless of whether the standalone bundle is listed **before or
71
+ after** `dsh-remote` in `dsh.profile.bundles` (order-independent guard since
72
+ 0.8.7; earlier versions crashed boot with `duplicate prefix route
73
+ "/sidebar/api"` when the standalone bundle came after `dsh-remote`).
74
+
75
+ > **Upgrading from ≤0.8.6 with a standalone sidebar?** You may keep the
76
+ > standalone `dsh-better-sidebar` bundle (any order) — 0.8.7+ no longer
77
+ > crashes. Or remove it from `bundles` and let dsh-remote mount the embedded
78
+ > copy (version ^0.18.1 since 0.8.15 — earlier releases pinned 0.14.x, whose
79
+ > `import { settingsNamespace } from "@deepseek-ai/dsh-settings"` broke once
80
+ > dsh-settings 0.1.2-alpha.2 made that symbol private; issue #29).
81
+
82
+ > **Requires the profile's pnpm linker to be `hoisted`** (the DSH profile
83
+ > default, `nodeLinker: hoisted` in `pnpm-workspace.yaml`). The loader resolves
84
+ > plugin packages from the profile root, so the sidebar must be reachable in
85
+ > the top-level `node_modules`. If your `pnpm-workspace.yaml` was rewritten
86
+ > without `nodeLinker: hoisted`, add it back (`nodeLinker: hoisted`) and run
87
+ > `pnpm install` once — otherwise the embedded sidebar row fails with
88
+ > `Cannot find package 'dsh-better-sidebar'`.
89
+
90
+ (or `npm install dsh-remote` + add `- id: dsh-remote / name: dsh-remote` in `cordis.patch.yml`).
91
+
92
+ ## Quick start
93
+
94
+ 1. **Add a machine** — Settings → 远程工作区 → add host/port/user + key or password → (optional) set it current.
95
+ 2. **Open a workspace** — click **Add workspace** in the sidebar / conversation:
96
+ - **本机** → system folder chooser (or type a local path) → local workspace.
97
+ On hosts without a usable OS dialog (DSH Desktop's browse backend, headless
98
+ SSH hosts without zenity/kdialog) the in-app directory browser pops up
99
+ instead — breadcrumbs, Windows drive switch, new-folder, pick-and-fill.
100
+ - **远程** → choose the machine → browse to a remote directory (or type `/path`) → "设为远程工作区" ⇒ a local mirror workspace is created and adopted.
101
+ 3. **Work with the agent** — treat it like any workspace:
102
+ - `rw_list_dir(path?)`/`rw_read_file` — inspect remote files
103
+ - `rw_write_file(path, content)` / `rw_edit(path, old, new)` — create / patch a remote file directly
104
+ - `rw_stat(path)` / `rw_mkdir(path)` / `rw_remove(path, recursive?)` / `rw_move(path, dest)` — manage remote paths
105
+ - `rw_search(pattern, path?)` — grep remote files (SFTP walk, Windows OK)
106
+ - `rw_exec(command, cwd?, pty?)` — run remote shell commands (defaults to the workspace dir)
107
+ - `rw_forward(listenPort, targetHost?, targetPort?)` — open an SSH tunnel
108
+ - `rw_sync(dryRun?/force?/async?)` / `rw_push(dryRun?/force?/async?)` — conflict-aware mirror pull/push
109
+
110
+ ## CLI defaults (optional)
111
+
112
+ Provide a default machine in `cordis.patch.yml`:
113
+
114
+ ```yaml
115
+ # Example only — use values for your own machine.
116
+ - id: dsh-remote
117
+ name: dsh-remote
118
+ config:
119
+ host: 203.0.113.10 # or your real host / hostname
120
+ port: 22
121
+ username: dev
122
+ privateKeyPath: ~/.ssh/id_rsa
123
+ # or password: '…'
124
+ workspace: ~/project
125
+ ```
126
+
127
+ If `host` is empty the plugin starts disconnected and you configure machines in the UI.
128
+
129
+ ## CLI quick reference
130
+
131
+ Installing and driving DSH may live in different shells, so both the `dsh` binary and the `npx` form are shown. Always tell DSH **which profile** to use with `--profile <name>` (usually `web`).
132
+
133
+ ```bash
134
+ # install the bundle into a profile (npm is pulled by pnpm; recommended)
135
+ dsh plugin --profile web add dsh-remote
136
+ # same but when `dsh` is not on PATH (e.g. Windows PowerShell inside a repo)
137
+ npx --yes @deepseek-ai/dsh plugin --profile web add dsh-remote
138
+
139
+ # confirm it is installed wire
140
+ dsh plugin --profile web list
141
+ npx --yes @deepseek-ai/dsh plugin --profile web list
142
+
143
+ # start the web surface (reload profile; the plugin activates on boot)
144
+ dsh --profile web
145
+ npx --yes @deepseek-ai/dsh --profile web # http://127.0.0.1:3080
146
+
147
+ # use a local checkout instead of the npm version (dev iteration)
148
+ npx --yes @deepseek-ai/dsh plugin --profile web add /path/to/dsh-remote
149
+ npx --yes @deepseek-ai/dsh plugin --profile web remove dsh-remote # back to release
150
+ ```
151
+
152
+ After a successful start, `Settings → 远程工作区` appears and the "Add workspace" flow gains the 本机 / 远程 tabs (screenshots above).
153
+
154
+ ## Development (sandbox, not product)
155
+
156
+ Iterate **in the sandbox**, never by hand-editing a product profile — the
157
+ product profile is re-managed by the plugin manager and reverts hand-deployed
158
+ files on reinstall. Use the helper script:
159
+
160
+ ```bash
161
+ scripts/dev-run.sh --restart # start / restart the isolated sandbox
162
+ scripts/dev-run.sh --stop # stop it
163
+ scripts/dev-run.sh --status # is it running?
164
+ ```
165
+
166
+ - Runs its own DSH instance (`dev-harness/harness` inside this repo) with the
167
+ plugin copied in from `lib/` — it boots through the same `bin.js web --patch`
168
+ path as the desktop app, so the sandbox reproduces the product boot behavior.
169
+ - The sandbox web UI serves on `http://127.0.0.1:50599` and the plugin routes
170
+ are live immediately (e.g. `GET /dsh-remote/machines`).
171
+ - **Host-half changes** (`lib/index.js`) need a sandbox restart (`--restart`);
172
+ **client-half changes** (`lib/client.js`) need a page refresh.
173
+ - Node ESM resolves dependencies from the importing file's real path, so the
174
+ script **copies** `lib/` (hardlink copy, `cp -al`) into the sandbox profile
175
+ instead of symlinking — a symlink breaks `@deepseek-ai/*` resolution.
176
+ - Run `scripts/check.mjs` (static framework-constraint gate: command-name
177
+ regex, …) before every commit; `scripts/boot-smoke.sh` boots an isolated
178
+ instance to prove the plugin still starts.
179
+ - Full rules live in `scripts/dev-standards.md` (command names, cordis service
180
+ access via `ctx.get()` only, optional framework services may never register,
181
+ verify third-party callback contracts against the real runtime, …).
182
+
183
+ Deploying to a product profile is a separate, explicit action (`./sync.sh`)
184
+ and should be done only when you intend to release.
185
+
186
+ ## Configuration
187
+
188
+ | Key | Type | Default | Meaning |
189
+ | --- | --- | --- | --- |
190
+ | `host` | string | `''` | default SSH host (else start disconnected) |
191
+ | `port` | int | `22` | default SSH port |
192
+ | `username` | string | `''` | default SSH user |
193
+ | `password` | string | `''` | default SSH password (non-empty overrides key) |
194
+ | `privateKeyPath` | string | `''` | private key path (used only when explicitly provided) |
195
+ | `passphrase` | string | `''` | passphrase for an encrypted private key |
196
+ | `workspace` | string | `''` | default remote workspace path |
197
+ | `shell` | string | `''` | remote command terminal strategy: `''`=auto-detect (Git Bash on Windows remotes), `'git-bash'`=prefer Git Bash, `'native'`=never wrap, anything else=explicit bash.exe path (e.g. `C:\Program Files\Git\bin\bash.exe`) |
198
+ | `commandTimeoutMs` | int | 20000 | per remote command timeout |
199
+ | `connectTimeoutMs` | int | 15000 | SSH connect timeout |
200
+ | `maxFileBytes` | int | 52428800 | skip mirroring/reading files larger than this (0 = no cap) |
201
+ | `hostKeyMode` | string | `accept-new` | host-key policy: `accept-new` (TOFU), `verify` (reject unknown hosts), `off` (skip) |
202
+ | `useAgent` | bool | `false` | authenticate via the OpenSSH agent (`SSH_AUTH_SOCK`) |
203
+ | `keyboardInteractive` | bool | `false` | allow keyboard-interactive auth (OTP/MFA) with the configured password |
204
+ | `proxy` | object | — | jump host: `{ host, port?, username?, password?, privateKeyPath? }` |
205
+ | `autoPush` | bool | `false` | auto-push edited mirror files back to the remote (watcher, debounced) |
206
+ | `auditLog` | bool | `true` | append executed commands to `$DSH_HOME/remote-workspaces/audit.log` |
207
+ | `encoding` | string | `utf-8` | text encoding for remote file reads/writes (e.g. `gbk`) |
208
+
209
+ ## FAQ / troubleshooting
210
+
211
+ **Host key 变了 / 提示可能中间人** — 主机重装过或密钥更换过:`/remote-forget-key`(或设置页 → 机器 → 重新信任),下次连接重新记录。
212
+
213
+ **连接报"认证失败"** — 检查用户名/密码/私钥路径;私钥加密了要填 Passphrase;公司机器要求 OTP/动态码时勾选 keyboard-interactive。
214
+
215
+ **连不上内网机器** — 走跳板机:机器表单里填「跳板机」主机(也可以先把它本身配成一台机器)。主机不可达类错误会给出分类提示。
216
+
217
+ **rw_sync/rw_push 报冲突** — 远端和本地都改过同一个文件时会跳过并列出冲突(绝不静默覆盖)。处理:手动合并后重新同步,或用 `force=true` 以一边为准。
218
+
219
+ **Windows 远程** — 列表/读写/搜索/同步全部走 SFTP 协议,不依赖 POSIX shell;中文文件用 `encoding=gbk` 读。
220
+
221
+ **镜像里没有某个目录** — 默认 ignore 规则(`.git`、`node_modules`、`target` 等)会跳过;在 `$DSH_HOME/remote-workspaces/.dsh-remote-ignore` 加 `!` 之外的条目即可调整(gitignore 语法)。
222
+
223
+ **侧边栏远程文件保存失败(409)** — 远端文件在你打开后已被改动,重新读取后再编辑(mtime 乐观锁保护)。
224
+
225
+ **密码怎么加密保存** — 机器表单勾选「加密保存密码」:macOS 用系统钥匙串(security),Windows 用 DPAPI,Linux 需要 secret-tool(libsecret);后端不可用时自动回退明文。
226
+
227
+ ## Safety
228
+
229
+ Giving the plugin a machine's credentials lets the agent run **shell commands as your user** on that host. Only add machines you trust. Passwords are saved on the local machine file (or the OS keychain when enabled); treat it as sensitive (you may lock file ACLs). Every executed command is recorded in the audit log when `auditLog` is on — review it from the Settings page.
230
+
231
+ ## License
232
+
233
+ MIT
234
+
235
+ ## Changelog
236
+
235
237
  See [CHANGELOG.md](./CHANGELOG.md).