dsh-stop-service 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -7
- package/lib/client.js +190 -78
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,13 +4,15 @@ A stop-service button for DeepSeek Harness (DSH) Web: one confirmed click gracef
|
|
|
4
4
|
|
|
5
5
|
## 功能 / Features
|
|
6
6
|
|
|
7
|
-
- **实时服务信息**:进程 PID、启动时间、运行时长、活跃会话数、内存占用、CPU 占用、Node / DSH 宿主 / 插件版本、平台——每 5
|
|
7
|
+
- **实时服务信息**:进程 PID、启动时间、运行时长、活跃会话数、内存占用、CPU 占用、Node / DSH 宿主 / 插件版本、平台——每 5 秒自动刷新,附内存/CPU 趋势迷你图;CPU ≥ 80% 或内存 ≥ 1024 MB 时对应行高亮警示
|
|
8
|
+
- **更新提醒**:面板顶部对比 npm registry 最新版,DSH 宿主或插件有新版时提示并附升级命令(浏览器端直查 registry,宿主不外联)
|
|
8
9
|
- **复制诊断信息**:一键复制版本与运行状态摘要,提 issue 时直接粘贴
|
|
9
10
|
- 带**确认弹窗**的「终止服务」按钮:确认框显示将断开的活跃会话数;宿主进程收到 SIGTERM 优雅退出——会话落盘、端口释放,效果与在终端执行 `kill <pid>` 完全一致
|
|
10
11
|
- **服务回归自动刷新**:停止后持续探测,终端里重启完成时页面自动刷新接回
|
|
11
12
|
- 界面适配 `--dsw-*` 主题变量,跟随皮肤切换;中英双语文案
|
|
12
13
|
|
|
13
|
-
- Live service info: PID, start time, uptime, active sessions, memory, CPU, Node/DSH/plugin versions, platform — auto-refreshed every 5s, with
|
|
14
|
+
- Live service info: PID, start time, uptime, active sessions, memory, CPU, Node/DSH/plugin versions, platform — auto-refreshed every 5s, with memory/CPU sparklines; rows highlight when CPU ≥ 80% or memory ≥ 1024 MB.
|
|
15
|
+
- Update banner: compares against npm dist-tags from the browser (the host never phones out) and shows the upgrade command when a newer DSH host or plugin exists.
|
|
14
16
|
- Copy-diagnostics button for issue reports.
|
|
15
17
|
- Confirm-guarded stop button that names the live session count; SIGTERM graceful shutdown, identical to `kill <pid>` from a terminal.
|
|
16
18
|
- Auto-reload once the restarted host comes back.
|
|
@@ -23,17 +25,19 @@ A stop-service button for DeepSeek Harness (DSH) Web: one confirmed click gracef
|
|
|
23
25
|
|
|
24
26
|
## 安装 / Install
|
|
25
27
|
|
|
26
|
-
|
|
27
|
-
> 本插件**未发布到 npm**——`npm install dsh-stop-service` 与 `dsh plugin add dsh-stop-service` 均不可用,请使用下方 Git 安装方式。
|
|
28
|
-
> This plugin is **NOT published on npm** — the npm-based install commands will not work. Use the Git-based methods below.
|
|
28
|
+
方式一 / Option 1 — npm 直装(推荐 / recommended):
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
```sh
|
|
31
|
+
dsh plugin --profile web add dsh-stop-service
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
方式二 / Option 2 — GitHub 直装:
|
|
31
35
|
|
|
32
36
|
```sh
|
|
33
37
|
dsh plugin --profile web add github:KhaosGx/dsh-stop-service
|
|
34
38
|
```
|
|
35
39
|
|
|
36
|
-
|
|
40
|
+
方式三 / Option 3 — 克隆后本地 link(适合参与开发 / for development):
|
|
37
41
|
|
|
38
42
|
```sh
|
|
39
43
|
git clone https://github.com/KhaosGx/dsh-stop-service.git
|
package/lib/client.js
CHANGED
|
@@ -62,9 +62,39 @@ window.__ModuleLoader__.load({
|
|
|
62
62
|
return `${sec}${t("secUnit")}`;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const
|
|
65
|
+
/** Normalize values into a 100x28 viewBox polyline. */
|
|
66
|
+
function sparklinePoints(values) {
|
|
67
|
+
const min = Math.min(...values);
|
|
68
|
+
const max = Math.max(...values);
|
|
69
|
+
const span = max - min || 1;
|
|
70
|
+
const step = 100 / (values.length - 1);
|
|
71
|
+
return values
|
|
72
|
+
.map((v, i) => `${(i * step).toFixed(1)},${(25 - ((v - min) / span) * 22).toFixed(1)}`)
|
|
73
|
+
.join(" ");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** One labeled sparkline; renders nothing before two samples exist. */
|
|
77
|
+
const TrendChart = ({ label, values }) => values.length >= 2
|
|
78
|
+
? jsx("div", { style: { marginTop: 10 }, children: [
|
|
79
|
+
jsx("div", { style: { fontSize: 11, opacity: 0.55, marginBottom: 4 }, children: label }),
|
|
80
|
+
jsx("svg", {
|
|
81
|
+
width: "100%",
|
|
82
|
+
height: 28,
|
|
83
|
+
viewBox: "0 0 100 28",
|
|
84
|
+
preserveAspectRatio: "none",
|
|
85
|
+
style: { display: "block" },
|
|
86
|
+
children: jsx("polyline", {
|
|
87
|
+
points: sparklinePoints(values),
|
|
88
|
+
fill: "none",
|
|
89
|
+
stroke: "var(--dsw-accent, #4a90d9)",
|
|
90
|
+
strokeWidth: 1.4,
|
|
91
|
+
vectorEffect: "non-scaling-stroke"
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
] })
|
|
95
|
+
: null;
|
|
96
|
+
|
|
97
|
+
const InfoCard = ({ t, info, history, cpuHistory }) => {
|
|
68
98
|
const [copied, setCopied] = react.useState(false);
|
|
69
99
|
|
|
70
100
|
const copyDiagnostics = async () => {
|
|
@@ -87,40 +117,24 @@ window.__ModuleLoader__.load({
|
|
|
87
117
|
};
|
|
88
118
|
|
|
89
119
|
react.useEffect(() => {
|
|
90
|
-
|
|
91
|
-
const load = async () => {
|
|
92
|
-
try {
|
|
93
|
-
const res = await fetch("/api/dsh-stop-service/info");
|
|
94
|
-
const data = await res.json();
|
|
95
|
-
if (alive && data?.ok) {
|
|
96
|
-
setInfo(data);
|
|
97
|
-
if (typeof data.memoryRssMb === "number") {
|
|
98
|
-
setHistory((prev) => [...prev, data.memoryRssMb].slice(-36));
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
} catch {
|
|
102
|
-
// Host unreachable (e.g. stopping); keep the last snapshot.
|
|
103
|
-
}
|
|
104
|
-
};
|
|
105
|
-
load();
|
|
106
|
-
const timer = window.setInterval(load, 5000);
|
|
107
|
-
return () => {
|
|
108
|
-
alive = false;
|
|
109
|
-
window.clearInterval(timer);
|
|
110
|
-
};
|
|
120
|
+
// Polling lives in StopServiceTab; this card only renders.
|
|
111
121
|
}, []);
|
|
112
122
|
|
|
113
123
|
if (info === null) {
|
|
114
124
|
return jsx("p", { style: { fontSize: 13, opacity: 0.6 }, children: t("loading") });
|
|
115
125
|
}
|
|
116
126
|
|
|
127
|
+
/** Display thresholds for the warning highlight. */
|
|
128
|
+
const CPU_WARN_PERCENT = 80;
|
|
129
|
+
const MEM_WARN_MB = 1024;
|
|
130
|
+
|
|
117
131
|
const rows = [
|
|
118
132
|
[t("infoPid"), String(info.pid)],
|
|
119
133
|
[t("infoStartedAt"), new Date(info.startedAt).toLocaleString()],
|
|
120
134
|
[t("infoUptime"), formatUptime(info.uptimeMs, t)],
|
|
121
135
|
[t("infoSessions"), typeof info.sessionCount === "number" ? String(info.sessionCount) : "-"],
|
|
122
|
-
[t("infoMemory"), `${info.memoryRssMb} MB
|
|
123
|
-
[t("infoCpu"), typeof info.cpuPercent === "number" ? `${info.cpuPercent}%` : "-"],
|
|
136
|
+
[t("infoMemory"), `${info.memoryRssMb} MB`, info.memoryRssMb >= MEM_WARN_MB],
|
|
137
|
+
[t("infoCpu"), typeof info.cpuPercent === "number" ? `${info.cpuPercent}%` : "-", typeof info.cpuPercent === "number" && info.cpuPercent >= CPU_WARN_PERCENT],
|
|
124
138
|
[t("infoNode"), info.node ?? "-"],
|
|
125
139
|
[t("infoHostVersion"), info.hostVersion ?? "-"],
|
|
126
140
|
[t("infoPluginVersion"), info.pluginVersion ?? "-"],
|
|
@@ -155,38 +169,25 @@ window.__ModuleLoader__.load({
|
|
|
155
169
|
})
|
|
156
170
|
]
|
|
157
171
|
}),
|
|
158
|
-
...rows.map(([label, value]) => jsx("div", { style: rowStyle, children: [
|
|
172
|
+
...rows.map(([label, value, warn]) => jsx("div", { style: rowStyle, children: [
|
|
159
173
|
jsx("span", { style: labelStyle, children: label }),
|
|
160
|
-
jsx("span", {
|
|
174
|
+
jsx("span", {
|
|
175
|
+
style: warn === true
|
|
176
|
+
? { ...valueStyle, color: "var(--dsw-danger, #d33)", fontWeight: 600 }
|
|
177
|
+
: valueStyle,
|
|
178
|
+
children: value
|
|
179
|
+
})
|
|
161
180
|
] }, label)),
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
children: jsx("polyline", {
|
|
173
|
-
points: (() => {
|
|
174
|
-
const min = Math.min(...history);
|
|
175
|
-
const max = Math.max(...history);
|
|
176
|
-
const span = max - min || 1;
|
|
177
|
-
const step = 100 / (history.length - 1);
|
|
178
|
-
return history
|
|
179
|
-
.map((v, i) => `${(i * step).toFixed(1)},${(25 - ((v - min) / span) * 22).toFixed(1)}`)
|
|
180
|
-
.join(" ");
|
|
181
|
-
})(),
|
|
182
|
-
fill: "none",
|
|
183
|
-
stroke: "var(--dsw-accent, #4a90d9)",
|
|
184
|
-
strokeWidth: 1.4,
|
|
185
|
-
vectorEffect: "non-scaling-stroke"
|
|
186
|
-
})
|
|
187
|
-
})
|
|
188
|
-
] })
|
|
189
|
-
: null,
|
|
181
|
+
jsx(TrendChart, {
|
|
182
|
+
key: "mem-chart",
|
|
183
|
+
label: `${t("memTrend")} (${history.length * 5}s)`,
|
|
184
|
+
values: history
|
|
185
|
+
}),
|
|
186
|
+
jsx(TrendChart, {
|
|
187
|
+
key: "cpu-chart",
|
|
188
|
+
label: `${t("cpuTrend")} (${cpuHistory.length * 5}s)`,
|
|
189
|
+
values: cpuHistory
|
|
190
|
+
}),
|
|
190
191
|
]
|
|
191
192
|
});
|
|
192
193
|
};
|
|
@@ -246,31 +247,31 @@ window.__ModuleLoader__.load({
|
|
|
246
247
|
}
|
|
247
248
|
|
|
248
249
|
return jsx("div", {
|
|
249
|
-
style:
|
|
250
|
+
style: {
|
|
251
|
+
...cardStyle,
|
|
252
|
+
padding: "12px 16px",
|
|
253
|
+
display: "flex",
|
|
254
|
+
justifyContent: "space-between",
|
|
255
|
+
alignItems: "center",
|
|
256
|
+
gap: 16,
|
|
257
|
+
marginBottom: 20,
|
|
258
|
+
flexWrap: "wrap",
|
|
259
|
+
},
|
|
250
260
|
children: [
|
|
251
|
-
jsx("
|
|
252
|
-
key: "title",
|
|
253
|
-
style: { margin: "0 0 8px", fontSize: 15 },
|
|
254
|
-
children: t("title")
|
|
255
|
-
}),
|
|
256
|
-
jsx("p", {
|
|
261
|
+
jsx("span", {
|
|
257
262
|
key: "desc",
|
|
258
|
-
style: {
|
|
259
|
-
children: t("
|
|
260
|
-
}),
|
|
261
|
-
jsx("p", {
|
|
262
|
-
key: "hint",
|
|
263
|
-
style: { margin: "0 0 16px", fontSize: 12, opacity: 0.6, lineHeight: 1.7 },
|
|
264
|
-
children: t("hint")
|
|
263
|
+
style: { fontSize: 12.5, opacity: 0.8, lineHeight: 1.6 },
|
|
264
|
+
children: t("stopRowDesc")
|
|
265
265
|
}),
|
|
266
266
|
jsx("button", {
|
|
267
267
|
key: "button",
|
|
268
268
|
onClick: onStop,
|
|
269
269
|
disabled: state === "stopping",
|
|
270
270
|
style: {
|
|
271
|
-
padding: "
|
|
271
|
+
padding: "7px 16px",
|
|
272
272
|
fontSize: 13,
|
|
273
273
|
borderRadius: 8,
|
|
274
|
+
flexShrink: 0,
|
|
274
275
|
border: "1px solid var(--dsw-danger, #d33)",
|
|
275
276
|
background: state === "stopping" ? "transparent" : "var(--dsw-danger, #d33)",
|
|
276
277
|
color: state === "stopping" ? "var(--dsw-danger, #d33)" : "var(--dsw-danger-text, #fff)",
|
|
@@ -282,12 +283,121 @@ window.__ModuleLoader__.load({
|
|
|
282
283
|
});
|
|
283
284
|
};
|
|
284
285
|
|
|
286
|
+
/** Minimal semver compare (numeric fields; release outranks prerelease; unknown → 0). */
|
|
287
|
+
function compareVersion(a, b) {
|
|
288
|
+
const pattern = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/;
|
|
289
|
+
const pa = pattern.exec(String(a ?? ""));
|
|
290
|
+
const pb = pattern.exec(String(b ?? ""));
|
|
291
|
+
if (pa === null || pb === null) return 0;
|
|
292
|
+
for (let i = 1; i <= 3; i += 1) {
|
|
293
|
+
const diff = Number(pa[i]) - Number(pb[i]);
|
|
294
|
+
if (diff !== 0) return diff > 0 ? 1 : -1;
|
|
295
|
+
}
|
|
296
|
+
if (pa[4] === pb[4]) return 0;
|
|
297
|
+
if (pa[4] === undefined) return 1;
|
|
298
|
+
if (pb[4] === undefined) return -1;
|
|
299
|
+
return 0;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Update banner: compares local host/plugin versions (from /info) against
|
|
304
|
+
* the npm registry's public dist-tags, queried directly from the browser.
|
|
305
|
+
* Offline-tolerant — the banner simply stays hidden.
|
|
306
|
+
*/
|
|
307
|
+
const UpdateBanner = ({ t, info }) => {
|
|
308
|
+
const [lines, setLines] = react.useState(null);
|
|
309
|
+
|
|
310
|
+
react.useEffect(() => {
|
|
311
|
+
if (info === null) return;
|
|
312
|
+
let alive = true;
|
|
313
|
+
(async () => {
|
|
314
|
+
let hostLatest = null;
|
|
315
|
+
let pluginLatest = null;
|
|
316
|
+
try {
|
|
317
|
+
const res = await fetch("https://registry.npmjs.org/-/package/@deepseek-ai/dsh/dist-tags");
|
|
318
|
+
if (res.ok) {
|
|
319
|
+
const tags = await res.json();
|
|
320
|
+
if (typeof tags?.latest === "string") hostLatest = tags.latest;
|
|
321
|
+
}
|
|
322
|
+
} catch {
|
|
323
|
+
// Registry unreachable; hide the banner.
|
|
324
|
+
}
|
|
325
|
+
try {
|
|
326
|
+
const res = await fetch("https://registry.npmjs.org/-/package/dsh-stop-service/dist-tags");
|
|
327
|
+
if (res.ok) {
|
|
328
|
+
const tags = await res.json();
|
|
329
|
+
if (typeof tags?.latest === "string") pluginLatest = tags.latest;
|
|
330
|
+
}
|
|
331
|
+
} catch {
|
|
332
|
+
// Registry unreachable; hide the banner.
|
|
333
|
+
}
|
|
334
|
+
if (!alive) return;
|
|
335
|
+
const found = [];
|
|
336
|
+
if (hostLatest !== null && info.hostVersion && compareVersion(hostLatest, info.hostVersion) > 0) {
|
|
337
|
+
found.push(t("updHost").replace("{v}", hostLatest).replace("{c}", info.hostVersion));
|
|
338
|
+
}
|
|
339
|
+
if (pluginLatest !== null && info.pluginVersion && compareVersion(pluginLatest, info.pluginVersion) > 0) {
|
|
340
|
+
found.push(t("updPlugin").replace("{v}", pluginLatest).replace("{c}", info.pluginVersion));
|
|
341
|
+
}
|
|
342
|
+
if (found.length > 0) setLines(found);
|
|
343
|
+
})();
|
|
344
|
+
return () => {
|
|
345
|
+
alive = false;
|
|
346
|
+
};
|
|
347
|
+
}, [info === null ? null : info.hostVersion, info === null ? null : info.pluginVersion]);
|
|
348
|
+
|
|
349
|
+
if (lines === null) return null;
|
|
350
|
+
return jsx("div", {
|
|
351
|
+
style: {
|
|
352
|
+
border: "1px solid var(--dsw-accent, #4a90d9)",
|
|
353
|
+
borderRadius: 10,
|
|
354
|
+
padding: "10px 14px",
|
|
355
|
+
marginBottom: 16,
|
|
356
|
+
fontSize: 13,
|
|
357
|
+
lineHeight: 1.7,
|
|
358
|
+
},
|
|
359
|
+
children: lines.map((line) => jsx("p", { style: { margin: 0 }, children: line }, line))
|
|
360
|
+
});
|
|
361
|
+
};
|
|
362
|
+
|
|
285
363
|
const StopServiceTab = ({ t }) => {
|
|
364
|
+
const [info, setInfo] = react.useState(null);
|
|
365
|
+
const [history, setHistory] = react.useState([]);
|
|
366
|
+
const [cpuHistory, setCpuHistory] = react.useState([]);
|
|
367
|
+
|
|
368
|
+
react.useEffect(() => {
|
|
369
|
+
let alive = true;
|
|
370
|
+
const load = async () => {
|
|
371
|
+
try {
|
|
372
|
+
const res = await fetch("/api/dsh-stop-service/info");
|
|
373
|
+
const data = await res.json();
|
|
374
|
+
if (alive && data?.ok) {
|
|
375
|
+
setInfo(data);
|
|
376
|
+
if (typeof data.memoryRssMb === "number") {
|
|
377
|
+
setHistory((prev) => [...prev, data.memoryRssMb].slice(-36));
|
|
378
|
+
}
|
|
379
|
+
if (typeof data.cpuPercent === "number") {
|
|
380
|
+
setCpuHistory((prev) => [...prev, data.cpuPercent].slice(-36));
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
} catch {
|
|
384
|
+
// Host unreachable (e.g. stopping); keep the last snapshot.
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
load();
|
|
388
|
+
const timer = window.setInterval(load, 5000);
|
|
389
|
+
return () => {
|
|
390
|
+
alive = false;
|
|
391
|
+
window.clearInterval(timer);
|
|
392
|
+
};
|
|
393
|
+
}, []);
|
|
394
|
+
|
|
286
395
|
return jsx("div", {
|
|
287
396
|
style: { padding: "24px 8px", maxWidth: 560 },
|
|
288
397
|
children: [
|
|
289
|
-
jsx(
|
|
398
|
+
jsx(UpdateBanner, { key: "updates", t: t, info: info }),
|
|
290
399
|
jsx(StopCard, { key: "stop", t: t }),
|
|
400
|
+
jsx(InfoCard, { key: "info", t: t, info: info, history: history, cpuHistory: cpuHistory }),
|
|
291
401
|
]
|
|
292
402
|
});
|
|
293
403
|
};
|
|
@@ -311,6 +421,7 @@ window.__ModuleLoader__.load({
|
|
|
311
421
|
infoMemory: "内存占用 (RSS)",
|
|
312
422
|
infoCpu: "CPU 占用",
|
|
313
423
|
memTrend: "内存趋势",
|
|
424
|
+
cpuTrend: "CPU 趋势",
|
|
314
425
|
infoNode: "Node 版本",
|
|
315
426
|
infoHostVersion: "DSH 宿主版本",
|
|
316
427
|
infoPluginVersion: "插件版本",
|
|
@@ -319,9 +430,9 @@ window.__ModuleLoader__.load({
|
|
|
319
430
|
hourUnit: "小时",
|
|
320
431
|
minUnit: "分",
|
|
321
432
|
secUnit: "秒",
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
433
|
+
stopRowDesc: "优雅停止宿主进程(SIGTERM,与终端 kill 等效),不留孤儿进程。",
|
|
434
|
+
updHost: "DSH 宿主有新版本 {v} 可用(当前 {c}),可在终端升级:npm install -g @deepseek-ai/dsh@{v}",
|
|
435
|
+
updPlugin: "插件有新版本 {v} 可用(当前 {c}):dsh plugin --profile web add dsh-stop-service@{v}",
|
|
325
436
|
stop: "终止服务",
|
|
326
437
|
stopping: "正在停止…",
|
|
327
438
|
confirm: "确定要终止当前 DeepSeek Harness 服务吗?所有会话将断开(已保存的会话不受影响)。",
|
|
@@ -341,6 +452,7 @@ window.__ModuleLoader__.load({
|
|
|
341
452
|
infoMemory: "Memory (RSS)",
|
|
342
453
|
infoCpu: "CPU",
|
|
343
454
|
memTrend: "Memory trend",
|
|
455
|
+
cpuTrend: "CPU trend",
|
|
344
456
|
infoNode: "Node",
|
|
345
457
|
infoHostVersion: "DSH host",
|
|
346
458
|
infoPluginVersion: "Plugin",
|
|
@@ -349,9 +461,9 @@ window.__ModuleLoader__.load({
|
|
|
349
461
|
hourUnit: "h ",
|
|
350
462
|
minUnit: "m ",
|
|
351
463
|
secUnit: "s",
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
464
|
+
stopRowDesc: "Gracefully stop the host process (SIGTERM, like a terminal kill) — no orphaned processes.",
|
|
465
|
+
updHost: "A newer DSH host {v} is available (current {c}) — upgrade from a terminal: npm install -g @deepseek-ai/dsh@{v}",
|
|
466
|
+
updPlugin: "A newer plugin {v} is available (current {c}) — dsh plugin --profile web add dsh-stop-service@{v}",
|
|
355
467
|
stop: "Stop Service",
|
|
356
468
|
stopping: "Stopping…",
|
|
357
469
|
confirm: "Stop the current DeepSeek Harness service? All sessions disconnect (saved sessions are unaffected).",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-stop-service",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"description": "A stop-service button for DeepSeek Harness Web: one confirmed click gracefully terminates the running dsh host (SIGTERM) — no orphaned background processes",
|