dshmarket 1.66.29 → 1.66.31

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/install.js CHANGED
@@ -339,6 +339,21 @@ export async function withHoistRecovery(run, profile, pluginArgs, profileDirecto
339
339
  : `这台宿主(官方桌面端)自己执行安装,不接受市场附加的 pnpm 参数(${unavailableOption}),所以这一步没有自动重试。 / This host (the official desktop app) runs the install itself and takes no pnpm options from the market (${unavailableOption}), so that automatic retry could not be attempted.`;
340
340
  result = { ...result, stderr: `${result.stderr}\n\n${note}` };
341
341
  }
342
+ // pnpm can append a second rule even on success (#883, #878), including
343
+ // after a recovery retry or an official Desktop manager install. Repair
344
+ // the final file now, before another plugin operation reads it; a successful
345
+ // command must never be retried just because its exclusions needed merging.
346
+ if (ok(result) && (verb === 'add' || verb === 'remove' || verb === 'install' || verb === 'update')) {
347
+ try {
348
+ const merged = mergeDuplicateReleaseAgeExcludes(profile, profileDirectory);
349
+ if (merged.length > 0) {
350
+ logEvent('warn', 'install', `merged duplicate minimumReleaseAgeExclude rules for ${merged.join(', ')} after success (#883, #878)`);
351
+ }
352
+ }
353
+ catch (error) {
354
+ logEvent('warn', 'install', `could not merge minimumReleaseAgeExclude after success (#883, #878): ${String(error)}`);
355
+ }
356
+ }
342
357
  return result;
343
358
  }
344
359
  /**
@@ -362,8 +362,27 @@ export function classifyPnpmFailure(output, exitCode) {
362
362
  // (MINIMUM_RELEASE_AGE_VIOLATION), and a later add can fail re-resolving
363
363
  // the young dep (NO_MATURE_MATCHING_VERSION). Recovery is a one-shot
364
364
  // --config.minimum-release-age=0 retry, automated in withHoistRecovery.
365
+ //
366
+ // That gate has a THIRD face, and it is the one that fails AFTER the
367
+ // lockfile check has said yes. pnpm skips that check when the verification
368
+ // cache holds a run whose recorded window is at least as wide as the
369
+ // current one (`canTrustPastCheck`: past >= current, pnpm's own
370
+ // lockfile-verified cache), and the resolver then applies the same policy
371
+ // from scratch. A re-resolved dependency still inside the window comes back
372
+ // as a `policyViolation`, which the CLI only knows how to consume through a
373
+ // handler it never wires on this call path — so it throws
374
+ // RESOLUTION_POLICY_VIOLATIONS_UNHANDLED instead of reporting anything the
375
+ // user can act on. Measured on the bundled pnpm 11.7.0 (DSH Desktop), with
376
+ // a cached pass in place: verification prints "passes supply-chain
377
+ // policies", and the next phase dies with "2 resolution-policy violations
378
+ // were produced but no handleResolutionPolicyViolations callback was wired".
379
+ // Same cause, same cure as the two shapes above — pnpm's hint names
380
+ // `setupPolicyHandlers`, which an integration calling the CLI cannot wire,
381
+ // so the only answer is the one-shot override. Classifying it here is what
382
+ // connects it to that retry; without it the raw internal hint was surfaced.
365
383
  if (output.includes('ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION')
366
- || output.includes('ERR_PNPM_NO_MATURE_MATCHING_VERSION')) {
384
+ || output.includes('ERR_PNPM_NO_MATURE_MATCHING_VERSION')
385
+ || output.includes('ERR_PNPM_RESOLUTION_POLICY_VIOLATIONS_UNHANDLED')) {
367
386
  return {
368
387
  code: 'release-age-violation',
369
388
  recoverable: false,
package/lib/profile.js CHANGED
@@ -139,7 +139,13 @@ export function readManifestDeps(profile, explicitDir) {
139
139
  export function readDependencyOwners(profile, names, explicitDir) {
140
140
  const installed = new Set(names);
141
141
  const owners = {};
142
- for (const owner of [...names].sort()) {
142
+ // Who may be an OWNER is not the same question as who gets a card (#892).
143
+ // pnpm hoists a transitive dependency's directory to `node_modules/<name>`
144
+ // as well, so `ws` — a dependency of a plugin, never a profile dependency —
145
+ // is where `bufferutil` and `utf-8-validate` are declared. Scanning only the
146
+ // profile's own entries could therefore never answer "who asked for these",
147
+ // and the two native peers stayed on the list as plugins nobody chose.
148
+ for (const owner of topLevelPackageNames(profile, names, explicitDir)) {
143
149
  const manifest = readInstalledManifest(profile, owner, explicitDir);
144
150
  if (typeof manifest !== 'object' || manifest === null)
145
151
  continue;
@@ -156,6 +162,48 @@ export function readDependencyOwners(profile, names, explicitDir) {
156
162
  }
157
163
  return owners;
158
164
  }
165
+ /**
166
+ * Every package that could be an owner, sorted: the profile's own entries
167
+ * plus every package sitting at the profile's top level.
168
+ *
169
+ * The manifest's keys are the floor, not the ceiling — a missing or
170
+ * unreadable `node_modules` leaves the answer exactly as it was before the
171
+ * top level was scanned, so this can only ever find MORE owners.
172
+ *
173
+ * One manifest read per top-level package, measured at ~30 ms for 300
174
+ * packages on a warm cache, once per `/installed` request — which already
175
+ * reads a manifest, a patch file and an entry artifact for every card.
176
+ */
177
+ function topLevelPackageNames(profile, names, explicitDir) {
178
+ const found = new Set(names);
179
+ const root = join(profileDir(profile, explicitDir), 'node_modules');
180
+ let entries = [];
181
+ try {
182
+ entries = readdirSync(root, { withFileTypes: true })
183
+ // A scope directory is named `@scope`, so the leading `@` belongs here;
184
+ // the package's own namespace is added below.
185
+ .filter(dirent => dirent.isDirectory() && /^@?[A-Za-z0-9._~-]+$/.test(dirent.name))
186
+ .map(dirent => dirent.name);
187
+ }
188
+ catch {
189
+ return [...found].sort();
190
+ }
191
+ for (const entry of entries) {
192
+ if (!entry.startsWith('@')) {
193
+ found.add(entry);
194
+ continue;
195
+ }
196
+ // A scope directory holds the packages themselves.
197
+ try {
198
+ for (const inner of readdirSync(join(root, entry), { withFileTypes: true })) {
199
+ if (inner.isDirectory() && /^[A-Za-z0-9._~-]+$/.test(inner.name))
200
+ found.add(`${entry}/${inner.name}`);
201
+ }
202
+ }
203
+ catch { /* unreadable scope — its packages simply cannot own anything */ }
204
+ }
205
+ return [...found].sort();
206
+ }
159
207
  function objectRecord(value) {
160
208
  return typeof value === 'object' && value !== null && !Array.isArray(value)
161
209
  ? value
package/lib/verify.js CHANGED
@@ -33,7 +33,7 @@ import { join } from 'node:path';
33
33
  import { listHotMounts, parseSimplePatch } from './hot.js';
34
34
  import { userPatchPackageReferences } from './patch.js';
35
35
  import { nameMatchesPackage } from './entry-identity.js';
36
- import { bundlePatchInsertedIds, declaredBundlePatchFiles, hasDshManifest, hasLoadableEntry, profileDir, readInstalled } from './profile.js';
36
+ import { bundlePatchInsertedIds, declaredBundlePatchFiles, hasDshManifest, hasLoadableEntry, pluginSubdirs, profileDir, readInstalled } from './profile.js';
37
37
  import { compareSemver, composeLayers, parsePatchFile, satisfiesRange } from './check.js';
38
38
  import { dshHostInfo } from './dsh-install.js';
39
39
  /**
@@ -422,19 +422,33 @@ export function verifyActivation(profile, name, live = new Set(listHotMounts()),
422
422
  hot: false,
423
423
  };
424
424
  }
425
- return inBundles
426
- ? {
425
+ if (inBundles) {
426
+ return {
427
427
  state: 'restart',
428
428
  reasons: ['由你自己的 cordis.patch.yml 按名加载,重启后生效 / loaded by name from your own cordis.patch.yml — live after a restart'],
429
429
  bundle: true,
430
430
  hot: false,
431
- }
432
- : {
433
- state: 'inert',
434
- reasons: ['普通依赖(未声明 dsh 元数据),不是 profile 层插件;若它由某个 bundle patch 按名加载,启动后会显示为已加载 / a plain dependency with no dsh metadata — not a profile-layer plugin; if some bundle patch loads it by name it will read as live once running'],
435
- bundle: false,
436
- hot: false,
437
431
  };
432
+ }
433
+ // A collection repository's ROOT package says nothing about itself and
434
+ // holds the plugins one level down (#892). Installed whole — which the
435
+ // market used to do before it learned to retarget one, and which a user
436
+ // can still do by hand — it lands on this list as "installed, not in
437
+ // effect" with no way out of that sentence. Name the plugins it does
438
+ // carry and the way to install one: the dead end is the bug.
439
+ const carried = pluginSubdirs(dir);
440
+ const shown = carried.slice(0, 3).join('、');
441
+ const more = carried.length > 3 ? `等 ${carried.length} 个` : '';
442
+ const shownEn = carried.slice(0, 3).join(', ');
443
+ const moreEn = carried.length > 3 ? ` and ${carried.length - 3} more` : '';
444
+ return {
445
+ state: 'inert',
446
+ reasons: [carried.length > 0
447
+ ? `这是集合仓库的根包,本身不是插件。可安装的子包:${shown}${more}。请装子包而不是它(仓库地址后加 #path:/子包目录) / this is a collection repository's root package, not a plugin itself; install one of its plugins instead — ${shownEn}${moreEn} — by adding #path:/<subdirectory> to the repository URL`
448
+ : '普通依赖(未声明 dsh 元数据),不是 profile 层插件;若它由某个 bundle patch 按名加载,启动后会显示为已加载 / a plain dependency with no dsh metadata — not a profile-layer plugin; if some bundle patch loads it by name it will read as live once running'],
449
+ bundle: false,
450
+ hot: false,
451
+ };
438
452
  }
439
453
  // Carrier bundles (#103) ship no entry of their own — what they mount is
440
454
  // the point — so judge by "is anything loadable", not by this package's
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.29",
4
+ "version": "1.66.31",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/types/index.d.ts",
package/src/install.ts CHANGED
@@ -345,6 +345,20 @@ export async function withHoistRecovery(
345
345
  : `这台宿主(官方桌面端)自己执行安装,不接受市场附加的 pnpm 参数(${unavailableOption}),所以这一步没有自动重试。 / This host (the official desktop app) runs the install itself and takes no pnpm options from the market (${unavailableOption}), so that automatic retry could not be attempted.`
346
346
  result = { ...result, stderr: `${result.stderr}\n\n${note}` }
347
347
  }
348
+ // pnpm can append a second rule even on success (#883, #878), including
349
+ // after a recovery retry or an official Desktop manager install. Repair
350
+ // the final file now, before another plugin operation reads it; a successful
351
+ // command must never be retried just because its exclusions needed merging.
352
+ if (ok(result) && (verb === 'add' || verb === 'remove' || verb === 'install' || verb === 'update')) {
353
+ try {
354
+ const merged = mergeDuplicateReleaseAgeExcludes(profile, profileDirectory)
355
+ if (merged.length > 0) {
356
+ logEvent('warn', 'install', `merged duplicate minimumReleaseAgeExclude rules for ${merged.join(', ')} after success (#883, #878)`)
357
+ }
358
+ } catch (error) {
359
+ logEvent('warn', 'install', `could not merge minimumReleaseAgeExclude after success (#883, #878): ${String(error)}`)
360
+ }
361
+ }
348
362
  return result
349
363
  }
350
364
 
@@ -399,8 +399,27 @@ export function classifyPnpmFailure(output: string, exitCode?: number | null): P
399
399
  // (MINIMUM_RELEASE_AGE_VIOLATION), and a later add can fail re-resolving
400
400
  // the young dep (NO_MATURE_MATCHING_VERSION). Recovery is a one-shot
401
401
  // --config.minimum-release-age=0 retry, automated in withHoistRecovery.
402
+ //
403
+ // That gate has a THIRD face, and it is the one that fails AFTER the
404
+ // lockfile check has said yes. pnpm skips that check when the verification
405
+ // cache holds a run whose recorded window is at least as wide as the
406
+ // current one (`canTrustPastCheck`: past >= current, pnpm's own
407
+ // lockfile-verified cache), and the resolver then applies the same policy
408
+ // from scratch. A re-resolved dependency still inside the window comes back
409
+ // as a `policyViolation`, which the CLI only knows how to consume through a
410
+ // handler it never wires on this call path — so it throws
411
+ // RESOLUTION_POLICY_VIOLATIONS_UNHANDLED instead of reporting anything the
412
+ // user can act on. Measured on the bundled pnpm 11.7.0 (DSH Desktop), with
413
+ // a cached pass in place: verification prints "passes supply-chain
414
+ // policies", and the next phase dies with "2 resolution-policy violations
415
+ // were produced but no handleResolutionPolicyViolations callback was wired".
416
+ // Same cause, same cure as the two shapes above — pnpm's hint names
417
+ // `setupPolicyHandlers`, which an integration calling the CLI cannot wire,
418
+ // so the only answer is the one-shot override. Classifying it here is what
419
+ // connects it to that retry; without it the raw internal hint was surfaced.
402
420
  if (output.includes('ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION')
403
- || output.includes('ERR_PNPM_NO_MATURE_MATCHING_VERSION')) {
421
+ || output.includes('ERR_PNPM_NO_MATURE_MATCHING_VERSION')
422
+ || output.includes('ERR_PNPM_RESOLUTION_POLICY_VIOLATIONS_UNHANDLED')) {
404
423
  return {
405
424
  code: 'release-age-violation',
406
425
  recoverable: false,
package/src/profile.ts CHANGED
@@ -151,7 +151,13 @@ export function readDependencyOwners(
151
151
  ): Record<string, string> {
152
152
  const installed = new Set(names)
153
153
  const owners: Record<string, string> = {}
154
- for (const owner of [...names].sort()) {
154
+ // Who may be an OWNER is not the same question as who gets a card (#892).
155
+ // pnpm hoists a transitive dependency's directory to `node_modules/<name>`
156
+ // as well, so `ws` — a dependency of a plugin, never a profile dependency —
157
+ // is where `bufferutil` and `utf-8-validate` are declared. Scanning only the
158
+ // profile's own entries could therefore never answer "who asked for these",
159
+ // and the two native peers stayed on the list as plugins nobody chose.
160
+ for (const owner of topLevelPackageNames(profile, names, explicitDir)) {
155
161
  const manifest = readInstalledManifest(profile, owner, explicitDir)
156
162
  if (typeof manifest !== 'object' || manifest === null) continue
157
163
  const declared = manifest as { dependencies?: unknown; peerDependencies?: unknown }
@@ -166,6 +172,50 @@ export function readDependencyOwners(
166
172
  return owners
167
173
  }
168
174
 
175
+ /**
176
+ * Every package that could be an owner, sorted: the profile's own entries
177
+ * plus every package sitting at the profile's top level.
178
+ *
179
+ * The manifest's keys are the floor, not the ceiling — a missing or
180
+ * unreadable `node_modules` leaves the answer exactly as it was before the
181
+ * top level was scanned, so this can only ever find MORE owners.
182
+ *
183
+ * One manifest read per top-level package, measured at ~30 ms for 300
184
+ * packages on a warm cache, once per `/installed` request — which already
185
+ * reads a manifest, a patch file and an entry artifact for every card.
186
+ */
187
+ function topLevelPackageNames(
188
+ profile: string,
189
+ names: readonly string[],
190
+ explicitDir?: string,
191
+ ): string[] {
192
+ const found = new Set(names)
193
+ const root = join(profileDir(profile, explicitDir), 'node_modules')
194
+ let entries: string[] = []
195
+ try {
196
+ entries = readdirSync(root, { withFileTypes: true })
197
+ // A scope directory is named `@scope`, so the leading `@` belongs here;
198
+ // the package's own namespace is added below.
199
+ .filter(dirent => dirent.isDirectory() && /^@?[A-Za-z0-9._~-]+$/.test(dirent.name))
200
+ .map(dirent => dirent.name)
201
+ } catch {
202
+ return [...found].sort()
203
+ }
204
+ for (const entry of entries) {
205
+ if (!entry.startsWith('@')) {
206
+ found.add(entry)
207
+ continue
208
+ }
209
+ // A scope directory holds the packages themselves.
210
+ try {
211
+ for (const inner of readdirSync(join(root, entry), { withFileTypes: true })) {
212
+ if (inner.isDirectory() && /^[A-Za-z0-9._~-]+$/.test(inner.name)) found.add(`${entry}/${inner.name}`)
213
+ }
214
+ } catch { /* unreadable scope — its packages simply cannot own anything */ }
215
+ }
216
+ return [...found].sort()
217
+ }
218
+
169
219
  /** Exact rollback state owned by one profile package operation. */
170
220
  export interface ProfileManifestSnapshot {
171
221
  dependencies: Record<string, string>
package/src/verify.ts CHANGED
@@ -34,7 +34,7 @@ import { join } from 'node:path'
34
34
  import { listHotMounts, parseSimplePatch } from './hot.ts'
35
35
  import { userPatchPackageReferences } from './patch.ts'
36
36
  import { nameMatchesPackage } from './entry-identity.ts'
37
- import { bundlePatchInsertedIds, declaredBundlePatchFiles, hasDshManifest, hasLoadableEntry, profileDir, readInstalled } from './profile.ts'
37
+ import { bundlePatchInsertedIds, declaredBundlePatchFiles, hasDshManifest, hasLoadableEntry, pluginSubdirs, profileDir, readInstalled } from './profile.ts'
38
38
  import { compareSemver, composeLayers, parsePatchFile, satisfiesRange } from './check.ts'
39
39
  import { dshHostInfo } from './dsh-install.ts'
40
40
 
@@ -498,19 +498,33 @@ export function verifyActivation(
498
498
  hot: false,
499
499
  }
500
500
  }
501
- return inBundles
502
- ? {
503
- state: 'restart',
504
- reasons: ['由你自己的 cordis.patch.yml 按名加载,重启后生效 / loaded by name from your own cordis.patch.yml — live after a restart'],
505
- bundle: true,
506
- hot: false,
507
- }
508
- : {
509
- state: 'inert',
510
- reasons: ['普通依赖(未声明 dsh 元数据),不是 profile 层插件;若它由某个 bundle patch 按名加载,启动后会显示为已加载 / a plain dependency with no dsh metadata — not a profile-layer plugin; if some bundle patch loads it by name it will read as live once running'],
511
- bundle: false,
512
- hot: false,
513
- }
501
+ if (inBundles) {
502
+ return {
503
+ state: 'restart',
504
+ reasons: ['由你自己的 cordis.patch.yml 按名加载,重启后生效 / loaded by name from your own cordis.patch.yml — live after a restart'],
505
+ bundle: true,
506
+ hot: false,
507
+ }
508
+ }
509
+ // A collection repository's ROOT package says nothing about itself and
510
+ // holds the plugins one level down (#892). Installed whole — which the
511
+ // market used to do before it learned to retarget one, and which a user
512
+ // can still do by hand — it lands on this list as "installed, not in
513
+ // effect" with no way out of that sentence. Name the plugins it does
514
+ // carry and the way to install one: the dead end is the bug.
515
+ const carried = pluginSubdirs(dir)
516
+ const shown = carried.slice(0, 3).join('、')
517
+ const more = carried.length > 3 ? `等 ${carried.length} 个` : ''
518
+ const shownEn = carried.slice(0, 3).join(', ')
519
+ const moreEn = carried.length > 3 ? ` and ${carried.length - 3} more` : ''
520
+ return {
521
+ state: 'inert',
522
+ reasons: [carried.length > 0
523
+ ? `这是集合仓库的根包,本身不是插件。可安装的子包:${shown}${more}。请装子包而不是它(仓库地址后加 #path:/子包目录) / this is a collection repository's root package, not a plugin itself; install one of its plugins instead — ${shownEn}${moreEn} — by adding #path:/<subdirectory> to the repository URL`
524
+ : '普通依赖(未声明 dsh 元数据),不是 profile 层插件;若它由某个 bundle patch 按名加载,启动后会显示为已加载 / a plain dependency with no dsh metadata — not a profile-layer plugin; if some bundle patch loads it by name it will read as live once running'],
525
+ bundle: false,
526
+ hot: false,
527
+ }
514
528
  }
515
529
  // Carrier bundles (#103) ship no entry of their own — what they mount is
516
530
  // the point — so judge by "is anything loadable", not by this package's