dsh-selfupdater 0.3.0 → 0.3.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/lib/index.js CHANGED
@@ -10,6 +10,7 @@
10
10
  * - 升级动作通过锁文件防并发,双端校验。
11
11
  */
12
12
  import { spawn } from 'node:child_process';
13
+ import { createRequire } from 'node:module';
13
14
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
14
15
  import { basename, dirname, join, resolve } from 'node:path';
15
16
  import { fileURLToPath } from 'node:url';
@@ -127,9 +128,30 @@ function resolveAppDir() {
127
128
  throw new Error(`无法定位 ${PKG} 的应用目录`);
128
129
  }
129
130
 
130
- /** 解析工作区目录(与 runner.js 的优先级一致)。 */
131
+ /**
132
+ * 解析工作区目录(插件清单 .dsh/profiles 的真正落盘处)。
133
+ * 与 runner.js 的推导链保持一致:
134
+ * 1. TRIM_DATA_SHARE_PATHS 第一个共享目录(飞牛声明的工作区,runner 会优先用它);
135
+ * 2. HOME 环境变量(runner 启动 DSH 时把 HOME 指到工作区,DSH 的 profile 就在 $HOME/.dsh 下);
136
+ * 3. TRIM_VAR / appDir/data 兜底(本地开发场景)。
137
+ * 注意:不能只看 TRIM_VAR —— 飞牛部署时它指向 app data 目录而非共享目录,
138
+ * 而 DSH 实际把 profiles 写在 HOME/.dsh 下;此前只读 TRIM_VAR 导致
139
+ * "版本更新"卡片始终显示"未发现已安装插件"。
140
+ */
131
141
  function resolveWorkspace(appDir) {
132
- return resolve(process.env.TRIM_VAR ?? appDir);
142
+ const shares = (process.env.TRIM_DATA_SHARE_PATHS ?? '').split(':').map((s) => s.trim()).filter(Boolean);
143
+ const candidates = [
144
+ ...shares,
145
+ process.env.HOME ?? '',
146
+ process.env.TRIM_VAR ?? '',
147
+ join(appDir, 'data'),
148
+ ].filter((v) => v !== '');
149
+ // 候选目录下若已存在 .dsh 则视为工作区,立即采用;
150
+ // 否则按 runner.js 同样的优先级取第一个候选(与实际落盘位置保持一致)。
151
+ for (const dir of candidates) {
152
+ if (existsSync(join(dir, '.dsh'))) return resolve(dir);
153
+ }
154
+ return resolve(candidates[0] ?? appDir);
133
155
  }
134
156
 
135
157
  /** 服务端口:与 runner.js 保持一致的环境变量与默认值。 */
@@ -255,6 +277,21 @@ async function fetchPluginLatest(pkgName) {
255
277
  return fetchLatestVersion(pkgName);
256
278
  }
257
279
 
280
+ /**
281
+ * 读本插件自身的已装版本:优先 import 自己的 package.json(ESM 顶层 await
282
+ * 不适合这里,改用 createRequire 同步读取),失败时回退硬编码兜底值。
283
+ * 用途:让"检测更新"能覆盖 dsh-selfupdater 自己 —— 此前清单来自 profile
284
+ * 的 dependencies,但"检测自己"语义上不依赖那份清单。
285
+ */
286
+ function readOwnVersion() {
287
+ try {
288
+ const require = createRequire(import.meta.url);
289
+ return require('../package.json').version;
290
+ } catch {
291
+ return '0.0.0';
292
+ }
293
+ }
294
+
258
295
  /* ------------------------------------------------------------------ *
259
296
  * apply 入口
260
297
  * ------------------------------------------------------------------ */
@@ -272,6 +309,9 @@ export function apply(ctx) {
272
309
  const lockFile = join(dshStateDir, 'selfupdate.lock');
273
310
  const pluginLockFile = join(dshStateDir, 'pluginupdate.lock');
274
311
  const port = servicePort();
312
+ /** 本插件的包名与已装版本(用于"检测自己"的更新能力)。 */
313
+ const SELF_NAME = name;
314
+ const SELF_INSTALLED = readOwnVersion();
275
315
 
276
316
  /** 升级是否正在进行(锁文件存在)。 */
277
317
  const isBusy = () => existsSync(lockFile);
@@ -387,7 +427,11 @@ export function apply(ctx) {
387
427
  });
388
428
 
389
429
  registerRoute('GET', '/dsh-selfupdater/plugins', (_req, res) => {
390
- const installed = listInstalledPlugins(workspace);
430
+ // 清单 = 自己 + profile 已装插件(自己排最前,保证 UI 始终展示自身)。
431
+ const installed = [
432
+ { name: SELF_NAME, installedVersion: SELF_INSTALLED },
433
+ ...listInstalledPlugins(workspace).filter((p) => p.name !== SELF_NAME),
434
+ ];
391
435
  const status = pluginStatusView();
392
436
  // 把上次检查得到的 latest 缓存合并进列表,UI 无需二次请求。
393
437
  const cache = status.updates ?? {};
@@ -413,12 +457,13 @@ export function apply(ctx) {
413
457
  sendJson(res, 403, { error: 'untrusted request' });
414
458
  return;
415
459
  }
416
- const installed = listInstalledPlugins(workspace);
417
- if (installed.length === 0) {
418
- sendJson(res, 200, { plugins: [], note: '未发现已安装插件' });
419
- return;
420
- }
421
- // 并发查询所有插件的 npm 最新版;单个失败不拖垮整体。
460
+ // 清单 = 自己 + profile 已装插件(与 GET /plugins 保持一致)。
461
+ const installed = [
462
+ { name: SELF_NAME, installedVersion: SELF_INSTALLED },
463
+ ...listInstalledPlugins(workspace).filter((p) => p.name !== SELF_NAME),
464
+ ];
465
+ // 并发查询所有插件(清单已保证至少包含自己,不可能为空)的最新版;
466
+ // 单个失败不拖垮整体。
422
467
  const results = await Promise.allSettled(installed.map(async (p) => {
423
468
  const latestVersion = await fetchPluginLatest(p.name);
424
469
  return {
@@ -122,8 +122,20 @@ const args = parseArgs(process.argv);
122
122
 
123
123
  /** DSH 应用目录(含 bin/node 与 node_modules/@deepseek-ai/dsh)。 */
124
124
  const appDir = resolve(String(args['app-dir'] ?? ''));
125
- /** 工作区目录(存放 .dsh 状态目录与插件 profile)。 */
126
- const workspace = resolve(String(args.workspace ?? process.env.TRIM_VAR ?? appDir));
125
+ /**
126
+ * 工作区目录(存放 .dsh 状态目录与插件 profile)。
127
+ * 与 index.js 的 resolveWorkspace 推导链一致:共享目录 > HOME > TRIM_VAR > appDir。
128
+ * index.js 已解析出正确 workspace 并通过 --workspace 传入,这里仅在参数缺失时兜底;
129
+ * 不能只信 TRIM_VAR —— 飞牛部署时 profiles 实际写在 $HOME/.dsh 下。
130
+ */
131
+ const workspace = (() => {
132
+ if (args.workspace) return resolve(String(args.workspace));
133
+ const shares = (process.env.TRIM_DATA_SHARE_PATHS ?? '').split(':').map((s) => s.trim()).filter(Boolean);
134
+ for (const dir of [...shares, process.env.HOME ?? '', process.env.TRIM_VAR ?? '', appDir]) {
135
+ if (dir !== '' && existsSync(join(dir, '.dsh'))) return resolve(dir);
136
+ }
137
+ return resolve(shares[0] ?? process.env.HOME ?? process.env.TRIM_VAR ?? appDir);
138
+ })();
127
139
  /** Web 服务端口,健康检查用。 */
128
140
  const port = parseInt(String(args.port ?? process.env.DSH_PORT ?? '3081'), 10);
129
141
 
@@ -316,18 +328,28 @@ async function main() {
316
328
  // 锁文件双端校验:路由触发前会检查;这里再补一道防手动重复执行。
317
329
  if (existsSync(lockFile)) throw new Error('已有一次插件更新在进行中(锁文件存在)');
318
330
 
319
- // 读已安装清单:profile/package.json 的 dependencies 是最权威的数据源。
331
+ // 读已安装清单:profile/package.json 的 dependencies 是最权威的数据源;
332
+ // 再补上本插件自身(dsh-selfupdater),保证"自己也能被更新"。
320
333
  let deps;
321
334
  try {
322
335
  deps = JSON.parse(readFileSync(join(profileDir, 'package.json'), 'utf8')).dependencies ?? {};
323
336
  } catch {
324
- setState('idle', '未发现已安装插件,无需更新');
325
- finish(0);
337
+ deps = {};
326
338
  }
339
+ /** 本插件自身的已装版本:直接读自己脚本身旁的 package.json,最可靠。 */
340
+ let selfVersion = '';
341
+ try {
342
+ selfVersion = JSON.parse(
343
+ readFileSync(new URL('./../package.json', import.meta.url), 'utf8'),
344
+ ).version ?? '';
345
+ } catch { /* 读不到则跳过自更 */ }
327
346
  const installed = Object.entries(deps).map(([name, range]) => ({
328
347
  name,
329
348
  version: String(range).replace(/^[~^]\s*/, ''),
330
- })).filter((p) => p.version !== '');
349
+ })).filter((p) => p.version !== '' && p.name !== 'dsh-selfupdater');
350
+ if (selfVersion !== '') {
351
+ installed.unshift({ name: 'dsh-selfupdater', version: selfVersion });
352
+ }
331
353
 
332
354
  if (installed.length === 0) {
333
355
  setState('idle', '未发现已安装插件,无需更新');
package/lib/updater.mjs CHANGED
@@ -130,8 +130,20 @@ const args = parseArgs(process.argv);
130
130
 
131
131
  /** DSH 应用目录(含 node_modules / bin/runner.js),由插件本体解析后传入。 */
132
132
  const appDir = resolve(String(args['app-dir'] ?? ''));
133
- /** 工作区目录(存放 .dsh 状态目录)。 */
134
- const workspace = resolve(String(args.workspace ?? process.env.TRIM_VAR ?? appDir));
133
+ /**
134
+ * 工作区目录(存放 .dsh 状态目录)。
135
+ * 与 index.js / plugin-updater.mjs 的推导链一致:共享目录 > HOME > TRIM_VAR > appDir。
136
+ * 正常情况下 index.js 已通过 --workspace 传入解析结果,这里仅兜底;
137
+ * 不能只信 TRIM_VAR —— 飞牛部署时 .dsh 实际写在 $HOME(工作区)下。
138
+ */
139
+ const workspace = (() => {
140
+ if (args.workspace) return resolve(String(args.workspace));
141
+ const shares = (process.env.TRIM_DATA_SHARE_PATHS ?? '').split(':').map((s) => s.trim()).filter(Boolean);
142
+ for (const dir of [...shares, process.env.HOME ?? '', process.env.TRIM_VAR ?? '', appDir]) {
143
+ if (dir !== '' && existsSync(join(dir, '.dsh'))) return resolve(dir);
144
+ }
145
+ return resolve(shares[0] ?? process.env.HOME ?? process.env.TRIM_VAR ?? appDir);
146
+ })();
135
147
  /** Web 服务端口,健康检查用。 */
136
148
  const port = parseInt(String(args.port ?? process.env.DSH_PORT ?? '3081'), 10);
137
149
  /** 要升级的包名。 */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-selfupdater",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Self-update plugin for DeepSeek Harness: check npm for newer @deepseek-ai/dsh, swap node_modules atomically, restart, health-check and roll back on failure; also supports one-click online update of installed DSH plugins. DSH 主程序与已装插件的一站式在线更新插件。",
5
5
  "license": "MIT",
6
6
  "type": "module",