dshmarket 1.66.41 → 1.66.42

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/changelog.js CHANGED
@@ -23,9 +23,9 @@
23
23
  import { fileFromTarball } from './catalog-npm.js';
24
24
  import { marketFetch } from './net.js';
25
25
  import { activeRegion, routesFor } from './regions.js';
26
- import { profileDir, readGitResolutionCommit, readInstalled, readInstalledRepoEvidence, readLockCommits } from './profile.js';
26
+ import { profileDir, readGitResolutionCommit, readInstalled, readInstalledRepoEvidence, readInstalledVersion, readLockCommits } from './profile.js';
27
27
  import { githubRepoOfRemote, hostedRepoKey, isGitHostedSpec, lookupRepoFromUrl, parseHostShorthand, parseSourceUrl, repoOf, repoOfTarget } from './sources.js';
28
- import { checkUpdates } from './updates.js';
28
+ import { checkUpdates, compareVersions } from './updates.js';
29
29
  import { loadRegistry } from './registry.js';
30
30
  const UPDATES_PACKAGE = 'dsh-plugin-updates';
31
31
  const UPDATES_FILE = 'package/updates.json';
@@ -118,6 +118,20 @@ export function sliceCommitsAt(items, installed) {
118
118
  }
119
119
  return { items: out, found: false };
120
120
  }
121
+ /**
122
+ * The version a release tag names, when it names one.
123
+ *
124
+ * Tags come as `v1.2.3`, `1.2.3`, `@scope/pkg@1.2.3` or `pkg-v1.2.3`; the
125
+ * FIRST semver-shaped run is the version in all of them. A tag that names no
126
+ * version (`nightly`) returns null, and callers must treat "unknown" as "do
127
+ * not judge" rather than "older".
128
+ */
129
+ export function releaseVersionOf(tag) {
130
+ if (tag === null)
131
+ return null;
132
+ const match = /(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)/.exec(tag);
133
+ return match?.[1] ?? null;
134
+ }
121
135
  /** The repo url key an entry's update notes live under, or null. */
122
136
  export function repoKeyOf(spec) {
123
137
  const repo = githubIdentityOf(spec)?.split('#')[0] ?? null;
@@ -407,7 +421,23 @@ export async function updateNotesFor(profile, explicitDir, name) {
407
421
  current = statuses?.[name]?.current ?? null;
408
422
  }
409
423
  if (entry?.release !== undefined && entry.release !== null && (entry.release.body !== '' || entry.release.tag !== null)) {
410
- return { kind: 'release', release: entry.release };
424
+ // The release feed is a DAILY snapshot while the update target is read
425
+ // live from npm, so the snapshot's newest release is often the version
426
+ // already installed (or older). Showing it next to "1.66.39 → 1.66.40"
427
+ // describes a change the user already has (#925). When the release
428
+ // names a version no newer than the installed one AND a better tier
429
+ // exists, take the better tier: the commit tail is sliced at this
430
+ // install's sha, and npm publish times are newer than the install by
431
+ // construction. With nothing better, the stale release still beats an
432
+ // empty dialog.
433
+ const installed = readInstalledVersion(profile, name, activeProfileDir);
434
+ const released = releaseVersionOf(entry.release.tag);
435
+ const behind = installed !== null && released !== null
436
+ && (compareVersions(released, installed) ?? 0) <= 0;
437
+ const hasCommits = (entry.commits?.length ?? 0) > 0;
438
+ if (!(behind && (hasCommits || (specKey === null && npmName !== null)))) {
439
+ return { kind: 'release', release: entry.release };
440
+ }
411
441
  }
412
442
  if (entry?.commits !== undefined && entry.commits.length > 0) {
413
443
  return { kind: 'commits', commits: sliceCommitsAt(entry.commits, current) };
package/lib/install.js CHANGED
@@ -662,7 +662,10 @@ export async function validateAddedPlugins(run, profile, before, explicitDir, ho
662
662
  const packageDir = join(dir, 'node_modules', n);
663
663
  // hasLoadableEntry, not entryArtifactExists: carrier bundles legitimately
664
664
  // ship no entry of their own (#103) and must not be uninstalled here.
665
- if (!hasDshManifest(packageDir) || !hasLoadableEntry(dir, n)) {
665
+ // The host install tree is passed too (#906): a carrier mounting only
666
+ // in-box packages resolves nowhere under the profile, and judging it by
667
+ // that alone deleted a package the host had just installed correctly.
668
+ if (!hasDshManifest(packageDir) || !hasLoadableEntry(dir, n, hostDirectory ?? findDshInstallDir())) {
666
669
  removedBroken.push(n);
667
670
  await removeAndReconcile(run, profile, dir, n, hostDirectory);
668
671
  continue;
package/lib/profile.js CHANGED
@@ -4,9 +4,11 @@
4
4
  * functions of the directory contents; no processes, no network.
5
5
  */
6
6
  import { existsSync, readdirSync, readFileSync, realpathSync, renameSync, statSync, writeFileSync } from 'node:fs';
7
+ import { createRequire } from 'node:module';
7
8
  import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
8
9
  import { isDeepStrictEqual } from 'node:util';
9
10
  import { JSON_SCHEMA, load } from 'js-yaml';
11
+ import { findDshInstallDir } from './dsh-install.js';
10
12
  import { resolveDshHome } from './home-paths.js';
11
13
  import { githubRemoteIdentities, githubRepoIdentities, hostedRepoKey, isGenerationLink, isGitHostedSpec, repoOfTarget } from './sources.js';
12
14
  /**
@@ -1296,7 +1298,7 @@ export function conflictingEntryIds(profileDirectory, candidate, installedBundle
1296
1298
  * @param profileDirectory - resolved profile directory (host-authoritative under Desktop).
1297
1299
  * @param name - installed package name.
1298
1300
  */
1299
- export function hasLoadableEntry(profileDirectory, name) {
1301
+ export function hasLoadableEntry(profileDirectory, name, hostDirectory) {
1300
1302
  const dir = join(profileDirectory, 'node_modules', name);
1301
1303
  if (entryArtifactExists(dir))
1302
1304
  return true;
@@ -1310,11 +1312,41 @@ export function hasLoadableEntry(profileDirectory, name) {
1310
1312
  // exiting 0 was immediately followed by the market removing what it had
1311
1313
  // just, correctly, installed.
1312
1314
  const workspaceRoot = dirname(profileDirectory);
1313
- return bundlePatchTargets(dir)
1314
- .filter(target => target !== name)
1315
- .some(target => entryArtifactExists(join(profileDirectory, 'node_modules', target))
1315
+ const targets = bundlePatchTargets(dir).filter(target => target !== name);
1316
+ if (targets.some(target => entryArtifactExists(join(profileDirectory, 'node_modules', target))
1316
1317
  || entryArtifactExists(join(dir, 'node_modules', target))
1317
- || entryArtifactExists(join(workspaceRoot, 'node_modules', target)));
1318
+ || entryArtifactExists(join(workspaceRoot, 'node_modules', target))))
1319
+ return true;
1320
+ // #906: the host's OWN packages live in its install tree, not in any
1321
+ // profile node_modules — `<install>/dsh/node_modules/...`, often inside
1322
+ // app.asar. A carrier whose patch mounts nothing but in-box packages
1323
+ // (@deepseek-ai/dsh-mcp-client and the like) therefore failed every root
1324
+ // above, was reported "broken", and — through validateAddedPlugins — was
1325
+ // uninstalled right after a successful install. `undefined` resolves the
1326
+ // install dir lazily (only carriers pay for it); `null` means "do not look".
1327
+ //
1328
+ // Inside app.asar the probe is blind for EVERY @deepseek-ai/ name (the
1329
+ // same measurement behind #676), so there identity decides instead of the
1330
+ // filesystem — the host publishes that scope, and our own composer loads
1331
+ // those packages from the archive at boot.
1332
+ const host = hostDirectory === undefined ? findDshInstallDir() : hostDirectory;
1333
+ if (host === null || targets.length === 0)
1334
+ return false;
1335
+ const packaged = /[/\\]app\.asar[/\\]/iu.test(host);
1336
+ return targets.some(target => packaged && target.startsWith('@deepseek-ai/')
1337
+ || packageResolvableFrom(join(host, 'package.json'), target));
1338
+ }
1339
+ /** Whether `target` resolves from the host's own dependency tree. */
1340
+ function packageResolvableFrom(anchorPackageJson, target) {
1341
+ try {
1342
+ const paths = createRequire(anchorPackageJson).resolve.paths(target) ?? [];
1343
+ return paths.some(searchPath => existsSync(join(searchPath, target, 'package.json')));
1344
+ }
1345
+ catch {
1346
+ // An unreadable anchor says nothing about the target: treat as absent
1347
+ // rather than loadable, so the caller keeps its conservative answer.
1348
+ return false;
1349
+ }
1318
1350
  }
1319
1351
  /** Plugin subdirectories (depth 2) of a collection checkout, as relative paths. */
1320
1352
  export function pluginSubdirs(root) {
@@ -63,6 +63,15 @@ export declare function sliceCommitsAt(items: CommitNote[], installed: string |
63
63
  items: CommitNote[];
64
64
  found: boolean;
65
65
  };
66
+ /**
67
+ * The version a release tag names, when it names one.
68
+ *
69
+ * Tags come as `v1.2.3`, `1.2.3`, `@scope/pkg@1.2.3` or `pkg-v1.2.3`; the
70
+ * FIRST semver-shaped run is the version in all of them. A tag that names no
71
+ * version (`nightly`) returns null, and callers must treat "unknown" as "do
72
+ * not judge" rather than "older".
73
+ */
74
+ export declare function releaseVersionOf(tag: string | null): string | null;
66
75
  /** The repo url key an entry's update notes live under, or null. */
67
76
  export declare function repoKeyOf(spec: string): string | null;
68
77
  /**
@@ -412,7 +412,7 @@ export declare function conflictingEntryIds(profileDirectory: string, candidate:
412
412
  * @param profileDirectory - resolved profile directory (host-authoritative under Desktop).
413
413
  * @param name - installed package name.
414
414
  */
415
- export declare function hasLoadableEntry(profileDirectory: string, name: string): boolean;
415
+ export declare function hasLoadableEntry(profileDirectory: string, name: string, hostDirectory?: string | null): boolean;
416
416
  /** Plugin subdirectories (depth 2) of a collection checkout, as relative paths. */
417
417
  export declare function pluginSubdirs(root: string): string[];
418
418
  /**
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.41",
4
+ "version": "1.66.42",
5
5
  "icon": "assets/logo.svg",
6
6
  "type": "module",
7
7
  "main": "lib/index.js",
package/src/changelog.ts CHANGED
@@ -24,9 +24,9 @@
24
24
  import { fileFromTarball } from './catalog-npm.ts'
25
25
  import { marketFetch } from './net.ts'
26
26
  import { activeRegion, routesFor } from './regions.ts'
27
- import { profileDir, readGitResolutionCommit, readInstalled, readInstalledRepoEvidence, readLockCommits } from './profile.ts'
27
+ import { profileDir, readGitResolutionCommit, readInstalled, readInstalledRepoEvidence, readInstalledVersion, readLockCommits } from './profile.ts'
28
28
  import { githubRepoOfRemote, hostedRepoKey, isGitHostedSpec, lookupRepoFromUrl, parseHostShorthand, parseSourceUrl, repoOf, repoOfTarget } from './sources.ts'
29
- import { checkUpdates } from './updates.ts'
29
+ import { checkUpdates, compareVersions } from './updates.ts'
30
30
  import { loadRegistry, type RegistryPlugin } from './registry.ts'
31
31
 
32
32
  const UPDATES_PACKAGE = 'dsh-plugin-updates'
@@ -141,6 +141,20 @@ export function sliceCommitsAt(items: CommitNote[], installed: string | null): {
141
141
  return { items: out, found: false }
142
142
  }
143
143
 
144
+ /**
145
+ * The version a release tag names, when it names one.
146
+ *
147
+ * Tags come as `v1.2.3`, `1.2.3`, `@scope/pkg@1.2.3` or `pkg-v1.2.3`; the
148
+ * FIRST semver-shaped run is the version in all of them. A tag that names no
149
+ * version (`nightly`) returns null, and callers must treat "unknown" as "do
150
+ * not judge" rather than "older".
151
+ */
152
+ export function releaseVersionOf(tag: string | null): string | null {
153
+ if (tag === null) return null
154
+ const match = /(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)/.exec(tag)
155
+ return match?.[1] ?? null
156
+ }
157
+
144
158
  /** The repo url key an entry's update notes live under, or null. */
145
159
  export function repoKeyOf(spec: string): string | null {
146
160
  const repo = githubIdentityOf(spec)?.split('#')[0] ?? null
@@ -450,7 +464,23 @@ export async function updateNotesFor(
450
464
  }
451
465
 
452
466
  if (entry?.release !== undefined && entry.release !== null && (entry.release.body !== '' || entry.release.tag !== null)) {
453
- return { kind: 'release', release: entry.release }
467
+ // The release feed is a DAILY snapshot while the update target is read
468
+ // live from npm, so the snapshot's newest release is often the version
469
+ // already installed (or older). Showing it next to "1.66.39 → 1.66.40"
470
+ // describes a change the user already has (#925). When the release
471
+ // names a version no newer than the installed one AND a better tier
472
+ // exists, take the better tier: the commit tail is sliced at this
473
+ // install's sha, and npm publish times are newer than the install by
474
+ // construction. With nothing better, the stale release still beats an
475
+ // empty dialog.
476
+ const installed = readInstalledVersion(profile, name, activeProfileDir)
477
+ const released = releaseVersionOf(entry.release.tag)
478
+ const behind = installed !== null && released !== null
479
+ && (compareVersions(released, installed) ?? 0) <= 0
480
+ const hasCommits = (entry.commits?.length ?? 0) > 0
481
+ if (!(behind && (hasCommits || (specKey === null && npmName !== null)))) {
482
+ return { kind: 'release', release: entry.release }
483
+ }
454
484
  }
455
485
  if (entry?.commits !== undefined && entry.commits.length > 0) {
456
486
  return { kind: 'commits', commits: sliceCommitsAt(entry.commits, current) }
package/src/install.ts CHANGED
@@ -659,7 +659,10 @@ export async function validateAddedPlugins(
659
659
  const packageDir = join(dir, 'node_modules', n)
660
660
  // hasLoadableEntry, not entryArtifactExists: carrier bundles legitimately
661
661
  // ship no entry of their own (#103) and must not be uninstalled here.
662
- if (!hasDshManifest(packageDir) || !hasLoadableEntry(dir, n)) {
662
+ // The host install tree is passed too (#906): a carrier mounting only
663
+ // in-box packages resolves nowhere under the profile, and judging it by
664
+ // that alone deleted a package the host had just installed correctly.
665
+ if (!hasDshManifest(packageDir) || !hasLoadableEntry(dir, n, hostDirectory ?? findDshInstallDir())) {
663
666
  removedBroken.push(n)
664
667
  await removeAndReconcile(run, profile, dir, n, hostDirectory)
665
668
  continue
package/src/profile.ts CHANGED
@@ -5,9 +5,11 @@
5
5
  */
6
6
 
7
7
  import { existsSync, readdirSync, readFileSync, realpathSync, renameSync, statSync, writeFileSync } from 'node:fs'
8
+ import { createRequire } from 'node:module'
8
9
  import { dirname, isAbsolute, join, relative, resolve } from 'node:path'
9
10
  import { isDeepStrictEqual } from 'node:util'
10
11
  import { JSON_SCHEMA, load } from 'js-yaml'
12
+ import { findDshInstallDir } from './dsh-install.ts'
11
13
  import { resolveDshHome } from './home-paths.ts'
12
14
  import { githubRemoteIdentities, githubRepoIdentities, hostedRepoKey, isGenerationLink, isGitHostedSpec, repoOfTarget } from './sources.ts'
13
15
 
@@ -1340,7 +1342,7 @@ export function conflictingEntryIds(
1340
1342
  * @param profileDirectory - resolved profile directory (host-authoritative under Desktop).
1341
1343
  * @param name - installed package name.
1342
1344
  */
1343
- export function hasLoadableEntry(profileDirectory: string, name: string): boolean {
1345
+ export function hasLoadableEntry(profileDirectory: string, name: string, hostDirectory?: string | null): boolean {
1344
1346
  const dir = join(profileDirectory, 'node_modules', name)
1345
1347
  if (entryArtifactExists(dir)) return true
1346
1348
  // A carrier is only sound when something it mounts is itself loadable.
@@ -1353,11 +1355,39 @@ export function hasLoadableEntry(profileDirectory: string, name: string): boolea
1353
1355
  // exiting 0 was immediately followed by the market removing what it had
1354
1356
  // just, correctly, installed.
1355
1357
  const workspaceRoot = dirname(profileDirectory)
1356
- return bundlePatchTargets(dir)
1357
- .filter(target => target !== name)
1358
- .some(target => entryArtifactExists(join(profileDirectory, 'node_modules', target))
1359
- || entryArtifactExists(join(dir, 'node_modules', target))
1360
- || entryArtifactExists(join(workspaceRoot, 'node_modules', target)))
1358
+ const targets = bundlePatchTargets(dir).filter(target => target !== name)
1359
+ if (targets.some(target => entryArtifactExists(join(profileDirectory, 'node_modules', target))
1360
+ || entryArtifactExists(join(dir, 'node_modules', target))
1361
+ || entryArtifactExists(join(workspaceRoot, 'node_modules', target)))) return true
1362
+ // #906: the host's OWN packages live in its install tree, not in any
1363
+ // profile node_modules — `<install>/dsh/node_modules/...`, often inside
1364
+ // app.asar. A carrier whose patch mounts nothing but in-box packages
1365
+ // (@deepseek-ai/dsh-mcp-client and the like) therefore failed every root
1366
+ // above, was reported "broken", and — through validateAddedPlugins — was
1367
+ // uninstalled right after a successful install. `undefined` resolves the
1368
+ // install dir lazily (only carriers pay for it); `null` means "do not look".
1369
+ //
1370
+ // Inside app.asar the probe is blind for EVERY @deepseek-ai/ name (the
1371
+ // same measurement behind #676), so there identity decides instead of the
1372
+ // filesystem — the host publishes that scope, and our own composer loads
1373
+ // those packages from the archive at boot.
1374
+ const host = hostDirectory === undefined ? findDshInstallDir() : hostDirectory
1375
+ if (host === null || targets.length === 0) return false
1376
+ const packaged = /[/\\]app\.asar[/\\]/iu.test(host)
1377
+ return targets.some(target => packaged && target.startsWith('@deepseek-ai/')
1378
+ || packageResolvableFrom(join(host, 'package.json'), target))
1379
+ }
1380
+
1381
+ /** Whether `target` resolves from the host's own dependency tree. */
1382
+ function packageResolvableFrom(anchorPackageJson: string, target: string): boolean {
1383
+ try {
1384
+ const paths = createRequire(anchorPackageJson).resolve.paths(target) ?? []
1385
+ return paths.some(searchPath => existsSync(join(searchPath, target, 'package.json')))
1386
+ } catch {
1387
+ // An unreadable anchor says nothing about the target: treat as absent
1388
+ // rather than loadable, so the caller keeps its conservative answer.
1389
+ return false
1390
+ }
1361
1391
  }
1362
1392
 
1363
1393
  /** Plugin subdirectories (depth 2) of a collection checkout, as relative paths. */