dsh-remote 0.8.65 → 0.8.66

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-remote",
3
- "version": "0.8.65",
3
+ "version": "0.8.66",
4
4
  "description": "Remote-work assistant for DeepSeek Harness: connect SSH (password/key/agent/keyboard-interactive, multi-hop ProxyJump), pick a remote workspace, operate on it with 22 rw_* tools, conflict-aware mirror sync, an embedded PTY terminal, one-click remote dsh deploy + web UI attach, port forwarding, host grouping, remote sandbox fencing, audit log, keychain passwords.",
5
5
  "license": "MIT",
6
6
  "author": "flymysql <flyphp@outlook.com>",
@@ -1,142 +0,0 @@
1
- // dsh-remote — host-side deploy task manager.
2
- //
3
- // WHY THIS EXISTS: `install` used to be a single long HTTP request (npm can
4
- // legitimately take minutes). If the browser tab closed — or the fetch was
5
- // cancelled — the RESPONSE was lost and, with it, every trace of what the
6
- // deploy did: the user could not see the progress again, could not tell whether
7
- // it succeeded, and clicking the button again would start a SECOND concurrent
8
- // npm install into the same prefix. High availability here means:
9
- //
10
- // · the deploy runs on the HOST, not tied to any request lifecycle;
11
- // · every step's outcome is recorded as it happens;
12
- // · the UI can re-attach at any time and see the live state;
13
- // · a second click while one is running RE-ATTACHES instead of restarting;
14
- // · the result (including the machine's webAttachCommand) is recorded even
15
- // if no client ever polls for it.
16
- //
17
- // One deploy per machine at a time — npm --prefix into the same directory from
18
- // two processes corrupts the tree. A NEW request for a machine that already has
19
- // a running or finished deploy returns the EXISTING task (resume semantics);
20
- // `force: true` starts a fresh one after the previous is finished.
21
-
22
- /** A deploy task's lifecycle: queued → running → done | failed | cancelled. */
23
- export class DeployTasks {
24
- constructor() {
25
- /** @type {Map<string, object>} machineId → task */
26
- this.byMachine = new Map()
27
- /** @type {Map<string, object>} taskId → task (for stable polling) */
28
- this.byId = new Map()
29
- this.nextId = 1
30
- }
31
-
32
- /**
33
- * The task for a machine, if one exists in ANY state.
34
- * @returns {object|null}
35
- */
36
- forMachine(machineId) {
37
- return this.byMachine.get(String(machineId || '')) || null
38
- }
39
-
40
- get(id) {
41
- return this.byId.get(String(id || '')) || null
42
- }
43
-
44
- /**
45
- * Start a deploy for a machine, unless one is already running.
46
- *
47
- * @param {string} machineId
48
- * @param {Function} fn - async (ctx) => result. ctx = { progress(patch) }.
49
- * @returns {{task: object, reused: boolean}} the task plus whether it already
50
- * existed (caller reports "resumed" so the UI does not claim a fresh start).
51
- */
52
- start(machineId, label, fn) {
53
- const mid = String(machineId || '')
54
- const existing = this.byMachine.get(mid)
55
- if (existing && (existing.status === 'queued' || existing.status === 'running')) {
56
- return { task: existing, reused: true }
57
- }
58
- const id = `deploy-${this.nextId++}`
59
- const task = {
60
- id,
61
- machineId: mid,
62
- kind: 'deploy',
63
- label: String(label || ''),
64
- status: 'queued',
65
- // Steps are appended as they finish; `steps` is what the UI renders.
66
- steps: [],
67
- // The current in-flight step, so a re-attached UI can show "正在安装…"
68
- // rather than a stale list with no hint that more is coming.
69
- current: '',
70
- progress: {},
71
- result: null,
72
- error: null,
73
- startedAt: null,
74
- finishedAt: null,
75
- cancelled: false,
76
- }
77
- this.byMachine.set(mid, task)
78
- this.byId.set(id, task)
79
- // Deliberately NOT queued through the shared TaskManager: that one is
80
- // single-flight across ALL tasks (one sync at a time), while deploys to
81
- // DIFFERENT machines must run in parallel.
82
- task.status = 'running'
83
- task.startedAt = new Date().toISOString()
84
- const ctx = {
85
- progress: (p) => { task.progress = { ...task.progress, ...p } },
86
- get cancelled() { return !!task.cancelled },
87
- }
88
- Promise.resolve()
89
- .then(() => fn(ctx))
90
- .then((result) => {
91
- task.result = result || null
92
- task.status = task.cancelled ? 'cancelled' : 'done'
93
- })
94
- .catch((err) => {
95
- task.error = String((err && err.message) || err)
96
- // A step-level failure carries the step list (and hints) as structured
97
- // data so the UI can render exactly what the old single-response flow
98
- // rendered — re-attaching must lose nothing.
99
- if (err && err.detail && typeof err.detail === 'object') task.detail = err.detail
100
- task.status = task.cancelled ? 'cancelled' : 'failed'
101
- })
102
- .finally(() => {
103
- task.current = ''
104
- task.finishedAt = new Date().toISOString()
105
- })
106
- return { task, reused: false }
107
- }
108
-
109
- /** Cooperative cancel (a running npm step still finishes; later steps stop). */
110
- cancel(machineId) {
111
- const t = this.byMachine.get(String(machineId || ''))
112
- if (!t) return false
113
- if (t.status === 'queued' || t.status === 'running') {
114
- t.cancelled = true
115
- return true
116
- }
117
- return false
118
- }
119
-
120
- /** Serializable view for the UI. */
121
- describe(t) {
122
- if (!t) return null
123
- return {
124
- id: t.id,
125
- machineId: t.machineId,
126
- kind: t.kind,
127
- status: t.status, // queued | running | done | failed | cancelled
128
- steps: t.steps, // [{id,title,ok,code,output}]
129
- current: t.current, // in-flight step title ('' when none)
130
- progress: t.progress,
131
- result: t.result,
132
- error: t.error,
133
- detail: t.detail || null,
134
- startedAt: t.startedAt,
135
- finishedAt: t.finishedAt,
136
- }
137
- }
138
-
139
- list() {
140
- return [...this.byMachine.values()].map((t) => this.describe(t))
141
- }
142
- }
@@ -1,205 +0,0 @@
1
- // dsh-remote — built-in agent skill for remote DSH deployment (issue #46, P3).
2
- //
3
- // Layer 1 (`lib/web-deploy.js`) handles the deterministic path: probe the
4
- // remote, install to a private prefix, verify, attach. This module is the
5
- // ESCAPE HATCH for everything that path cannot express — a missing package
6
- // manager, a proxy, sudo-only systems, an internal mirror, a Windows remote, or
7
- // a failure whose message does not match any known signature.
8
- //
9
- // It is a skill rather than more code because the value here is JUDGEMENT over
10
- // arbitrary output, and because there is no closed set of remote environments to
11
- // enumerate. The skill body is therefore written as a diagnosis-first playbook:
12
- // the symptoms actually measured while building issue #46 come first, because
13
- // those are the ones that waste the most time.
14
- //
15
- // Two deliberate properties:
16
- //
17
- // 1. **The skill is registered at runtime, not shipped as a file.** The plugin
18
- // is a single npm package; a filesystem skill would need a writable
19
- // `$DSH_HOME/skills` and would collide with whatever the user keeps there.
20
- // `ctx.skills.register()` keeps it self-contained and namespaced.
21
- // 2. **It is user-invocable and model-invocable.** The settings button starts a
22
- // session that already has the skill selected; the model may also reach for
23
- // it on its own when an `rw_*` call fails with a deployment-shaped error.
24
- import { DEFAULT_INSTALL_VERSION, DEFAULT_PREFIX_SUFFIX } from './web-deploy.js'
25
-
26
- /** Stable skill name; kebab-case is required by the registry. */
27
- export const SKILL_NAME = 'dsh-remote-deploy'
28
-
29
- /**
30
- * The skill body.
31
- *
32
- * Written for an agent that already has this plugin's `rw_*` tools. It is
33
- * intentionally explicit about the two failure signatures measured on a real
34
- * host, because a generic "install it" instruction would have been useless for
35
- * both of them.
36
- */
37
- export const SKILL_BODY = `# 在远端机器部署并验证 DSH
38
-
39
- 用途:用户想连一台机器的 DSH 网页界面(\`dsh web\`),但连不上。你负责把环境修好并**证明**
40
- 它真的能用。确定性流程(探测/私有前缀安装/校验)已经由插件的「部署并验证」按钮实现;
41
- 你只在它做不到或失败时才被叫来。
42
-
43
- ## 先用工具,不要一上来就手工
44
-
45
- **第一件事:调 \`rw_deploy_probe\`。** 它是只读的,给出结构化体检:平台/架构、node/npm、
46
- 现有 dsh 及版本、**原生模块能否启动**、是否认识 \`--no-open\`、Git Bash、代理与 npm 源、
47
- 安装前缀可写性,以及判定结论(blocker/warn/info)与修复建议。先读它的结论,不要自己拼命令猜。
48
-
49
- 它**只读、不安装**(刻意的)。要装就用设置页的「部署并验证」卡片——它装到私有前缀并逐步校验。
50
- 你在这里的价值是处理那条路径覆盖不到的情况(见下文的长尾)。真需要自己安装时照本文流程做,
51
- 并在结论里说明为什么自动路径不适用。
52
-
53
- ## 两个已实测的失败签名(最重要)
54
-
55
- ### 1. \`Failed to load native module: pty.node\`
56
-
57
- **根因**:dsh 依赖 node-pty,而某些版本发布的包里**没有当前平台的预编译产物**。
58
- 实测:\`node-pty@1.1.0\`(dsh 0.1.0-rc.6 依赖它)只带 darwin/win32 的 prebuild,
59
- **没有 linux-x64**,所以 \`dsh web\` 在 Linux 上**根本起不来**。
60
- \`node-pty@1.2.0-beta.15\`(dsh 0.1.5-rc.2 起)带 Linux 产物。
61
-
62
- **判断**:\`ls <dsh根>/node_modules/node-pty/prebuilds/\` —— 少了 \`linux-x64\` 就是这个病。
63
- **修复**:装 \`@deepseek-ai/dsh@${DEFAULT_INSTALL_VERSION}\` 或更新到私有前缀(见下)。
64
-
65
- ### 2. \`error: unknown option '--no-open'\`
66
-
67
- **根因**:\`--no-open\` 是较新版本的 flag;老版本(实测 0.1.0-rc.6)不认识它,直接退出。
68
- **修复**:插件会自动去掉该 flag 重试一次,通常**不需要你处理**。只有当你手工拼命令时才注意。
69
-
70
- ### 不是部署问题的报错(别修错方向)
71
-
72
- - \`NO_ADAPTER: no adapter registered for provider ...\` / \`no API key for provider route\`
73
- ⇒ 是**宿主模型/provider 配置**问题,与远端部署无关。
74
- - \`NO_COOKIE\` / 401 ⇒ 登录态问题。
75
- - \`QUOTA\` / \`Insufficient Balance\` ⇒ 额度问题。
76
-
77
- ## 标准部署流程(私有前缀,绝不污染用户环境)
78
-
79
- **原则**:默认**不要** \`npm i -g\`。全局安装需要系统目录写权限、改用户 PATH、
80
- 还可能覆盖用户正在用的版本。用私有前缀:
81
-
82
- \`\`\`bash
83
- PREFIX="$HOME/${DEFAULT_PREFIX_SUFFIX}"
84
- mkdir -p "$PREFIX"
85
- printf '%s\\n' '{"name":"dsh-remote-install","private":true,"version":"0.0.0"}' > "$PREFIX/package.json"
86
- cd "$PREFIX" && npm install --prefix "$PREFIX" --no-audit --no-fund \\
87
- '@deepseek-ai/dsh@${DEFAULT_INSTALL_VERSION}'
88
- "$PREFIX/node_modules/.bin/dsh" --version
89
- \`\`\`
90
-
91
- (写一个私有 \`package.json\` 是为了防止 npm 向上找到无关项目、改掉它的依赖树。)
92
-
93
- **校验**(三步都要过,缺一步不算成功):
94
- \`\`\`bash
95
- BIN="$PREFIX/node_modules/.bin/dsh"
96
- test -x "$BIN" && "$BIN" --version # 1. 二进制存在
97
- ls "$PREFIX/node_modules/node-pty/prebuilds/" # 2. 有当前平台产物
98
- "$BIN" web --help | grep -q -- '--port' # 3. web 子命令在
99
- \`\`\`
100
-
101
- ⚠️ **常见误判**:\`.bin/dsh\` 是符号链接。\`realpath\` 后 \`dirname(dirname(...))\` 会落在
102
- \`<prefix>/node_modules/@deepseek-ai/dsh\`(包**内部**),那里**没有**兄弟 \`node-pty\`,
103
- 于是好安装被判成「缺 pty」。正确做法是**向上找到第一个含 \`node_modules\` 目录的祖先**。
104
-
105
- **最后必须端到端证明**(否则「装好了」没有意义):
106
- \`\`\`bash
107
- # 起一个只监听 127.0.0.1 的 web,抓到它打印的 token 行
108
- DSH_HOME=/tmp/dshweb-check setsid nohup "$BIN" --profile web --port 0 --no-open \\
109
- >/tmp/dshweb-check.log 2>&1 </dev/null &
110
- sleep 8; grep -m1 'dsh web:' /tmp/dshweb-check.log
111
- \`\`\`
112
- 拿到 \`http://127.0.0.1:<port>/?token=…\` 即成功。**用完要清理**(kill 掉并删 log)。
113
-
114
- ## 长尾情况
115
-
116
- - **没有 node/npm**:先装 Node(nvm 最不侵入)。装不了就问用户要权限。
117
- - **公司内网/镜像源**:用 \`npm install --registry <url>\`,或让用户指路。**不要**擅自改
118
- 用户全局 npm 配置。
119
- - **Windows 远端**:启动命令是 POSIX shell(依赖 \`setsid\`/\`nohup\`)。远端有 Git Bash
120
- 时可工作(插件会经 \`bash -s\` 执行);没有则如实说明暂不支持,不要硬试。
121
- - **代理**:\`HTTP_PROXY\`/\`HTTPS_PROXY\` 对 npm 生效;先探测是否需要。
122
- - **需要 sudo**:**停下来问用户**,不要自作主张提权。
123
-
124
- ## 纪律
125
-
126
- 1. **只读优先**:先诊断,再动手。诊断阶段不要写任何东西。
127
- 2. **不改用户环境**:不 \`npm i -g\`(除非用户明确要求)、不改 PATH、不动 \`~/.dsh\`。
128
- 3. **可回退**:安装只落在私有前缀,删掉那个目录就完全恢复。告诉用户路径。
129
- 4. **收尾清理**:你起的进程、写的临时 log、用的临时 DSH_HOME 都要清掉。
130
- 5. **报结论要带证据**:贴出关键命令的实际输出,不要只说「已修复」。
131
- 6. **不确定就问**:涉及提权、改全局配置、删文件时先确认。
132
- `
133
-
134
- /**
135
- * Register the deployment skill with the session skill registry.
136
- *
137
- * Optional by design: a composition without `ctx.skills` simply has no skill,
138
- * and the deterministic deploy path keeps working. Failures are contained —
139
- * a skill that fails to register must never take down the plugin.
140
- *
141
- * @param {object} ctx - plugin context.
142
- * @returns {boolean} whether the skill was registered.
143
- */
144
- export function registerDeploySkill(ctx) {
145
- try {
146
- const skills = ctx.get('skills')
147
- if (!skills || typeof skills.register !== 'function') return false
148
- skills.register({
149
- name: SKILL_NAME,
150
- description: '在远端机器部署并验证 DSH 网页界面:诊断「连不上」的原因、'
151
- + '装到私有前缀、校验原生模块与 web 子命令,并端到端证明可用。'
152
- + '当远端 dsh 缺失、版本过旧、或 dsh web 起不来时使用。',
153
- whenToUse: '远端 DSH 界面连不上;远端没有 dsh 或版本过旧;'
154
- + '出现 Failed to load native module: pty.node;需要判断是部署问题还是宿主问题。',
155
- content: SKILL_BODY,
156
- source: 'dsh-remote',
157
- // Both entry points are wanted: the settings button starts a session with
158
- // it pre-selected, and the model may reach for it when an rw_* call fails
159
- // in a deployment-shaped way.
160
- invocation: { modelInvocable: true, userInvocable: true },
161
- })
162
- return true
163
- } catch (err) {
164
- // A missing skill only costs the AI fallback; never break the plugin.
165
- try { ctx.logger?.warn?.(new Error('dsh-remote: deploy skill not registered: ' + String((err && err.message) || err))) } catch { /* no logger */ }
166
- return false
167
- }
168
- }
169
-
170
- /**
171
- * Build the prompt handed to the AI when the user asks it to investigate.
172
- *
173
- * The failure context is included verbatim because the whole point is that the
174
- * deterministic path could not classify it — paraphrasing would throw away the
175
- * only evidence there is.
176
- *
177
- * @param {object} context - what the deterministic path learned.
178
- * @returns {string} the prompt text.
179
- */
180
- export function buildInvestigatePrompt(context = {}) {
181
- const c = context || {}
182
- const lines = [
183
- `请用 ${SKILL_NAME} 技能,帮我修好这台远端机器的 DSH 网页界面问题。`,
184
- '',
185
- `远端:${c.host || '(未知主机)'}${c.user ? ' (' + c.user + ')' : ''}`,
186
- ]
187
- if (c.prefix) lines.push(`建议安装位置:${c.prefix}`)
188
- if (c.findings && c.findings.length) {
189
- lines.push('', '自动体检已发现:')
190
- for (const f of c.findings) {
191
- lines.push(`- [${f.severity}] ${f.summary}${f.detail ? ' —— ' + f.detail : ''}`)
192
- }
193
- }
194
- if (c.error) lines.push('', '自动部署/连接失败信息:', '```', String(c.error).slice(0, 2000), '```')
195
- if (c.facts && Object.keys(c.facts).length) {
196
- lines.push('', '体检原始事实:', '```json', JSON.stringify(c.facts, null, 2).slice(0, 2000), '```')
197
- }
198
- lines.push(
199
- '',
200
- '要求:先诊断、只读优先;装到私有前缀而不是全局;装完要**端到端证明**能起 web',
201
- '(拿到 `dsh web: http://127.0.0.1:<port>/?token=…`);收尾清理你起的进程和临时文件;',
202
- '最后用证据说明结论。涉及提权或改全局配置时先问我。',
203
- )
204
- return lines.join('\n')
205
- }