dshmarket 1.66.10 → 1.66.11

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
@@ -924,47 +924,80 @@ export function buildBundleLayers(profileDirectory, bundleNames, specs, dshInsta
924
924
  * @param dshInstall - install directory the analysis located, or null.
925
925
  * @returns the overlay layer, or null when this installation declares none.
926
926
  */
927
- function installOverlayLayer(dshInstall) {
927
+ function installOverlayLayer(dshInstall, profileDirectory, declaredBundles) {
928
928
  for (const directory of desktopApplicationRoots(dshInstall)) {
929
- let manifest;
930
- try {
931
- manifest = JSON.parse(readFileSync(join(directory, 'package.json'), 'utf8'));
932
- }
933
- catch {
934
- continue;
935
- }
936
- const declared = manifest.dsh?.bundle?.patch;
937
- const declaredList = typeof declared === 'string'
938
- ? [declared]
939
- : Array.isArray(declared)
940
- ? declared.filter((relative) => typeof relative === 'string')
941
- : [];
942
- if (declaredList.length === 0)
943
- continue;
944
- // A bundle patch alone does not make a directory an installation: ANY
945
- // package can declare one — this repository does, and so does every plugin
946
- // repo. Without this bound the ancestor walk accepted the nearest such
947
- // project above the install directory and composed its rows as if they
948
- // were the installation's own, which invents rows and can mask a real
949
- // orphan warning (#749 review). A DSH installation ships the in-box web
950
- // bundle, and the launcher splices this very overlay directly behind that
951
- // bundle's layer — where it is absent, the launcher applies no overlay
952
- // either, so composing one here would describe a composition nobody runs.
953
- if (readNodeModulesVersion(directory, '@deepseek-ai/dsh-web-app') === null)
954
- continue;
955
- const label = typeof manifest.name === 'string' && manifest.name !== '' ? manifest.name : 'install-overlay';
956
- const paths = declaredList.map(relative => join(directory, relative));
957
- if (paths.some(path => !existsSync(path))) {
958
- return { label, kind: 'bundle', patches: [], parseError: 'declared patch is missing' };
959
- }
960
- const parsed = paths.map(path => parsePatchFile(path));
961
- if (parsed.some(patches => patches === null)) {
962
- return { label, kind: 'bundle', patches: [], parseError: 'patch file is not a valid entry list' };
929
+ // A DSH installation ships the in-box web bundle, and the launcher splices
930
+ // this very overlay directly behind that bundle's layer — where it is
931
+ // absent, the launcher applies no overlay either, so composing one here
932
+ // would describe a composition nobody runs (#749 review).
933
+ const layer = overlayFromDirectory(directory, manifest => readNodeModulesVersion(directory, '@deepseek-ai/dsh-web-app') !== null);
934
+ if (layer !== null)
935
+ return layer;
936
+ }
937
+ // The official Desktop shell plugin's own patch (#807). On a build that packs
938
+ // the whole runtime into one app.asar, no application-root candidate declares
939
+ // it: the asar's root is `@deepseek-ai/dsh-desktop` and the nested
940
+ // `app.asar/dsh` is `@deepseek-ai/dsh-desktop-runtime`, neither with a
941
+ // `dsh.bundle.patch`, while the file that really supplies the shell's rows
942
+ // (`desktop-shell`, `desktop-notifications`, …) is this package's own
943
+ // `cordis.patch.yml`. Without it every user-patch row aimed at a shell row is
944
+ // reported as an orphan, and a user who believes the warning deletes rows
945
+ // that carry the window material and the notification preferences.
946
+ //
947
+ // Bounded by identity, not by location: the candidate must BE that package,
948
+ // resolved from the profile's own module search, and must declare a patch.
949
+ // A profile that lists it in `dsh.profile.bundles` gets it as an ordinary
950
+ // bundle layer instead — composing both would add every row twice.
951
+ if (!declaredBundles.includes(DESKTOP_SHELL_PLUGIN)) {
952
+ const directory = resolvePackageDir(join(profileDirectory, 'package.json'), DESKTOP_SHELL_PLUGIN);
953
+ if (directory !== null) {
954
+ return overlayFromDirectory(directory, manifest => manifest.name === DESKTOP_SHELL_PLUGIN);
963
955
  }
964
- return { label, kind: 'bundle', patches: parsed.flatMap(patches => patches), parseError: null };
965
956
  }
966
957
  return null;
967
958
  }
959
+ /** The official Desktop shell plugin, whose patch the launcher applies by hand (#807). */
960
+ const DESKTOP_SHELL_PLUGIN = 'dsh-plugin-desktop';
961
+ /**
962
+ * The overlay one candidate directory declares, or null when it declares none.
963
+ *
964
+ * `accept` is the caller's bound on what may stand in for the installation;
965
+ * resolution and parsing are shared, because a second parsing path would be a
966
+ * second set of answers about the same file.
967
+ */
968
+ function overlayFromDirectory(directory, accept) {
969
+ let manifest;
970
+ try {
971
+ manifest = JSON.parse(readFileSync(join(directory, 'package.json'), 'utf8'));
972
+ }
973
+ catch {
974
+ return null;
975
+ }
976
+ const declared = manifest.dsh?.bundle?.patch;
977
+ const declaredList = typeof declared === 'string'
978
+ ? [declared]
979
+ : Array.isArray(declared)
980
+ ? declared.filter((relative) => typeof relative === 'string')
981
+ : [];
982
+ // A bundle patch alone does not make a directory an installation: ANY package
983
+ // can declare one — this repository does, and so does every plugin repo.
984
+ // Without the caller's bound the ancestor walk accepted the nearest such
985
+ // project above the install directory and composed its rows as if they were
986
+ // the installation's own, which invents rows and can mask a real orphan
987
+ // warning (#749 review).
988
+ if (declaredList.length === 0 || !accept(manifest))
989
+ return null;
990
+ const label = typeof manifest.name === 'string' && manifest.name !== '' ? manifest.name : 'install-overlay';
991
+ const paths = declaredList.map(relative => join(directory, relative));
992
+ if (paths.some(path => !existsSync(path))) {
993
+ return { label, kind: 'bundle', patches: [], parseError: 'declared patch is missing' };
994
+ }
995
+ const parsed = paths.map(path => parsePatchFile(path));
996
+ if (parsed.some(patches => patches === null)) {
997
+ return { label, kind: 'bundle', patches: [], parseError: 'patch file is not a valid entry list' };
998
+ }
999
+ return { label, kind: 'bundle', patches: parsed.flatMap(patches => patches), parseError: null };
1000
+ }
968
1001
  /**
969
1002
  * Which directories in `node_modules` are leftovers (#663).
970
1003
  *
@@ -1080,7 +1113,7 @@ export function analyzeProfile(profileDirectory, options = {}) {
1080
1113
  // The installation's own overlay rides directly behind the bundle the
1081
1114
  // Desktop launcher keys off, exactly where the launcher splices it — see
1082
1115
  // installOverlayLayer.
1083
- const installOverlay = installOverlayLayer(dshInstall);
1116
+ const installOverlay = installOverlayLayer(dshInstall, profileDirectory, bundleNames);
1084
1117
  if (installOverlay !== null) {
1085
1118
  const overlayAfter = bundleLayers.findIndex(layer => layer.label === '@deepseek-ai/dsh-web-app');
1086
1119
  if (overlayAfter < 0)
package/lib/routes.js CHANGED
@@ -224,6 +224,28 @@ function blockedBuilds(result) {
224
224
  printedBuildKeys.set(pending, parsePrepareKey(result.stdout, result.stderr));
225
225
  return [pending];
226
226
  }
227
+ /**
228
+ * Whether a FAILED run failed BECAUSE pnpm skipped a build script (#754).
229
+ *
230
+ * `blockedBuilds` answers a different question — "did pnpm skip anything" —
231
+ * and pnpm reports skipped builds for transitive dependencies on almost any
232
+ * run. So a failure with an unrelated cause (a peer-resolution refusal, a 404,
233
+ * an OOM) still came back carrying the approve-and-retry banner, and pressing
234
+ * it re-ran the same command to the same failure. The reporter's case is the
235
+ * sharp end: the skipped package was a transitive `@types/*` that is not in
236
+ * the profile and not in the catalog, so approval had nothing to approve and
237
+ * the button answered `no installed packages given` — while the real cause
238
+ * (`generation peer validation failed`) was buried under it.
239
+ *
240
+ * The classifier already decides this from pnpm's own output, so ask it rather
241
+ * than pattern-matching a second time. `git-prepare-not-allowed` is the same
242
+ * refusal for a git dependency, which is the other code the approval path can
243
+ * clear.
244
+ */
245
+ function failedOnBlockedBuilds(result) {
246
+ const code = classifyPnpmFailure(`${result.stderr}\n${result.stdout}`, result.exitCode)?.code;
247
+ return code === 'ignored-builds' || code === 'git-prepare-not-allowed';
248
+ }
227
249
  /**
228
250
  * Register the market's HTTP routes.
229
251
  * @param host - Acquired webServer + shell services.
@@ -4526,7 +4548,10 @@ export function mountMarketRoutes(host, config, commandRuntime, agentsLookup, ho
4526
4548
  // build-required dep fails the add with ERR_PNPM_IGNORED_BUILDS.
4527
4549
  // Reporting the blocked packages here gives the client the same
4528
4550
  // approve-and-retry banner the install flow has had since #6.
4529
- const ignoredBuilds = ok || cancelled ? undefined : blockedBuilds(result);
4551
+ // Same rule as the install route (#754): a failure that merely
4552
+ // MENTIONS a skipped build is not a failure ABOUT one, and offering
4553
+ // to approve it sends the user to press a button that cannot help.
4554
+ const ignoredBuilds = ok || cancelled || !failedOnBlockedBuilds(result) ? undefined : blockedBuilds(result);
4530
4555
  if (ok)
4531
4556
  clearBrokenPlugin(name);
4532
4557
  // A package still installed but no longer in dsh.profile.bundles
@@ -5884,7 +5909,10 @@ export function mountMarketRoutes(host, config, commandRuntime, agentsLookup, ho
5884
5909
  if (ok)
5885
5910
  clearBrokenPlugin(entry.name);
5886
5911
  logEvent(ok || cancelled ? 'info' : 'error', 'install', `${target} exit=${String(result.exitCode)}${result.timedOut ? ' TIMEOUT' : ''}${cancelled ? ' CANCELLED' : ''}${ok ? ` hot=${String(hot)}` : cancelled ? '' : ` err=${failureDetail(result)}`}`);
5887
- const ignoredBuilds = blockedBuilds(result);
5912
+ // On success a skipped build is a notice (#6); after a FAILURE the
5913
+ // list is what offers the approve-and-retry banner, so it is sent
5914
+ // only when the blocked build IS why the run failed (#754).
5915
+ const ignoredBuilds = ok || cancelled || failedOnBlockedBuilds(result) ? blockedBuilds(result) : undefined;
5888
5916
  sendJson(response, ok || cancelled ? 200 : result.busy === true ? 409 : 502, {
5889
5917
  ok,
5890
5918
  cancelled: cancelled || undefined,
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.66.10",
4
+ "version": "1.66.11",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/types/index.d.ts",
package/src/check.ts CHANGED
@@ -1145,45 +1145,88 @@ export function buildBundleLayers(
1145
1145
  * @param dshInstall - install directory the analysis located, or null.
1146
1146
  * @returns the overlay layer, or null when this installation declares none.
1147
1147
  */
1148
- function installOverlayLayer(dshInstall: string | null): LayerInput | null {
1148
+ function installOverlayLayer(
1149
+ dshInstall: string | null,
1150
+ profileDirectory: string,
1151
+ declaredBundles: readonly string[],
1152
+ ): LayerInput | null {
1149
1153
  for (const directory of desktopApplicationRoots(dshInstall)) {
1150
- let manifest: { name?: unknown; dsh?: { bundle?: { patch?: unknown } } }
1151
- try {
1152
- manifest = JSON.parse(readFileSync(join(directory, 'package.json'), 'utf8')) as typeof manifest
1153
- } catch {
1154
- continue
1155
- }
1156
- const declared = manifest.dsh?.bundle?.patch
1157
- const declaredList = typeof declared === 'string'
1158
- ? [declared]
1159
- : Array.isArray(declared)
1160
- ? declared.filter((relative): relative is string => typeof relative === 'string')
1161
- : []
1162
- if (declaredList.length === 0) continue
1163
- // A bundle patch alone does not make a directory an installation: ANY
1164
- // package can declare one — this repository does, and so does every plugin
1165
- // repo. Without this bound the ancestor walk accepted the nearest such
1166
- // project above the install directory and composed its rows as if they
1167
- // were the installation's own, which invents rows and can mask a real
1168
- // orphan warning (#749 review). A DSH installation ships the in-box web
1169
- // bundle, and the launcher splices this very overlay directly behind that
1170
- // bundle's layer — where it is absent, the launcher applies no overlay
1171
- // either, so composing one here would describe a composition nobody runs.
1172
- if (readNodeModulesVersion(directory, '@deepseek-ai/dsh-web-app') === null) continue
1173
- const label = typeof manifest.name === 'string' && manifest.name !== '' ? manifest.name : 'install-overlay'
1174
- const paths = declaredList.map(relative => join(directory, relative))
1175
- if (paths.some(path => !existsSync(path))) {
1176
- return { label, kind: 'bundle', patches: [], parseError: 'declared patch is missing' }
1177
- }
1178
- const parsed = paths.map(path => parsePatchFile(path))
1179
- if (parsed.some(patches => patches === null)) {
1180
- return { label, kind: 'bundle', patches: [], parseError: 'patch file is not a valid entry list' }
1154
+ // A DSH installation ships the in-box web bundle, and the launcher splices
1155
+ // this very overlay directly behind that bundle's layer — where it is
1156
+ // absent, the launcher applies no overlay either, so composing one here
1157
+ // would describe a composition nobody runs (#749 review).
1158
+ const layer = overlayFromDirectory(directory, manifest =>
1159
+ readNodeModulesVersion(directory, '@deepseek-ai/dsh-web-app') !== null)
1160
+ if (layer !== null) return layer
1161
+ }
1162
+ // The official Desktop shell plugin's own patch (#807). On a build that packs
1163
+ // the whole runtime into one app.asar, no application-root candidate declares
1164
+ // it: the asar's root is `@deepseek-ai/dsh-desktop` and the nested
1165
+ // `app.asar/dsh` is `@deepseek-ai/dsh-desktop-runtime`, neither with a
1166
+ // `dsh.bundle.patch`, while the file that really supplies the shell's rows
1167
+ // (`desktop-shell`, `desktop-notifications`, …) is this package's own
1168
+ // `cordis.patch.yml`. Without it every user-patch row aimed at a shell row is
1169
+ // reported as an orphan, and a user who believes the warning deletes rows
1170
+ // that carry the window material and the notification preferences.
1171
+ //
1172
+ // Bounded by identity, not by location: the candidate must BE that package,
1173
+ // resolved from the profile's own module search, and must declare a patch.
1174
+ // A profile that lists it in `dsh.profile.bundles` gets it as an ordinary
1175
+ // bundle layer instead — composing both would add every row twice.
1176
+ if (!declaredBundles.includes(DESKTOP_SHELL_PLUGIN)) {
1177
+ const directory = resolvePackageDir(join(profileDirectory, 'package.json'), DESKTOP_SHELL_PLUGIN)
1178
+ if (directory !== null) {
1179
+ return overlayFromDirectory(directory, manifest => manifest.name === DESKTOP_SHELL_PLUGIN)
1181
1180
  }
1182
- return { label, kind: 'bundle', patches: parsed.flatMap(patches => patches), parseError: null }
1183
1181
  }
1184
1182
  return null
1185
1183
  }
1186
1184
 
1185
+ /** The official Desktop shell plugin, whose patch the launcher applies by hand (#807). */
1186
+ const DESKTOP_SHELL_PLUGIN = 'dsh-plugin-desktop'
1187
+
1188
+ /**
1189
+ * The overlay one candidate directory declares, or null when it declares none.
1190
+ *
1191
+ * `accept` is the caller's bound on what may stand in for the installation;
1192
+ * resolution and parsing are shared, because a second parsing path would be a
1193
+ * second set of answers about the same file.
1194
+ */
1195
+ function overlayFromDirectory(
1196
+ directory: string,
1197
+ accept: (manifest: { name?: unknown }) => boolean,
1198
+ ): LayerInput | null {
1199
+ let manifest: { name?: unknown; dsh?: { bundle?: { patch?: unknown } } }
1200
+ try {
1201
+ manifest = JSON.parse(readFileSync(join(directory, 'package.json'), 'utf8')) as typeof manifest
1202
+ } catch {
1203
+ return null
1204
+ }
1205
+ const declared = manifest.dsh?.bundle?.patch
1206
+ const declaredList = typeof declared === 'string'
1207
+ ? [declared]
1208
+ : Array.isArray(declared)
1209
+ ? declared.filter((relative): relative is string => typeof relative === 'string')
1210
+ : []
1211
+ // A bundle patch alone does not make a directory an installation: ANY package
1212
+ // can declare one — this repository does, and so does every plugin repo.
1213
+ // Without the caller's bound the ancestor walk accepted the nearest such
1214
+ // project above the install directory and composed its rows as if they were
1215
+ // the installation's own, which invents rows and can mask a real orphan
1216
+ // warning (#749 review).
1217
+ if (declaredList.length === 0 || !accept(manifest)) return null
1218
+ const label = typeof manifest.name === 'string' && manifest.name !== '' ? manifest.name : 'install-overlay'
1219
+ const paths = declaredList.map(relative => join(directory, relative))
1220
+ if (paths.some(path => !existsSync(path))) {
1221
+ return { label, kind: 'bundle', patches: [], parseError: 'declared patch is missing' }
1222
+ }
1223
+ const parsed = paths.map(path => parsePatchFile(path))
1224
+ if (parsed.some(patches => patches === null)) {
1225
+ return { label, kind: 'bundle', patches: [], parseError: 'patch file is not a valid entry list' }
1226
+ }
1227
+ return { label, kind: 'bundle', patches: parsed.flatMap(patches => patches), parseError: null }
1228
+ }
1229
+
1187
1230
  /**
1188
1231
  * Which directories in `node_modules` are leftovers (#663).
1189
1232
  *
@@ -1299,7 +1342,7 @@ export function analyzeProfile(profileDirectory: string, options: CheckOptions =
1299
1342
  // The installation's own overlay rides directly behind the bundle the
1300
1343
  // Desktop launcher keys off, exactly where the launcher splices it — see
1301
1344
  // installOverlayLayer.
1302
- const installOverlay = installOverlayLayer(dshInstall)
1345
+ const installOverlay = installOverlayLayer(dshInstall, profileDirectory, bundleNames)
1303
1346
  if (installOverlay !== null) {
1304
1347
  const overlayAfter = bundleLayers.findIndex(layer => layer.label === '@deepseek-ai/dsh-web-app')
1305
1348
  if (overlayAfter < 0) bundleLayers.push(installOverlay)
package/src/routes.ts CHANGED
@@ -315,6 +315,29 @@ function blockedBuilds(result: { ignoredBuilds?: unknown; stdout: string; stderr
315
315
  return [pending]
316
316
  }
317
317
 
318
+ /**
319
+ * Whether a FAILED run failed BECAUSE pnpm skipped a build script (#754).
320
+ *
321
+ * `blockedBuilds` answers a different question — "did pnpm skip anything" —
322
+ * and pnpm reports skipped builds for transitive dependencies on almost any
323
+ * run. So a failure with an unrelated cause (a peer-resolution refusal, a 404,
324
+ * an OOM) still came back carrying the approve-and-retry banner, and pressing
325
+ * it re-ran the same command to the same failure. The reporter's case is the
326
+ * sharp end: the skipped package was a transitive `@types/*` that is not in
327
+ * the profile and not in the catalog, so approval had nothing to approve and
328
+ * the button answered `no installed packages given` — while the real cause
329
+ * (`generation peer validation failed`) was buried under it.
330
+ *
331
+ * The classifier already decides this from pnpm's own output, so ask it rather
332
+ * than pattern-matching a second time. `git-prepare-not-allowed` is the same
333
+ * refusal for a git dependency, which is the other code the approval path can
334
+ * clear.
335
+ */
336
+ function failedOnBlockedBuilds(result: { exitCode: number | null; stdout: string; stderr: string }): boolean {
337
+ const code = classifyPnpmFailure(`${result.stderr}\n${result.stdout}`, result.exitCode)?.code
338
+ return code === 'ignored-builds' || code === 'git-prepare-not-allowed'
339
+ }
340
+
318
341
  /**
319
342
  * Register the market's HTTP routes.
320
343
  * @param host - Acquired webServer + shell services.
@@ -4709,7 +4732,10 @@ sendJson(response, 200, { updates })
4709
4732
  // build-required dep fails the add with ERR_PNPM_IGNORED_BUILDS.
4710
4733
  // Reporting the blocked packages here gives the client the same
4711
4734
  // approve-and-retry banner the install flow has had since #6.
4712
- const ignoredBuilds = ok || cancelled ? undefined : blockedBuilds(result)
4735
+ // Same rule as the install route (#754): a failure that merely
4736
+ // MENTIONS a skipped build is not a failure ABOUT one, and offering
4737
+ // to approve it sends the user to press a button that cannot help.
4738
+ const ignoredBuilds = ok || cancelled || !failedOnBlockedBuilds(result) ? undefined : blockedBuilds(result)
4713
4739
  if (ok) clearBrokenPlugin(name)
4714
4740
  // A package still installed but no longer in dsh.profile.bundles
4715
4741
  // after this run stops loading and says nothing (#720). The market
@@ -6068,7 +6094,10 @@ sendJson(response, 200, { updates })
6068
6094
  if (ok) clearBrokenPlugin(entry.name)
6069
6095
  logEvent(ok || cancelled ? 'info' : 'error', 'install',
6070
6096
  `${target} exit=${String(result.exitCode)}${result.timedOut ? ' TIMEOUT' : ''}${cancelled ? ' CANCELLED' : ''}${ok ? ` hot=${String(hot)}` : cancelled ? '' : ` err=${failureDetail(result)}`}`)
6071
- const ignoredBuilds = blockedBuilds(result)
6097
+ // On success a skipped build is a notice (#6); after a FAILURE the
6098
+ // list is what offers the approve-and-retry banner, so it is sent
6099
+ // only when the blocked build IS why the run failed (#754).
6100
+ const ignoredBuilds = ok || cancelled || failedOnBlockedBuilds(result) ? blockedBuilds(result) : undefined
6072
6101
  sendJson(response, ok || cancelled ? 200 : result.busy === true ? 409 : 502, {
6073
6102
  ok,
6074
6103
  cancelled: cancelled || undefined,