dshmarket 1.46.0 → 1.46.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/routes.js CHANGED
@@ -433,14 +433,31 @@ export function mountMarketRoutes(host, config, commandRuntime, agentsLookup) {
433
433
  return { changed: [...changed], partial: changed.size > 0 };
434
434
  }
435
435
  /**
436
- * Apply one enable/disable request: persist the choice in state.json, then
437
- * drive the live composition. Covers every mount form — hot mounts and
438
- * client-only shims go through hotUnmount/hotMount, bundle-layer entries
439
- * through setEntryDisabled. Enabling a THEME goes through the caller's
440
- * activateTheme instead so the Themes tab's exclusivity stays intact.
436
+ * Apply one enable/disable request: drive the live composition, then
437
+ * persist the choice in state.json. Covers every mount form — hot mounts
438
+ * and client-only shims go through hotUnmount/hotMount, bundle-layer
439
+ * entries through setEntryDisabled. Enabling a THEME goes through the
440
+ * caller's activateTheme instead so the Themes tab's exclusivity stays
441
+ * intact.
442
+ *
443
+ * A FAILED ENABLE LEAVES EVERYTHING AS IT WAS (#575). The choice used to be
444
+ * recorded before the mount was attempted and persisted whatever happened,
445
+ * so enabling a plugin that crashes on import — deterministically, every
446
+ * time — wrote "enabled" into state.json anyway. The next boot tried the
447
+ * import again and died again; the reporter measured 24 restarts before
448
+ * restoring the disable by hand. The toggle route's patch-layer gate
449
+ * (@JINITAIMEI121 in #584) closed the same hole in cordis.patch.yml; this
450
+ * closes it in the market's own store, which is the ONLY durable state a
451
+ * client-only plugin has — that plugin kind has no bundle rows, so the
452
+ * patch gate never runs for it.
453
+ *
454
+ * A failed DISABLE still persists, and that asymmetry is deliberate: the
455
+ * user asked for OFF, and a failed unmount leaves the plugin live only for
456
+ * this session. There the durable disable is the contract, not an error.
441
457
  */
442
458
  async function setPluginEnabled(name, enabled) {
443
459
  const dir = activeProfileDir;
460
+ const wasDisabled = disabled.has(name);
444
461
  if (enabled)
445
462
  disabled.delete(name);
446
463
  else
@@ -474,6 +491,14 @@ export function mountMarketRoutes(host, config, commandRuntime, agentsLookup) {
474
491
  ok = true;
475
492
  }
476
493
  }
494
+ if (!ok && enabled) {
495
+ // Put the in-memory view back before persisting: it is the same object
496
+ // the route reports as `disabled`, so restoring it keeps the reply, the
497
+ // store and the patch layer telling one story.
498
+ if (wasDisabled)
499
+ disabled.add(name);
500
+ logEvent('warn', 'toggle', `${name}: enable failed; leaving it disabled rather than persisting a state that crashes at boot (#575)`);
501
+ }
477
502
  writeMarketState(dir, { disabled, groups, groupOrder });
478
503
  return { ok, reason };
479
504
  }
@@ -2004,7 +2029,18 @@ export function mountMarketRoutes(host, config, commandRuntime, agentsLookup) {
2004
2029
  }
2005
2030
  }
2006
2031
  let patchWrite = null;
2007
- if (patchRows.length > 0) {
2032
+ // #575: a failed ENABLE must not flip the durable patch layer.
2033
+ // The hot-mount failure may be deterministic (a plugin that
2034
+ // crashes on import), and persisting "enabled" turns a transient
2035
+ // in-session error into a boot crash loop — the loader re-applies
2036
+ // the flipped rows on every start. The frontend already shows the
2037
+ // plugin as still disabled, and the next explicit enable retries
2038
+ // cleanly. Disables keep their unconditional write: a failed
2039
+ // unmount leaves the plugin live in-session, and the user asked
2040
+ // for it OFF — the durable disable is then the contract, not an
2041
+ // error.
2042
+ const patchGate = ok || !enabled;
2043
+ if (patchRows.length > 0 && patchGate) {
2008
2044
  for (const rowId of patchRows) {
2009
2045
  const result = enabled ? await enableRow(userPatchPath, rowId) : await disableRow(userPatchPath, rowId);
2010
2046
  if (!result.ok && patchWrite === null)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dshmarket",
3
3
  "description": "Visual plugin market inside DeepSeek Harness — browse, search, and one-click install community plugins. · DSH 可视化插件市场:逛一逛,点一下,装好。",
4
- "version": "1.46.0",
4
+ "version": "1.46.1",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/types/index.d.ts",
package/src/routes.ts CHANGED
@@ -487,14 +487,31 @@ export function mountMarketRoutes(
487
487
  }
488
488
 
489
489
  /**
490
- * Apply one enable/disable request: persist the choice in state.json, then
491
- * drive the live composition. Covers every mount form — hot mounts and
492
- * client-only shims go through hotUnmount/hotMount, bundle-layer entries
493
- * through setEntryDisabled. Enabling a THEME goes through the caller's
494
- * activateTheme instead so the Themes tab's exclusivity stays intact.
490
+ * Apply one enable/disable request: drive the live composition, then
491
+ * persist the choice in state.json. Covers every mount form — hot mounts
492
+ * and client-only shims go through hotUnmount/hotMount, bundle-layer
493
+ * entries through setEntryDisabled. Enabling a THEME goes through the
494
+ * caller's activateTheme instead so the Themes tab's exclusivity stays
495
+ * intact.
496
+ *
497
+ * A FAILED ENABLE LEAVES EVERYTHING AS IT WAS (#575). The choice used to be
498
+ * recorded before the mount was attempted and persisted whatever happened,
499
+ * so enabling a plugin that crashes on import — deterministically, every
500
+ * time — wrote "enabled" into state.json anyway. The next boot tried the
501
+ * import again and died again; the reporter measured 24 restarts before
502
+ * restoring the disable by hand. The toggle route's patch-layer gate
503
+ * (@JINITAIMEI121 in #584) closed the same hole in cordis.patch.yml; this
504
+ * closes it in the market's own store, which is the ONLY durable state a
505
+ * client-only plugin has — that plugin kind has no bundle rows, so the
506
+ * patch gate never runs for it.
507
+ *
508
+ * A failed DISABLE still persists, and that asymmetry is deliberate: the
509
+ * user asked for OFF, and a failed unmount leaves the plugin live only for
510
+ * this session. There the durable disable is the contract, not an error.
495
511
  */
496
512
  async function setPluginEnabled(name: string, enabled: boolean): Promise<{ ok: boolean; reason?: string }> {
497
513
  const dir = activeProfileDir
514
+ const wasDisabled = disabled.has(name)
498
515
  if (enabled) disabled.delete(name)
499
516
  else disabled.add(name)
500
517
  let ok: boolean
@@ -522,6 +539,13 @@ export function mountMarketRoutes(
522
539
  ok = true
523
540
  }
524
541
  }
542
+ if (!ok && enabled) {
543
+ // Put the in-memory view back before persisting: it is the same object
544
+ // the route reports as `disabled`, so restoring it keeps the reply, the
545
+ // store and the patch layer telling one story.
546
+ if (wasDisabled) disabled.add(name)
547
+ logEvent('warn', 'toggle', `${name}: enable failed; leaving it disabled rather than persisting a state that crashes at boot (#575)`)
548
+ }
525
549
  writeMarketState(dir, { disabled, groups, groupOrder })
526
550
  return { ok, reason }
527
551
  }
@@ -2109,7 +2133,18 @@ export function mountMarketRoutes(
2109
2133
  }
2110
2134
  }
2111
2135
  let patchWrite: { ok: boolean; reason: string | null } | null = null
2112
- if (patchRows.length > 0) {
2136
+ // #575: a failed ENABLE must not flip the durable patch layer.
2137
+ // The hot-mount failure may be deterministic (a plugin that
2138
+ // crashes on import), and persisting "enabled" turns a transient
2139
+ // in-session error into a boot crash loop — the loader re-applies
2140
+ // the flipped rows on every start. The frontend already shows the
2141
+ // plugin as still disabled, and the next explicit enable retries
2142
+ // cleanly. Disables keep their unconditional write: a failed
2143
+ // unmount leaves the plugin live in-session, and the user asked
2144
+ // for it OFF — the durable disable is then the contract, not an
2145
+ // error.
2146
+ const patchGate = ok || !enabled
2147
+ if (patchRows.length > 0 && patchGate) {
2113
2148
  for (const rowId of patchRows) {
2114
2149
  const result = enabled ? await enableRow(userPatchPath, rowId) : await disableRow(userPatchPath, rowId)
2115
2150
  if (!result.ok && patchWrite === null) patchWrite = result