dshmarket 1.57.0 → 1.58.0

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/check.js CHANGED
@@ -732,6 +732,7 @@ export function buildBundleLayers(profileDirectory, bundleNames, specs, dshInsta
732
732
  kind: INBOX_BUNDLES.has(name) ? 'official' : 'community',
733
733
  directory,
734
734
  patchPath: null,
735
+ patchPaths: [],
735
736
  error: null,
736
737
  entries: [],
737
738
  parseError: null,
@@ -763,23 +764,34 @@ export function buildBundleLayers(profileDirectory, bundleNames, specs, dshInsta
763
764
  layer.error = 'bundle package.json is unreadable';
764
765
  return layer;
765
766
  }
767
+ // A bundle may declare ONE patch file or a LIST of them: dsh 0.1.7's own
768
+ // `@deepseek-ai/dsh-web-app` ships five (a base patch plus four presets),
769
+ // and the official headless template includes that bundle — so requiring
770
+ // a string reported "the profile will fail to boot" for the DEFAULT
771
+ // layout, while `dsh --dump-config` composed it fine (#676).
766
772
  const declared = bundleManifest.dsh?.bundle?.patch;
767
- if (typeof declared !== 'string') {
773
+ const declaredList = typeof declared === 'string'
774
+ ? [declared]
775
+ : Array.isArray(declared) ? declared.filter((item) => typeof item === 'string') : [];
776
+ if (declaredList.length === 0) {
768
777
  layer.error = 'bundle declares no dsh.bundle.patch — the profile will fail to boot';
769
778
  return layer;
770
779
  }
771
- const patchPath = join(directory, declared);
772
- if (!existsSync(patchPath)) {
773
- layer.error = `declared patch ${declared} is missing — the profile will fail to boot`;
780
+ const missing = declaredList.find(relative => !existsSync(join(directory, relative)));
781
+ if (missing !== undefined) {
782
+ layer.error = `declared patch ${missing} is missing — the profile will fail to boot`;
774
783
  return layer;
775
784
  }
776
- layer.patchPath = patchPath;
777
- const patches = parsePatchFile(patchPath);
778
- if (patches === null) {
785
+ // The first declared file is the layer's patch for reporting; every one
786
+ // of them contributes entries, because the composer applies them all.
787
+ layer.patchPath = join(directory, declaredList[0]);
788
+ layer.patchPaths = declaredList.map(relative => join(directory, relative));
789
+ const parsed = declaredList.map(relative => parsePatchFile(join(directory, relative)));
790
+ if (parsed.some(patches => patches === null)) {
779
791
  layer.parseError = 'patch file is not a valid entry list';
780
792
  return layer;
781
793
  }
782
- layer.entries = collectInsertIds(patches);
794
+ layer.entries = parsed.flatMap(patches => collectInsertIds(patches));
783
795
  const order = bundleManifest.dsh?.bundle?.order;
784
796
  if (order !== null && typeof order === 'object' && !Array.isArray(order)) {
785
797
  const listOf = (value) => Array.isArray(value)
@@ -796,7 +808,12 @@ export function buildBundleLayers(profileDirectory, bundleNames, specs, dshInsta
796
808
  const layers = bundles.map((bundle) => ({
797
809
  label: bundle.name,
798
810
  kind: 'bundle',
799
- patches: bundle.patchPath !== null && bundle.parseError === null ? parsePatchFile(bundle.patchPath) ?? [] : [],
811
+ // EVERY declared file, not the first: the composer applies them all, and
812
+ // a composition built from one file would miss the preset rows — so a
813
+ // duplicate id or an orphan in a second file was invisible here (#688).
814
+ patches: bundle.parseError === null
815
+ ? bundle.patchPaths.flatMap(path => parsePatchFile(path) ?? [])
816
+ : [],
800
817
  parseError: bundle.parseError,
801
818
  }));
802
819
  return { bundles, layers };
package/lib/routes.js CHANGED
@@ -477,12 +477,15 @@ export function mountMarketRoutes(host, config, commandRuntime, agentsLookup) {
477
477
  const result = await hotMount(host, dir, name);
478
478
  ok = result.ok;
479
479
  reason = result.reason ?? undefined;
480
- // A mount that succeeded imported the module as it is on disk NOW,
481
- // so whatever was replaced under the old instance is no longer what
482
- // this process is serving. Off-and-on is a real way out of the
483
- // restart notice, and holding it after that would be wrong.
484
- if (result.ok)
485
- replacedWhileLive.delete(name);
480
+ // Deliberately NOT clearing replacedWhileLive here (#685). This used
481
+ // to say "a mount that succeeded imported the module as it is on
482
+ // disk NOW", which is false exactly when the flag is set: it is only
483
+ // set when the host half was LIVE at update time, i.e. this process
484
+ // has already evaluated that module URL, and Node's ESM cache serves
485
+ // any later import of the same URL — the profile layout is hoisted,
486
+ // so an update rewrites the files in place and the URL never changes.
487
+ // Off-and-on re-creates the fiber around the OLD module. Only a
488
+ // restart ends the process that holds it, and the flag with it.
486
489
  }
487
490
  }
488
491
  else {
@@ -2287,7 +2290,14 @@ export function mountMarketRoutes(host, config, commandRuntime, agentsLookup) {
2287
2290
  // A carrier toggle moves the bundle in/out of dsh.profile.bundles,
2288
2291
  // which only takes effect on the next composition — always a restart.
2289
2292
  // Non-carrier plugins keep the live-mount based decision.
2290
- const restart = isCarrier ? true : enabled ? !liveAfter : liveAfter;
2293
+ // A plugin replaced on disk while its host half was running is
2294
+ // still serving the module this process imported, whatever the
2295
+ // loader's inventory says — re-enabling it re-creates the fiber
2296
+ // around the cached old build (#685, measured end to end with a
2297
+ // module-scope version marker). Enabling cannot make it current;
2298
+ // only a restart can.
2299
+ const staleModule = enabled && replacedWhileLive.has(name);
2300
+ const restart = isCarrier || staleModule ? true : enabled ? !liveAfter : liveAfter;
2291
2301
  // A client-part plugin's UI is in the page already — toggling it
2292
2302
  // needs a browser refresh to show the change (same signal the
2293
2303
  // install flow uses for the hot banner).
@@ -2298,7 +2308,13 @@ export function mountMarketRoutes(host, config, commandRuntime, agentsLookup) {
2298
2308
  enabled,
2299
2309
  disabled: [...disabled],
2300
2310
  live: listHotMounts(),
2301
- activation: { [name]: verifyActivation(config.profile, name, liveNames(), activeProfileDir, offNow) },
2311
+ // The same verdict the listing gives (#685): the reply used the
2312
+ // loader inventory alone and said `live` for a plugin serving
2313
+ // its old build, while a refresh of the listing — which applies
2314
+ // activationAfterReplace — said `restart`. One moment, one story.
2315
+ activation: {
2316
+ [name]: activationAfterReplace(verifyActivation(config.profile, name, liveNames(), activeProfileDir, offNow), replacedWhileLive.has(name)),
2317
+ },
2302
2318
  reason,
2303
2319
  patchRows,
2304
2320
  patchWrite: patchWrite ?? { ok: true, reason: null },
@@ -3504,7 +3520,15 @@ export function mountMarketRoutes(host, config, commandRuntime, agentsLookup) {
3504
3520
  const trial = trialValidate(activeProfileDir, stack.community);
3505
3521
  if (!trial.ok) {
3506
3522
  ok = false;
3507
- const first = trial.errors[0]?.message ?? 'the composition would not boot';
3523
+ // Name the LAYER, not only the message: the first error is
3524
+ // often about a different bundle than the one being updated
3525
+ // (#688 — the official dsh-web-app's patch list, blamed on
3526
+ // whatever plugin the user happened to update), and a message
3527
+ // without the layer reads as an accusation of the wrong package.
3528
+ const firstIssue = trial.errors[0];
3529
+ const first = firstIssue === undefined
3530
+ ? 'the composition would not boot'
3531
+ : `${firstIssue.layer}: ${firstIssue.message}`;
3508
3532
  const rollback = await rollbackAttemptBuild();
3509
3533
  rollbackOk = rollback.ok;
3510
3534
  rollbackDetail = rollback.detail;
@@ -43,6 +43,8 @@ export interface BundleLayer {
43
43
  directory: string | null;
44
44
  /** Absolute path of the layer's patch file; null when undeclared/missing. */
45
45
  patchPath: string | null;
46
+ /** Every declared patch file, in declaration order; `patchPath` is the first (#688). */
47
+ patchPaths: string[];
46
48
  /**
47
49
  * An in-box bundle whose directory could not be located — a gap in what
48
50
  * this process can see, not a defect in the profile (#369). Distinct from
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.57.0",
4
+ "version": "1.58.0",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/types/index.d.ts",
package/src/check.ts CHANGED
@@ -62,6 +62,8 @@ export interface BundleLayer {
62
62
  directory: string | null
63
63
  /** Absolute path of the layer's patch file; null when undeclared/missing. */
64
64
  patchPath: string | null
65
+ /** Every declared patch file, in declaration order; `patchPath` is the first (#688). */
66
+ patchPaths: string[]
65
67
  /**
66
68
  * An in-box bundle whose directory could not be located — a gap in what
67
69
  * this process can see, not a defect in the profile (#369). Distinct from
@@ -916,6 +918,7 @@ export function buildBundleLayers(
916
918
  kind: INBOX_BUNDLES.has(name) ? 'official' : 'community',
917
919
  directory,
918
920
  patchPath: null,
921
+ patchPaths: [],
919
922
  error: null,
920
923
  entries: [],
921
924
  parseError: null,
@@ -946,23 +949,34 @@ export function buildBundleLayers(
946
949
  layer.error = 'bundle package.json is unreadable'
947
950
  return layer
948
951
  }
952
+ // A bundle may declare ONE patch file or a LIST of them: dsh 0.1.7's own
953
+ // `@deepseek-ai/dsh-web-app` ships five (a base patch plus four presets),
954
+ // and the official headless template includes that bundle — so requiring
955
+ // a string reported "the profile will fail to boot" for the DEFAULT
956
+ // layout, while `dsh --dump-config` composed it fine (#676).
949
957
  const declared = bundleManifest.dsh?.bundle?.patch
950
- if (typeof declared !== 'string') {
958
+ const declaredList = typeof declared === 'string'
959
+ ? [declared]
960
+ : Array.isArray(declared) ? declared.filter((item): item is string => typeof item === 'string') : []
961
+ if (declaredList.length === 0) {
951
962
  layer.error = 'bundle declares no dsh.bundle.patch — the profile will fail to boot'
952
963
  return layer
953
964
  }
954
- const patchPath = join(directory, declared)
955
- if (!existsSync(patchPath)) {
956
- layer.error = `declared patch ${declared} is missing — the profile will fail to boot`
965
+ const missing = declaredList.find(relative => !existsSync(join(directory, relative)))
966
+ if (missing !== undefined) {
967
+ layer.error = `declared patch ${missing} is missing — the profile will fail to boot`
957
968
  return layer
958
969
  }
959
- layer.patchPath = patchPath
960
- const patches = parsePatchFile(patchPath)
961
- if (patches === null) {
970
+ // The first declared file is the layer's patch for reporting; every one
971
+ // of them contributes entries, because the composer applies them all.
972
+ layer.patchPath = join(directory, declaredList[0]!)
973
+ layer.patchPaths = declaredList.map(relative => join(directory, relative))
974
+ const parsed = declaredList.map(relative => parsePatchFile(join(directory, relative)))
975
+ if (parsed.some(patches => patches === null)) {
962
976
  layer.parseError = 'patch file is not a valid entry list'
963
977
  return layer
964
978
  }
965
- layer.entries = collectInsertIds(patches)
979
+ layer.entries = parsed.flatMap(patches => collectInsertIds(patches!))
966
980
  const order = bundleManifest.dsh?.bundle?.order
967
981
  if (order !== null && typeof order === 'object' && !Array.isArray(order)) {
968
982
  const listOf = (value: unknown): string[] | undefined => Array.isArray(value)
@@ -979,7 +993,12 @@ export function buildBundleLayers(
979
993
  const layers: LayerInput[] = bundles.map((bundle) => ({
980
994
  label: bundle.name,
981
995
  kind: 'bundle' as const,
982
- patches: bundle.patchPath !== null && bundle.parseError === null ? parsePatchFile(bundle.patchPath) ?? [] : [],
996
+ // EVERY declared file, not the first: the composer applies them all, and
997
+ // a composition built from one file would miss the preset rows — so a
998
+ // duplicate id or an orphan in a second file was invisible here (#688).
999
+ patches: bundle.parseError === null
1000
+ ? bundle.patchPaths.flatMap(path => parsePatchFile(path) ?? [])
1001
+ : [],
983
1002
  parseError: bundle.parseError,
984
1003
  }))
985
1004
  return { bundles, layers }
package/src/routes.ts CHANGED
@@ -538,11 +538,15 @@ export function mountMarketRoutes(
538
538
  const result = await hotMount(host, dir, name)
539
539
  ok = result.ok
540
540
  reason = result.reason ?? undefined
541
- // A mount that succeeded imported the module as it is on disk NOW,
542
- // so whatever was replaced under the old instance is no longer what
543
- // this process is serving. Off-and-on is a real way out of the
544
- // restart notice, and holding it after that would be wrong.
545
- if (result.ok) replacedWhileLive.delete(name)
541
+ // Deliberately NOT clearing replacedWhileLive here (#685). This used
542
+ // to say "a mount that succeeded imported the module as it is on
543
+ // disk NOW", which is false exactly when the flag is set: it is only
544
+ // set when the host half was LIVE at update time, i.e. this process
545
+ // has already evaluated that module URL, and Node's ESM cache serves
546
+ // any later import of the same URL — the profile layout is hoisted,
547
+ // so an update rewrites the files in place and the URL never changes.
548
+ // Off-and-on re-creates the fiber around the OLD module. Only a
549
+ // restart ends the process that holds it, and the flag with it.
546
550
  }
547
551
  } else {
548
552
  ok = await hotUnmount(name) || await themes.setEntryDisabled(name, true)
@@ -2421,7 +2425,14 @@ export function mountMarketRoutes(
2421
2425
  // A carrier toggle moves the bundle in/out of dsh.profile.bundles,
2422
2426
  // which only takes effect on the next composition — always a restart.
2423
2427
  // Non-carrier plugins keep the live-mount based decision.
2424
- const restart = isCarrier ? true : enabled ? !liveAfter : liveAfter
2428
+ // A plugin replaced on disk while its host half was running is
2429
+ // still serving the module this process imported, whatever the
2430
+ // loader's inventory says — re-enabling it re-creates the fiber
2431
+ // around the cached old build (#685, measured end to end with a
2432
+ // module-scope version marker). Enabling cannot make it current;
2433
+ // only a restart can.
2434
+ const staleModule = enabled && replacedWhileLive.has(name)
2435
+ const restart = isCarrier || staleModule ? true : enabled ? !liveAfter : liveAfter
2425
2436
  // A client-part plugin's UI is in the page already — toggling it
2426
2437
  // needs a browser refresh to show the change (same signal the
2427
2438
  // install flow uses for the hot banner).
@@ -2432,7 +2443,16 @@ export function mountMarketRoutes(
2432
2443
  enabled,
2433
2444
  disabled: [...disabled],
2434
2445
  live: listHotMounts(),
2435
- activation: { [name]: verifyActivation(config.profile, name, liveNames(), activeProfileDir, offNow) },
2446
+ // The same verdict the listing gives (#685): the reply used the
2447
+ // loader inventory alone and said `live` for a plugin serving
2448
+ // its old build, while a refresh of the listing — which applies
2449
+ // activationAfterReplace — said `restart`. One moment, one story.
2450
+ activation: {
2451
+ [name]: activationAfterReplace(
2452
+ verifyActivation(config.profile, name, liveNames(), activeProfileDir, offNow),
2453
+ replacedWhileLive.has(name),
2454
+ ),
2455
+ },
2436
2456
  reason,
2437
2457
  patchRows,
2438
2458
  patchWrite: patchWrite ?? { ok: true, reason: null },
@@ -3641,7 +3661,15 @@ sendJson(response, 200, { updates })
3641
3661
  const trial = trialValidate(activeProfileDir, stack.community)
3642
3662
  if (!trial.ok) {
3643
3663
  ok = false
3644
- const first = trial.errors[0]?.message ?? 'the composition would not boot'
3664
+ // Name the LAYER, not only the message: the first error is
3665
+ // often about a different bundle than the one being updated
3666
+ // (#688 — the official dsh-web-app's patch list, blamed on
3667
+ // whatever plugin the user happened to update), and a message
3668
+ // without the layer reads as an accusation of the wrong package.
3669
+ const firstIssue = trial.errors[0]
3670
+ const first = firstIssue === undefined
3671
+ ? 'the composition would not boot'
3672
+ : `${firstIssue.layer}: ${firstIssue.message}`
3645
3673
  const rollback = await rollbackAttemptBuild()
3646
3674
  rollbackOk = rollback.ok
3647
3675
  rollbackDetail = rollback.detail