dsh-selfupdater 0.4.6 → 0.4.8

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.
@@ -135,15 +135,26 @@ function readJson(file) {
135
135
  }
136
136
  }
137
137
 
138
- /** 读 profile 清单里记录的本插件版本声明(可能带 ^/~ 前缀);无记录返回 null。 */
138
+ /** 读 profile 清单里记录的本插件依赖声明(可能是 ^0.4.6,也可能是 file:/…tgz);无记录返回 null。 */
139
139
  function profileDeclaredVersion(profileDir) {
140
140
  const dep = readJson(join(profileDir, 'package.json'))?.dependencies?.[SELF_NAME];
141
141
  return typeof dep === 'string' && dep !== '' ? dep : null;
142
142
  }
143
143
 
144
- /** 去掉 ^/~ 等范围前缀后比较是否等于目标版本。 */
145
- function declarationMatches(declared, expected) {
146
- return declared.replace(/^[\^~>=<\s]+/, '') === expected;
144
+ /**
145
+ * profile 下 node_modules 里实际安装的自身版本 —— 唯一权威来源。
146
+ * FPK 等安装方式会把清单声明写成 file:/…tgz 而非版本号(0.4.6 就因此
147
+ * 把"当前版本"显示成了整个 tgz 路径,连带 isNewer 误判"已是最新");
148
+ * 且更新装好但宿主未重启时,也只有这里的 package.json 是新版本。读不到返回 null。
149
+ */
150
+ function installedVersionFromModules(profileDir) {
151
+ const version = readJson(join(profileDir, 'node_modules', SELF_NAME, 'package.json'))?.version;
152
+ return typeof version === 'string' && version !== '' ? version : null;
153
+ }
154
+
155
+ /** 字符串是否为合法 semver(file: URL、git URL 等非法声明返回 false)。 */
156
+ function isSemverString(v) {
157
+ return parseSemver(v) !== null;
147
158
  }
148
159
 
149
160
  /** 当前正在运行的自身版本(读本包 package.json;ESM 下用 createRequire 同步读)。 */
@@ -157,15 +168,29 @@ function runningVersion() {
157
168
  }
158
169
 
159
170
  /**
160
- * 已安装的自身版本(供"检查更新/状态展示"使用)。
161
- * 优先读 profile 清单 dependencies 里声明的版本:更新装好但宿主未重启时,
162
- * 它才是"已安装版本",能让 UI 立即显示新版号并停止误报"发现新版本"
163
- * 清单里没有记录时回退到正在运行的版本(首次安装/手工部署场景)。
171
+ * 解析"已安装的自身版本"(检查更新/状态展示/更新任务三方共用),三级兜底:
172
+ * 1. profile/node_modules 实际落盘的版本(权威;覆盖 file: 安装与未重启场景);
173
+ * 2. 清单 dependencies 声明(仅当是合法 semverfile: 等 URL 声明不可用);
174
+ * 3. 正在运行的版本(本地开发/异常兜底)。
175
+ */
176
+ function resolveInstalledVersion(profileDir) {
177
+ const fromModules = installedVersionFromModules(profileDir);
178
+ if (fromModules !== null) return fromModules;
179
+ const declared = profileDeclaredVersion(profileDir);
180
+ if (declared !== null && isSemverString(declared.replace(/^[\^~>=<\s]+/, ''))) {
181
+ return declared.replace(/^[\^~>=<\s]+/, '');
182
+ }
183
+ return runningVersion();
184
+ }
185
+
186
+ /**
187
+ * 已安装的自身版本(供"检查更新/状态展示"路由使用)。
188
+ * 必须实时读盘而非模块加载时快照:更新装好但宿主未重启期间,
189
+ * UI 要立即显示新版号,"检查更新"也不能再误报"发现新版本"。
164
190
  */
165
191
  export function installedSelfVersion(workspace) {
166
192
  const profileDir = join(workspace, '.dsh', 'profiles', process.env.DSH_PLUGIN_PROFILE ?? 'web');
167
- const declared = profileDeclaredVersion(profileDir);
168
- return declared !== null ? declared.replace(/^[\^~>=<\s]+/, '') : runningVersion();
193
+ return resolveInstalledVersion(profileDir);
169
194
  }
170
195
 
171
196
  /** 定位 dsh CLI 入口:打包布局优先,其次 node_modules 标准布局。 */
@@ -268,10 +293,9 @@ export async function runPluginUpdateTask({ appDir, workspace, logger }) {
268
293
  rmSync(orphanBak, { recursive: true, force: true });
269
294
  }
270
295
 
271
- /* 1. 确定当前版本:优先 profile 清单记录(更新后未重启时它才是新值),
272
- 无记录时退回正在运行的版本。 */
273
- const declared = profileDeclaredVersion(profileDir);
274
- const installed = declared !== null ? declared.replace(/^[\^~>=<\s]+/, '') : runningVersion();
296
+ /* 1. 确定当前版本:读实际落盘的 node_modules(权威来源,见
297
+ resolveInstalledVersion 注释——清单声明可能是 file:/…tgz)。 */
298
+ const installed = resolveInstalledVersion(profileDir);
275
299
  setState('running', '正在查询最新版本…', { startedAt: new Date().toISOString() });
276
300
 
277
301
  /* 2. 查询最新版本与下载地址。 */
@@ -300,15 +324,29 @@ export async function runPluginUpdateTask({ appDir, workspace, logger }) {
300
324
  INSTALL_TIMEOUT_MS,
301
325
  );
302
326
 
303
- /* 6. 校验:清单版本号一致 + 入口文件真实落盘。 */
304
- const after = profileDeclaredVersion(profileDir);
305
- if (after === null || !declarationMatches(after, release.version)) {
306
- throw new Error(`安装后清单版本异常:期望 ${release.version},实际 ${after ?? '缺失'}`);
327
+ /* 6. 校验:实际落盘版本一致 + 入口文件存在。
328
+ 不读清单声明——本地 tgz 安装会把声明写成 file: 指向暂存文件,不可靠。 */
329
+ const installedNow = installedVersionFromModules(profileDir);
330
+ if (installedNow !== release.version) {
331
+ throw new Error(`安装后版本异常:期望 ${release.version},实际 ${installedNow ?? '未安装'}`);
307
332
  }
308
333
  if (!existsSync(join(profileDir, 'node_modules', SELF_NAME, 'lib', 'index.js'))) {
309
334
  throw new Error('入口文件未落盘,安装可能不完整');
310
335
  }
311
336
 
337
+ /* 6.5 修正清单声明:dsh plugin add 装本地 tgz 会把依赖写成 file: 指向
338
+ 暂存文件,而暂存文件随后会被删除(引用悬空);改写为精确版本号,
339
+ 保证下次重建依赖时能从 registry 正常解析。失败不影响本次安装。 */
340
+ try {
341
+ const manifest = readJson(manifestPath);
342
+ if (manifest.dependencies?.[SELF_NAME] !== release.version) {
343
+ manifest.dependencies = { ...manifest.dependencies, [SELF_NAME]: release.version };
344
+ writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
345
+ }
346
+ } catch (err) {
347
+ warnLog(`清单声明修正失败(不影响已安装文件): ${err.message}`);
348
+ }
349
+
312
350
  /* 7. 成功:不重启宿主,提示用户重启使新版本生效。 */
313
351
  setState('done_pending_restart',
314
352
  `插件已更新到 ${release.version},请重启 DeepSeek Harness 使新版本生效`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-selfupdater",
3
- "version": "0.4.6",
3
+ "version": "0.4.8",
4
4
  "description": "Self-update plugin for DeepSeek Harness: DSH core upgrades via detached swap script; plugin self-update installs in-place without killing the host and prompts for restart. DSH 主程序与已装插件的一站式在线更新插件。",
5
5
  "license": "MIT",
6
6
  "type": "module",