dshmarket 1.66.30 → 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/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.30",
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/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