dsh-m 0.10.0 → 0.10.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/CHANGELOG.md CHANGED
@@ -8,6 +8,12 @@ The full release history of dsh-m, maintained bilingually: **Chinese first, Engl
8
8
 
9
9
  ## 中文
10
10
 
11
+ ### 0.10.1 修复:0.10.0 真机 E2E 实证缺陷三连——enable 竞速重试普适化 + 回装失败账实复读 + 守卫命中一律两步化
12
+
13
+ - **需求(2026-10-17,0.10.0 装机当日 E2E 实证,真机 skip-auth@0.4.1 与卸载→重装两个事件)**:①插件树重载窗口的 enable 阶段激活竞速(`pending (waiting for service: webRuntime)`)会让两步升级的第二腿、兜底回装腿与 D3 自升级通道直接终态失败——0.9.94 时代的「enable 失败自动重试一次(重试即愈)」本地增强只覆盖了首腿;②回装腿报失败 ≠ 真缺席——pnpm 可能已写入账面(激活阶段失败),此时建议「手动重装旧版」会误导用户降级;③卸载后同会话重装:宿主活跃态/manifest 字面仍视其在装(守卫命中),而 previousVersion 探测口径为「未安装」,0.10.0 的 previousVersion 前置门使守卫命中原样透传 already-installed。
14
+ - **方案**:① enable 竞速自动重试普适化——首腿、两步第二腿、兜底回装腿、D3 通道同享受限单次重试(`installOnceRetryingEnable`,退避与豁免形态不变);② 宣称缺席前实读账面(listInstalled)——已写入 → 「请重启 Desktop 完成激活」(details.installedVersion 如实透出),真缺席 → 重装指引,两条路径零误导性降级建议;③ install 路径守卫命中一律两步化(remove+add 即官方语义,去掉 previousVersion 前置门),无旧版可回装时失败走同款账实复读。附带:check-release 门禁行分割容忍 CRLF(本机工作树双语顶条恒找不到的修复)。
15
+ - **验证**:typecheck 零错误;全量 1595 例(新增 8 钉:T-A1/A2/A3 enable 竞速三腿 + T-B1/B2 账实复读两形态 + T-C1/C2/C3 无旧版守卫两步化三态;18 例既有失败为 028 本机环境清单零新增);真机恢复实录:skip-auth 0.4.1 经「重启激活」恢复(零重装),重启后守卫锚点干净。
16
+
11
17
  ### 0.10.0 特性+修复:宿主 0.2.1 already-installed 适配——第三方两步升级 + 兜底回装降级恢复 + 自升级专用通道(D3)
12
18
 
13
19
  - **需求(2026-10-17,主人)**:新版 DSH(0.2.1-rc.1/rc.2)中 dsh-m 无法升级与自升级任何包。根因定谳:宿主 `installBundle()` 新增 already-installed 安装守卫——spec 解析出的包名已在 profile manifest 就先于 pnpm 拒绝,官方升级语义改为「先卸载再安装」;tarball/git 形态 spec 包名在 pnpm 前不可知、不受守卫约束(真机 spike 实证:URL 覆盖安装已装 dsh-m 成功、激活形态 restart-required)。影响面:产品内 desktop 升级/自升级全断;卸载、开关、全新安装、web 路径无恙。跳过 0.9.95+ 直接发 0.10.0。
@@ -667,6 +673,12 @@ The full release history of dsh-m, maintained bilingually: **Chinese first, Engl
667
673
 
668
674
  ## English
669
675
 
676
+ ### Fixes in 0.10.1 — three defects proven by same-day real-machine E2E of 0.10.0: enable-race retry universalized + account-reality re-read on rollback failure + guard hit always two-steps
677
+
678
+ - **Need (2026-10-17, proven during the same-day real-machine E2E of 0.10.0 — the skip-auth@0.4.1 upgrade event and the uninstall→reinstall event)**: ① the enable-stage activation race during plugin-tree reload windows (`pending (waiting for service: webRuntime)`) made the two-step upgrade's second leg, the rollback reinstall leg, and the D3 self-upgrade channel fail terminally — the 0.9.94-era "retry enable failures once (retry heals)" local enhancement covered only the first leg; ② a failed reinstall leg ≠ true absence — pnpm may have already written the package (activation failed afterwards), where advising "manually reinstall the old version" would mislead the user into a downgrade; ③ uninstall followed by a same-session reinstall: the host's live state / literal manifest still considers the package installed (guard fires) while the previousVersion probe reports "not installed", so 0.10.0's previousVersion precondition let the raw already-installed error surface.
679
+ - **Approach**: ① universalize the enable-race auto-retry — the first leg, the two-step second leg, the rollback reinstall leg, and the D3 channel all share the limited single retry (`installOnceRetryingEnable`; backoff and exempted failure shapes unchanged); ② re-read the ledger (listInstalled) before claiming absence — if written → "restart Desktop to finish activation" (with `details.installedVersion` surfaced truthfully), if truly absent → reinstall guidance; neither path suggests a misleading downgrade; ③ on the install path a guard hit always two-steps (remove+add is the official semantics; the previousVersion precondition is removed), and a failure without an old version to roll back to goes through the same ledger re-read. Incidental: the check-release gate now splits lines CRLF-tolerantly (fixing the bilingual top-entry false negative on CRLF working trees).
680
+ - **Verification**: typecheck clean; full suite 1595 tests (8 new pins: T-A1/A2/A3 enable-race on three legs + T-B1/B2 both ledger-re-read shapes + T-C1/C2/C3 three states of guard-hit two-step without an old version; 18 pre-existing failures are the 028 machine-environment set, zero new); real-machine recovery record: skip-auth 0.4.1 recovered via "restart to activate" (zero reinstall), guard journal anchors clean after restart.
681
+
670
682
  ### Feature + fix in 0.10.0 — host 0.2.1 already-installed adaptation: third-party two-step upgrade + rollback-reinstall degraded restore + dedicated self-upgrade channel (D3)
671
683
 
672
684
  - **Need (2026-10-17, the owner)**: on the new DSH (0.2.1-rc.1/rc.2) dsh-m could no longer upgrade or self-upgrade any package. Root cause: the host's `installBundle()` gained an already-installed guard — when the package name resolved from a registry spec already exists in the profile manifest it is rejected before pnpm runs, and the official upgrade semantics became "uninstall first, then install"; tarball/git specs are exempt because their package name is unknowable before pnpm (verified on a real machine: installing a tarball URL over the installed dsh-m succeeded, activation shape restart-required). Scope: in-product desktop upgrade/self-upgrade fully broken; uninstall, toggle, fresh install, and the web path unaffected. Version 0.9.95+ skipped straight to 0.10.0.
@@ -340,6 +340,18 @@ async function runManagedInstall(service, ctx, deps = {}) {
340
340
  enabled: true,
341
341
  ...(ctx.approvedBuilds && ctx.approvedBuilds.length > 0 ? { approvedBuilds: ctx.approvedBuilds } : {}),
342
342
  }));
343
+ // enable 阶段竞速自动重试(2026-10-02 本地增强的普适化,0.10.1 E2E 实证扩面):
344
+ // 高频装卸/插件树重载窗口下 enable 阶段与应用侧竞速(实机实证「重试即愈」,skip-auth@0.4.1 事件二度实证)——
345
+ // 首腿、两步第二腿、兜底回装腿同享受限单次重试;cancelled / build-blocked / release-age 等其他失败形态不重试。
346
+ const installOnceRetryingEnable = async (spec) => {
347
+ const first = await installOnce(spec);
348
+ if (first.stage === 'enable' && (first.application === 'failed' || first.error)) {
349
+ if (enableRetryDelayMs > 0)
350
+ await sleepMs(enableRetryDelayMs);
351
+ return installOnce(spec);
352
+ }
353
+ return first;
354
+ };
343
355
  /** 失败映射(既有语义不变:cancelled / build-blocked 审批 / release-age 等待 / enable / 通用)。 */
344
356
  const mapFailure = async (change) => {
345
357
  if (change.application === 'cancelled') {
@@ -395,14 +407,8 @@ async function runManagedInstall(service, ctx, deps = {}) {
395
407
  }
396
408
  return { change, bundleName };
397
409
  };
398
- let change = await installOnce();
399
- // 本地增强:enable 阶段失败自动重试一次(受限单次 + 短退避;与高频装卸/插件树重载的竞速重试即愈;
400
- // cancelled / build-blocked / release-age 等其他失败形态不重试)
401
- if (change.stage === 'enable' && (change.application === 'failed' || change.error)) {
402
- if (enableRetryDelayMs > 0)
403
- await sleepMs(enableRetryDelayMs);
404
- change = await installOnce();
405
- }
410
+ let change = await installOnceRetryingEnable(ctx.spec);
411
+ // (enable 阶段竞速自动重试已普适化进 installOnceRetryingEnable——首腿/两步第二腿/兜底回装腿/D3 同享)
406
412
  // ADR-0009 挂点3:双码失败 → 治理 → changed 则重试该命令一次(每次调用至多一次;
407
413
  // cancelled / build-blocked 不治理不重试)。仍败走既有 mapFailure(含账实分裂复读);
408
414
  // 挂点3 已落盘的合并以短语并入失败 message 与成功 output(留痕契约含挂点3)。
@@ -419,49 +425,96 @@ async function runManagedInstall(service, ctx, deps = {}) {
419
425
  // (removeBundle 旧版 → 重试安装同一 spec,至多一次、无循环);第二腿终态失败 → 兜底回装旧版
420
426
  // (remove 后旧名已不在 manifest,守卫不适用):回装成功 = 降级恢复结局(degraded,R4);
421
427
  // cancelled / build-blocked 语义优先(不冒充降级);回装失败 = 缺席硬错误(rollback-failed)。
422
- // 无 previousVersion 不两步化——R3 兜底网的存在前提(无旧版即无回装能力)。
428
+ // 0.10.1(E2E 实证 ③):去掉 previousVersion 前置门——卸载后同会话重装时,宿主活跃态/manifest
429
+ // 字面仍视其在装(守卫命中),而探测口径为「未安装」;守卫命中即 remove+add(官方语义),
430
+ // 无旧版可回装时失败走账实复读给恢复指引。
423
431
  let twoStepNote;
424
- if (isAlreadyInstalledGuard(change) && ctx.previousVersion && typeof service.removeBundle === 'function') {
432
+ if (isAlreadyInstalledGuard(change) && typeof service.removeBundle === 'function') {
425
433
  twoStepNote = 'already-installed → two-step';
426
434
  const removeChange = changeOf(await service.removeBundle(ctx.pkg));
427
435
  if (isTerminalFailure(removeChange)) {
428
436
  // remove 腿失败短路(不回装):旧版尚未移除,状态以官方插件页为准
429
437
  throw new DesktopOpsError('remove-failed', `两步升级第一步(卸载旧版)失败(${ctx.pkg};application=${removeChange.application ?? 'failed'}${removeChange.error?.code ? `、error=${removeChange.error.code}` : ''}):未执行升级,请到官方 Desktop 插件页确认 ${ctx.pkg} 状态`);
430
438
  }
431
- change = await installOnce();
439
+ change = await installOnceRetryingEnable(ctx.spec);
432
440
  if (isTerminalFailure(change)) {
433
441
  const pendingAdd = pendingOf(change);
434
442
  const addDiagnostic = `${change.error?.diagnostic ?? ''}${change.error?.code ? `\n${change.error.code}` : ''}`.trim();
435
443
  const addCancelled = change.application === 'cancelled';
436
444
  const addBuildBlocked = change.packageResult?.kind === 'build-blocked' && pendingAdd.length > 0;
437
- // 兜底回装旧版(R3):回装腿失败(含其自身 build-blocked)= 缺席硬错误,降级失败真相优先
438
- const rollbackChange = await installOnce(`${ctx.pkg}@${ctx.previousVersion}`);
439
- const pendingRollback = pendingOf(rollbackChange);
440
- if (isTerminalFailure(rollbackChange) || (rollbackChange.packageResult?.kind === 'build-blocked' && pendingRollback.length > 0)) {
441
- throw new DesktopOpsError('rollback-failed', `升级失败且自动回装旧版也失败(${ctx.pkg}@${ctx.previousVersion}):插件当前可能缺席,请在官方 Desktop 插件页手动重装 ${ctx.pkg}@${ctx.previousVersion}${addDiagnostic !== '' ? `;升级失败原因:${addDiagnostic}` : ''}`, {
442
- pkg: ctx.pkg,
443
- previousVersion: ctx.previousVersion,
444
- ...(addDiagnostic !== '' ? { addDiagnostic } : {}),
445
- ...(pendingRollback.length > 0 ? { pendingBuilds: pendingRollback } : {}),
446
- });
445
+ if (ctx.previousVersion) {
446
+ // 兜底回装旧版(R3):回装腿失败(含其自身 build-blocked)= 缺席硬错误,降级失败真相优先
447
+ const rollbackChange = await installOnceRetryingEnable(`${ctx.pkg}@${ctx.previousVersion}`);
448
+ const pendingRollback = pendingOf(rollbackChange);
449
+ if (isTerminalFailure(rollbackChange) || (rollbackChange.packageResult?.kind === 'build-blocked' && pendingRollback.length > 0)) {
450
+ // 0.10.1(E2E 实证,skip-auth@0.4.1 事件):回装腿失败 ≠ 一定缺席——pnpm 可能已写入账面
451
+ // (激活阶段失败)。宣称缺席前实读账面,按实况分派恢复指引(账实相符优先,零误导性降级建议)。
452
+ let installedVersion;
453
+ if (ctx.profileDir && ctx.listInstalled) {
454
+ try {
455
+ const row = (await ctx.listInstalled(ctx.profileDir)).items.find((it) => it.pkg === ctx.pkg);
456
+ installedVersion = row?.version;
457
+ }
458
+ catch {
459
+ // 复读失败按未知处理:保留既有缺席文案(含「可能」字样,不断言)
460
+ }
461
+ }
462
+ const message = installedVersion !== undefined
463
+ ? `升级失败且自动回装未完成,但账面显示 ${ctx.pkg}@${installedVersion} 已写入(激活阶段可能未完成):请重启 Desktop 完成激活;如重启后仍异常,请在官方 Desktop 插件页手动处理${addDiagnostic !== '' ? `;升级失败原因:${addDiagnostic}` : ''}`
464
+ : `升级失败且自动回装旧版也失败(${ctx.pkg}@${ctx.previousVersion}):插件当前可能缺席,请在官方 Desktop 插件页手动重装 ${ctx.pkg}@${ctx.previousVersion}${addDiagnostic !== '' ? `;升级失败原因:${addDiagnostic}` : ''}`;
465
+ throw new DesktopOpsError('rollback-failed', message, {
466
+ pkg: ctx.pkg,
467
+ previousVersion: ctx.previousVersion,
468
+ ...(addDiagnostic !== '' ? { addDiagnostic } : {}),
469
+ ...(pendingRollback.length > 0 ? { pendingBuilds: pendingRollback } : {}),
470
+ ...(installedVersion !== undefined ? { installedVersion } : {}),
471
+ });
472
+ }
473
+ const verifiedRollback = await verifyAndBundle(rollbackChange);
474
+ if (addBuildBlocked) {
475
+ // build-blocked 语义优先:旧版已兜住,仍抛审批请求(不冒充降级成功)
476
+ throw new DesktopBuildApprovalNeeded(pendingAdd);
477
+ }
478
+ if (addCancelled) {
479
+ // 取消不冒充降级成功:已恢复旧版、未做升级
480
+ throw new DesktopOpsError('install-refused', `升级已被取消(${ctx.pkg}):已恢复旧版 ${ctx.previousVersion},未做升级`);
481
+ }
482
+ // 降级恢复(R4):成功形态 + degraded 标记;跳过挂点2 登记——实际在装的是旧版,登记新版本会是假账
483
+ return {
484
+ ...verifiedRollback,
485
+ ...(governRetryNote !== undefined ? { governRetryNote } : {}),
486
+ degraded: true,
487
+ restoredTo: ctx.previousVersion,
488
+ twoStepNote,
489
+ };
447
490
  }
448
- const verifiedRollback = await verifyAndBundle(rollbackChange);
491
+ // 0.10.1(E2E 实证 ③):无 previousVersion——remove 已按官方语义执行、无旧版可回装。
492
+ // build-blocked / cancelled 语义照旧;其余按账实复读给恢复指引(缺席硬错误族)。
449
493
  if (addBuildBlocked) {
450
- // build-blocked 语义优先:旧版已兜住,仍抛审批请求(不冒充降级成功)
451
494
  throw new DesktopBuildApprovalNeeded(pendingAdd);
452
495
  }
453
496
  if (addCancelled) {
454
- // 取消不冒充降级成功:已恢复旧版、未做升级
455
- throw new DesktopOpsError('install-refused', `升级已被取消(${ctx.pkg}):已恢复旧版 ${ctx.previousVersion},未做升级`);
497
+ throw new DesktopOpsError('install-refused', `安装已被取消(${ctx.pkg}):未安装`);
498
+ }
499
+ let installedVersion;
500
+ if (ctx.profileDir && ctx.listInstalled) {
501
+ try {
502
+ const row = (await ctx.listInstalled(ctx.profileDir)).items.find((it) => it.pkg === ctx.pkg);
503
+ installedVersion = row?.version;
504
+ }
505
+ catch {
506
+ // 复读失败按未知处理:缺席文案含「可能」字样,不断言
507
+ }
456
508
  }
457
- // 降级恢复(R4):成功形态 + degraded 标记;跳过挂点2 登记——实际在装的是旧版,登记新版本会是假账
458
- return {
459
- ...verifiedRollback,
460
- ...(governRetryNote !== undefined ? { governRetryNote } : {}),
461
- degraded: true,
462
- restoredTo: ctx.previousVersion,
463
- twoStepNote,
464
- };
509
+ const message = installedVersion !== undefined
510
+ ? `安装未完成,但账面显示 ${ctx.pkg}@${installedVersion} 已写入(激活阶段可能未完成):请重启 Desktop 完成激活;如重启后仍异常,请在官方 Desktop 插件页手动处理${addDiagnostic !== '' ? `;失败原因:${addDiagnostic}` : ''}`
511
+ : `安装失败且无法自动恢复(${ctx.pkg}):插件当前可能缺席,请在官方 Desktop 插件页手动重装${addDiagnostic !== '' ? `;失败原因:${addDiagnostic}` : ''}`;
512
+ throw new DesktopOpsError('rollback-failed', message, {
513
+ pkg: ctx.pkg,
514
+ ...(addDiagnostic !== '' ? { addDiagnostic } : {}),
515
+ ...(pendingAdd.length > 0 ? { pendingBuilds: pendingAdd } : {}),
516
+ ...(installedVersion !== undefined ? { installedVersion } : {}),
517
+ });
465
518
  }
466
519
  }
467
520
  if (change.application === 'failed' || change.application === 'cancelled' || change.error) {
@@ -610,7 +663,13 @@ async function desktopSelfUpgradeViaTarballLocked(pkgName, currentVersion, opts,
610
663
  });
611
664
  const resolveIntent = deps.resolveUpgradeIntent ?? defaultResolveUpgradeIntent;
612
665
  try {
613
- const change = changeOf(await service.installBundle(spec, { enabled: true }));
666
+ let change = changeOf(await service.installBundle(spec, { enabled: true }));
667
+ // 0.10.1:enable 阶段竞速自动重试(与 runManagedInstall 各腿同款;skip-auth@0.4.1 真机事件实证竞速窗口真实)
668
+ if (change.stage === 'enable' && (change.application === 'failed' || change.error)) {
669
+ if (enableRetryDelayMs > 0)
670
+ await sleepMs(enableRetryDelayMs);
671
+ change = changeOf(await service.installBundle(spec, { enabled: true }));
672
+ }
614
673
  if (isTerminalFailure(change)) {
615
674
  const pending = pendingOf(change);
616
675
  if (change.application === 'cancelled') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-m",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
4
4
  "description": "可自定义收录清单的 DSH 插件市场:收录、安装、卸载、升级一站完成,附 agent 工具与 dshm CLI。",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -46,6 +46,7 @@
46
46
  "build": "node scripts/build.mjs",
47
47
  "typecheck": "tsc -p tsconfig.json --noEmit",
48
48
  "test": "node --test tests/*.test.mjs",
49
+ "check-release": "node scripts/check-release.mjs",
49
50
  "pretest": "npm run build",
50
51
  "prepare": "npm run build",
51
52
  "capture:readme": "node scripts/capture-readme.mjs"