dsh-global-todo 0.12.4 → 0.12.5

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 CHANGED
@@ -1,3 +1,19 @@
1
+ ## [0.12.5] — 2026-10-21
2
+
3
+ A check for the one install mistake that is invisible until the desktop complains.
4
+
5
+ - **`dsh-global-todo-doctor`** (`npx dsh-global-todo-doctor <dir>`, also `npm run doctor`): inspects
6
+ a directory that is supposed to contain the plugin — manifest, `dsh.bundle.patch`, host and
7
+ client entries, `locale/`, `docs/icon.svg`, `lib/` — and exits non-zero when something required
8
+ is missing. The case it exists for: a hand-copied `plugins-local` directory without `locale/`
9
+ still loads, while the desktop app prints `Plugin metadata … ENOENT … \locale`, which points at
10
+ the symptom and not the cause.
11
+ - **Six tests** drive the checker against real temporary directories: complete copy, missing
12
+ `locale/`, missing client entry, missing patch file, missing icon (warning only), and a directory
13
+ that is not the plugin at all.
14
+ - FAQ §15 now starts with the command instead of guessing, and both READMEs document it where a
15
+ hand-copied install would go wrong.
16
+
1
17
  ## [0.12.4] — 2026-10-21
2
18
 
3
19
  The repository leaked its own location. Three real paths, and a guard that could not see them.
package/README.md CHANGED
@@ -182,9 +182,19 @@ The plugin is a Loader row, not a hot-loaded script: **restart the host** (`dsh
182
182
  after installing, then refresh the page. A "待办 / Todos" entry appears in the sidebar.
183
183
 
184
184
  The npm package is the source of truth. A community catalog can lag behind it or point at an
185
- older release; pin the version you want (`dsh plugin --profile web add dsh-global-todo@0.10.0`)
185
+ older release; pin the version you want (`dsh plugin --profile web add dsh-global-todo@0.12.4`)
186
186
  when it matters.
187
187
 
188
+ **Hand-copied a directory instead?** Check it before wondering why the plugins page looks wrong:
189
+
190
+ ```sh
191
+ npx dsh-global-todo-doctor "$HOME/.dsh/plugins-local/全局待办插件"
192
+ ```
193
+
194
+ It verifies the manifest, the patch file, both entries, `locale/`, the icon and `lib/`, and exits
195
+ non-zero when something required is missing — the `locale/` case is the field failure that makes
196
+ the desktop app print `Plugin metadata … ENOENT … \locale` while the plugin still loads.
197
+
188
198
  To roll back:
189
199
 
190
200
  ```sh
package/README.zh.md CHANGED
@@ -162,6 +162,17 @@ dsh plugin --profile web add /绝对路径/dsh-global-todo
162
162
  插件是宿主组合里的**一行 Loader**,不是热加载脚本:**装完要重启宿主**(`dsh web`),
163
163
  再刷新页面。侧栏会出现「待办」入口。
164
164
 
165
+ **如果你是手动拷贝目录(而不是从 npm 装)**,先自检一遍,别靠猜:
166
+
167
+ ```sh
168
+ npx dsh-global-todo-doctor "$HOME/.dsh/plugins-local/全局待办插件"
169
+ # Windows: npx dsh-global-todo-doctor "$env:USERPROFILE\.dsh\plugins-local\全局待办插件"
170
+ ```
171
+
172
+ 它逐项检查 manifest、patch 文件、两个入口、`locale/`、图标和 `lib/`;缺必需项时退出码为 1。
173
+ 最容易漏的是 `locale/`——少了它插件照样加载,但桌面端会报
174
+ `Plugin metadata … ENOENT … \locale`。
175
+
165
176
  npm 上的包才是事实来源。社区目录可能落后于 npm、或指向某个旧 release;需要确定版本时
166
177
  直接钉版本(`dsh plugin --profile web add dsh-global-todo@0.10.0`)。
167
178
 
package/docs/FAQ.md CHANGED
@@ -166,6 +166,16 @@ rm -r "$DSH_HOME/global-todo"
166
166
  也就是说:**你那份本地插件目录缺 `locale/` 文件夹**。常见于把仓库文件夹手动复制到
167
167
  `%USERPROFILE%\.dsh\plugins-local\` 时只拷了 `lib`、`package.json`、`cordis.patch.yml`。
168
168
 
169
+ 先确认到底缺什么,别靠猜:
170
+
171
+ ```sh
172
+ npx dsh-global-todo-doctor "$HOME/.dsh/plugins-local/全局待办插件"
173
+ # Windows: npx dsh-global-todo-doctor "$env:USERPROFILE\.dsh\plugins-local\全局待办插件"
174
+ ```
175
+
176
+ 它逐项检查 `package.json`、patch 文件、两个入口、`locale/`、图标与 `lib/`,缺哪样直接点出来,
177
+ 缺必需项时退出码为 1(可以放进安装脚本里当门禁)。
178
+
169
179
  修法二选一:
170
180
 
171
181
  1. **推荐**:别用本地目录,直接装 npm 包(包里有 `locale/en.json`、`locale/zh.json`):
package/lib/index.js CHANGED
@@ -36,7 +36,7 @@ import { createStore, resolveStorePath } from './store.js'
36
36
  import { createTools } from './tools.js'
37
37
 
38
38
  /** Must equal `package.json`'s version; `npm run check` enforces that. */
39
- export const VERSION = '0.12.4'
39
+ export const VERSION = '0.12.5'
40
40
 
41
41
  /** Plugin id. Also the Loader row id in `cordis.patch.yml`. */
42
42
  export const name = 'dsh-global-todo'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-global-todo",
3
- "version": "0.12.4",
3
+ "version": "0.12.5",
4
4
  "icon": "./docs/icon.svg",
5
5
  "description": "Cross-session todo list for DeepSeek Harness: one JSON file a person and an agent both write to, with step plans that ship as a numbered hand-off, session read-back, and a human confirmation before an agent's \"done\" counts.",
6
6
  "type": "module",
@@ -52,6 +52,8 @@
52
52
  "docs/FAQ.md",
53
53
  "docs/icon.svg",
54
54
  "docs/screenshots",
55
+ "tools/doctor.mjs",
56
+ "tools/doctor-lib.mjs",
55
57
  "!docs/screenshots/demo.mp4",
56
58
  "!docs/index.html",
57
59
  "!docs/assets",
@@ -63,8 +65,12 @@
63
65
  "CONTRIBUTING.md",
64
66
  "CODE_OF_CONDUCT.md"
65
67
  ],
68
+ "bin": {
69
+ "dsh-global-todo-doctor": "./tools/doctor.mjs"
70
+ },
66
71
  "scripts": {
67
72
  "check": "node scripts/check.mjs",
73
+ "doctor": "node tools/doctor.mjs",
68
74
  "test": "node --test \"test/**/*.test.mjs\"",
69
75
  "verify": "npm run check && npm run check:render && npm test",
70
76
  "preview": "node tools/preview.mjs",
@@ -0,0 +1,123 @@
1
+ /**
2
+ * A pre-flight check for an installed copy of this plugin.
3
+ *
4
+ * The failure this exists for is specific: a hand-copied `plugins-local` directory
5
+ * usually keeps `lib/`, `package.json` and the patch file, and drops `locale/` —
6
+ * the plugin still loads, the panel still works, and the desktop app prints
7
+ * `Plugin metadata … ENOENT … \locale` on the plugins page. Node resolves the
8
+ * `./locale/*.json` export *without* checking that the file exists, and the Host then
9
+ * scans the directory, which is why the symptom appears far from the cause.
10
+ *
11
+ * Everything here is a pure function of the directory contents, so the test suite can
12
+ * call it without spawning anything.
13
+ */
14
+ import { readFile, readdir, stat } from 'node:fs/promises'
15
+ import path from 'node:path'
16
+
17
+ const EXPORT_LOCALE = './locale/*.json'
18
+
19
+ const readJson = async (file) => {
20
+ try {
21
+ return JSON.parse(await readFile(file, 'utf8'))
22
+ } catch {
23
+ return null
24
+ }
25
+ }
26
+
27
+ const exists = async (file) => {
28
+ try {
29
+ const info = await stat(file)
30
+ return info.isFile() || info.isDirectory()
31
+ } catch {
32
+ return false
33
+ }
34
+ }
35
+
36
+ /**
37
+ * @param {string} dir directory that should contain the plugin, e.g. …/plugins-local/全局待办插件
38
+ * @returns {Promise<{ok: boolean, dir: string, findings: {level: 'ok'|'warn'|'error', id: string, message: string, fix?: string}[], version?: string}>}
39
+ */
40
+ export async function doctor(dir) {
41
+ const findings = []
42
+ const add = (level, id, message, fix) => findings.push({ level, id, message, ...(fix === undefined ? {} : { fix }) })
43
+
44
+ const manifestFile = path.join(dir, 'package.json')
45
+ const manifest = await readJson(manifestFile)
46
+ if (manifest === null) {
47
+ add('error', 'package-missing', 'package.json 读不到或不是合法 JSON', '把插件目录指到包含 package.json 的那一层(例如 …/plugins-local/全局待办插件)')
48
+ return { ok: false, dir, findings }
49
+ }
50
+
51
+ if (manifest.name !== 'dsh-global-todo') {
52
+ add('warn', 'package-name', `package.json 里的 name 是 ${manifest.name ?? '(缺失)'},不是 dsh-global-todo`, '这个目录可能不是本插件')
53
+ } else {
54
+ add('ok', 'package-name', `${manifest.name}@${manifest.version ?? '?'}`)
55
+ }
56
+
57
+ // The patch file is what contributes the Loader row. Without it nothing loads.
58
+ const patch = manifest.dsh?.bundle?.patch
59
+ if (typeof patch !== 'string') {
60
+ add('error', 'patch-declared', 'package.json 没有声明 dsh.bundle.patch', '这不是一个可安装的 DSH bundle')
61
+ } else if (!(await exists(path.join(dir, patch)))) {
62
+ add('error', 'patch-file', `dsh.bundle.patch 指向 ${patch},但文件不存在`, '从 npm 包重新拷贝,或 `dsh plugin add dsh-global-todo`')
63
+ } else {
64
+ add('ok', 'patch-file', patch)
65
+ }
66
+
67
+ // Both halves must exist; the client half is what the panel loads.
68
+ const entry = manifest.exports?.['.']
69
+ const client = manifest.exports?.['./client']
70
+ for (const [label, target] of [['host entry', entry], ['client entry', client]]) {
71
+ if (typeof target !== 'string') {
72
+ add('warn', 'entry-declared', `package.json 没有 ${label} 的 exports`, '宿主可能用默认解析,先确认版本是否完整')
73
+ continue
74
+ }
75
+ if (await exists(path.join(dir, target))) add('ok', 'entry', target)
76
+ else add('error', 'entry-file', `${label} 指向 ${target},文件不存在`, '这个目录不是完整拷贝:重新从 npm 包复制 lib/')
77
+ }
78
+
79
+ // The locale export is the one that fails in the field: the export exists, the
80
+ // directory does not, and the Host reports it as plugin-metadata trouble.
81
+ const localeExport = manifest.exports?.[EXPORT_LOCALE]
82
+ const localeDir = path.join(dir, 'locale')
83
+ const localeExists = await exists(localeDir)
84
+ const files = localeExists ? (await readdir(localeDir).catch(() => [])).filter((name) => name.endsWith('.json')) : []
85
+ if (typeof localeExport === 'string' && !localeExists) {
86
+ add(
87
+ 'error',
88
+ 'locale-missing',
89
+ 'package.json 声明了 ./locale/*.json,但 locale/ 目录不存在 —— 桌面端会显示 Plugin metadata … ENOENT … \\locale',
90
+ '拷贝 locale/ 目录,或用 npm 包重装:dsh plugin add dsh-global-todo(插件本身仍能加载,坏的是插件页标题/描述)',
91
+ )
92
+ } else if (typeof localeExport === 'string' && files.length === 0) {
93
+ add('warn', 'locale-empty', 'locale/ 存在但没有 .json 文件', '至少需要 en.json 与 zh.json')
94
+ } else if (files.length > 0) {
95
+ add('ok', 'locale', `locale/ → ${files.sort().join(', ')}`)
96
+ }
97
+
98
+ // The icon is cosmetic: a missing one shows a blank mark, not an error.
99
+ if (await exists(path.join(dir, 'docs', 'icon.svg'))) add('ok', 'icon', 'docs/icon.svg')
100
+ else add('warn', 'icon-missing', 'docs/icon.svg 不存在,插件页会没有图标', '可选:一并拷过去即可,不影响加载')
101
+
102
+ const libDir = path.join(dir, 'lib')
103
+ const libFiles = (await exists(libDir)) ? (await readdir(libDir).catch(() => [])).filter((name) => name.endsWith('.js')) : []
104
+ if (libFiles.length === 0) add('error', 'lib-empty', 'lib/ 里没有 .js 文件', '从 npm 包重新复制 lib/')
105
+ else add('ok', 'lib', `lib/ → ${libFiles.length} 个模块`)
106
+
107
+ return { ok: !findings.some((finding) => finding.level === 'error'), dir, version: manifest.version, findings }
108
+ }
109
+
110
+ /** One line per finding, for a terminal. */
111
+ export function format(report) {
112
+ const mark = { ok: 'OK ', warn: 'WARN', error: 'FAIL' }
113
+ const lines = [`dsh-global-todo doctor — ${report.dir}`, '']
114
+ for (const finding of report.findings) {
115
+ lines.push(`${mark[finding.level]} ${finding.message}`)
116
+ if (finding.fix !== undefined) lines.push(` fix: ${finding.fix}`)
117
+ }
118
+ const errors = report.findings.filter((finding) => finding.level === 'error').length
119
+ const warns = report.findings.filter((finding) => finding.level === 'warn').length
120
+ lines.push('')
121
+ lines.push(errors === 0 ? `OK — ${warns} warning(s)` : `${errors} error(s), ${warns} warning(s)`)
122
+ return lines.join('\n')
123
+ }
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * dsh-global-todo-doctor — check that a directory really contains this plugin.
4
+ *
5
+ * npx dsh-global-todo-doctor "~/.dsh/plugins-local/全局待办插件"
6
+ * node tools/doctor.mjs . # from a checkout
7
+ *
8
+ * Exists because the field failure is silent-ish: a hand-copied plugin directory
9
+ * without `locale/` still loads, and only the desktop plugins page complains.
10
+ */
11
+ import { doctor, format } from './doctor-lib.mjs'
12
+ import path from 'node:path'
13
+
14
+ const args = process.argv.slice(2).filter((value) => value !== '')
15
+ if (args.includes('--help') || args.includes('-h')) {
16
+ process.stdout.write('用法 / usage:\n dsh-global-todo-doctor [插件目录]\n\n默认检查当前目录。\n')
17
+ process.exit(0)
18
+ }
19
+
20
+ const target = path.resolve(args[0] ?? process.cwd())
21
+ const report = await doctor(target)
22
+ process.stdout.write(format(report) + '\n')
23
+ process.exit(report.ok ? 0 : 1)