dshmarket 1.73.0 → 1.73.1

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
@@ -924,19 +924,64 @@ export function bundlePatchInsertedIds(dir) {
924
924
  * conventional root cordis.patch.yml a package may ship instead.
925
925
  */
926
926
  export function bundleOptOutRowIds(dir) {
927
+ return declaredPatchRowIds(dir, 'disabledIds');
928
+ }
929
+ /**
930
+ * Row ids a package's own patch sources declare for one role — the loop the
931
+ * two readers above and below share.
932
+ *
933
+ * They read the same two sources: every declared `dsh.bundle.patch` (one file,
934
+ * or an ordered list of them, #792) and the conventional root
935
+ * `cordis.patch.yml` a package may ship instead. Keeping that loop in one place
936
+ * is the point — `bundlePatchInsertedIds` above is declared-files-only, and a
937
+ * copy of this scan that forgot the root file is exactly how the two sides of
938
+ * one question drift apart.
939
+ *
940
+ * A source that cannot be read contributes nothing rather than throwing: this
941
+ * answers "what does the package say it ships", and a file that is not there
942
+ * says nothing. That is the tolerant half of the family by design — the strict
943
+ * one (`readBundlePatchRows`) refuses the whole declaration, because the host
944
+ * loads none of a patch it cannot read completely.
945
+ */
946
+ function declaredPatchRowIds(dir, role) {
927
947
  const ids = new Set();
928
948
  const read = (file) => {
929
949
  try {
930
- for (const id of parsePatchRows(readFileSync(file, 'utf8')).disabledIds)
950
+ for (const id of parsePatchRows(readFileSync(file, 'utf8'))[role])
931
951
  ids.add(id);
932
952
  }
933
- catch { /* no such patch — nothing shipped off through it */ }
953
+ catch { /* no such patch — nothing declared through it */ }
934
954
  };
935
955
  for (const file of declaredBundlePatchFiles(dir))
936
956
  read(file);
937
957
  read(join(dir, 'cordis.patch.yml'));
938
958
  return [...ids];
939
959
  }
960
+ /**
961
+ * Row ids the installed copy of a package DECLARES it inserts (#1012).
962
+ *
963
+ * The evidence half of `rowIdsForPackage`: the same two patch sources (the
964
+ * declared `dsh.bundle.patch`, one file or an ordered list, and the
965
+ * conventional root `cordis.patch.yml`), insert rows only, and — unlike that
966
+ * reader — no live loader rows.
967
+ *
968
+ * Live rows are exactly what this cannot use. A rename transaction asks the
969
+ * question twice: of the old package just before it is removed (whose rows are
970
+ * still live, but whose patch files are about to be the only copy left) and of
971
+ * the new package right after its first install (which has never been booted,
972
+ * so the loader has never heard of it). Only the package's own declaration can
973
+ * answer for both.
974
+ *
975
+ * That is the same question, and the same parser, as `renamePredecessor` asks
976
+ * on the install path — an install never boots the package either, so it
977
+ * compares declarations too and the two refusals agree on what "one plugin"
978
+ * means (#1012). Where they differ is the root `cordis.patch.yml`: see
979
+ * `declaredPatchRowIds` for why this reader covers it and that gate's
980
+ * declared-only reader does not.
981
+ */
982
+ export function shippedInsertedRowIds(dir) {
983
+ return declaredPatchRowIds(dir, 'insertedIds');
984
+ }
940
985
  /**
941
986
  * Rows of one patch file. Exported because a package may ship its patch at
942
987
  * the conventional path INSTEAD of declaring `dsh.bundle.patch`, and the
package/lib/routes.js CHANGED
@@ -24,7 +24,7 @@ import { diagnosePackageManifests } from './diagnostics.js';
24
24
  import { BOOT_ID, cancelActive, probePnpm, progress, provisionPnpm, runDshPlugin, setBuildEnvSource, TARGET_RE, } from './dsh-cli.js';
25
25
  import { packageOfEntryName } from './entry-identity.js';
26
26
  import { PendingRowJournal, repairOrphanedRows } from './pending-rows.js';
27
- import { addProfileBundle, bundleOptOutRowIds, bundlePatchInsertedIds, bundlesDroppedFromProfile, declareProfileDependency, dropFromManifest, generationManagedNames, hasLoadableEntry, holdsNativeAddon, INBOX_BUNDLES, isDshProfileName, profileDir, readDependencyOwners, readGitResolutionCommit, readInstalled, readInstalledManifest, readInstalledPackageName, readInstalledRepoEvidence, readInstalledVersion, readLockCommits, readProfileBundles, readProfileManifestSnapshot, removeProfileBundle, parsePatchRows, restoreProfileManifest, setAllowBuilds } from './profile.js';
27
+ import { addProfileBundle, bundleOptOutRowIds, bundlePatchInsertedIds, bundlesDroppedFromProfile, declareProfileDependency, dropFromManifest, generationManagedNames, hasLoadableEntry, holdsNativeAddon, INBOX_BUNDLES, isDshProfileName, profileDir, readDependencyOwners, readGitResolutionCommit, readInstalled, readInstalledManifest, readInstalledPackageName, readInstalledRepoEvidence, readInstalledVersion, readLockCommits, readProfileBundles, readProfileManifestSnapshot, removeProfileBundle, parsePatchRows, restoreProfileManifest, setAllowBuilds, shippedInsertedRowIds } from './profile.js';
28
28
  import { assessProfile, classifyPeer, introducedDuplicateNames, introducedRisks } from './compatibility.js';
29
29
  import { runningAgentIds } from './agents.js';
30
30
  import { analyzeProfile, corePackageNames } from './check.js';
@@ -4052,6 +4052,12 @@ export function mountMarketRoutes(host, config, commandRuntime, agentsLookup, ho
4052
4052
  const wasLive = verifyActivation(config.profile, name, liveNames(), activeProfileDir, disabled.has(name)).state === 'live'
4053
4053
  && hasHostHalf(config.profile, name, activeProfileDir);
4054
4054
  const oldRows = rowIdsForPackage(host, activeProfileDir, name);
4055
+ // Identity evidence for a rename (#1012), read BEFORE the removal:
4056
+ // once `remove` runs, the installed old package — and with it the
4057
+ // only record of what it registers — is gone.
4058
+ const oldInsertIds = kind === 'npm-rename'
4059
+ ? shippedInsertedRowIds(join(activeProfileDir, 'node_modules', name))
4060
+ : [];
4055
4061
  const patchFlags = packagePatchFlags(host, activeProfileDir, [name], readUserPatchState(userPatchPath));
4056
4062
  const wasDisabled = disabled.has(name);
4057
4063
  const manifestBefore = manifestCapture.snapshot;
@@ -4077,13 +4083,21 @@ export function mountMarketRoutes(host, config, commandRuntime, agentsLookup, ho
4077
4083
  ? { ok: true, detail: null }
4078
4084
  : { ok: false, detail: `${restoredEn} was restored without a loadable entry` };
4079
4085
  };
4080
- const failWithRollback = async (detail, extra = {}) => {
4086
+ // `noteZh`/`noteEn` carry the "what you can do next" sentence, and
4087
+ // only the branch that actually restored the old install may say
4088
+ // it: after a failed rollback there is nothing to reassure the
4089
+ // user with, so the note is dropped rather than contradicting the
4090
+ // "could not be verified" fallout.
4091
+ const failWithRollback = async (detail, extra = {}, status = 500, noteZh = '', noteEn = '') => {
4081
4092
  const rollback = await rollbackMigration();
4082
4093
  logEvent(rollback.ok ? 'warn' : 'error', 'source-migration', `${name}: migration failed — ${detail}; ${rollback.ok ? `${restoredEn} restored` : `rollback failed: ${rollback.detail ?? 'unknown'}`}`);
4083
- sendJson(response, 500, {
4094
+ // A 409 whose rollback failed is not a conflict the user can walk
4095
+ // away from — the install itself is now unverified — so the
4096
+ // status follows the rollback, not the reason we got here.
4097
+ sendJson(response, rollback.ok ? status : 500, {
4084
4098
  ok: false,
4085
4099
  error: rollback.ok
4086
- ? `${detail};已恢复${restoredZh}。 / ${detail}; ${restoredEn} was restored.`
4100
+ ? `${detail};已恢复${restoredZh}。${noteZh === '' ? '' : ` ${noteZh}`} / ${detail}; ${restoredEn} was restored.${noteEn === '' ? '' : ` ${noteEn}`}`
4087
4101
  : `${detail};且${restoredZh}未能验证恢复(${rollback.detail ?? 'unknown'})。 / ${detail}; restoration of ${restoredEn} could not be verified (${rollback.detail ?? 'unknown'}).`,
4088
4102
  rollback: rollback.ok,
4089
4103
  ...extra,
@@ -4107,6 +4121,37 @@ export function mountMarketRoutes(host, config, commandRuntime, agentsLookup, ho
4107
4121
  await failWithRollback('npm 目标安装完成后未形成可加载且唯一的依赖。 / The npm target did not produce one loadable replacement dependency.');
4108
4122
  return;
4109
4123
  }
4124
+ // The catalog's `previousNpm` is a CLAIM that the two package names
4125
+ // are one plugin, and the transaction above already acted on it by
4126
+ // replacing what the user had. Ask the packages themselves: the
4127
+ // rows each side declares it inserts are registry evidence, and two
4128
+ // packages that register disjoint rows are not the same plugin —
4129
+ // whatever the catalog says. Refuse and put the old one back rather
4130
+ // than leave a stranger's rows running under the old plugin's name
4131
+ // (#1012). Same question, same parser as the install-path gate
4132
+ // (`renamePredecessor`), which compares the installed package's
4133
+ // declared rows against the incoming registry `cordis.patch.yml`;
4134
+ // here both packages are already on disk, so both sides are read
4135
+ // the same way — including the conventional root patch file, which
4136
+ // that gate only knows on the incoming side.
4137
+ //
4138
+ // Both sides must SHOW evidence: a package with no patch file
4139
+ // declares nothing, and silence is not a contradiction.
4140
+ if (kind === 'npm-rename') {
4141
+ const newInsertIds = shippedInsertedRowIds(join(activeProfileDir, 'node_modules', targetName));
4142
+ if (oldInsertIds.length > 0 && newInsertIds.length > 0 && !sameLoaderRows(oldInsertIds, newInsertIds)) {
4143
+ // Both sides are third-party YAML: clip each id so a hostile
4144
+ // patch cannot turn this refusal into a megabyte response.
4145
+ const rows = (ids) => {
4146
+ const clip = (id) => (id.length <= 48 ? id : `${id.slice(0, 48)}…`);
4147
+ return ids.length <= 3
4148
+ ? ids.map(clip).join(', ')
4149
+ : `${ids.slice(0, 3).map(clip).join(', ')}, +${ids.length - 3}`;
4150
+ };
4151
+ await failWithRollback(`目录声明 ${name} 已改名为 ${targetName},但两边的注册行不同(${name}: ${rows(oldInsertIds)};${targetName}: ${rows(newInsertIds)}),说明它们不是同一个插件。 / The catalog declares ${name} was renamed to ${targetName}, but the two packages register different plugin rows (${name}: ${rows(oldInsertIds)}; ${targetName}: ${rows(newInsertIds)}), so they are not the same plugin.`, {}, 409, `原来的版本仍然可用;如果目录写错了,请报告这一条。`, `The original install is still usable; if the catalog is wrong, report that entry.`);
4152
+ return;
4153
+ }
4154
+ }
4110
4155
  const stack = readBundleStack(activeProfileDir);
4111
4156
  const trial = trialValidate(activeProfileDir, stack.community, hostAnchorOption);
4112
4157
  if (!trial.ok) {
@@ -5374,6 +5419,29 @@ export function mountMarketRoutes(host, config, commandRuntime, agentsLookup, ho
5374
5419
  logEvent('warn', 'update-bundles-restored', `${name}: the desktop app removed its bundle row before refusing the update (bundle-in-use) — restored it so the plugin still loads on the next boot (#935)`);
5375
5420
  }
5376
5421
  }
5422
+ // A FAILED update can also drop rows it never intended to touch.
5423
+ // The rollback path ("restoration of the previous build could not
5424
+ // be verified") rewrites the manifest through pnpm and the plugin
5425
+ // command, and neither carries the bundle rows it did not own: a
5426
+ // bystander keeps its dependency and its node_modules but loses
5427
+ // its `dsh.profile.bundles` row, so it silently stops loading on
5428
+ // the next boot while this operation reported a failure about
5429
+ // something else entirely.
5430
+ //
5431
+ // Restoring is sound exactly because the operation FAILED: nobody
5432
+ // expressed a deliberate removal, and the package is still on
5433
+ // disk — putting the row back restores what the user had before
5434
+ // the operation. A successful update keeps the read-only stance
5435
+ // (#720/#696), where a missing row may be a deliberate switch-off.
5436
+ if (!ok && !cancelled) {
5437
+ const lostRows = bundlesDroppedFromProfile(manifestBefore, config.profile, activeProfileDir, new Set(removedDeclaration === null ? [] : [removedDeclaration.name])).filter(bundle => bundle !== name
5438
+ && existsSync(join(activeProfileDir, 'node_modules', bundle, 'package.json')));
5439
+ for (const bundle of lostRows) {
5440
+ if (addProfileBundle(activeProfileDir, bundle)) {
5441
+ logEvent('warn', 'update-bundles-restored', `${bundle}: lost its dsh.profile.bundles row while ${name}'s update failed — restored it so the plugin still loads on the next boot (#720)`);
5442
+ }
5443
+ }
5444
+ }
5377
5445
  logEvent(ok || cancelled ? 'info' : 'error', 'update', `${name} -> ${target} exit=${String(result.exitCode)}${result.timedOut ? ' TIMEOUT' : ''}${cancelled ? ' CANCELLED' : ''}${stale ? ` STALE(${staleReason ?? 'unknown'})` : ''}${ok || cancelled ? '' : ` err=${failureDetail(result)}`}`);
5378
5446
  // A user-cancelled run is a quiet outcome, not an error.
5379
5447
  sendJson(response, ok || cancelled ? 200 : result.busy === true ? 409 : 502, {
@@ -324,6 +324,29 @@ export declare function bundlePatchInsertedIds(dir: string): string[];
324
324
  * conventional root cordis.patch.yml a package may ship instead.
325
325
  */
326
326
  export declare function bundleOptOutRowIds(dir: string): string[];
327
+ /**
328
+ * Row ids the installed copy of a package DECLARES it inserts (#1012).
329
+ *
330
+ * The evidence half of `rowIdsForPackage`: the same two patch sources (the
331
+ * declared `dsh.bundle.patch`, one file or an ordered list, and the
332
+ * conventional root `cordis.patch.yml`), insert rows only, and — unlike that
333
+ * reader — no live loader rows.
334
+ *
335
+ * Live rows are exactly what this cannot use. A rename transaction asks the
336
+ * question twice: of the old package just before it is removed (whose rows are
337
+ * still live, but whose patch files are about to be the only copy left) and of
338
+ * the new package right after its first install (which has never been booted,
339
+ * so the loader has never heard of it). Only the package's own declaration can
340
+ * answer for both.
341
+ *
342
+ * That is the same question, and the same parser, as `renamePredecessor` asks
343
+ * on the install path — an install never boots the package either, so it
344
+ * compares declarations too and the two refusals agree on what "one plugin"
345
+ * means (#1012). Where they differ is the root `cordis.patch.yml`: see
346
+ * `declaredPatchRowIds` for why this reader covers it and that gate's
347
+ * declared-only reader does not.
348
+ */
349
+ export declare function shippedInsertedRowIds(dir: string): string[];
327
350
  /**
328
351
  * Rows of one patch file. Exported because a package may ship its patch at
329
352
  * the conventional path INSTEAD of declaring `dsh.bundle.patch`, and the
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.73.0",
4
+ "version": "1.73.1",
5
5
  "icon": "assets/logo.svg",
6
6
  "type": "module",
7
7
  "main": "lib/index.js",