@kira905/dsh-diagnostics-extra 0.1.0 → 0.1.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 +35 -7
- package/lib/cli-args.mjs +183 -0
- package/owner-todo-scan.mjs +16 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# dsh-diagnostics-extra · DSH 诊断与对账小工具
|
|
2
2
|
|
|
3
|
+
> **本仓是命令行工具,不挂宿主界面,不需要 `dsh.bundle`、不需要写 `cordis.patch.yml`。** 装它 = `npm i -g @kira905/dsh-diagnostics-extra`(或 `npx -p @kira905/dsh-diagnostics-extra dsh-why`)。装进 profile 也对,但那条 `declares no dsh.bundle` 警告是**正常现象**,不是故障。
|
|
4
|
+
|
|
5
|
+
## 装完自检(三条只读验法)
|
|
6
|
+
|
|
7
|
+
`<profile>` 换成你实际用的 profile(默认 `web`),`<数据根>` 换成你的 DSH 数据根(默认 `~/.dsh`)。
|
|
8
|
+
|
|
9
|
+
```powershell
|
|
10
|
+
# ① 组合树里有没有它(只读、约 0.6 秒、不启服务)
|
|
11
|
+
dsh --profile <profile> --dump-config | Select-String "dsh-diagnostics-extra"
|
|
12
|
+
# 期望:bundle 层出现一行 "# == <登记名>"、insert 层出现 "- id: <entry>"(问的都是「它挂上没」);
|
|
13
|
+
# 本仓包名 @kira905/dsh-diagnostics-extra —— 带 scope 装的写带 scope 名、老工装 / 手写的可能是裸名,两种都搜一遍
|
|
14
|
+
# ⚠️ 若该包会 insert 一行,还要数一遍"同一个 id 出现了几次",必须 == 1
|
|
15
|
+
|
|
16
|
+
# ② 组合列表里有没有它(只读)
|
|
17
|
+
Get-Content <数据根>/profiles/<profile>/package.json | Select-String "@kira905/dsh-diagnostics-extra"
|
|
18
|
+
# 期望:dependencies 与 dsh.profile.bundles 两处都有(CLI 类只需要 dependencies;bundles 里也可能写裸名 dsh-diagnostics-extra)
|
|
19
|
+
|
|
20
|
+
# ③ 命令在不在 —— 【只查存在,不执行】
|
|
21
|
+
Test-Path <数据根>/profiles/<profile>/node_modules/.bin/dsh-why.CMD # 装进 profile 的
|
|
22
|
+
Test-Path "$env:APPDATA\npm\dsh-why.CMD" # npm i -g 装的
|
|
23
|
+
# 期望:True。两条路径按你实际用的装法挑一条
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
> 探针只做存在性检查(只看命令文件在不在,不执行任何命令)。
|
|
27
|
+
> 要不要用 `--help` 验,见本仓当前说明与该命令的参数表。
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
3
31
|
> DeepSeek Harness(DSH)生态里**出问题时真正会去跑**的几件只读诊断工具:
|
|
4
32
|
> **图片附件对账 / 取回**(从别的机器同步来的含图会话为什么一续聊就报 TRANSPORT)、
|
|
5
33
|
> **一条命令答清「现在哪台机在跑、dsh 与守护鱼何时起、3080 端口连了谁」**、
|
|
@@ -11,13 +39,13 @@
|
|
|
11
39
|
|
|
12
40
|
## ⚠️ 先读这段(比任何用法都重要)
|
|
13
41
|
|
|
14
|
-
1.
|
|
15
|
-
`
|
|
16
|
-
|
|
17
|
-
⇒
|
|
18
|
-
2.
|
|
19
|
-
|
|
20
|
-
|
|
42
|
+
1. **`owner-todo-scan.mjs` 自 2026-10-30 起已支持 `--help`(打印用法、退出码 0)与未知参数拒收
|
|
43
|
+
(退出码 2,绝不继续执行)** —— 敲错参数会被拦下,不会带着 `--write` 去重写清单。
|
|
44
|
+
用法文本里写明了它唯一的写动作:`--write` 会**整体重写 `docs\待你操作-<设备>.md`**。
|
|
45
|
+
⇒ 拿不准就先 `--help`,或先读脚本头部注释。
|
|
46
|
+
2. **另外三件**:`attachments-audit.mjs` / `attachments-fetch.mjs` / `dsh-why.mjs`
|
|
47
|
+
也都实现了 `-h` 与帮助输出,**未知参数会报错并停在用法提示**(不会执行真动作)。
|
|
48
|
+
本文件仍不在代码块里给帮助示例 —— 四件的参数写法差异大,照抄逐件表最稳。
|
|
21
49
|
3. **写动作只有两处**:`attachments-fetch.mjs` 的取回落盘(可 `--dry-run` 预演、`--report` 留报告、`--rollback` 回退)
|
|
22
50
|
与 `owner-todo-scan.mjs --write` 的清单刷新(整体重写)。其余全部只读。
|
|
23
51
|
4. 本仓工具**不做任何网络请求、不读凭据文件**。它们只读本机 DSH 数据根与 docs 树。
|
package/lib/cli-args.mjs
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
// cli-args.mjs —— 用户 CLI 公共参数件(零依赖)
|
|
2
|
+
//
|
|
3
|
+
// 三条纪律(本仓所有 bin 共用):
|
|
4
|
+
// 1. `-h` / `--help` ⇒ 打印用法、退出码 0,**绝不执行动作**;
|
|
5
|
+
// 2. 出现不认识的参数 ⇒ 打印错误、退出码 2,**绝不继续执行**;
|
|
6
|
+
// 3. 用法文本必须写清「这个命令会写什么」,用户得知道有没有副作用。
|
|
7
|
+
//
|
|
8
|
+
// 接入方式(bin 顶部):
|
|
9
|
+
// import { cliGuard } from '../lib/cli-args.mjs';
|
|
10
|
+
// const argv = cliGuard(spec); // 默认取 process.argv.slice(2)
|
|
11
|
+
//
|
|
12
|
+
// spec 字段(都可选,但 summary / usage / writes / exits 建议必填):
|
|
13
|
+
// cmd, summary, usage,
|
|
14
|
+
// commands: [{ name, desc }],
|
|
15
|
+
// flags: [{ name, alias: ['-x'], arg: 'N', desc }],
|
|
16
|
+
// positional: [{ name, desc }],
|
|
17
|
+
// env: [{ name, desc }],
|
|
18
|
+
// writes, dryRun, exits, examples: [], notes: []
|
|
19
|
+
|
|
20
|
+
import fs from 'node:fs';
|
|
21
|
+
|
|
22
|
+
export const EXIT_USAGE = 2;
|
|
23
|
+
|
|
24
|
+
// 同步写 fd:`process.stdout` 在管道下是异步流,若随后同步 process.exit() 会把内容截断;
|
|
25
|
+
// 而 `--help` 必须「打印完立刻退出、后面一行都不许跑」⇒ 一律用 writeSync 写原始字节。
|
|
26
|
+
function writeSyncFd(fd, text) {
|
|
27
|
+
try { fs.writeSync(fd, text); } catch { (fd === 1 ? process.stdout : process.stderr).write(text); }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** 取 token 的「参数名」部分:`--hours=6` → `--hours`;非 `-` 开头返回 null */
|
|
31
|
+
function tokenName(a) {
|
|
32
|
+
if (typeof a !== 'string' || !a.startsWith('-')) return null;
|
|
33
|
+
const eq = a.indexOf('=');
|
|
34
|
+
return eq > 0 ? a.slice(0, eq) : a;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const dashless = (n) => n.replace(/^-+/, '');
|
|
38
|
+
|
|
39
|
+
function knownSet(spec) {
|
|
40
|
+
const s = new Set(['-h', '--help']);
|
|
41
|
+
for (const f of spec.flags ?? []) {
|
|
42
|
+
if (f.name) s.add(f.name);
|
|
43
|
+
for (const a of f.alias ?? []) s.add(a);
|
|
44
|
+
}
|
|
45
|
+
return s;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function valueSet(spec) {
|
|
49
|
+
const s = new Set();
|
|
50
|
+
for (const f of spec.flags ?? []) {
|
|
51
|
+
if (!f.arg) continue;
|
|
52
|
+
if (f.name) s.add(f.name);
|
|
53
|
+
for (const a of f.alias ?? []) s.add(a);
|
|
54
|
+
}
|
|
55
|
+
return s;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** 识别帮助:`hasFlag(argv, '-h', '--help')` */
|
|
59
|
+
export function hasFlag(argv, ...names) {
|
|
60
|
+
for (const a of argv ?? []) {
|
|
61
|
+
const n = tokenName(a);
|
|
62
|
+
if (n !== null && names.includes(n)) return true;
|
|
63
|
+
}
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** 渲染用法文本(纯函数,便于测试) */
|
|
68
|
+
export function renderHelp(spec = {}) {
|
|
69
|
+
const L = [];
|
|
70
|
+
L.push('用法');
|
|
71
|
+
L.push(` ${spec.usage ?? spec.cmd ?? '<命令>'}`);
|
|
72
|
+
|
|
73
|
+
if (spec.summary) L.push('', spec.summary);
|
|
74
|
+
|
|
75
|
+
if (spec.commands?.length) {
|
|
76
|
+
L.push('', '子命令');
|
|
77
|
+
for (const c of spec.commands) L.push(` ${c.name} ${c.desc}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
L.push('', '选项');
|
|
81
|
+
for (const f of spec.flags ?? []) {
|
|
82
|
+
const head = [f.name, ...(f.alias ?? [])].join(', ') + (f.arg ? ` <${f.arg}>` : '');
|
|
83
|
+
L.push(` ${head}`);
|
|
84
|
+
if (f.desc) L.push(` ${f.desc}`);
|
|
85
|
+
}
|
|
86
|
+
L.push(' -h, --help');
|
|
87
|
+
L.push(' 打印本用法并退出(退出码 0),不执行任何动作');
|
|
88
|
+
|
|
89
|
+
if (spec.positional?.length) {
|
|
90
|
+
L.push('', '位置参数');
|
|
91
|
+
for (const p of spec.positional) L.push(` ${p.name}`);
|
|
92
|
+
for (const p of spec.positional) if (p.desc) L.push(` ${p.name} ${p.desc}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (spec.writes) L.push('', '写什么', ` ${spec.writes}`);
|
|
96
|
+
if (spec.dryRun) L.push('', '干跑', ` ${spec.dryRun}`);
|
|
97
|
+
if (spec.env?.length) {
|
|
98
|
+
L.push('', '环境变量');
|
|
99
|
+
for (const e of spec.env) L.push(` ${e.name} ${e.desc ?? ''}`);
|
|
100
|
+
}
|
|
101
|
+
if (spec.exits) L.push('', '退出码', ` ${spec.exits}`);
|
|
102
|
+
if (spec.examples?.length) {
|
|
103
|
+
L.push('', '示例');
|
|
104
|
+
for (const e of spec.examples) L.push(` ${e}`);
|
|
105
|
+
}
|
|
106
|
+
if (spec.notes?.length) {
|
|
107
|
+
L.push('', '注意');
|
|
108
|
+
for (const n of spec.notes) L.push(` ${n}`);
|
|
109
|
+
}
|
|
110
|
+
return L.join('\n') + '\n';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** 打印用法并按需以指定码退出(默认 rc=0):同步写 + 同步退出,保证后续代码一行都不跑 */
|
|
114
|
+
export function printHelp(spec = {}, { exit = true, exitCode = 0 } = {}) {
|
|
115
|
+
const text = renderHelp(spec);
|
|
116
|
+
if (!exit) { process.stdout.write(text); return text; }
|
|
117
|
+
writeSyncFd(1, text);
|
|
118
|
+
process.exit(exitCode);
|
|
119
|
+
return text;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** 未知参数:打印错误并 rc=2 退出(同步写 + 同步退出,绝不继续执行) */
|
|
123
|
+
export function usageError(spec = {}, arg) {
|
|
124
|
+
const cmd = spec.cmd ? `${spec.cmd} --help` : '--help';
|
|
125
|
+
writeSyncFd(2, `错误:不认识的参数 ${arg}\n(运行 \`${cmd}\` 看用法)\n`);
|
|
126
|
+
process.exit(EXIT_USAGE);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** 列出 argv 里不认识的参数(不含数值形如 -1 的取值、不含 `--` 之后的内容) */
|
|
130
|
+
export function findUnknown(argv = [], spec = {}) {
|
|
131
|
+
const known = knownSet(spec);
|
|
132
|
+
const out = [];
|
|
133
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
134
|
+
const a = argv[i];
|
|
135
|
+
if (a === '--') break;
|
|
136
|
+
if (/^-\d/.test(a)) continue; // 负数取值,不是参数名
|
|
137
|
+
const n = tokenName(a);
|
|
138
|
+
if (n === null) continue;
|
|
139
|
+
if (!known.has(n)) out.push(a);
|
|
140
|
+
}
|
|
141
|
+
return out;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* 解析参数。遇到不认识的参数 ⇒ 打印错误 + rc=2 退出(绝不继续执行)。
|
|
146
|
+
* 返回 `{ flags, positional }`;`--x=1` 与 `--x 1` 都归一成 `flags.x`。
|
|
147
|
+
*/
|
|
148
|
+
export function parseArgs(argv = [], spec = {}) {
|
|
149
|
+
const known = knownSet(spec);
|
|
150
|
+
const valued = valueSet(spec);
|
|
151
|
+
const flags = {};
|
|
152
|
+
const positional = [];
|
|
153
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
154
|
+
const a = argv[i];
|
|
155
|
+
if (a === '--') { positional.push(...argv.slice(i + 1)); break; }
|
|
156
|
+
if (/^-\d/.test(a)) { positional.push(a); continue; }
|
|
157
|
+
const n = tokenName(a);
|
|
158
|
+
if (n === null) { positional.push(a); continue; }
|
|
159
|
+
if (!known.has(n)) usageError(spec, a);
|
|
160
|
+
const key = dashless(n);
|
|
161
|
+
const eq = a.indexOf('=');
|
|
162
|
+
if (eq > 0) { flags[key] = a.slice(eq + 1); continue; }
|
|
163
|
+
const next = argv[i + 1];
|
|
164
|
+
if (valued.has(n) && next !== undefined && tokenName(next) === null) {
|
|
165
|
+
flags[key] = next;
|
|
166
|
+
i += 1;
|
|
167
|
+
} else {
|
|
168
|
+
flags[key] = true;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return { flags, positional };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* 一道闸:先认 `-h/--help`,再拦未知参数,通过则把 argv 原样交回。
|
|
176
|
+
* 用法(改动最小的接法):`const argv = cliGuard(SPEC, process.argv.slice(2));`
|
|
177
|
+
*/
|
|
178
|
+
export function cliGuard(spec = {}, argv = process.argv.slice(2)) {
|
|
179
|
+
if (hasFlag(argv, '-h', '--help')) printHelp(spec);
|
|
180
|
+
const unknown = findUnknown(argv, spec);
|
|
181
|
+
if (unknown.length) usageError(spec, unknown[0]);
|
|
182
|
+
return argv;
|
|
183
|
+
}
|
package/owner-todo-scan.mjs
CHANGED
|
@@ -30,6 +30,7 @@ import process from "node:process";
|
|
|
30
30
|
import { pathToFileURL } from "node:url";
|
|
31
31
|
import { resolveDshHome, resolveDshWorkspace } from "./lib/dsh-home.mjs";
|
|
32
32
|
import { firstH1Index, insertAtFor, stampDocMark } from "./lib/generated-docs.mjs"; // §2.2 权威源(档级标记)
|
|
33
|
+
import { cliGuard } from "./lib/cli-args.mjs";
|
|
33
34
|
// 升级锁现场(③ 包 C-6 段①②的第三处视图):判据唯一源 = lib/upgrade-lock.mjs 的 status()
|
|
34
35
|
// —— **绝不在此复制判定逻辑**(锁的心跳/pid 判据改一处就够)。
|
|
35
36
|
// ⚠️ 该模块是**可选依赖**:它属于 DSH 生态内核(私有工具树),本仓不下发 ⇒ 改为动态加载 + 降级(见下方「可选内核」段)。
|
|
@@ -46,6 +47,21 @@ const HOME = resolveDshHome();
|
|
|
46
47
|
// 这三个判据的真实实现长在 DSH 私有工具树(dsh-butler/lib)里,本仓不下发:
|
|
47
48
|
// · 目录存在 ⇒ 行为与私有树逐字一致;
|
|
48
49
|
// · 目录不存在 ⇒ 升级锁本节显式打印「不可探测」并继续;另两节静默跳过(它们是噪音源,不是"有你的事")。
|
|
50
|
+
const SPEC = {
|
|
51
|
+
cmd: 'dsh-owner-todo-scan',
|
|
52
|
+
summary: '把「等你操作 / 待拍板」的悬项从四处登记里合成一张清单(现场锁 / 看板卡 / 交接档 / 对端执行单)。',
|
|
53
|
+
usage: 'dsh-owner-todo-scan [--all] [--json] [--write]',
|
|
54
|
+
flags: [
|
|
55
|
+
{ name: '--all', desc: '连已 done 卡里的历史段也扫(噪声多,兜底用)' },
|
|
56
|
+
{ name: '--json', desc: '机器可读输出(含每条的档位判定)' },
|
|
57
|
+
{ name: '--write', desc: '把清单整体重写成 docs 下的「待你操作-<设备>.md」' },
|
|
58
|
+
],
|
|
59
|
+
writes: '默认只打印。--write 会整体重写一份 docs 下的清单文件(覆盖式刷新)。',
|
|
60
|
+
exits: '0 成功 · 1 采集异常 · 2 参数错误',
|
|
61
|
+
examples: ['dsh-owner-todo-scan', 'dsh-owner-todo-scan --json'],
|
|
62
|
+
};
|
|
63
|
+
cliGuard(SPEC);
|
|
64
|
+
|
|
49
65
|
const TOOLS_DIR = process.env.DSH_TOOLS_DIR || path.join(resolveDshWorkspace(), "tools");
|
|
50
66
|
const BUTLER_LIB = process.env.DSH_BUTLER_LIB || path.join(TOOLS_DIR, "dsh-butler", "lib");
|
|
51
67
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kira905/dsh-diagnostics-extra",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "DeepSeek Harness (DSH) diagnostics & reconciliation utilities: image-attachment audit/fetch (TRANSPORT root cause), one-command health check (host / dsh & guard start times / port connections), and an owner-action scanner. Zero third-party runtime dependencies.",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"private": false,
|