@zhengjunyao/dsh-restart 0.1.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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,27 @@
1
+ # Changelog
2
+
3
+ 本文件记录 `@zhengjunyao/dsh-restart` 的所有可见变更。版本纪律:破坏性变更不得走 patch(0.x 走 minor、1.x+ 走 major)。
4
+
5
+ ## [Unreleased]
6
+
7
+ ## [0.1.0] - 2026-09-12
8
+
9
+ 首个版本。
10
+
11
+ ### Added
12
+
13
+ - **一键重启**:设置页「重启」卡片 + 左侧边栏入口(与其它插件入口并排)+ 全屏重启遮罩;点一下即把重启交给分离的重启助手。
14
+ - **两种重启策略,自动识别**:
15
+ - launchd 托管(本机默认场景)→ 助手执行 `launchctl kickstart -k`,让 launchd 自己拉起,助手退居观察者并跟随 plist 日志;
16
+ - 非托管 → 助手用**完全相同的 argv/cwd/env** 重新拉起,等端口真正释放而不是赌固定延时。
17
+ - **自动重连**:页面探测 `/api/dsh-restart/probe`,新宿主一应答就自动刷新载入新代码;状态写 `sessionStorage`,重启中途刷新页面也能续跑(`autoReload` 可关)。
18
+ - **失败可读**:新进程 stdout/stderr 实时进日志,疑似报错行被单独挑出显示;DSH 已完全挂掉时,助手自带的恢复控制台(默认 `http://127.0.0.1:3099`,CORS 开放)仍可用(阶段/日志/报错行/退出码 + 一键重试)。
19
+ - **失败报告**:失败时写一份自包含单文件 `~/.dsh/dsh-restart/last-failure.md`,控制台 `GET /report` 与页面「复制完整报告」一键取用。
20
+ - **agent 工具**:`dsh_restart_status`(只读)、`dsh_restart`(需 `confirm: true`,重启前必须先获得用户同意)。
21
+ - 配置 `~/.dsh/dsh-restart.json`(0600);日志 `~/.dsh/dsh-restart/logs/`;重启历史 `history.json`。
22
+
23
+ ### Notes
24
+
25
+ - 插件目录按 DSH 约定解析:`DSH_RESTART_HOME` → `DSH_HOME` → `~/.dsh`。
26
+ - 同机多实例共享插件目录时,助手状态按 `oldPid`/`childPid` 判归属,不会把兄弟实例的失败算到本机头上。
27
+ - 已知限制:UI 目前为中文单语;headless / cron profile 因 `inject` 要求 `webServer` 而不会加载。
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 zhengjy01
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 ADDED
@@ -0,0 +1,116 @@
1
+ # @zhengjunyao/dsh-restart
2
+
3
+ English | [中文](README.zh.md)
4
+
5
+ A restart button for DeepSeek Harness. Installing or updating a plugin changes
6
+ host-side code, and only a fresh `dsh web` process picks it up — that used to
7
+ mean going back to a terminal. Now it is one click in the Web GUI: the page
8
+ reconnects on its own, and **if the new process fails to boot, the error is
9
+ shown right there** (an in-page overlay, plus a recovery console on its own port
10
+ that stays up even when DSH is dead).
11
+
12
+ ## Why
13
+
14
+ A self-restart is the moment things go wrong invisibly: the tab loses its
15
+ server, the new process crashes, and the only trace is in a terminal you already
16
+ left. `dsh-restart` handles all three:
17
+
18
+ - **one click** from the settings card or the sidebar entry;
19
+ - **two strategies, auto-detected** — when the host is a launchd job
20
+ (`com.dsh.web`, `KeepAlive`), the helper delegates with
21
+ `launchctl kickstart -k` and only *observes* (spawning our own host would race
22
+ the job for the port); otherwise it relaunches the exact same command
23
+ (argv / cwd / env / Node flags) after waiting for the port to be *actually*
24
+ released, instead of guessing with a fixed delay;
25
+ - **the new process's stdout+stderr stream into a log**, the lines that look like
26
+ errors are lifted out, and the helper's recovery console
27
+ (`http://127.0.0.1:3099` by default, CORS-open) keeps answering after DSH is
28
+ gone — showing phase, boot log, error lines and exit code, with a one-click
29
+ retry.
30
+
31
+ ## Features
32
+
33
+ - Settings card 「重启」, sidebar entry next to your other plugin entries, and a
34
+ full-screen overlay while the handoff happens.
35
+ - Auto-reconnect: the page probes `/api/dsh-restart/probe` and reloads itself as
36
+ soon as the new host answers (`autoReload`, on by default).
37
+ - Failure handling: error lines are detected (Error / EADDRINUSE /
38
+ MODULE_NOT_FOUND / stack frames), the boot log is shown in the overlay, and
39
+ `maxAttempts` (default 2) automatic retries run before giving up.
40
+ - Agent tools: `dsh_restart_status` (read-only) and `dsh_restart`, which demands
41
+ `confirm: true` because the local standing rule is that DSH is never restarted
42
+ without explicit user consent.
43
+ - History at `~/.dsh/dsh-restart/history.json`; logs under
44
+ `~/.dsh/dsh-restart/logs/`; config at `~/.dsh/dsh-restart.json` (0600).
45
+
46
+ ## Install
47
+
48
+ ```sh
49
+ dsh plugin --profile web add @zhengjunyao/dsh-restart # npm
50
+ dsh plugin --profile web add link:/path/to/dsh-restart
51
+ # or, from GitHub (repo tagged with the dsh-plugin topic)
52
+ dsh plugin --profile web add github:zhengjy01/dsh-restart
53
+ ```
54
+
55
+ Restart `dsh web` once to load it — the last manual restart you need.
56
+
57
+ ## HTTP surface
58
+
59
+ All loopback-only, same-origin, matching the other `dsh-*` panels:
60
+ `GET /status`, `GET /probe`, `POST /restart`, `GET /logs`, `GET /history`,
61
+ `POST /config`, `GET /helper`, `POST /helper/retry` — all under
62
+ `/api/dsh-restart/`.
63
+
64
+ ## How it works
65
+
66
+ ```
67
+ panel / dsh_restart
68
+ │ POST /api/dsh-restart/restart
69
+
70
+ host (old process) ──writes pending-spec.json──▶ detached helper (zero deps)
71
+ │ replies 202, SIGTERMs itself ~0.7s later │
72
+ ▼ │ waits for the port
73
+ process exits ─────────────────────────────────────────────┤
74
+
75
+ spawns the new host with the same command
76
+ (stdout/stderr → logs/<stamp>-<pid>.log)
77
+
78
+ status.json ◀── phase/progress/errors ───┤
79
+ http://127.0.0.1:3099 ◀── recovery console┘
80
+
81
+ page polls /probe ──▶ new host answers ──▶ location.reload()┘
82
+ ```
83
+
84
+ ## Compatibility
85
+
86
+ - **Requires** DeepSeek Harness **≥ 0.1.5-rc.1** (declared as `dsh.engines.dsh`).
87
+ - **Verified against**: **0.1.5-rc.1** on macOS with Node 25.8.1 — host half, browser half and a real restart.
88
+ - **Platforms**: only **macOS** has actually been exercised. When the host is launchd-managed, the helper delegates with `launchctl kickstart -k`; elsewhere it falls back to relaunching the same command itself (the launchd path is platform-guarded; Linux and Windows are untested).
89
+ - Compatibility is also derived from the union of the `@deepseek-ai/dsh-*` `peerDependencies` ranges, which is what the plugin market displays.
90
+
91
+ ## Tests
92
+
93
+ ```sh
94
+ pnpm test # 129 assertions across five suites
95
+ ```
96
+
97
+ `smoke` (config/history/log detection/host identity), `helper` (the real helper
98
+ against fake hosts: crash capture + console + manual retry, and a successful
99
+ relaunch), `routes` (synthetic req/res, including the loopback/cross-site/method
100
+ guards), `handoff` (end-to-end on fake ports: restart → old process really exits
101
+ → helper relaunches generation 2 → the port answers with a new pid), `launchd`
102
+ (pid→job matching and observe mode, which must never spawn).
103
+
104
+ ## Limits
105
+
106
+ - Only restarts: it does not install plugins or repair a profile (that is
107
+ `dsh-doctor`'s job).
108
+ - A restart always interrupts the current turn and connection — replacing the
109
+ host process cannot be invisible. What is guaranteed is that the interruption
110
+ is *observable, recoverable, and its errors readable*.
111
+ - The helper registers no OS-level service; it lives for one restart and exits
112
+ after readiness (or stays put on failure until you deal with it).
113
+
114
+ ## License
115
+
116
+ MIT
package/README.zh.md ADDED
@@ -0,0 +1,163 @@
1
+ # @zhengjunyao/dsh-restart
2
+
3
+ [English](README.md) | 中文
4
+
5
+ 给 DeepSeek Harness 装一个「重启」按钮:装完/更新插件后不用再回终端敲命令,
6
+ 在 Web GUI 里点一下就把 DSH 重启了——页面自己重连、自己刷新;**如果新进程起不来,
7
+ 它会把报错直接显示出来**(页内遮罩 + 一个独立端口的恢复控制台)。
8
+
9
+ ## 为什么需要它
10
+
11
+ 宿主侧的插件代码(`lib/index.js`、bundle 清单、`dsh.client` 清单)只有换一个
12
+ `dsh web` 进程才会生效,所以每次装插件都得手动重启一次。而这个「手动重启」本身
13
+ 是最容易出事的环节:
14
+
15
+ - 重启后进程起不来 → 浏览器一片空白,看不到任何原因;
16
+ - 新进程崩溃/端口占用 → 只有终端能看见报错;
17
+ - 重启期间页面直接失联 → 只能靠人手刷新。
18
+
19
+ `dsh-restart` 把这三件事都接住了。
20
+
21
+ ## 能力
22
+
23
+ - **一键重启**:设置页「重启」卡片、左侧边栏「重启」入口(与其它插件入口并排),
24
+ 点一下即完成交接。
25
+ - **两种重启策略,自动识别**:
26
+ - **launchd 托管(本机默认)**:宿主是 launchd 任务(`com.dsh.web`、`KeepAlive`)时,
27
+ 自己 spawn 一个宿主会和 launchd 抢端口,所以改为助手 `launchctl kickstart -k` 让
28
+ launchd 自己重启,助手退居**观察者**(只等端口回来 + 跟随 plist 的 stdout/stderr 日志),
29
+ 绝不重复拉起。
30
+ - **自拉起**:非托管宿主则由助手用**完全相同的 argv/cwd/env** 重新拉起,
31
+ 先等端口真正释放(不是赌固定延时)再 spawn,不会撞 `EADDRINUSE`。
32
+ - 助手本身是 detached 的零依赖纯 Node ESM(`helper/restart-helper.mjs`),
33
+ 两种策略共用同一套日志/状态/控制台。
34
+ - **自动重连**:页面按配置的间隔探测 `/api/dsh-restart/probe`,新宿主一应答就自动
35
+ 刷新(`autoReload`,默认开),载入新代码。
36
+ - **报错直显**:新进程的 stdout+stderr 实时流入 `~/.dsh/dsh-restart/logs/*.log`;
37
+ 助手把疑似报错的行(Error / EADDRINUSE / MODULE_NOT_FOUND / 调用栈 …)单独挑出来,
38
+ 在页内遮罩里显示;DSH 已经完全挂掉时,助手自己的恢复控制台
39
+ (默认 `http://127.0.0.1:3099`,CORS 开放)仍然可用,能看到阶段、启动日志、
40
+ 报错行、退出码,并可一键「重试启动」。
41
+ - **失败自动重试**:`maxAttempts`(默认 2)次内自动重试;都失败就停在那里等人工处理,
42
+ 不会把端口/日志丢掉。
43
+ - **Agent 工具**:`dsh_restart_status`(只读:宿主 pid/端口/版本/启动时长/启动命令、
44
+ 助手阶段与失败原因、最近重启记录、上次启动日志里的疑似报错行)、
45
+ `dsh_restart`(真正重启;**必须已获得用户同意**并传 `confirm: true`,
46
+ 否则只返回提示不执行)。
47
+ - **重启历史**:`~/.dsh/dsh-restart/history.json` 记录每次重启的时间、来源、原因、
48
+ 新旧 pid 与日志路径。
49
+
50
+ ## 安装
51
+
52
+ ```sh
53
+ # 本地开发
54
+ dsh plugin --profile web add @zhengjunyao/dsh-restart # npm
55
+ dsh plugin --profile web add link:/path/to/dsh-restart
56
+ # 从 GitHub(仓库打 dsh-plugin topic)
57
+ dsh plugin --profile web add github:zhengjy01/dsh-restart
58
+ ```
59
+
60
+ 装完需要重启一次 `dsh web` 才会加载——**这一次是最后一次手动重启**。
61
+
62
+ ## 使用
63
+
64
+ 1. 打开「设置 → 插件配置 → Web 插件 → 重启」(或点左侧边栏的重启图标)。
65
+ 2. 点「立即重启」:
66
+ - 出现重启遮罩,显示阶段(下发指令 → 旧进程退出 → 新宿主启动 → 已就绪)与已等待时间;
67
+ - 宿主退出(launchd 托管时由 `launchctl kickstart -k` 触发)、助手接管、新宿主起来后页面自动刷新;
68
+ - 若新宿主起不来,遮罩里直接出现 `Error: …` 与日志尾部,可「复制报错」「让助手重试启动」
69
+ 「打开恢复控制台」。
70
+ 3. 面板里还能看到:宿主信息、上次启动日志(含疑似报错行)、重启记录、插件设置。
71
+
72
+ ## 配置
73
+
74
+ `~/.dsh/dsh-restart.json`(0600,首次加载时按插件行种子生成;面板可改):
75
+
76
+ | 键 | 默认 | 含义 |
77
+ | --- | --- | --- |
78
+ | `enabled` | `true` | 总开关(关闭后不挂载路由与工具) |
79
+ | `announceToAgent` | `true` | 在系统提示里公告插件能力 |
80
+ | `entry` | `sidebar` | 入口位置:`sidebar` / `ball` / `both` / `off` |
81
+ | `restartMode` | `auto` | `auto`(识别到 launchd 就交给它)/ `launchd`(强制,找不到任务则报错)/ `helper`(强制自拉起) |
82
+ | `fallbackPort` | `3099` | 恢复控制台端口(被占用时自动 +1…+9) |
83
+ | `bootTimeoutMs` | `120000` | 新宿主多久没应答算这次尝试失败 |
84
+ | `maxAttempts` | `2` | 单次重启请求的启动尝试次数 |
85
+ | `killGraceMs` | `6000` | 端口迟迟不释放时,助手 SIGKILL 旧进程前的宽限 |
86
+ | `portFreeTimeoutMs` | `25000` | 等旧进程释放端口的上限 |
87
+ | `lingerMs` | `4000` | 就绪后助手退出前保留控制台的时间 |
88
+ | `logLines` | `200` | 面板/接口返回的日志行数 |
89
+ | `autoReload` | `true` | 新宿主应答后自动刷新页面 |
90
+ | `showOverlay` | `true` | 重启时显示全屏遮罩 |
91
+ | `probeIntervalMs` | `1200` | 重连探测间隔 |
92
+ | `historyLimit` | `30` | 重启历史保留条数 |
93
+
94
+ ## HTTP 接口
95
+
96
+ 全部 loopback-only(127.0.0.1 / ::1,同源),沿用其它 dsh-* 面板的守卫:
97
+
98
+ | 方法 | 路径 | 说明 |
99
+ | --- | --- | --- |
100
+ | GET | `/api/dsh-restart/status` | 宿主 + 助手 + 配置 + 历史 |
101
+ | GET | `/api/dsh-restart/probe` | 极小存活探针(重连时高频轮询) |
102
+ | POST | `/api/dsh-restart/restart` | 交接重启,先回 202 再退出本进程 |
103
+ | GET | `/api/dsh-restart/logs` | 启动日志尾部 + 疑似报错行 |
104
+ | GET | `/api/dsh-restart/history` | 重启记录 |
105
+ | POST | `/api/dsh-restart/config` | 改配置 / `reset: true` 恢复默认 |
106
+ | GET | `/api/dsh-restart/helper` | 经宿主读取助手实时状态 |
107
+ | POST | `/api/dsh-restart/helper/retry` | 让失败的助手再试一次 |
108
+
109
+ ## 工作方式
110
+
111
+ ```
112
+ 面板 / dsh_restart
113
+ │ POST /api/dsh-restart/restart
114
+
115
+ 宿主(旧进程)──写 pending-spec.json──▶ 分离的助手进程(detached,零依赖)
116
+ │ 回 202,延迟 ~0.7s 后 SIGTERM 自己 │
117
+ ▼ │ 等端口释放
118
+ 进程退出 ─────────────────────────────────────────────────┤
119
+
120
+ 用完全相同的命令 spawn 新宿主
121
+ (stdout/stderr → logs/<时间>-<pid>.log)
122
+
123
+ status.json ◀── 阶段/进度/报错 ──────────┤
124
+ http://127.0.0.1:3099 ◀── 恢复控制台 ───┘
125
+
126
+ 页面轮询 /probe ──▶ 新宿主应答 ──▶ location.reload() ◀─────┘
127
+ ```
128
+
129
+ ## 兼容性
130
+
131
+ - **要求**:DeepSeek Harness **≥ 0.1.5-rc.1**(即 `package.json` 的 `dsh.engines.dsh`)。
132
+ - **实测通过**:**0.1.5-rc.1**(macOS + Node 25.8.1;宿主半、客户端半、真实重启全流程)。
133
+ - **平台**:只在 **macOS** 上实测过。macOS 且宿主由 launchd 托管时走 `launchctl kickstart -k`;其它平台自动落到「分离助手自拉起」路径(代码里对 launchd 有平台守卫,Linux / Windows 未实测)。
134
+ - 兼容性判定也来自 `peerDependencies` 的 `@deepseek-ai/dsh-*` 范围并集(插件市场展示的是这一项)。
135
+
136
+ ## 测试
137
+
138
+ ```sh
139
+ pnpm test # 129 项断言,五个套件
140
+ ```
141
+
142
+ - `tests/smoke.mjs` — 配置读写与钳制、历史、日志尾部与报错识别、启动签名、宿主信息;
143
+ - `tests/helper.mjs` — **真的**跑助手:崩溃路径(捕获退出码、stderr 报错行、控制台页面、
144
+ 手动重试)与成功路径(等端口 → 拉起 → 就绪计时);
145
+ - `tests/routes.mjs` — 合成的 req/res 打全部路由,含 loopback/跨站/方法守卫;
146
+ - `tests/handoff.mjs` — 端到端:假宿主进程 → POST 重启 → 旧进程真的退出 →
147
+ 助手用相同命令拉起第二代 → 端口重新应答(新 pid)、`restarted: true`、历史落盘;
148
+ - `tests/launchd.mjs` — launchd 识别(**按 pid 匹配 `launchctl list`**,因为 Node 会把
149
+ `XPC_SERVICE_NAME` 改写成 `0`)+ observe 模式(助手执行 kickCommand、跟随托管方日志、
150
+ **绝不自己 spawn**)。
151
+
152
+ ## 边界
153
+
154
+ - 只负责「重启」这一件事:不做插件安装、不做配置修复(那是 `dsh-doctor` 的领域)。
155
+ - 重启一定会中断当前回合与连接——这是宿主进程被替换的必然结果;本插件保证的是
156
+ **中断可见、可恢复、报错可读**,而不是「不中断」。
157
+ - 助手不会注册任何 OS 级后台服务;它就活一次重启,成功后就绪 + 保留控制台数秒即退出,
158
+ 失败时留在原地等你处理(可随时 `kill`)。
159
+ - `dsh_restart` 工具强制 `confirm: true`:本机规则是未经用户同意不得重启 DSH。
160
+
161
+ ## License
162
+
163
+ MIT
@@ -0,0 +1,17 @@
1
+ # dsh-restart bundle patch: inserts the plugin row into the profile roster.
2
+ # Applied as a profile bundle layer (declared by the `dsh.bundle.patch`
3
+ # manifest field in package.json). Activate with:
4
+ #
5
+ # # local development (link)
6
+ # dsh plugin --profile web add @zhengjunyao/dsh-restart
7
+ # # after publishing to GitHub (repo tagged with the `dsh-plugin` topic)
8
+ # dsh plugin --profile web add github:zhengjy01/dsh-restart
9
+ - insert:
10
+ - id: restart
11
+ name: '@zhengjunyao/dsh-restart'
12
+ config:
13
+ announceToAgent: true # 在系统提示中公告插件(工具 + 能力说明)
14
+ enabled: true # 插件总开关
15
+ # 其余设置只是「首次写入 ~/.dsh/dsh-restart.json 的种子」;真正的
16
+ # 开关在配置文件里(面板「插件设置」或直接改文件),重启助手每次
17
+ # 重启都会重新读取。