@fffangchenghao/dsh-autoupdater 0.6.1 → 0.6.3

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.
Files changed (3) hide show
  1. package/README.md +19 -2
  2. package/main.js +156 -47
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -31,6 +31,8 @@ dsh plugin --profile web add github:fangchenghao/dsh-autoupdater
31
31
  ```
32
32
 
33
33
  > 用 scoped 包名是为了不和上游 `dsh-autoupdater` 抢注同名包;bundle id 仍是 `dsh-autoupdater`,配置与状态路径不变。
34
+ >
35
+ > Windows 桌面版更推荐直接在 **插件管理器 → 安装插件** 里填包名安装;装完重启 App。
34
36
 
35
37
  安装后重启 DSH(systemd 环境:`sudo systemctl restart <你的dsh服务名>`),然后在 **插件管理器 → 自动更新** 卡片配置。
36
38
 
@@ -87,11 +89,23 @@ dsh plugin --profile web add github:fangchenghao/dsh-autoupdater
87
89
 
88
90
  因此桌面版的工作方式是:**静默把插件换版到磁盘 → 面板提示重启 → 你下次打开 App 时生效**。
89
91
 
92
+ ### Windows 支持(0.6.2+)
93
+
94
+ 0.6.2 起 Windows 桌面版与 macOS 使用同一条换版路径,并修掉了 0.6.1 里的几处 Unix-only 假设:
95
+
96
+ - **空闲判定**不再调用 `find -newermt`(Windows 的 `find.exe` 是文本搜索,不支持这些参数,会让插件永远认为“忙”),改为纯 Node 递归扫描 `%DSH_HOME%\sessions`。目录不存在 = 没有会话 = 空闲;只有根目录不可读才保守当忙。
97
+ - **`dsh.cmd` 调用**:PATH / sibling 找到的 `.cmd` 经 `cmd.exe` 执行并正确加引号;超时用 `taskkill /T /F` 结束整棵进程树,避免留下 pnpm 子进程。
98
+ - **环境变量**:没有 `HOME` 时用 `homedir()`(Windows 的 `USERPROFILE`);PATH 兼容 `Path` 键,并额外查找 `%APPDATA%\npm` / `%LOCALAPPDATA%\pnpm`。
99
+ - Windows 上不执行 DSH 本体更新(交给安装器/App 自带更新器),插件换版后同样只提示重启。
100
+ - **多 profile 选择**:优先使用桌面 App 注入的 `DSH_PROFILE_DIR` / `DSH_PROFILE`;桌面版在两者都缺失时默认 `desktop`,不会因为某个测试 profile 的 `node_modules` 更新时间更近而误选。
101
+
102
+ 开发机是 macOS;Windows 分支是按 Node/Electron 的跨平台语义改的,并做了逻辑测试,但**尚未在真实 Windows 机器上端到端跑过**。如果 Windows 上仍失败,请把面板日志和 `%USERPROFILE%\.dsh\autoupdater\history.jsonl` 发到 GitHub issue。
103
+
90
104
  ## 维护说明
91
105
 
92
106
  - 上游项目:[niubi6668889595/dsh-autoupdater](https://github.com/niubi6668889595/dsh-autoupdater)(MIT)。
93
- - 本分支新增:桌面版 CLI 换版、npmmirror 优先、跳过 `link:`/git 来源、只有真正升级成功才算“已应用”;完整差异见 git log。
94
- - 发布渠道:npm `@fffangchenghao/dsh-autoupdater`,GitHub `fangchenghao/dsh-autoupdater`,tag `v0.6.1`。
107
+ - 本分支新增:桌面版 CLI 换版、npmmirror 优先、跳过 `link:`/git 来源、只有真正升级成功才算“已应用”、Windows 空闲判定/.cmd 调用适配;完整差异见 git log。
108
+ - 发布渠道:npm `@fffangchenghao/dsh-autoupdater`,GitHub `fangchenghao/dsh-autoupdater`,tag `v0.6.3`。
95
109
  - 与上游同步:把上游加为 remote,rebase/merge 到本分支后重新发版即可。
96
110
 
97
111
  ## 安全模型
@@ -106,6 +120,7 @@ dsh plugin --profile web add github:fangchenghao/dsh-autoupdater
106
120
  - 插件更新只覆盖声明 `dsh.bundle` 的 npm 形态插件;GitHub 源码安装的插件版本比对不适用
107
121
  - 本体更新依赖 npm 全局安装形态(git checkout 形态请用 [dsh-updater](https://github.com/GodCC6/dsh-updater);桌面 App 不参与本体更新,由 App 自带更新器负责)
108
122
  - 空闲判定基于本机 `$DSH_HOME/sessions` 的文件 mtime,远程会话(SSH 场景)不计入
123
+ - Windows 原生环境尚未实机验证(0.6.2 已移除 Unix-only 依赖,仍建议先看面板日志确认 desktop CLI 路径)
109
124
 
110
125
  ## 致谢
111
126
 
@@ -133,4 +148,6 @@ MIT
133
148
 
134
149
  Install: `dsh plugin --profile web add @fffangchenghao/dsh-autoupdater` (or `github:fangchenghao/dsh-autoupdater`), then restart DSH and configure under **Plugin Manager → dsh-autoupdater**.
135
150
 
151
+ **Windows (0.6.2+)**: the desktop path is shared with macOS. 0.6.2 removes the Unix `find` idle check, invokes `dsh.cmd` through `cmd.exe`, falls back to `homedir()` when `HOME` is absent, and leaves DSH core updates to the installer/app. Windows has not been tested end-to-end on a real machine yet; logs are welcome in GitHub issues.
152
+
136
153
  See the Chinese sections above for full details — the configuration table and idle-detection notes apply to both languages.
package/main.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { readFile, writeFile, mkdir, appendFile } from 'node:fs/promises';
2
2
  import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs';
3
- import { execSync, spawn } from 'node:child_process';
4
- import { basename, delimiter, dirname, join } from 'node:path';
3
+ import { execFileSync, spawn } from 'node:child_process';
4
+ import { basename, delimiter, dirname, join, resolve } from 'node:path';
5
+ import { homedir } from 'node:os';
5
6
 
6
7
  /** 本插件的真实包名(上游名或发布用的 scoped 名),用于把自己排除在自动更新之外。 */
7
8
  const SELF_NAME = (() => {
@@ -139,6 +140,62 @@ async function readJson(path) {
139
140
  try { return JSON.parse(await readFile(path, 'utf8')); } catch { return null; }
140
141
  }
141
142
 
143
+ /**
144
+ * 与 @deepseek-ai/dsh-home-paths 的 resolveDshHome 对齐:
145
+ * $DSH_HOME 优先(支持 ~ / ~\ 前缀,空/全空白视为未设置),否则 ~/.dsh。
146
+ * 这样 Windows 上没有 HOME、只设置 DSH_HOME 或 USERPROFILE 时都能落到同一目录。
147
+ */
148
+ function resolveDshHome(env = process.env) {
149
+ const raw = typeof env.DSH_HOME === 'string' ? env.DSH_HOME : '';
150
+ if (raw.trim().length === 0) return join(homedir(), '.dsh');
151
+ let expanded = raw;
152
+ if (raw === '~') expanded = homedir();
153
+ else if (raw.startsWith('~/') || raw.startsWith('~\\')) expanded = join(homedir(), raw.slice(2));
154
+ return resolve(expanded);
155
+ }
156
+
157
+ /**
158
+ * 递归找出目录里最新的文件 mtime(毫秒)。
159
+ * 目录不存在 → 0(没有会话 = 空闲);根目录读不了 → 抛错(调用方保守当忙)。
160
+ * 用纯 Node 实现,避免 Windows 上没有 Unix `find` 导致空闲判定永远为忙。
161
+ */
162
+ function newestFileMtimeMs(dir) {
163
+ if (!existsSync(dir)) return 0;
164
+ let newest = 0;
165
+ const stack = [dir];
166
+ while (stack.length > 0) {
167
+ const current = stack.pop();
168
+ let entries;
169
+ try {
170
+ entries = readdirSync(current, { withFileTypes: true });
171
+ } catch (e) {
172
+ if (current === dir) throw e;
173
+ continue; // 子目录权限不足不应让整个探测失败
174
+ }
175
+ for (const entry of entries) {
176
+ const full = join(current, entry.name);
177
+ if (entry.isDirectory()) { stack.push(full); continue; }
178
+ if (!entry.isFile()) continue;
179
+ try {
180
+ const m = statSync(full).mtimeMs;
181
+ if (m > newest) newest = m;
182
+ } catch {}
183
+ }
184
+ }
185
+ return newest;
186
+ }
187
+
188
+ /**
189
+ * Windows 的 npm 全局命令是 dsh.cmd,不能直接 spawn(必须经 cmd.exe)。
190
+ * 返回已加引号的命令行;其他平台或 .exe 返回 null。
191
+ * 平台参数只用于测试,生产调用走默认值。
192
+ */
193
+ function windowsShellCommand(command, args, platform = process.platform) {
194
+ if (platform !== 'win32' || !/\.(cmd|bat)$/i.test(command)) return null;
195
+ const quote = (value) => `"${String(value).replace(/"/g, '""')}"`;
196
+ return [quote(command), ...args.map(quote)].join(' ');
197
+ }
198
+
142
199
  /** 运行子进程并收集 stdout+stderr;超时或无法启动 → code -1。 */
143
200
  function spawnCapture(command, args, { env = process.env, timeoutMs = 300000 } = {}) {
144
201
  return new Promise((resolve) => {
@@ -150,15 +207,30 @@ function spawnCapture(command, args, { env = process.env, timeoutMs = 300000 } =
150
207
  settled = true;
151
208
  resolve({ code, output });
152
209
  };
210
+ const shellCommand = windowsShellCommand(command, args);
153
211
  try {
154
- child = spawn(command, args, { stdio: ['ignore', 'pipe', 'pipe'], env });
212
+ child = shellCommand
213
+ ? spawn(shellCommand, [], { stdio: ['ignore', 'pipe', 'pipe'], env, shell: true, windowsHide: true })
214
+ : spawn(command, args, { stdio: ['ignore', 'pipe', 'pipe'], env, windowsHide: true });
155
215
  } catch (e) {
156
216
  output += String(e?.message ?? e);
157
217
  finish(-1);
158
218
  return;
159
219
  }
220
+ const killTree = () => {
221
+ if (!child?.pid) return;
222
+ if (process.platform === 'win32') {
223
+ try {
224
+ execFileSync('taskkill', ['/pid', String(child.pid), '/T', '/F'], {
225
+ stdio: 'ignore', windowsHide: true,
226
+ });
227
+ } catch {}
228
+ } else {
229
+ try { child.kill('SIGKILL'); } catch {}
230
+ }
231
+ };
160
232
  const timer = setTimeout(() => {
161
- try { child.kill(); } catch {}
233
+ killTree();
162
234
  output += `\n[dsh-autoupdater] timeout after ${timeoutMs}ms`;
163
235
  finish(-1);
164
236
  }, timeoutMs);
@@ -185,12 +257,13 @@ function lastMeaningfulLine(text, limit = 220) {
185
257
  export const inject = ['agents', 'pluginManager', 'webServer'];
186
258
 
187
259
  export function apply(ctx, rawConfig = {}) {
188
- const home = process.env.DSH_HOME || `${process.env.HOME}/.dsh`;
189
- const stateDir = `${home}/autoupdater`;
190
- const statePath = `${stateDir}/status.json`;
191
- const historyPath = `${stateDir}/history.jsonl`;
192
- const pendingPath = `${stateDir}/pending-update.json`;
193
- const hotConfigPath = `${stateDir}/config.json`;
260
+ // Windows 上通常没有 HOME,只有 USERPROFILE;homedir() 会选对。
261
+ const home = resolveDshHome();
262
+ const stateDir = join(home, 'autoupdater');
263
+ const statePath = join(stateDir, 'status.json');
264
+ const historyPath = join(stateDir, 'history.jsonl');
265
+ const pendingPath = join(stateDir, 'pending-update.json');
266
+ const hotConfigPath = join(stateDir, 'config.json');
194
267
 
195
268
  let state = { lastRun: null, lastNotice: null, updates: [], lastError: null };
196
269
  readJson(statePath).then((s) => { if (s) state = { ...state, ...s }; }).catch(() => {});
@@ -203,28 +276,38 @@ export function apply(ctx, rawConfig = {}) {
203
276
  if (hot) config = normalizeConfig({ ...rawConfig, ...hot });
204
277
  }
205
278
 
279
+ /** 是否运行在 Electron 桌面 App 的进程里(macOS/Windows 安装包)。 */
280
+ const isDesktopApp = () => typeof process.versions?.electron === 'string';
281
+
206
282
  // ── profile 目录探测(磁盘版本回读用)──
207
- // DSH_PROFILE_DIR 优先;否则在 $DSH_HOME/profiles 下找含 node_modules 的目录,
208
- // 多个时取 node_modules 最近修改的。找不到 = 磁盘回读退化为仅信安装结果。
283
+ // 优先级与 DSH 自身一致:DSH_PROFILE_DIR → DSH_PROFILE → 扫描 profiles。
284
+ // 桌面版在 DSH_PROFILE 缺失时默认 desktop,避免多 profile 时误选最近修改的测试 profile。
209
285
  let profileDir = process.env.DSH_PROFILE_DIR || null;
286
+ if (profileDir && !existsSync(profileDir)) profileDir = null;
287
+ if (!profileDir) {
288
+ const configuredProfile = process.env.DSH_PROFILE || (isDesktopApp() ? 'desktop' : '');
289
+ if (configuredProfile) {
290
+ const candidate = join(home, 'profiles', configuredProfile);
291
+ if (existsSync(candidate)) profileDir = candidate;
292
+ }
293
+ }
210
294
  try {
211
- const base = `${home}/profiles`;
212
- const candidates = readdirSync(base)
213
- .map((name) => `${base}/${name}`)
214
- .filter((dir) => existsSync(`${dir}/node_modules`));
215
- if (candidates.length === 1) {
216
- profileDir = candidates[0];
217
- } else if (candidates.length > 1) {
218
- profileDir = candidates
219
- .map((dir) => ({ dir, m: statSync(`${dir}/node_modules`).mtimeMs }))
220
- .sort((a, b) => b.m - a.m)[0].dir;
295
+ if (!profileDir) {
296
+ const base = join(home, 'profiles');
297
+ const candidates = readdirSync(base)
298
+ .map((name) => join(base, name))
299
+ .filter((dir) => existsSync(join(dir, 'node_modules')));
300
+ if (candidates.length === 1) {
301
+ profileDir = candidates[0];
302
+ } else if (candidates.length > 1) {
303
+ profileDir = candidates
304
+ .map((dir) => ({ dir, m: statSync(join(dir, 'node_modules')).mtimeMs }))
305
+ .sort((a, b) => b.m - a.m)[0].dir;
306
+ }
221
307
  }
222
308
  if (profileDir && config.verbose) log('profileDir detected:', profileDir);
223
309
  } catch {}
224
310
 
225
- /** 是否运行在 Electron 桌面 App 的进程里(macOS/Windows 安装包)。 */
226
- const isDesktopApp = () => typeof process.versions?.electron === 'string';
227
-
228
311
  /**
229
312
  * 找一条能真正写 profile 的 `dsh plugin` 入口。
230
313
  *
@@ -237,9 +320,15 @@ export function apply(ctx, rawConfig = {}) {
237
320
  */
238
321
  function resolvePluginCli() {
239
322
  if (isDesktopApp() && process.resourcesPath) {
240
- const desktopCli = join(process.resourcesPath, 'app.asar', 'dsh', 'node_modules',
241
- '@deepseek-ai', 'dsh-desktop-host', 'lib', 'cli.js');
242
- if (existsSync(desktopCli)) {
323
+ const desktopCliCandidates = [
324
+ join(process.resourcesPath, 'app.asar', 'dsh', 'node_modules',
325
+ '@deepseek-ai', 'dsh-desktop-host', 'lib', 'cli.js'),
326
+ // 未打包/开发模式:resources/app/dsh/...
327
+ join(process.resourcesPath, 'app', 'dsh', 'node_modules',
328
+ '@deepseek-ai', 'dsh-desktop-host', 'lib', 'cli.js'),
329
+ ];
330
+ const desktopCli = desktopCliCandidates.find((candidate) => existsSync(candidate));
331
+ if (desktopCli) {
243
332
  return {
244
333
  command: process.execPath,
245
334
  prefixArgs: [desktopCli],
@@ -247,15 +336,21 @@ export function apply(ctx, rawConfig = {}) {
247
336
  label: 'desktop-cli',
248
337
  };
249
338
  }
250
- log('desktop CLI not found at', desktopCli);
339
+ log('desktop CLI not found at', desktopCliCandidates[0]);
251
340
  }
252
341
  // 普通 npm 全局安装:dsh 与 node 同目录(Linux 常见布局)
253
- const sibling = join(dirname(process.execPath), process.platform === 'win32' ? 'dsh.cmd' : 'dsh');
342
+ const binName = process.platform === 'win32' ? 'dsh.cmd' : 'dsh';
343
+ const sibling = join(dirname(process.execPath), binName);
254
344
  if (existsSync(sibling)) return { command: sibling, prefixArgs: [], env: {}, label: 'sibling-cli' };
255
345
  // 最后退回 PATH(macOS 上 npm 全局安装常见于 ~/.local/bin,而非 node 同目录)
256
- const binName = process.platform === 'win32' ? 'dsh.cmd' : 'dsh';
257
- for (const dir of String(process.env.PATH ?? '').split(delimiter)) {
258
- if (!dir) continue;
346
+ // Windows 环境变量大小写不敏感,但键名通常是 Path 而不是 PATH。
347
+ const pathEnv = process.env.Path ?? process.env.PATH ?? '';
348
+ const pathDirs = String(pathEnv).split(delimiter).filter(Boolean);
349
+ if (process.platform === 'win32') {
350
+ if (process.env.APPDATA) pathDirs.push(join(process.env.APPDATA, 'npm'));
351
+ if (process.env.LOCALAPPDATA) pathDirs.push(join(process.env.LOCALAPPDATA, 'pnpm'));
352
+ }
353
+ for (const dir of pathDirs) {
259
354
  const candidate = join(dir, binName);
260
355
  if (existsSync(candidate)) return { command: candidate, prefixArgs: [], env: {}, label: 'path-cli' };
261
356
  }
@@ -277,20 +372,18 @@ export function apply(ctx, rawConfig = {}) {
277
372
  // 借鉴 dsh-self-upgrade 的判定(实测比 agent.status 可靠):
278
373
  // 会话日志 2 分钟内无任何写入 = 无进行中的工作。agent.status 存在 turn 结束
279
374
  // 后仍停留 'running' 的情况(本机实测),会永久阻塞空闲判定,故弃用。
280
- const sessionsDir = `${home}/sessions`;
375
+ const sessionsDir = join(home, 'sessions');
281
376
  let sessionsBusyCache = { at: 0, busy: true };
282
377
  const anyAgentRunning = () => {
283
378
  const now = Date.now();
284
379
  if (now - sessionsBusyCache.at < 8000) return sessionsBusyCache.busy; // 8s 缓存
285
380
  try {
286
- // mtime 探测:-newermt 用固定秒窗避免 -mmin 的分钟取整误差
287
- const cutoff = new Date(Date.now() - 120_000).toISOString();
288
- const out = execSync(
289
- `find ${JSON.stringify(sessionsDir)} -type f -newermt ${JSON.stringify(cutoff)} 2>/dev/null | head -1`,
290
- { timeout: 8000 },
291
- ).toString().trim();
292
- sessionsBusyCache = { at: now, busy: out !== '' };
293
- } catch { sessionsBusyCache = { at: now, busy: true }; } // 探测失败保守当忙
381
+ // 2 分钟内有文件写入 = 有进行中的工作;目录不存在 = 没有会话 = 空闲。
382
+ const newest = newestFileMtimeMs(sessionsDir);
383
+ sessionsBusyCache = { at: now, busy: newest > now - 120_000 };
384
+ } catch {
385
+ sessionsBusyCache = { at: now, busy: true }; // 根目录探测失败保守当忙
386
+ }
294
387
  return sessionsBusyCache.busy;
295
388
  };
296
389
 
@@ -407,11 +500,13 @@ export function apply(ctx, rawConfig = {}) {
407
500
  async function runPluginCliAdd(pkg, version) {
408
501
  const cli = resolvePluginCli();
409
502
  if (!cli) {
410
- const reason = '找不到可用的 dsh CLI(桌面 App 自带的 dsh-desktop-host 与 PATH 上的 dsh 都不可用)';
503
+ const reason = `找不到可用的 dsh CLI(platform=${process.platform}, resourcesPath=${process.resourcesPath ?? 'n/a'}, execPath=${process.execPath})`;
411
504
  log('CLI add:', reason);
412
505
  return { ok: false, code: -1, output: '', reason };
413
506
  }
414
- const profileName = profileDir ? basename(profileDir) : (process.env.DSH_PROFILE || 'web');
507
+ const profileName = profileDir
508
+ ? basename(profileDir)
509
+ : (process.env.DSH_PROFILE || (isDesktopApp() ? 'desktop' : 'web'));
415
510
  // pnpm 自身不会在 registry 之间回退。国内链路 registry.npmjs.org 实测会连续
416
511
  // CONNECT_TIMEOUT(一次 130 个包安装用了 3 分 14 秒),而版本查询用的 npmmirror
417
512
  // 同一版本有同步副本、1~3 秒装完。先问 npmmirror,失败再退回 pnpm 默认源。
@@ -487,6 +582,7 @@ export function apply(ctx, rawConfig = {}) {
487
582
 
488
583
  /** 自动探测管理本进程的 systemd 服务名(无则返回空 = 裸进程模式) */
489
584
  function detectSystemdUnit() {
585
+ if (process.platform !== 'linux') return '';
490
586
  try {
491
587
  const cgroup = readFileSync('/proc/self/cgroup', 'utf8');
492
588
  const m = cgroup.match(/\/system\.slice\/([a-z0-9@._-]+\.service)/i);
@@ -524,7 +620,6 @@ export function apply(ctx, rawConfig = {}) {
524
620
  */
525
621
  async function applyHarnessUpdateNow() {
526
622
  const pendingExists = existsSync(pendingPath);
527
- const unit = config.systemdUnit || detectSystemdUnit();
528
623
 
529
624
  // 桌面 App:DSH 本体由 electron-updater 负责,npm -g 不能也不该覆盖 .app 内的运行时;
530
625
  // 插件更新也不能从进程内安全 relaunch(会连带杀掉正在跑的会话)。统一标记“待重启”。
@@ -534,6 +629,15 @@ export function apply(ctx, rawConfig = {}) {
534
629
  return;
535
630
  }
536
631
 
632
+ // Windows 非桌面模式:没有 systemd,也不支持 apply-update.mjs 的 POSIX 重启方式。
633
+ if (process.platform === 'win32') {
634
+ log('Windows: in-process restart/apply unsupported — restart required');
635
+ await appendHistory({ kind: 'restart-required', reason: 'win32', pending: pendingExists });
636
+ return;
637
+ }
638
+
639
+ const unit = config.systemdUnit || detectSystemdUnit();
640
+
537
641
  // 路径 A:仅插件更新(无 pending 文件)→ 无需 npm install,直接重启激活新插件
538
642
  if (!pendingExists) {
539
643
  log('no pending harness update — plugin-only restart');
@@ -616,6 +720,11 @@ export function apply(ctx, rawConfig = {}) {
616
720
  if (config.verbose) log('desktop build: DSH core updates are owned by the app updater — skip harness check');
617
721
  return;
618
722
  }
723
+ if (process.platform === 'win32') {
724
+ // Windows 的 npm 全局布局与 POSIX 不同,且没有 systemd 重启路径;本体交给安装器/App 更新。
725
+ if (config.verbose) log('Windows: DSH core updates are handled by the installer/app updater — skip harness check');
726
+ return;
727
+ }
619
728
  if (existsSync(pendingPath)) {
620
729
  if (config.verbose) log('pending update already staged, skip harness check');
621
730
  return;
@@ -654,7 +763,7 @@ export function apply(ctx, rawConfig = {}) {
654
763
  /** 扫描 profile 的 cordis.patch.yml,列出 !!js ctx.<service> 这类跨版本易失效的引用 */
655
764
  function scanPatchRisks() {
656
765
  try {
657
- const patchPath = `${profileDir || `${home}/profiles/web`}/cordis.patch.yml`;
766
+ const patchPath = join(profileDir || join(home, 'profiles', 'web'), 'cordis.patch.yml');
658
767
  if (!existsSync(patchPath)) return [];
659
768
  const text = readFileSync(patchPath, 'utf8');
660
769
  const found = new Set();
@@ -752,7 +861,7 @@ export function apply(ctx, rawConfig = {}) {
752
861
  // 装完回读磁盘真实版本验证,不再信返回值
753
862
  let diskVer = null;
754
863
  if (profileDir) {
755
- const diskPkg = await readJson(`${profileDir}/node_modules/${b.name}/package.json`).catch(() => null);
864
+ const diskPkg = await readJson(join(profileDir, 'node_modules', b.name, 'package.json')).catch(() => null);
756
865
  diskVer = diskPkg?.version ?? null;
757
866
  }
758
867
  const reallyUpdated = diskVer == null ? true : diskVer !== b.version; // 无 profileDir 时退化为信安装结果
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fffangchenghao/dsh-autoupdater",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Maintained fork of dsh-autoupdater with macOS/Windows desktop fixes. Scheduled update checks for DSH itself and installed plugins, applied only in idle windows.",