dsh-plugin-shop 0.5.4 → 0.6.0-beta.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/client.js CHANGED
@@ -4310,14 +4310,16 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4310
4310
  "url": string(),
4311
4311
  "sha256": string()
4312
4312
  })]).optional(),
4313
- "added": union([_undefined(), string()]).optional()
4313
+ "added": union([_undefined(), string()]).optional(),
4314
+ "peers": union([_undefined(), array(string())]).optional()
4314
4315
  })),
4315
4316
  "denied": array(object({
4316
4317
  "name": string(),
4317
4318
  "detail": string(),
4318
4319
  "replacement": union([_undefined(), string()]).optional()
4319
4320
  })),
4320
- "stars": record(string(), number())
4321
+ "stars": record(string(), number()),
4322
+ "incompatible": record(string(), array(string()))
4321
4323
  });
4322
4324
  const dsh_plugin_shop_shop_installed_result$schema = array(object({
4323
4325
  "name": string(),
@@ -4435,7 +4437,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4435
4437
  },
4436
4438
  sourceLocation: {
4437
4439
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4438
- "line": 525,
4440
+ "line": 543,
4439
4441
  "column": 9
4440
4442
  }
4441
4443
  },
@@ -4453,7 +4455,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4453
4455
  },
4454
4456
  sourceLocation: {
4455
4457
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4456
- "line": 681,
4458
+ "line": 723,
4457
4459
  "column": 9
4458
4460
  }
4459
4461
  },
@@ -4481,7 +4483,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4481
4483
  },
4482
4484
  sourceLocation: {
4483
4485
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4484
- "line": 551,
4486
+ "line": 593,
4485
4487
  "column": 9
4486
4488
  }
4487
4489
  },
@@ -4508,7 +4510,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4508
4510
  },
4509
4511
  sourceLocation: {
4510
4512
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4511
- "line": 670,
4513
+ "line": 712,
4512
4514
  "column": 3
4513
4515
  }
4514
4516
  },
@@ -4526,7 +4528,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4526
4528
  },
4527
4529
  sourceLocation: {
4528
4530
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4529
- "line": 831,
4531
+ "line": 873,
4530
4532
  "column": 9
4531
4533
  }
4532
4534
  },
@@ -4553,7 +4555,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4553
4555
  },
4554
4556
  sourceLocation: {
4555
4557
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4556
- "line": 442,
4558
+ "line": 460,
4557
4559
  "column": 9
4558
4560
  }
4559
4561
  },
@@ -4581,7 +4583,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4581
4583
  },
4582
4584
  sourceLocation: {
4583
4585
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4584
- "line": 773,
4586
+ "line": 815,
4585
4587
  "column": 9
4586
4588
  }
4587
4589
  },
@@ -4608,7 +4610,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4608
4610
  },
4609
4611
  sourceLocation: {
4610
4612
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4611
- "line": 891,
4613
+ "line": 933,
4612
4614
  "column": 9
4613
4615
  }
4614
4616
  },
@@ -4626,7 +4628,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4626
4628
  },
4627
4629
  sourceLocation: {
4628
4630
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4629
- "line": 875,
4631
+ "line": 917,
4630
4632
  "column": 9
4631
4633
  }
4632
4634
  }
@@ -4655,6 +4657,12 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4655
4657
  case "community": return "tierCommunity";
4656
4658
  }
4657
4659
  }
4660
+ /** The peers the Host said this installation does not provide, or none when it
4661
+ * said nothing — a plugin that runs here and one the Host could not judge are
4662
+ * both rendered as no warning at all. */
4663
+ function missingPeersOf(incompatible, name) {
4664
+ return incompatible[name] ?? [];
4665
+ }
4658
4666
  /** Spec §9.3 verbatim — the community-tier acknowledgement. The zh dictionary
4659
4667
  * states the same facts in its own register; never soften this wording. */
4660
4668
  const ACKNOWLEDGEMENT_EN = "Once installed, this plugin holds the same privileges as a built-in one: reading and writing your files, running shell commands, and reading and modifying the requests sent to the model. It has not been reviewed.";
@@ -4831,6 +4839,30 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4831
4839
  * A github entry answers null: it is installed from a repo and has no npm
4832
4840
  * package page.
4833
4841
  */
4842
+ /**
4843
+ * Who put this entry where it is — the fact a reader wants when two listings
4844
+ * carry the same text from different hands.
4845
+ *
4846
+ * The two sources answer it differently and neither is npm's `author` field:
4847
+ * that one is free text the publisher writes, and a clone inherits it
4848
+ * verbatim. An npm entry answers with the account npm recorded for the
4849
+ * version (`publisher`); a github entry answers with its repository owner,
4850
+ * which is already half of the `owner/slug` that IS its identity — so nothing
4851
+ * new has to be harvested or emitted for it.
4852
+ *
4853
+ * `repo` reaches the client as an unvalidated string, so the owner is taken
4854
+ * only from a value that really is `owner/slug` with an owner GitHub would
4855
+ * accept (1-39 chars of alphanumerics and hyphens). Anything else yields
4856
+ * null: a misleading fragment is worse than saying nothing, the same rule
4857
+ * npmPageUrl applies to a package name.
4858
+ */
4859
+ function authorOf(entry) {
4860
+ if (entry.source === "github") {
4861
+ if (entry.repo === void 0) return null;
4862
+ return /^([A-Za-z0-9](?:[A-Za-z0-9-]{0,38}))\/[^/]+$/.exec(entry.repo)?.[1] ?? null;
4863
+ }
4864
+ return entry.publisher ?? null;
4865
+ }
4834
4866
  function npmPageUrl(entry) {
4835
4867
  if (entry.source !== "npm") return null;
4836
4868
  const name = entry.name;
@@ -4906,6 +4938,10 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4906
4938
  acknowledgementBody: ACKNOWLEDGEMENT_ZH,
4907
4939
  installing: "正在安装…",
4908
4940
  installedRestartNotice: "已安装;重启 dsh 后生效",
4941
+ incompatibleBadge: "缺少依赖",
4942
+ incompatibleDetail: "此插件需要 {modules},你当前的 dsh 未提供",
4943
+ incompatibleInstallWarning: "此插件需要 {modules},你当前的 dsh 未提供;安装后它很可能无法加载。",
4944
+ incompatibleUpdateDetail: "更新此插件需要 {modules},你当前的 dsh 未提供",
4909
4945
  hotNoPatchNotice: "该插件没有可热挂载的补丁文件;重启 dsh 后生效",
4910
4946
  hotNotSimpleNotice: "该插件的补丁包含无法热挂载的配置;重启 dsh 后生效",
4911
4947
  hotHostUnsupportedNotice: "当前环境不支持热挂载;重启 dsh 后生效",
@@ -4919,7 +4955,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4919
4955
  showing: "显示前 {shown} 个(共 {total})· 继续滚动加载",
4920
4956
  repository: "仓库",
4921
4957
  npmPage: "npm 页面",
4922
- publishedBy: "维护者 {publisher}",
4958
+ authorLine: "作者:{author}",
4923
4959
  license: "许可",
4924
4960
  customLicense: "自定义许可",
4925
4961
  reviewedVersionLine: "已审核 v{reviewed} / 当前 v{current} 未审核",
@@ -4990,6 +5026,10 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4990
5026
  acknowledgementBody: ACKNOWLEDGEMENT_EN,
4991
5027
  installing: "Installing…",
4992
5028
  installedRestartNotice: "installed; restart dsh to activate",
5029
+ incompatibleBadge: "missing dependency",
5030
+ incompatibleDetail: "This plugin needs {modules}, which your dsh does not provide",
5031
+ incompatibleInstallWarning: "This plugin needs {modules}, which your dsh does not provide; it will most likely fail to load once installed.",
5032
+ incompatibleUpdateDetail: "Updating this plugin needs {modules}, which your dsh does not provide",
4993
5033
  hotNoPatchNotice: "no patch file to hot-mount; restart dsh to activate",
4994
5034
  hotNotSimpleNotice: "the patch has config rows that cannot be hot-mounted; restart dsh to activate",
4995
5035
  hotHostUnsupportedNotice: "this harness cannot hot-mount; restart dsh to activate",
@@ -5003,7 +5043,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5003
5043
  showing: "showing first {shown} of {total} — scroll to load more",
5004
5044
  repository: "Repository",
5005
5045
  npmPage: "npm page",
5006
- publishedBy: "maintained by {publisher}",
5046
+ authorLine: "Author: {author}",
5007
5047
  license: "License",
5008
5048
  customLicense: "Custom license",
5009
5049
  reviewedVersionLine: "reviewed v{reviewed} / current v{current} unreviewed",
@@ -5193,7 +5233,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5193
5233
  }
5194
5234
  //#endregion
5195
5235
  //#region \0dsh-plugin-shop-css:e3675a89
5196
- const css = "/* Shop tab styles — a market shelf inside the settings surface. The visual\n * language is the dsh theme's alias tokens ONLY (the four tokens the previous\n * styles referenced that do not exist — bg-layer-3, interactive-bg-hover,\n * state-error, state-success — are corrected to bg-layer-2 and the\n * state-*-primary set). The signature is the category spine: one HUE per\n * category (seven fixed hues, not the brand token — the brand resolves to\n * near-black in the light theme, so seven brand opacities read as seven\n * shades of gray), with the category name as the cover label in the same hue. Class names are mapped to content-derived names at bundle time\n * (tsdown.s8583e3_client.s942a79_config.s5f67aa_ts) and stubbed in tests; only the keys are relied\n * on.\n *\n * One token rule the theme forces: the background LAYERS do not carry\n * contrast. In the light theme bg-base, bg-layer-1, bg-layer-2 and\n * bg-layer-3 all resolve to the same white; only the dark theme spreads them\n * (950/875/850/800). So a layer-2 fill on a layer-1 ground is invisible for\n * half of all users, and anything whose fill is its ONLY affordance — the\n * loading skeleton, the borderless capability chips — derives its fill from\n * label-primary instead, which inverts with the theme by construction.\n * Elements that also carry a border or text may keep a layer fill.\n * `css-tokens.s8583e3_client.sfd9d8a_spec.s5f67aa_ts` pins this. */\n\n.s255e5a_panel {\n display: flex;\n flex-direction: column;\n gap: 14px;\n /* The shelf never scrolls sideways: a card too wide for its track wraps\n * its badges rather than widening the panel. */\n overflow-x: hidden;\n}\n\n.s44a3ec_stateLine {\n margin: 0;\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n}\n\n/* Visually hidden but screen-reader readable (the loading copy while the\n * skeleton stands in visually). */\n.s3a0ece_srOnly {\n position: absolute;\n width: 1px;\n height: 1px;\n margin: -1px;\n padding: 0;\n border: 0;\n clip: rect(0 0 0 0);\n clip-path: inset(50%);\n overflow: hidden;\n white-space: nowrap;\n}\n\n/* The loading shelf: ghost cards with the same geometry as the real grid, so\n * the swap to content is a same-shape handoff instead of a jump. */\n.se0c3b4_skeletonGrid {\n display: grid;\n grid-template-columns: 1fr;\n gap: 8px;\n}\n\n.s520416_skeletonCard {\n position: relative;\n display: flex;\n flex-direction: column;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 10px;\n background: var(--dsw-alias-bg-layer-1);\n overflow: hidden;\n}\n\n/* The shimmer sweep: one gradient band travels across each ghost card while\n * the bars breathe, so the loading state reads as \"the shelf is scanning\". */\n.s520416_skeletonCard::after {\n content: '';\n position: absolute;\n inset: 0;\n background: linear-gradient(\n 100deg,\n transparent 25%,\n color-mix(in srgb, var(--dsw-alias-label-primary) 14%, transparent) 50%,\n transparent 75%\n );\n transform: translateX(-100%);\n animation: dshShopSkeletonSweep 1.4s ease-in-out infinite;\n pointer-events: none;\n}\n\n@keyframes dshShopSkeletonSweep {\n to { transform: translateX(100%); }\n}\n\n.s8c7d6f_skeletonName {\n width: 55%;\n height: 14px;\n margin: 10px 12px 0;\n border-radius: 4px;\n background: color-mix(in srgb, var(--dsw-alias-label-primary) 8%, transparent);\n}\n\n.sf5179e_skeletonSummary {\n width: 92%;\n height: 12px;\n margin: 10px 12px 0;\n border-radius: 4px;\n background: color-mix(in srgb, var(--dsw-alias-label-primary) 8%, transparent);\n}\n\n.sbb0d25_skeletonSummaryShort {\n width: 68%;\n height: 12px;\n margin: 6px 12px 10px;\n border-radius: 4px;\n background: color-mix(in srgb, var(--dsw-alias-label-primary) 8%, transparent);\n}\n\n/* The ghost of the action line, right where the real buttons land. */\n.s96e910_skeletonActions {\n display: block;\n width: 84px;\n height: 24px;\n margin: 0 12px 10px;\n border-radius: 999px;\n background: color-mix(in srgb, var(--dsw-alias-label-primary) 8%, transparent);\n}\n\n.s520416_skeletonCard span {\n animation: dshShopSkeletonPulse 1.6s ease-in-out infinite;\n}\n\n@keyframes dshShopSkeletonPulse {\n 0%, 100% { opacity: 0.5; }\n 50% { opacity: 1; }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .s520416_skeletonCard span { animation: none; opacity: 0.75; }\n .s520416_skeletonCard::after { content: none; }\n}\n\n.s73242a_actionButton {\n align-self: flex-start;\n padding: 5px 14px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 6px;\n background: var(--dsw-alias-bg-layer-2);\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n cursor: pointer;\n}\n\n.s73242a_actionButton:hover {\n border-color: color-mix(in srgb, var(--dsw-alias-brand-primary) 45%, var(--dsw-alias-border-l2));\n color: var(--dsw-alias-brand-primary);\n}\n\n.s73242a_actionButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.s646509_toolbar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n flex-wrap: wrap;\n}\n\n.s745e3c_toolbarRight {\n display: flex;\n align-items: center;\n gap: 12px;\n flex-wrap: wrap;\n}\n\n/* The shop's own version, right of the search box: `v0.4.4`, plus the\n * update button when the version check found a newer release. */\n.s4fe61f_versionBlock {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.sb9b924_versionText {\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 12px;\n letter-spacing: 0.02em;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sa04455_updateSelfButton {\n padding: 2px 10px;\n border: 1px solid var(--dsw-alias-brand-primary);\n border-radius: 999px;\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent);\n color: var(--dsw-alias-brand-primary);\n font-size: 12px;\n font-weight: 600;\n cursor: pointer;\n}\n\n.sa04455_updateSelfButton:hover {\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 20%, transparent);\n}\n\n.sa04455_updateSelfButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* The on-demand re-check next to the version number: a quiet pill; the\n * border and text carry it in the light theme where bg layers are white. */\n.s614a38_checkUpdateButton {\n padding: 2px 10px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 999px;\n background: var(--dsw-alias-bg-layer-2);\n color: var(--dsw-alias-label-secondary);\n font-size: 12px;\n cursor: pointer;\n}\n\n.s614a38_checkUpdateButton:hover {\n border-color: var(--dsw-alias-label-secondary);\n color: var(--dsw-alias-label-primary);\n}\n\n.s614a38_checkUpdateButton:disabled {\n opacity: 0.6;\n cursor: default;\n}\n\n.s614a38_checkUpdateButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* The project's GitHub mark, right of the version row: a quiet icon link.\n * The octocat carries itself — color shift on hover, no text, no border. */\n.sa987f7_githubLink {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n padding: 3px;\n border-radius: 6px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sa987f7_githubLink:hover {\n color: var(--dsw-alias-label-primary);\n background: var(--dsw-alias-bg-layer-2);\n}\n\n.sa987f7_githubLink:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* The self-update's expanded states (running log / done + restart /\n * failure detail) sit under the toolbar, above the category bar. */\n.sb81ba5_selfUpdatePanel {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-top: 8px;\n}\n\n.s232ec4_searchInput {\n width: min(280px, 100%);\n padding: 6px 10px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 6px;\n background: var(--dsw-alias-bg-layer-1);\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n}\n\n.s232ec4_searchInput:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.s1555aa_staleBlock {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.s3946f1_staleBadge {\n padding: 2px 8px;\n border-radius: 999px;\n background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 14%, transparent);\n border: 1px solid color-mix(in srgb, var(--dsw-alias-state-warn-primary) 40%, transparent);\n color: var(--dsw-alias-state-warn-primary);\n font-size: 12px;\n}\n\n.sb2c859_categoryBar {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n margin-top: 6px;\n}\n\n.s26d58e_categoryButton {\n padding: 3px 10px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 999px;\n background: var(--dsw-alias-bg-layer-1);\n color: var(--dsw-alias-label-secondary);\n font-size: 12px;\n cursor: pointer;\n}\n\n.s26d58e_categoryButton:hover {\n border-color: color-mix(in srgb, var(--dsw-alias-brand-primary) 45%, var(--dsw-alias-border-l2));\n color: var(--dsw-alias-brand-primary);\n}\n\n.s5f68c4_categoryButtonOn {\n border-color: var(--dsw-alias-brand-primary);\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent);\n color: var(--dsw-alias-brand-primary);\n font-weight: 600;\n}\n\n.s26d58e_categoryButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.s668774_catalogHeading {\n margin: 2px 0 0;\n font-size: 16px;\n font-weight: 600;\n letter-spacing: 0.02em;\n color: var(--dsw-alias-label-primary);\n}\n\n.sccc4d1_catalogStats {\n margin: 6px 0 0;\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 12px;\n letter-spacing: 0.02em;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s6de1ab_emptyLine {\n margin: 0;\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n}\n\n/* The shelf: a responsive grid of cards. */\n/* Single-line rows: one full-width column, one plugin per line. */\n.sf657f5_cards {\n display: grid;\n grid-template-columns: 1fr;\n max-width: 100%;\n gap: 8px;\n list-style: none;\n margin: 0;\n padding: 0;\n animation: dshShopCardsIn 220ms ease;\n}\n\n@keyframes dshShopCardsIn {\n from { opacity: 0; transform: translateY(3px); }\n to { opacity: 1; transform: none; }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .sf657f5_cards { animation: none; }\n}\n\n.s65f383_card {\n position: relative;\n display: flex;\n flex-direction: column;\n /* A grid item's min-width is auto by default: its content would widen the\n * 1fr track past the panel and overflow horizontally. */\n min-width: 0;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 10px;\n background: var(--dsw-alias-bg-layer-1);\n overflow: hidden;\n transition: border-color 120ms ease, transform 120ms ease, box-shadow 120ms ease;\n /* Off-screen cards skip layout and paint; the placeholder keeps scrollbar\n * estimation sane before the browser has measured the real height. */\n content-visibility: auto;\n contain-intrinsic-size: auto 140px;\n}\n\n/* The sentinel that triggers the next shelf batch: zero visual footprint. */\n.sd7ee01_cardsSentry {\n height: 1px;\n margin: 0;\n padding: 0;\n}\n\n.s705cc6_showingLine {\n margin: 0;\n font-size: 12px;\n letter-spacing: 0.02em;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s65f383_card:hover {\n border-color: color-mix(in srgb, var(--dsw-alias-brand-primary) 45%, var(--dsw-alias-border-l1));\n transform: translateY(-1px);\n box-shadow: 0 2px 8px color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .s65f383_card { transition: none; }\n .s65f383_card:hover { transform: none; }\n}\n\n/* The signature: a spine and cover in the category's own hue. The hues are\n * fixed mid-bright colors that read on both themes; `other` deliberately\n * stays a neutral gray so the fallback category does not compete. */\n.s4ce25d_cardSpine {\n position: absolute;\n top: 0;\n bottom: 0;\n left: 0;\n width: 3px;\n background: var(--spine-hue, #8B8E96);\n}\n\n.s65f383_card[data-category='tool'] { --spine-hue: #4C8DFF; }\n.s65f383_card[data-category='provider'] { --spine-hue: #A78BFA; }\n.s65f383_card[data-category='ui'] { --spine-hue: #2DD4BF; }\n.s65f383_card[data-category='workflow'] { --spine-hue: #F59E0B; }\n.s65f383_card[data-category='integration'] { --spine-hue: #34D399; }\n.s65f383_card[data-category='theme'] { --spine-hue: #F472B6; }\n.s65f383_card[data-category='other'] { --spine-hue: #8B8E96; }\n\n/* The category sits in the badge row as a quiet chip in the category's own\n * hue — the border carries it where the light theme's white layers cannot\n * (the same lesson as the check button). */\n.s9c8eaa_categoryBadge {\n padding: 1px 7px;\n border: 1px solid color-mix(in srgb, var(--spine-hue, #8B8E96) 45%, transparent);\n border-radius: 999px;\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n color: var(--spine-hue, #8B8E96);\n white-space: nowrap;\n}\n\n.sa97234_entryHeader {\n display: flex;\n flex-direction: row;\n align-items: center;\n gap: 12px;\n min-width: 0;\n padding: 8px 12px 2px;\n border: none;\n background: transparent;\n color: inherit;\n font: inherit;\n text-align: left;\n cursor: pointer;\n}\n\n.sa97234_entryHeader:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: -2px;\n}\n\n/* The name fills the row and truncates rather than wraps; the badges stay\n * intact beside it. */\n.sc3f149_name {\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 13px;\n font-weight: 600;\n flex: 1 1 auto;\n min-width: 0;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n color: var(--dsw-alias-label-primary);\n}\n\n.s513026_badges {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 6px;\n}\n\n/* The action line: buttons side by side; an active install/uninstall flow\n * (gate, log, notices) takes the full width below them. */\n.s9b5048_cardActions {\n display: flex;\n flex-direction: row;\n flex-wrap: wrap;\n align-items: center;\n gap: 8px;\n padding: 8px 12px 10px;\n}\n\n.s9b5048_cardActions > .sc4f2ac_gate,\n.s9b5048_cardActions > .s4d374c_installPanel,\n.s9b5048_cardActions > .sf53ca0_installedActions {\n flex-basis: 100%;\n}\n\n/* The entry's version, always visible on the card (the expanded detail\n * repeats it in the version row) — the same quiet mono as the toolbar. */\n.sc6ec6e_cardVersion {\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 12px;\n letter-spacing: 0.02em;\n color: var(--dsw-alias-label-secondary);\n}\n\n/* The octocat marking a github-sourced entry, beside its short commit. */\n.s71f10f_sourceBadge {\n display: inline-flex;\n align-items: center;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s5e82cb_tierBadge {\n padding: 1px 7px;\n border-radius: 999px;\n font-size: 11px;\n line-height: 1.6;\n}\n\n.s5e82cb_tierBadge[data-tier='verified'] {\n color: var(--dsw-alias-state-success-primary);\n background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 14%, transparent);\n border: 1px solid color-mix(in srgb, var(--dsw-alias-state-success-primary) 40%, transparent);\n}\n\n.s5e82cb_tierBadge[data-tier='verified-stale'] {\n color: var(--dsw-alias-state-warn-primary);\n background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 14%, transparent);\n border: 1px solid color-mix(in srgb, var(--dsw-alias-state-warn-primary) 40%, transparent);\n}\n\n.s5e82cb_tierBadge[data-tier='community'] {\n color: var(--dsw-alias-label-secondary);\n background: var(--dsw-alias-bg-layer-2);\n border: 1px solid var(--dsw-alias-border-l2);\n}\n\n.sff43b4_starsBadge {\n font-size: 11px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s73f7fb_chevron, .s61393c_chevronOpen {\n color: var(--dsw-alias-label-secondary);\n transition: transform 120ms ease;\n}\n\n.s61393c_chevronOpen { transform: rotate(180deg); }\n\n@media (prefers-reduced-motion: reduce) {\n .s73f7fb_chevron, .s61393c_chevronOpen { transition: none; }\n}\n\n.s820ebf_body {\n display: flex;\n flex-direction: column;\n gap: 8px;\n padding: 0 12px;\n}\n\n.sb8fbc8_summary {\n margin: 0;\n font-size: 13px;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n overflow: hidden;\n}\n\n.se507db_summaryZh {\n margin: -4px 0 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-secondary);\n display: -webkit-box;\n -webkit-line-clamp: 1;\n -webkit-box-orient: vertical;\n overflow: hidden;\n}\n\n/* Expanded cards show the full summary: the clamp is lifted, never raised —\n * a higher number would still truncate the author's text, just later. */\n.s10baf0_summaryExpanded {\n -webkit-line-clamp: unset;\n display: block;\n}\n\n.sb9dba9_summaryZhExpanded {\n -webkit-line-clamp: unset;\n display: block;\n}\n\n.s9d2029_capabilitiesBlock {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n.s3bdbd5_capabilitiesNote {\n margin: 0;\n font-size: 11px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s441702_capabilities {\n display: flex;\n flex-wrap: wrap;\n gap: 4px;\n list-style: none;\n margin: 0;\n padding: 0;\n}\n\n.s441702_capabilities li {\n padding: 1px 6px;\n border-radius: 4px;\n background: color-mix(in srgb, var(--dsw-alias-label-primary) 6%, transparent);\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 11px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sde3f0c_detail {\n border-top: 1px solid var(--dsw-alias-border-l1);\n padding-top: 8px;\n}\n\n.se98179_detailRows {\n display: flex;\n flex-direction: column;\n gap: 4px;\n margin: 0;\n}\n\n.sfb4d2f_detailRow {\n display: flex;\n justify-content: space-between;\n gap: 12px;\n font-size: 12px;\n}\n\n.sfb4d2f_detailRow dt {\n color: var(--dsw-alias-label-secondary);\n}\n\n.sfb4d2f_detailRow dd {\n margin: 0;\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n overflow-wrap: anywhere;\n text-align: right;\n color: var(--dsw-alias-label-primary);\n}\n\n.sfb4d2f_detailRow a {\n color: var(--dsw-alias-brand-primary);\n text-decoration: none;\n}\n\n.sfb4d2f_detailRow a:hover {\n text-decoration: underline;\n}\n\n.sfb4d2f_detailRow a:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* The publishing account, beside the npm link. Secondary label colour and a\n * middot separator carry the distinction — never a background fill, which is\n * invisible in the light theme where every bg-layer token is the same white. */\n.s9e06b8_publisher {\n color: var(--dsw-alias-label-secondary);\n}\n\n.s9e06b8_publisher::before {\n content: ' · ';\n color: var(--dsw-alias-label-secondary);\n}\n\n.sd58c6d_reviewedLine {\n margin: 8px 0 0;\n padding: 6px 8px;\n border-radius: 6px;\n background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 12%, transparent);\n font-size: 12px;\n color: var(--dsw-alias-state-warn-primary);\n}\n\n/* Install flow. */\n.s4d374c_installPanel {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-top: 2px;\n}\n\n.se7aaed_installButton, .sb43a49_confirmButton, .s6e45d1_cancelButton {\n align-self: flex-start;\n padding: 5px 14px;\n border-radius: 6px;\n font-size: 13px;\n cursor: pointer;\n}\n\n.se7aaed_installButton {\n border: 1px solid var(--dsw-alias-brand-primary);\n background: var(--dsw-alias-brand-primary);\n color: var(--dsw-alias-bg-base);\n}\n\n.se7aaed_installButton:hover {\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 88%, var(--dsw-alias-label-primary));\n}\n\n.sb43a49_confirmButton {\n border: 1px solid var(--dsw-alias-brand-primary);\n background: var(--dsw-alias-brand-primary);\n color: var(--dsw-alias-bg-base);\n}\n\n.sb43a49_confirmButton:hover {\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 88%, var(--dsw-alias-label-primary));\n}\n\n.s6e45d1_cancelButton {\n border: 1px solid var(--dsw-alias-border-l2);\n background: var(--dsw-alias-bg-layer-2);\n color: var(--dsw-alias-label-primary);\n}\n\n.s6e45d1_cancelButton:hover {\n border-color: var(--dsw-alias-label-secondary);\n}\n\n.se7aaed_installButton:focus-visible, .sb43a49_confirmButton:focus-visible, .s6e45d1_cancelButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* The shelf card of a plugin that is installed and current carries this\n * non-interactive label instead of an install button. Border + text do the\n * work in the light theme, where every bg-layer token resolves to white. */\n.sb6fc74_installedLabel {\n align-self: flex-start;\n margin: 0;\n padding: 5px 14px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 6px;\n font-size: 13px;\n color: var(--dsw-alias-label-secondary);\n background: var(--dsw-alias-bg-layer-2);\n}\n\n/* An installed card's controls: the installed label or the update button,\n * plus the uninstall button. The install panel's running/failed states are\n * column layouts and sit as one item of this row. */\n.sf53ca0_installedActions {\n display: flex;\n align-items: flex-start;\n flex-wrap: wrap;\n gap: 8px;\n}\n\n/* Uninstall revokes privilege; the quiet outline keeps it from competing\n * with the primary action, and the error tint says \"removes\". */\n.s96967f_uninstallButton {\n align-self: flex-start;\n padding: 5px 14px;\n border: 1px solid color-mix(in srgb, var(--dsw-alias-state-error-primary) 55%, transparent);\n border-radius: 6px;\n background: transparent;\n color: var(--dsw-alias-state-error-primary);\n font-size: 13px;\n cursor: pointer;\n}\n\n.s96967f_uninstallButton:hover {\n background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent);\n}\n\n.s96967f_uninstallButton:focus-visible {\n outline: 2px solid var(--dsw-alias-state-error-primary);\n outline-offset: 1px;\n}\n\n/* The restart dsh button on a done install: quiet outline, same rank as\n * the other secondary actions. */\n.s6ac237_restartButton {\n align-self: flex-start;\n padding: 5px 14px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 6px;\n background: var(--dsw-alias-bg-layer-2);\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n cursor: pointer;\n}\n\n.s6ac237_restartButton:hover {\n border-color: var(--dsw-alias-label-secondary);\n}\n\n.s6ac237_restartButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.sc4f2ac_gate {\n display: flex;\n flex-direction: column;\n gap: 8px;\n padding: 10px;\n border: 1px solid var(--dsw-alias-state-warn-primary);\n border-radius: 8px;\n background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 8%, transparent);\n}\n\n.s559c38_gateTitle {\n margin: 0;\n font-size: 13px;\n font-weight: 600;\n color: var(--dsw-alias-state-warn-primary);\n}\n\n.s3c0ace_gateBody {\n margin: 0;\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-primary);\n}\n\n.s4b2373_gateActions {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n}\n\n.s52b5a5_installing {\n margin: 0;\n font-size: 12px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s64ab71_log {\n max-height: 160px;\n overflow: auto;\n margin: 0;\n padding: 8px;\n border-radius: 6px;\n background: var(--dsw-alias-bg-base);\n border: 1px solid var(--dsw-alias-border-l1);\n list-style: none;\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 11px;\n line-height: 1.5;\n}\n\n.s1531a8_logLine {\n white-space: pre-wrap;\n word-break: break-all;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sf20dda_notice {\n margin: 0;\n font-size: 12px;\n color: var(--dsw-alias-state-success-primary);\n}\n\n.seb9b08_failedHeading {\n margin: 0;\n font-size: 12px;\n font-weight: 600;\n color: var(--dsw-alias-state-error-primary);\n}\n\n.sfd3021_failedDetail {\n margin: 0;\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-primary);\n}\n\n.s50ba35_rejectedCode {\n margin: 0;\n font-size: 12px;\n font-weight: 600;\n color: var(--dsw-alias-state-error-primary);\n}\n\n.se75489_rejectedDetail {\n margin: 0;\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-primary);\n}\n\n/* Installed section. */\n.sa276cf_outdatedSection {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-top: 6px;\n}\n\n.sceae6e_outdatedList {\n display: flex;\n flex-direction: column;\n gap: 8px;\n list-style: none;\n margin: 0;\n padding: 0;\n}\n\n.s8d2072_outdatedRow {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n flex-wrap: wrap;\n padding: 10px 12px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 8px;\n background: var(--dsw-alias-bg-layer-1);\n}\n\n.s5c2545_outdatedInfo {\n display: flex;\n flex-direction: column;\n gap: 2px;\n min-width: 0;\n}\n\n.s5c2545_outdatedInfo strong {\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 13px;\n word-break: break-all;\n color: var(--dsw-alias-label-primary);\n}\n\n.s877c68_outdatedVersions {\n /* Two inline labels with nothing between them in the markup ran together as\n * `installed v1.0.0latest v2.0.0`: JSX drops the whitespace-only line, and\n * the per-label class the markup asked for was never defined (singular\n * against this rule's plural). The container owns the gap, the same idiom\n * as .s513026_badges — the labels need no class of their own. */\n display: inline-flex;\n align-items: center;\n gap: 8px;\n font-size: 12px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sa1f556_outdatedActions {\n display: flex;\n align-items: center;\n gap: 10px;\n}\n\n/* The enable/disable switch. */\n.s11c019_switch {\n position: relative;\n width: 32px;\n height: 18px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 999px;\n background: var(--dsw-alias-bg-layer-2);\n cursor: pointer;\n transition: background 120ms ease, border-color 120ms ease;\n}\n\n.s7580e8_switchOn {\n border-color: var(--dsw-alias-state-success-primary);\n background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 55%, transparent);\n}\n\n.saab2b3_switchKnob {\n position: absolute;\n top: 2px;\n left: 2px;\n width: 12px;\n height: 12px;\n border-radius: 999px;\n background: var(--dsw-alias-label-primary);\n transition: transform 120ms ease, background 120ms ease;\n}\n\n.s7580e8_switchOn .saab2b3_switchKnob {\n transform: translateX(14px);\n background: var(--dsw-alias-bg-base);\n}\n\n.s11c019_switch:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .s11c019_switch, .saab2b3_switchKnob { transition: none; }\n}\n\n/* The hot enable/disable switch on installed rows: the knob button plus the\n * §8 hot-apply / failure notices rendered beside it. */\n.sd9f9e4_switchWrap {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n}\n";
5236
+ const css = "/* Shop tab styles — a market shelf inside the settings surface. The visual\n * language is the dsh theme's alias tokens ONLY (the four tokens the previous\n * styles referenced that do not exist — bg-layer-3, interactive-bg-hover,\n * state-error, state-success — are corrected to bg-layer-2 and the\n * state-*-primary set). The signature is the category spine: one HUE per\n * category (seven fixed hues, not the brand token — the brand resolves to\n * near-black in the light theme, so seven brand opacities read as seven\n * shades of gray), with the category name as the cover label in the same hue. Class names are mapped to content-derived names at bundle time\n * (tsdown.s8583e3_client.s942a79_config.s5f67aa_ts) and stubbed in tests; only the keys are relied\n * on.\n *\n * One token rule the theme forces: the background LAYERS do not carry\n * contrast. In the light theme bg-base, bg-layer-1, bg-layer-2 and\n * bg-layer-3 all resolve to the same white; only the dark theme spreads them\n * (950/875/850/800). So a layer-2 fill on a layer-1 ground is invisible for\n * half of all users, and anything whose fill is its ONLY affordance — the\n * loading skeleton, the borderless capability chips — derives its fill from\n * label-primary instead, which inverts with the theme by construction.\n * Elements that also carry a border or text may keep a layer fill.\n * `css-tokens.s8583e3_client.sfd9d8a_spec.s5f67aa_ts` pins this. */\n\n.s255e5a_panel {\n display: flex;\n flex-direction: column;\n gap: 14px;\n /* The shelf never scrolls sideways: a card too wide for its track wraps\n * its badges rather than widening the panel. */\n overflow-x: hidden;\n}\n\n.s44a3ec_stateLine {\n margin: 0;\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n}\n\n/* Visually hidden but screen-reader readable (the loading copy while the\n * skeleton stands in visually). */\n.s3a0ece_srOnly {\n position: absolute;\n width: 1px;\n height: 1px;\n margin: -1px;\n padding: 0;\n border: 0;\n clip: rect(0 0 0 0);\n clip-path: inset(50%);\n overflow: hidden;\n white-space: nowrap;\n}\n\n/* The loading shelf: ghost cards with the same geometry as the real grid, so\n * the swap to content is a same-shape handoff instead of a jump. */\n.se0c3b4_skeletonGrid {\n display: grid;\n grid-template-columns: 1fr;\n gap: 8px;\n}\n\n.s520416_skeletonCard {\n position: relative;\n display: flex;\n flex-direction: column;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 10px;\n background: var(--dsw-alias-bg-layer-1);\n overflow: hidden;\n}\n\n/* The shimmer sweep: one gradient band travels across each ghost card while\n * the bars breathe, so the loading state reads as \"the shelf is scanning\". */\n.s520416_skeletonCard::after {\n content: '';\n position: absolute;\n inset: 0;\n background: linear-gradient(\n 100deg,\n transparent 25%,\n color-mix(in srgb, var(--dsw-alias-label-primary) 14%, transparent) 50%,\n transparent 75%\n );\n transform: translateX(-100%);\n animation: dshShopSkeletonSweep 1.4s ease-in-out infinite;\n pointer-events: none;\n}\n\n@keyframes dshShopSkeletonSweep {\n to { transform: translateX(100%); }\n}\n\n.s8c7d6f_skeletonName {\n width: 55%;\n height: 14px;\n margin: 10px 12px 0;\n border-radius: 4px;\n background: color-mix(in srgb, var(--dsw-alias-label-primary) 8%, transparent);\n}\n\n.sf5179e_skeletonSummary {\n width: 92%;\n height: 12px;\n margin: 10px 12px 0;\n border-radius: 4px;\n background: color-mix(in srgb, var(--dsw-alias-label-primary) 8%, transparent);\n}\n\n.sbb0d25_skeletonSummaryShort {\n width: 68%;\n height: 12px;\n margin: 6px 12px 10px;\n border-radius: 4px;\n background: color-mix(in srgb, var(--dsw-alias-label-primary) 8%, transparent);\n}\n\n/* The ghost of the action line, right where the real buttons land. */\n.s96e910_skeletonActions {\n display: block;\n width: 84px;\n height: 24px;\n margin: 0 12px 10px;\n border-radius: 999px;\n background: color-mix(in srgb, var(--dsw-alias-label-primary) 8%, transparent);\n}\n\n.s520416_skeletonCard span {\n animation: dshShopSkeletonPulse 1.6s ease-in-out infinite;\n}\n\n@keyframes dshShopSkeletonPulse {\n 0%, 100% { opacity: 0.5; }\n 50% { opacity: 1; }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .s520416_skeletonCard span { animation: none; opacity: 0.75; }\n .s520416_skeletonCard::after { content: none; }\n}\n\n.s73242a_actionButton {\n align-self: flex-start;\n padding: 5px 14px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 6px;\n background: var(--dsw-alias-bg-layer-2);\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n cursor: pointer;\n}\n\n.s73242a_actionButton:hover {\n border-color: color-mix(in srgb, var(--dsw-alias-brand-primary) 45%, var(--dsw-alias-border-l2));\n color: var(--dsw-alias-brand-primary);\n}\n\n.s73242a_actionButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.s646509_toolbar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n flex-wrap: wrap;\n}\n\n.s745e3c_toolbarRight {\n display: flex;\n align-items: center;\n gap: 12px;\n flex-wrap: wrap;\n}\n\n/* The shop's own version, right of the search box: `v0.4.4`, plus the\n * update button when the version check found a newer release. */\n.s4fe61f_versionBlock {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.sb9b924_versionText {\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 12px;\n letter-spacing: 0.02em;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sa04455_updateSelfButton {\n padding: 2px 10px;\n border: 1px solid var(--dsw-alias-brand-primary);\n border-radius: 999px;\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent);\n color: var(--dsw-alias-brand-primary);\n font-size: 12px;\n font-weight: 600;\n cursor: pointer;\n}\n\n.sa04455_updateSelfButton:hover {\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 20%, transparent);\n}\n\n.sa04455_updateSelfButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* The on-demand re-check next to the version number: a quiet pill; the\n * border and text carry it in the light theme where bg layers are white. */\n.s614a38_checkUpdateButton {\n padding: 2px 10px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 999px;\n background: var(--dsw-alias-bg-layer-2);\n color: var(--dsw-alias-label-secondary);\n font-size: 12px;\n cursor: pointer;\n}\n\n.s614a38_checkUpdateButton:hover {\n border-color: var(--dsw-alias-label-secondary);\n color: var(--dsw-alias-label-primary);\n}\n\n.s614a38_checkUpdateButton:disabled {\n opacity: 0.6;\n cursor: default;\n}\n\n.s614a38_checkUpdateButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* The project's GitHub mark, right of the version row: a quiet icon link.\n * The octocat carries itself — color shift on hover, no text, no border. */\n.sa987f7_githubLink {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n padding: 3px;\n border-radius: 6px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sa987f7_githubLink:hover {\n color: var(--dsw-alias-label-primary);\n background: var(--dsw-alias-bg-layer-2);\n}\n\n.sa987f7_githubLink:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* The self-update's expanded states (running log / done + restart /\n * failure detail) sit under the toolbar, above the category bar. */\n.sb81ba5_selfUpdatePanel {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-top: 8px;\n}\n\n.s232ec4_searchInput {\n width: min(280px, 100%);\n padding: 6px 10px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 6px;\n background: var(--dsw-alias-bg-layer-1);\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n}\n\n.s232ec4_searchInput:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.s1555aa_staleBlock {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.s3946f1_staleBadge {\n padding: 2px 8px;\n border-radius: 999px;\n background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 14%, transparent);\n border: 1px solid color-mix(in srgb, var(--dsw-alias-state-warn-primary) 40%, transparent);\n color: var(--dsw-alias-state-warn-primary);\n font-size: 12px;\n}\n\n.sb2c859_categoryBar {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n margin-top: 6px;\n}\n\n.s26d58e_categoryButton {\n padding: 3px 10px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 999px;\n background: var(--dsw-alias-bg-layer-1);\n color: var(--dsw-alias-label-secondary);\n font-size: 12px;\n cursor: pointer;\n}\n\n.s26d58e_categoryButton:hover {\n border-color: color-mix(in srgb, var(--dsw-alias-brand-primary) 45%, var(--dsw-alias-border-l2));\n color: var(--dsw-alias-brand-primary);\n}\n\n.s5f68c4_categoryButtonOn {\n border-color: var(--dsw-alias-brand-primary);\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent);\n color: var(--dsw-alias-brand-primary);\n font-weight: 600;\n}\n\n.s26d58e_categoryButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.s668774_catalogHeading {\n margin: 2px 0 0;\n font-size: 16px;\n font-weight: 600;\n letter-spacing: 0.02em;\n color: var(--dsw-alias-label-primary);\n}\n\n.sccc4d1_catalogStats {\n margin: 6px 0 0;\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 12px;\n letter-spacing: 0.02em;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s6de1ab_emptyLine {\n margin: 0;\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n}\n\n/* The shelf: a responsive grid of cards. */\n/* Single-line rows: one full-width column, one plugin per line. */\n.sf657f5_cards {\n display: grid;\n grid-template-columns: 1fr;\n max-width: 100%;\n gap: 8px;\n list-style: none;\n margin: 0;\n padding: 0;\n animation: dshShopCardsIn 220ms ease;\n}\n\n@keyframes dshShopCardsIn {\n from { opacity: 0; transform: translateY(3px); }\n to { opacity: 1; transform: none; }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .sf657f5_cards { animation: none; }\n}\n\n.s65f383_card {\n position: relative;\n display: flex;\n flex-direction: column;\n /* A grid item's min-width is auto by default: its content would widen the\n * 1fr track past the panel and overflow horizontally. */\n min-width: 0;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 10px;\n background: var(--dsw-alias-bg-layer-1);\n overflow: hidden;\n transition: border-color 120ms ease, transform 120ms ease, box-shadow 120ms ease;\n /* Off-screen cards skip layout and paint; the placeholder keeps scrollbar\n * estimation sane before the browser has measured the real height. */\n content-visibility: auto;\n contain-intrinsic-size: auto 140px;\n}\n\n/* The sentinel that triggers the next shelf batch: zero visual footprint. */\n.sd7ee01_cardsSentry {\n height: 1px;\n margin: 0;\n padding: 0;\n}\n\n.s705cc6_showingLine {\n margin: 0;\n font-size: 12px;\n letter-spacing: 0.02em;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s65f383_card:hover {\n border-color: color-mix(in srgb, var(--dsw-alias-brand-primary) 45%, var(--dsw-alias-border-l1));\n transform: translateY(-1px);\n box-shadow: 0 2px 8px color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .s65f383_card { transition: none; }\n .s65f383_card:hover { transform: none; }\n}\n\n/* The signature: a spine and cover in the category's own hue. The hues are\n * fixed mid-bright colors that read on both themes; `other` deliberately\n * stays a neutral gray so the fallback category does not compete. */\n.s4ce25d_cardSpine {\n position: absolute;\n top: 0;\n bottom: 0;\n left: 0;\n width: 3px;\n background: var(--spine-hue, #8B8E96);\n}\n\n.s65f383_card[data-category='tool'] { --spine-hue: #4C8DFF; }\n.s65f383_card[data-category='provider'] { --spine-hue: #A78BFA; }\n.s65f383_card[data-category='ui'] { --spine-hue: #2DD4BF; }\n.s65f383_card[data-category='workflow'] { --spine-hue: #F59E0B; }\n.s65f383_card[data-category='integration'] { --spine-hue: #34D399; }\n.s65f383_card[data-category='theme'] { --spine-hue: #F472B6; }\n.s65f383_card[data-category='other'] { --spine-hue: #8B8E96; }\n\n/* The category sits in the badge row as a quiet chip in the category's own\n * hue — the border carries it where the light theme's white layers cannot\n * (the same lesson as the check button). */\n.s9c8eaa_categoryBadge {\n padding: 1px 7px;\n border: 1px solid color-mix(in srgb, var(--spine-hue, #8B8E96) 45%, transparent);\n border-radius: 999px;\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n color: var(--spine-hue, #8B8E96);\n white-space: nowrap;\n}\n\n.sa97234_entryHeader {\n display: flex;\n flex-direction: row;\n align-items: center;\n gap: 12px;\n min-width: 0;\n padding: 8px 12px 2px;\n border: none;\n background: transparent;\n color: inherit;\n font: inherit;\n text-align: left;\n cursor: pointer;\n}\n\n.sa97234_entryHeader:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: -2px;\n}\n\n/* The name fills the row and truncates rather than wraps; the badges stay\n * intact beside it. */\n.sc3f149_name {\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 13px;\n font-weight: 600;\n flex: 1 1 auto;\n min-width: 0;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n color: var(--dsw-alias-label-primary);\n}\n\n.s513026_badges {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 6px;\n}\n\n/* The action line: buttons side by side; an active install/uninstall flow\n * (gate, log, notices) takes the full width below them. */\n.s9b5048_cardActions {\n display: flex;\n flex-direction: row;\n flex-wrap: wrap;\n align-items: center;\n gap: 8px;\n padding: 8px 12px 10px;\n}\n\n.s9b5048_cardActions > .sc4f2ac_gate,\n.s9b5048_cardActions > .s4d374c_installPanel,\n.s9b5048_cardActions > .sf53ca0_installedActions {\n flex-basis: 100%;\n}\n\n/* The entry's version, always visible on the card (the expanded detail\n * repeats it in the version row) — the same quiet mono as the toolbar. */\n.sc6ec6e_cardVersion {\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 12px;\n letter-spacing: 0.02em;\n color: var(--dsw-alias-label-secondary);\n}\n\n/* The octocat marking a github-sourced entry, beside its short commit. */\n.s71f10f_sourceBadge {\n display: inline-flex;\n align-items: center;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s5e82cb_tierBadge {\n padding: 1px 7px;\n border-radius: 999px;\n font-size: 11px;\n line-height: 1.6;\n}\n\n.s5e82cb_tierBadge[data-tier='verified'] {\n color: var(--dsw-alias-state-success-primary);\n background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 14%, transparent);\n border: 1px solid color-mix(in srgb, var(--dsw-alias-state-success-primary) 40%, transparent);\n}\n\n.s5e82cb_tierBadge[data-tier='verified-stale'] {\n color: var(--dsw-alias-state-warn-primary);\n background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 14%, transparent);\n border: 1px solid color-mix(in srgb, var(--dsw-alias-state-warn-primary) 40%, transparent);\n}\n\n.s5e82cb_tierBadge[data-tier='community'] {\n color: var(--dsw-alias-label-secondary);\n background: var(--dsw-alias-bg-layer-2);\n border: 1px solid var(--dsw-alias-border-l2);\n}\n\n.s4bbe02_incompatibleBadge {\n padding: 1px 7px;\n border-radius: 999px;\n font-size: 11px;\n color: var(--dsw-alias-state-warn-primary);\n background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 14%, transparent);\n border: 1px solid color-mix(in srgb, var(--dsw-alias-state-warn-primary) 40%, transparent);\n}\n\n.sff43b4_starsBadge {\n font-size: 11px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s73f7fb_chevron, .s61393c_chevronOpen {\n color: var(--dsw-alias-label-secondary);\n transition: transform 120ms ease;\n}\n\n.s61393c_chevronOpen { transform: rotate(180deg); }\n\n@media (prefers-reduced-motion: reduce) {\n .s73f7fb_chevron, .s61393c_chevronOpen { transition: none; }\n}\n\n.s820ebf_body {\n display: flex;\n flex-direction: column;\n gap: 8px;\n padding: 0 12px;\n}\n\n.sb8fbc8_summary {\n margin: 0;\n font-size: 13px;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n overflow: hidden;\n}\n\n.se507db_summaryZh {\n margin: -4px 0 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-secondary);\n display: -webkit-box;\n -webkit-line-clamp: 1;\n -webkit-box-orient: vertical;\n overflow: hidden;\n}\n\n/* Expanded cards show the full summary: the clamp is lifted, never raised —\n * a higher number would still truncate the author's text, just later. */\n.s10baf0_summaryExpanded {\n -webkit-line-clamp: unset;\n display: block;\n}\n\n.sb9dba9_summaryZhExpanded {\n -webkit-line-clamp: unset;\n display: block;\n}\n\n.s6eca19_incompatibleDetail {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-state-warn-primary);\n}\n\n.s9d2029_capabilitiesBlock {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n.s3bdbd5_capabilitiesNote {\n margin: 0;\n font-size: 11px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s441702_capabilities {\n display: flex;\n flex-wrap: wrap;\n gap: 4px;\n list-style: none;\n margin: 0;\n padding: 0;\n}\n\n.s441702_capabilities li {\n padding: 1px 6px;\n border-radius: 4px;\n background: color-mix(in srgb, var(--dsw-alias-label-primary) 6%, transparent);\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 11px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sde3f0c_detail {\n border-top: 1px solid var(--dsw-alias-border-l1);\n padding-top: 8px;\n}\n\n.se98179_detailRows {\n display: flex;\n flex-direction: column;\n gap: 4px;\n margin: 0;\n}\n\n.sfb4d2f_detailRow {\n display: flex;\n justify-content: space-between;\n gap: 12px;\n font-size: 12px;\n}\n\n.sfb4d2f_detailRow dt {\n color: var(--dsw-alias-label-secondary);\n}\n\n.sfb4d2f_detailRow dd {\n margin: 0;\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n overflow-wrap: anywhere;\n text-align: right;\n color: var(--dsw-alias-label-primary);\n}\n\n.sfb4d2f_detailRow a {\n color: var(--dsw-alias-brand-primary);\n text-decoration: none;\n}\n\n.sfb4d2f_detailRow a:hover {\n text-decoration: underline;\n}\n\n.sfb4d2f_detailRow a:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* The publishing account, beside the npm link. Secondary label colour and a\n * middot separator carry the distinction — never a background fill, which is\n * invisible in the light theme where every bg-layer token is the same white. */\n/* Who published the entry, at the right edge of the action row. `margin-left:\n * auto` is what pushes it there; the row is a wrapping flex, so an active\n * install flow (which claims the full width) drops this to the next line\n * rather than squeezing it. */\n.s1ff66f_author {\n margin-left: auto;\n color: var(--dsw-alias-label-secondary);\n font-size: 12px;\n}\n\n.sd58c6d_reviewedLine {\n margin: 8px 0 0;\n padding: 6px 8px;\n border-radius: 6px;\n background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 12%, transparent);\n font-size: 12px;\n color: var(--dsw-alias-state-warn-primary);\n}\n\n/* Install flow. */\n.s4d374c_installPanel {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-top: 2px;\n}\n\n.se7aaed_installButton, .sb43a49_confirmButton, .s6e45d1_cancelButton {\n align-self: flex-start;\n padding: 5px 14px;\n border-radius: 6px;\n font-size: 13px;\n cursor: pointer;\n}\n\n.se7aaed_installButton {\n border: 1px solid var(--dsw-alias-brand-primary);\n background: var(--dsw-alias-brand-primary);\n color: var(--dsw-alias-bg-base);\n}\n\n.se7aaed_installButton:hover {\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 88%, var(--dsw-alias-label-primary));\n}\n\n.sb43a49_confirmButton {\n border: 1px solid var(--dsw-alias-brand-primary);\n background: var(--dsw-alias-brand-primary);\n color: var(--dsw-alias-bg-base);\n}\n\n.sb43a49_confirmButton:hover {\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 88%, var(--dsw-alias-label-primary));\n}\n\n.s6e45d1_cancelButton {\n border: 1px solid var(--dsw-alias-border-l2);\n background: var(--dsw-alias-bg-layer-2);\n color: var(--dsw-alias-label-primary);\n}\n\n.s6e45d1_cancelButton:hover {\n border-color: var(--dsw-alias-label-secondary);\n}\n\n.se7aaed_installButton:focus-visible, .sb43a49_confirmButton:focus-visible, .s6e45d1_cancelButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* The shelf card of a plugin that is installed and current carries this\n * non-interactive label instead of an install button. Border + text do the\n * work in the light theme, where every bg-layer token resolves to white. */\n.sb6fc74_installedLabel {\n align-self: flex-start;\n margin: 0;\n padding: 5px 14px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 6px;\n font-size: 13px;\n color: var(--dsw-alias-label-secondary);\n background: var(--dsw-alias-bg-layer-2);\n}\n\n/* An installed card's controls: the installed label or the update button,\n * plus the uninstall button. The install panel's running/failed states are\n * column layouts and sit as one item of this row. */\n.sf53ca0_installedActions {\n display: flex;\n align-items: flex-start;\n flex-wrap: wrap;\n gap: 8px;\n}\n\n/* Uninstall revokes privilege; the quiet outline keeps it from competing\n * with the primary action, and the error tint says \"removes\". */\n.s96967f_uninstallButton {\n align-self: flex-start;\n padding: 5px 14px;\n border: 1px solid color-mix(in srgb, var(--dsw-alias-state-error-primary) 55%, transparent);\n border-radius: 6px;\n background: transparent;\n color: var(--dsw-alias-state-error-primary);\n font-size: 13px;\n cursor: pointer;\n}\n\n.s96967f_uninstallButton:hover {\n background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent);\n}\n\n.s96967f_uninstallButton:focus-visible {\n outline: 2px solid var(--dsw-alias-state-error-primary);\n outline-offset: 1px;\n}\n\n/* The restart dsh button on a done install: quiet outline, same rank as\n * the other secondary actions. */\n.s6ac237_restartButton {\n align-self: flex-start;\n padding: 5px 14px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 6px;\n background: var(--dsw-alias-bg-layer-2);\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n cursor: pointer;\n}\n\n.s6ac237_restartButton:hover {\n border-color: var(--dsw-alias-label-secondary);\n}\n\n.s6ac237_restartButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.sc4f2ac_gate {\n display: flex;\n flex-direction: column;\n gap: 8px;\n padding: 10px;\n border: 1px solid var(--dsw-alias-state-warn-primary);\n border-radius: 8px;\n background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 8%, transparent);\n}\n\n.s559c38_gateTitle {\n margin: 0;\n font-size: 13px;\n font-weight: 600;\n color: var(--dsw-alias-state-warn-primary);\n}\n\n.s3c0ace_gateBody {\n margin: 0;\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-primary);\n}\n\n.sc323e5_gateWarning {\n margin: 0;\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-state-warn-primary);\n}\n\n.s4b2373_gateActions {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n}\n\n.s52b5a5_installing {\n margin: 0;\n font-size: 12px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s64ab71_log {\n max-height: 160px;\n overflow: auto;\n margin: 0;\n padding: 8px;\n border-radius: 6px;\n background: var(--dsw-alias-bg-base);\n border: 1px solid var(--dsw-alias-border-l1);\n list-style: none;\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 11px;\n line-height: 1.5;\n}\n\n.s1531a8_logLine {\n white-space: pre-wrap;\n word-break: break-all;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sf20dda_notice {\n margin: 0;\n font-size: 12px;\n color: var(--dsw-alias-state-success-primary);\n}\n\n.seb9b08_failedHeading {\n margin: 0;\n font-size: 12px;\n font-weight: 600;\n color: var(--dsw-alias-state-error-primary);\n}\n\n.sfd3021_failedDetail {\n margin: 0;\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-primary);\n}\n\n.s50ba35_rejectedCode {\n margin: 0;\n font-size: 12px;\n font-weight: 600;\n color: var(--dsw-alias-state-error-primary);\n}\n\n.se75489_rejectedDetail {\n margin: 0;\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-primary);\n}\n\n/* Installed section. */\n.sa276cf_outdatedSection {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-top: 6px;\n}\n\n.sceae6e_outdatedList {\n display: flex;\n flex-direction: column;\n gap: 8px;\n list-style: none;\n margin: 0;\n padding: 0;\n}\n\n.s8d2072_outdatedRow {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n flex-wrap: wrap;\n padding: 10px 12px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 8px;\n background: var(--dsw-alias-bg-layer-1);\n}\n\n.s5c2545_outdatedInfo {\n display: flex;\n flex-direction: column;\n gap: 2px;\n min-width: 0;\n}\n\n.s5c2545_outdatedInfo strong {\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 13px;\n word-break: break-all;\n color: var(--dsw-alias-label-primary);\n}\n\n.s877c68_outdatedVersions {\n /* Two inline labels with nothing between them in the markup ran together as\n * `installed v1.0.0latest v2.0.0`: JSX drops the whitespace-only line, and\n * the per-label class the markup asked for was never defined (singular\n * against this rule's plural). The container owns the gap, the same idiom\n * as .s513026_badges — the labels need no class of their own. */\n display: inline-flex;\n align-items: center;\n gap: 8px;\n font-size: 12px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sa1f556_outdatedActions {\n display: flex;\n align-items: center;\n gap: 10px;\n}\n\n/* The enable/disable switch. */\n.s11c019_switch {\n position: relative;\n width: 32px;\n height: 18px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 999px;\n background: var(--dsw-alias-bg-layer-2);\n cursor: pointer;\n transition: background 120ms ease, border-color 120ms ease;\n}\n\n.s7580e8_switchOn {\n border-color: var(--dsw-alias-state-success-primary);\n background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 55%, transparent);\n}\n\n.saab2b3_switchKnob {\n position: absolute;\n top: 2px;\n left: 2px;\n width: 12px;\n height: 12px;\n border-radius: 999px;\n background: var(--dsw-alias-label-primary);\n transition: transform 120ms ease, background 120ms ease;\n}\n\n.s7580e8_switchOn .saab2b3_switchKnob {\n transform: translateX(14px);\n background: var(--dsw-alias-bg-base);\n}\n\n.s11c019_switch:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .s11c019_switch, .saab2b3_switchKnob { transition: none; }\n}\n\n/* The hot enable/disable switch on installed rows: the knob button plus the\n * §8 hot-apply / failure notices rendered beside it. */\n.sd9f9e4_switchWrap {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n}\n";
5197
5237
  if (typeof document !== "undefined" && !document.querySelector("style[data-plugin-css=\"e3675a89\"]")) {
5198
5238
  const tag = document.createElement("style");
5199
5239
  tag.dataset.pluginCss = "e3675a89";
@@ -5248,6 +5288,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5248
5288
  "cardVersion": "sc6ec6e_cardVersion",
5249
5289
  "sourceBadge": "s71f10f_sourceBadge",
5250
5290
  "tierBadge": "s5e82cb_tierBadge",
5291
+ "incompatibleBadge": "s4bbe02_incompatibleBadge",
5251
5292
  "starsBadge": "sff43b4_starsBadge",
5252
5293
  "chevron": "s73f7fb_chevron",
5253
5294
  "chevronOpen": "s61393c_chevronOpen",
@@ -5256,13 +5297,14 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5256
5297
  "summaryZh": "se507db_summaryZh",
5257
5298
  "summaryExpanded": "s10baf0_summaryExpanded",
5258
5299
  "summaryZhExpanded": "sb9dba9_summaryZhExpanded",
5300
+ "incompatibleDetail": "s6eca19_incompatibleDetail",
5259
5301
  "capabilitiesBlock": "s9d2029_capabilitiesBlock",
5260
5302
  "capabilitiesNote": "s3bdbd5_capabilitiesNote",
5261
5303
  "capabilities": "s441702_capabilities",
5262
5304
  "detail": "sde3f0c_detail",
5263
5305
  "detailRows": "se98179_detailRows",
5264
5306
  "detailRow": "sfb4d2f_detailRow",
5265
- "publisher": "s9e06b8_publisher",
5307
+ "author": "s1ff66f_author",
5266
5308
  "reviewedLine": "sd58c6d_reviewedLine",
5267
5309
  "installButton": "se7aaed_installButton",
5268
5310
  "confirmButton": "sb43a49_confirmButton",
@@ -5272,6 +5314,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5272
5314
  "restartButton": "s6ac237_restartButton",
5273
5315
  "gateTitle": "s559c38_gateTitle",
5274
5316
  "gateBody": "s3c0ace_gateBody",
5317
+ "gateWarning": "sc323e5_gateWarning",
5275
5318
  "gateActions": "s4b2373_gateActions",
5276
5319
  "installing": "s52b5a5_installing",
5277
5320
  "log": "s64ab71_log",
@@ -5320,11 +5363,12 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5320
5363
  * install controls. An installed plugin's card carries its installed row:
5321
5364
  * current → the non-interactive installed label, behind → the update button;
5322
5365
  * uninstalled → the install button. */
5323
- const EntryCard = (0, react.memo)(function EntryCard({ entry, stars, installed, t, install, installStatus, uninstall, restart, restartSupported, setEnabled }) {
5366
+ const EntryCard = (0, react.memo)(function EntryCard({ entry, stars, installed, missing, t, install, installStatus, uninstall, restart, restartSupported, setEnabled }) {
5324
5367
  const [open, setOpen] = (0, react.useState)(false);
5325
5368
  const detailId = (0, react.useId)();
5326
5369
  const summary = entry.catalog?.summary;
5327
5370
  const npmUrl = npmPageUrl(entry);
5371
+ const author = authorOf(entry);
5328
5372
  const category = entry.catalog?.category ?? "other";
5329
5373
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5330
5374
  className: _dsh_plugin_shop_css_e3675a89_default.card,
@@ -5374,6 +5418,12 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5374
5418
  "data-tier": entry.tier,
5375
5419
  children: t(tierKey(entry.tier))
5376
5420
  }),
5421
+ missing.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5422
+ className: _dsh_plugin_shop_css_e3675a89_default.incompatibleBadge,
5423
+ "data-shop-incompatible": true,
5424
+ title: t("incompatibleDetail", { modules: missing.join(", ") }),
5425
+ children: t("incompatibleBadge")
5426
+ }),
5377
5427
  stars !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
5378
5428
  className: _dsh_plugin_shop_css_e3675a89_default.starsBadge,
5379
5429
  role: "img",
@@ -5395,6 +5445,10 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5395
5445
  className: open ? `${_dsh_plugin_shop_css_e3675a89_default.summaryZh} ${_dsh_plugin_shop_css_e3675a89_default.summaryZhExpanded}` : _dsh_plugin_shop_css_e3675a89_default.summaryZh,
5396
5446
  children: summary.zh
5397
5447
  }),
5448
+ missing.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5449
+ className: _dsh_plugin_shop_css_e3675a89_default.incompatibleDetail,
5450
+ children: t("incompatibleDetail", { modules: missing.join(", ") })
5451
+ }),
5398
5452
  open && entry.catalog !== void 0 && entry.catalog.capabilities.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5399
5453
  className: _dsh_plugin_shop_css_e3675a89_default.capabilitiesBlock,
5400
5454
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
@@ -5414,16 +5468,12 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5414
5468
  npmUrl !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5415
5469
  className: _dsh_plugin_shop_css_e3675a89_default.detailRow,
5416
5470
  "data-shop-npm": true,
5417
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("npmPage") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dd", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
5471
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("npmPage") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
5418
5472
  href: npmUrl,
5419
5473
  target: "_blank",
5420
5474
  rel: "noopener noreferrer",
5421
5475
  children: npmUrl
5422
- }), entry.publisher !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5423
- className: _dsh_plugin_shop_css_e3675a89_default.publisher,
5424
- "data-shop-publisher": true,
5425
- children: t("publishedBy", { publisher: entry.publisher })
5426
- })] })]
5476
+ }) })]
5427
5477
  }),
5428
5478
  entry.repository !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5429
5479
  className: _dsh_plugin_shop_css_e3675a89_default.detailRow,
@@ -5452,12 +5502,14 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5452
5502
  })
5453
5503
  ]
5454
5504
  }),
5455
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5505
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5456
5506
  className: _dsh_plugin_shop_css_e3675a89_default.cardActions,
5457
- children: installed === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InstallPanel, {
5507
+ "data-shop-actions": true,
5508
+ children: [installed === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InstallPanel, {
5458
5509
  name: entry.name,
5459
5510
  version: entry.version,
5460
5511
  tier: entry.tier,
5512
+ missing,
5461
5513
  t,
5462
5514
  install,
5463
5515
  installStatus,
@@ -5469,6 +5521,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5469
5521
  version: entry.version,
5470
5522
  tier: entry.tier,
5471
5523
  variant: "update",
5524
+ missing,
5472
5525
  t,
5473
5526
  install,
5474
5527
  installStatus,
@@ -5492,7 +5545,11 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5492
5545
  restart,
5493
5546
  restartSupported
5494
5547
  })
5495
- ] })
5548
+ ] }), author !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5549
+ className: _dsh_plugin_shop_css_e3675a89_default.author,
5550
+ "data-shop-author": true,
5551
+ children: t("authorLine", { author })
5552
+ })]
5496
5553
  })
5497
5554
  ]
5498
5555
  });
@@ -5502,7 +5559,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5502
5559
  * failure detail, rejection detail — driven by `useInstall`. Shared by the
5503
5560
  * catalog cards (`variant: 'install'`) and the outdated rows' update button
5504
5561
  * (`variant: 'update'`, which drives the same install flow for `name@latest`). */
5505
- function InstallPanel({ name, version, tier, variant = "install", t, install, installStatus, restart, restartSupported }) {
5562
+ function InstallPanel({ name, version, tier, missing, variant = "install", t, install, installStatus, restart, restartSupported }) {
5506
5563
  const [gateOpen, setGateOpen] = (0, react.useState)(false);
5507
5564
  const { view, start } = useInstall(install, installStatus);
5508
5565
  if (view.kind === "running") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -5578,6 +5635,11 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5578
5635
  className: _dsh_plugin_shop_css_e3675a89_default.gateBody,
5579
5636
  children: t("acknowledgementBody")
5580
5637
  }),
5638
+ missing.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5639
+ className: _dsh_plugin_shop_css_e3675a89_default.gateWarning,
5640
+ "data-shop-incompatible-warning": true,
5641
+ children: t("incompatibleInstallWarning", { modules: missing.join(", ") })
5642
+ }),
5581
5643
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5582
5644
  className: _dsh_plugin_shop_css_e3675a89_default.gateActions,
5583
5645
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
@@ -5847,15 +5909,23 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5847
5909
  /** One outdated install row (§7.3): the name, the installed and latest
5848
5910
  * versions, the hot enable/disable switch, and the update button (the
5849
5911
  * install flow for `name@latest`, reusing `InstallPanel`). */
5850
- function OutdatedRow({ row, tier, source, t, setEnabled, install, installStatus, restart, restartSupported }) {
5912
+ function OutdatedRow({ row, tier, source, missing, t, setEnabled, install, installStatus, restart, restartSupported }) {
5851
5913
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5852
5914
  className: _dsh_plugin_shop_css_e3675a89_default.outdatedRow,
5853
5915
  "data-shop-outdated-entry": row.name,
5854
5916
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5855
5917
  className: _dsh_plugin_shop_css_e3675a89_default.outdatedInfo,
5856
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5857
- className: _dsh_plugin_shop_css_e3675a89_default.name,
5858
- children: row.name
5918
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
5919
+ className: _dsh_plugin_shop_css_e3675a89_default.badges,
5920
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5921
+ className: _dsh_plugin_shop_css_e3675a89_default.name,
5922
+ children: row.name
5923
+ }), missing.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5924
+ className: _dsh_plugin_shop_css_e3675a89_default.incompatibleBadge,
5925
+ "data-shop-incompatible": true,
5926
+ title: t("incompatibleUpdateDetail", { modules: missing.join(", ") }),
5927
+ children: t("incompatibleBadge")
5928
+ })]
5859
5929
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
5860
5930
  className: _dsh_plugin_shop_css_e3675a89_default.outdatedVersions,
5861
5931
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("installedVersion", { version: source === "github" ? row.installed.slice(0, 7) : row.installed }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("latestVersion", { version: row.latest }) })]
@@ -5871,6 +5941,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5871
5941
  version: row.latest,
5872
5942
  tier,
5873
5943
  variant: "update",
5944
+ missing,
5874
5945
  t,
5875
5946
  install,
5876
5947
  installStatus,
@@ -5887,7 +5958,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5887
5958
  * tier for the update gate is looked up from the catalog by name (community →
5888
5959
  * acknowledgement); an entry absent from the catalog defaults to the
5889
5960
  * community gate (the safer read). */
5890
- function OutdatedSection({ state, tiers, sources, t, setEnabled, install, installStatus, restart, restartSupported }) {
5961
+ function OutdatedSection({ state, tiers, sources, missingByName, t, setEnabled, install, installStatus, restart, restartSupported }) {
5891
5962
  if (state.kind === "loading") return null;
5892
5963
  if (state.kind === "error") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5893
5964
  className: _dsh_plugin_shop_css_e3675a89_default.stateLine,
@@ -5908,6 +5979,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5908
5979
  row,
5909
5980
  tier: tiers.get(row.name) ?? "community",
5910
5981
  source: sources.get(row.name) ?? "npm",
5982
+ missing: missingByName.get(row.name) ?? [],
5911
5983
  t,
5912
5984
  setEnabled,
5913
5985
  install,
@@ -6069,6 +6141,11 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6069
6141
  if (catalogState.kind === "ready") for (const entry of catalogState.result.plugins) map.set(entry.name, entry.source);
6070
6142
  return map;
6071
6143
  }, [catalogState]);
6144
+ const missingByName = (0, react.useMemo)(() => {
6145
+ const map = /* @__PURE__ */ new Map();
6146
+ if (catalogState.kind === "ready") for (const entry of catalogState.result.plugins) map.set(entry.name, missingPeersOf(catalogState.result.incompatible, entry.name));
6147
+ return map;
6148
+ }, [catalogState]);
6072
6149
  if (catalogState.kind === "loading") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6073
6150
  className: _dsh_plugin_shop_css_e3675a89_default.panel,
6074
6151
  "data-shop-tab": true,
@@ -6307,6 +6384,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6307
6384
  entry,
6308
6385
  stars: starsOf(entry, stars),
6309
6386
  installed: installedByName.get(entry.name),
6387
+ missing: missingByName.get(entry.name) ?? [],
6310
6388
  t,
6311
6389
  install,
6312
6390
  installStatus,
@@ -6332,6 +6410,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6332
6410
  state: installedState,
6333
6411
  tiers,
6334
6412
  sources,
6413
+ missingByName,
6335
6414
  t,
6336
6415
  setEnabled,
6337
6416
  install,
package/lib/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { createRequire } from "node:module";
1
2
  import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
2
3
  import { loadOptionalPatches, readProfileManifest, resolveProfileDir } from "@deepseek-ai/dsh-app-boot";
3
4
  import { lt, minVersion, valid } from "semver";
@@ -68,7 +69,8 @@ const entrySchema = z.object({
68
69
  url: z.string(),
69
70
  sha256: z.string()
70
71
  }).optional(),
71
- publisher: z.string().optional()
72
+ publisher: z.string().optional(),
73
+ peers: z.array(z.string()).optional()
72
74
  });
73
75
  const dataSchema = z.object({
74
76
  schemaVersion: z.number(),
@@ -175,13 +177,13 @@ async function loadCatalog(options) {
175
177
  let data;
176
178
  try {
177
179
  pointer = pointerSchema.parse(JSON.parse(fsImpl.read(indexPath)));
178
- if (pointer.schemaVersion > 5) throw new Error(`catalog schemaVersion ${pointer.schemaVersion} is newer than this build supports (5)`);
180
+ if (pointer.schemaVersion > 6) throw new Error(`catalog schemaVersion ${pointer.schemaVersion} is newer than this build supports (6)`);
179
181
  const dataPath = join(cacheDir, basename(pointer.plugins.url));
180
182
  const dataText = fsImpl.read(dataPath);
181
183
  const actual = createHash("sha256").update(dataText).digest("hex");
182
184
  if (actual !== pointer.plugins.sha256) throw new Error(`cached catalog data failed integrity check: expected ${pointer.plugins.sha256}, got ${actual}`);
183
185
  data = dataSchema.parse(JSON.parse(dataText));
184
- if (data.schemaVersion > 5) throw new Error(`catalog schemaVersion ${data.schemaVersion} is newer than this build supports (5)`);
186
+ if (data.schemaVersion > 6) throw new Error(`catalog schemaVersion ${data.schemaVersion} is newer than this build supports (6)`);
185
187
  } catch {
186
188
  return null;
187
189
  }
@@ -223,7 +225,7 @@ async function loadCatalog(options) {
223
225
  throw error;
224
226
  }
225
227
  const pointer = pointerSchema.parse(JSON.parse(pointerText));
226
- if (pointer.schemaVersion > 5) throw new Error(`catalog schemaVersion ${pointer.schemaVersion} is newer than this build supports (5)`);
228
+ if (pointer.schemaVersion > 6) throw new Error(`catalog schemaVersion ${pointer.schemaVersion} is newer than this build supports (6)`);
227
229
  const dataUrl = resolveDataUrl(baseUrl, pointer.plugins.url);
228
230
  let dataText;
229
231
  try {
@@ -241,7 +243,7 @@ async function loadCatalog(options) {
241
243
  const actual = createHash("sha256").update(dataText).digest("hex");
242
244
  if (actual !== pointer.plugins.sha256) throw new Error(`catalog data failed integrity check: expected ${pointer.plugins.sha256}, got ${actual}`);
243
245
  const data = dataSchema.parse(JSON.parse(dataText));
244
- if (data.schemaVersion > 5) throw new Error(`catalog schemaVersion ${data.schemaVersion} is newer than this build supports (5)`);
246
+ if (data.schemaVersion > 6) throw new Error(`catalog schemaVersion ${data.schemaVersion} is newer than this build supports (6)`);
245
247
  validateEntryCoherence(data.plugins);
246
248
  let stars = {};
247
249
  if (pointer.stars !== void 0) try {
@@ -1016,6 +1018,64 @@ function ownsEntryId(owned, entryId) {
1016
1018
  return tail.startsWith("mkt-") && owned.has(tail.slice(4));
1017
1019
  }
1018
1020
  //#endregion
1021
+ //#region src/host/peers.ts
1022
+ /** Harness compatibility: which declared peers the running installation does
1023
+ * not provide (design 2026-09-01-harness-compatibility). */
1024
+ /**
1025
+ * The production resolver: the same question the harness's own
1026
+ * ClientModuleRegistry asks, through a require anchored at the profile. Asking
1027
+ * what the loader asks is what keeps this verdict and the runtime's behaviour
1028
+ * from drifting apart.
1029
+ */
1030
+ function nodeResolver(baseUrl) {
1031
+ const require = createRequire(baseUrl);
1032
+ return (spec) => {
1033
+ try {
1034
+ require.resolve(`${spec}/package.json`);
1035
+ return true;
1036
+ } catch (error) {
1037
+ if (error.code === "MODULE_NOT_FOUND") return false;
1038
+ throw error;
1039
+ }
1040
+ };
1041
+ }
1042
+ /**
1043
+ * Package name → the peer names that did not resolve. A key is present only
1044
+ * when at least one peer is missing, so an absent key means "runs here, or we
1045
+ * could not tell" — the client renders nothing for either.
1046
+ *
1047
+ * A resolver that throws yields NO verdict at all: an unavailable fact must
1048
+ * never read as an accusation, because one false warning teaches a reader to
1049
+ * ignore every warning.
1050
+ */
1051
+ function incompatibilityMap(entries, resolve) {
1052
+ const known = /* @__PURE__ */ new Map();
1053
+ const out = {};
1054
+ for (const entry of entries) {
1055
+ if (entry.peers === void 0 || entry.peers.length === 0) continue;
1056
+ const missing = [];
1057
+ let usable = true;
1058
+ for (const spec of entry.peers) {
1059
+ let present = known.get(spec);
1060
+ if (present === void 0) try {
1061
+ present = resolve(spec);
1062
+ known.set(spec, present);
1063
+ } catch {
1064
+ known.set(spec, null);
1065
+ usable = false;
1066
+ break;
1067
+ }
1068
+ else if (present === null) {
1069
+ usable = false;
1070
+ break;
1071
+ }
1072
+ if (!present) missing.push(spec);
1073
+ }
1074
+ if (usable && missing.length > 0) out[entry.name] = missing;
1075
+ }
1076
+ return out;
1077
+ }
1078
+ //#endregion
1019
1079
  //#region src/host/index.ts
1020
1080
  /** ShopGateway: the Host half of dsh-plugin-shop (§5.1). */
1021
1081
  var __runInitializers = function(thisArg, initializers, value) {
@@ -1274,6 +1334,16 @@ let ShopGateway = (() => {
1274
1334
  /** The install gate runs against the last loaded snapshot, never a fresh
1275
1335
  * fetch per request (§7.2: the Host's cached snapshot is the truth). */
1276
1336
  lastSnapshot = null;
1337
+ /** The incompatibility map already computed for `lastSnapshot`, keyed by
1338
+ * that snapshot's own object identity. Design §3 asks for the verdict
1339
+ * once per loaded snapshot, not once per RPC call: `loadCatalog` serves
1340
+ * the same snapshot from its on-disk cache for minutes at a time, so
1341
+ * without this, reopening the tab within that window would re-walk
1342
+ * `node_modules` for every distinct peer name again — and Node only
1343
+ * caches a SUCCESSFUL resolution, so a genuinely missing peer pays a full
1344
+ * failed walk on every single call. Recomputed only when `catalog()`
1345
+ * loads a snapshot that is not this exact object. */
1346
+ incompatibleCache = null;
1277
1347
  /** Install records, running and finished; a poll finds one here or reports not found. */
1278
1348
  installs = /* @__PURE__ */ new Map();
1279
1349
  /** Every install id in insertion order, oldest first; finished-record eviction walks this from the front. */
@@ -1466,13 +1536,28 @@ let ShopGateway = (() => {
1466
1536
  refresh: args?.refresh ?? false
1467
1537
  });
1468
1538
  this.lastSnapshot = snapshot;
1539
+ let incompatible;
1540
+ if (this.incompatibleCache !== null && this.incompatibleCache.snapshot === snapshot) incompatible = this.incompatibleCache.map;
1541
+ else {
1542
+ try {
1543
+ const resolve = this.options.resolvePeer ?? nodeResolver(pathToFileURL(join(this.profileDirResolved(), "cordis.yml")).href);
1544
+ incompatible = incompatibilityMap(snapshot.entries, resolve);
1545
+ } catch {
1546
+ incompatible = {};
1547
+ }
1548
+ this.incompatibleCache = {
1549
+ snapshot,
1550
+ map: incompatible
1551
+ };
1552
+ }
1469
1553
  return {
1470
1554
  schemaVersion: snapshot.schemaVersion,
1471
1555
  builtAt: snapshot.builtAt,
1472
1556
  stale,
1473
1557
  plugins: snapshot.entries,
1474
1558
  denied: snapshot.denied,
1475
- stars: snapshot.stars
1559
+ stars: snapshot.stars,
1560
+ incompatible
1476
1561
  };
1477
1562
  }
1478
1563
  /**
@@ -42,6 +42,7 @@ const dsh_plugin_shop_shop_catalog_result$schema = z.object({
42
42
  'sha256': z.string(),
43
43
  })]).optional(),
44
44
  'added': z.union([z.undefined(), z.string()]).optional(),
45
+ 'peers': z.union([z.undefined(), z.array(z.string())]).optional(),
45
46
  })),
46
47
  'denied': z.array(z.object({
47
48
  'name': z.string(),
@@ -49,6 +50,7 @@ const dsh_plugin_shop_shop_catalog_result$schema = z.object({
49
50
  'replacement': z.union([z.undefined(), z.string()]).optional(),
50
51
  })),
51
52
  'stars': z.record(z.string(), z.number()),
53
+ 'incompatible': z.record(z.string(), z.array(z.string())),
52
54
  })
53
55
  const dsh_plugin_shop_shop_installed_result$schema = z.array(z.object({
54
56
  'name': z.string(),
@@ -152,7 +154,7 @@ export const TYPERT = {
152
154
  typeSymbol: 'dsh-plugin-shop/types#ShopCatalogResult',
153
155
  schema: dsh_plugin_shop_shop_catalog_result$schema,
154
156
  },
155
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":525,"column":9},
157
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":543,"column":9},
156
158
  },
157
159
  {
158
160
  id: 'dsh-plugin-shop#shop/installed',
@@ -167,7 +169,7 @@ export const TYPERT = {
167
169
  typeSymbol: 'dsh-plugin-shop#shop/installed:result',
168
170
  schema: dsh_plugin_shop_shop_installed_result$schema,
169
171
  },
170
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":681,"column":9},
172
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":723,"column":9},
171
173
  },
172
174
  {
173
175
  id: 'dsh-plugin-shop#shop/installStart',
@@ -193,7 +195,7 @@ export const TYPERT = {
193
195
  typeSymbol: 'dsh-plugin-shop/types#ShopInstallResult',
194
196
  schema: dsh_plugin_shop_shop_installStart_result$schema,
195
197
  },
196
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":551,"column":9},
198
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":593,"column":9},
197
199
  },
198
200
  {
199
201
  id: 'dsh-plugin-shop#shop/installStatus',
@@ -218,7 +220,7 @@ export const TYPERT = {
218
220
  typeSymbol: 'dsh-plugin-shop/types#ShopInstallStatusResult',
219
221
  schema: dsh_plugin_shop_shop_installStatus_result$schema,
220
222
  },
221
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":670,"column":3},
223
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":712,"column":3},
222
224
  },
223
225
  {
224
226
  id: 'dsh-plugin-shop#shop/restart',
@@ -233,7 +235,7 @@ export const TYPERT = {
233
235
  typeSymbol: 'dsh-plugin-shop/types#ShopRestartResult',
234
236
  schema: dsh_plugin_shop_shop_restart_result$schema,
235
237
  },
236
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":831,"column":9},
238
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":873,"column":9},
237
239
  },
238
240
  {
239
241
  id: 'dsh-plugin-shop#shop/setEnabled',
@@ -258,7 +260,7 @@ export const TYPERT = {
258
260
  typeSymbol: 'dsh-plugin-shop/types#ShopSetEnabledResult',
259
261
  schema: dsh_plugin_shop_shop_setEnabled_result$schema,
260
262
  },
261
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":442,"column":9},
263
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":460,"column":9},
262
264
  },
263
265
  {
264
266
  id: 'dsh-plugin-shop#shop/uninstallStart',
@@ -284,7 +286,7 @@ export const TYPERT = {
284
286
  typeSymbol: 'dsh-plugin-shop/types#ShopUninstallResult',
285
287
  schema: dsh_plugin_shop_shop_uninstallStart_result$schema,
286
288
  },
287
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":773,"column":9},
289
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":815,"column":9},
288
290
  },
289
291
  {
290
292
  id: 'dsh-plugin-shop#shop/updateStart',
@@ -309,7 +311,7 @@ export const TYPERT = {
309
311
  typeSymbol: 'dsh-plugin-shop/types#ShopUpdateResult',
310
312
  schema: dsh_plugin_shop_shop_updateStart_result$schema,
311
313
  },
312
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":891,"column":9},
314
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":933,"column":9},
313
315
  },
314
316
  {
315
317
  id: 'dsh-plugin-shop#shop/version',
@@ -324,7 +326,7 @@ export const TYPERT = {
324
326
  typeSymbol: 'dsh-plugin-shop/types#ShopVersionResult',
325
327
  schema: dsh_plugin_shop_shop_version_result$schema,
326
328
  },
327
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":875,"column":9},
329
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":917,"column":9},
328
330
  },
329
331
  ],
330
332
  model: {
@@ -410,7 +412,7 @@ export const TYPERT = {
410
412
  "types": [
411
413
  {
412
414
  "name": "CatalogEntry",
413
- "declaration": "export interface CatalogEntry {\n name: string;\n version: string;\n integrity: string | null;\n publishedAt: string | null;\n repository: string | null;\n license: string | null;\n tier: 'verified' | 'verified-stale' | 'community';\n metadata: 'declared' | 'derived';\n review?: { reviewedVersion?: string; reviewedCommit?: string; reviewedSha256?: string; reviewer: string; reviewCommit: string; notes: string; };\n catalog?: CatalogSection;\n source: 'npm' | 'github';\n publisher?: string;\n repo?: string;\n subdir?: string;\n tarball?: { url: string; sha256: string; };\n added?: string;\n}"
415
+ "declaration": "export interface CatalogEntry {\n name: string;\n version: string;\n integrity: string | null;\n publishedAt: string | null;\n repository: string | null;\n license: string | null;\n tier: 'verified' | 'verified-stale' | 'community';\n metadata: 'declared' | 'derived';\n review?: { reviewedVersion?: string; reviewedCommit?: string; reviewedSha256?: string; reviewer: string; reviewCommit: string; notes: string; };\n catalog?: CatalogSection;\n source: 'npm' | 'github';\n publisher?: string;\n repo?: string;\n subdir?: string;\n tarball?: { url: string; sha256: string; };\n added?: string;\n peers?: string[];\n}"
414
416
  },
415
417
  {
416
418
  "name": "CatalogSection",
@@ -450,7 +452,7 @@ export const TYPERT = {
450
452
  },
451
453
  {
452
454
  "name": "ShopCatalogResult",
453
- "declaration": "export interface ShopCatalogResult {\n schemaVersion: number;\n builtAt: string;\n stale: boolean;\n plugins: CatalogEntry[];\n denied: DeniedEntry[];\n stars: Record<string, number>;\n}"
455
+ "declaration": "export interface ShopCatalogResult {\n schemaVersion: number;\n builtAt: string;\n stale: boolean;\n plugins: CatalogEntry[];\n denied: DeniedEntry[];\n stars: Record<string, number>;\n incompatible: Record<string, string[]>;\n}"
454
456
  },
455
457
  {
456
458
  "name": "ShopInstalledEntry",
@@ -42,6 +42,7 @@ const dsh_plugin_shop_shop_catalog_result$schema = z.object({
42
42
  'sha256': z.string(),
43
43
  })]).optional(),
44
44
  'added': z.union([z.undefined(), z.string()]).optional(),
45
+ 'peers': z.union([z.undefined(), z.array(z.string())]).optional(),
45
46
  })),
46
47
  'denied': z.array(z.object({
47
48
  'name': z.string(),
@@ -49,6 +50,7 @@ const dsh_plugin_shop_shop_catalog_result$schema = z.object({
49
50
  'replacement': z.union([z.undefined(), z.string()]).optional(),
50
51
  })),
51
52
  'stars': z.record(z.string(), z.number()),
53
+ 'incompatible': z.record(z.string(), z.array(z.string())),
52
54
  })
53
55
  const dsh_plugin_shop_shop_installed_result$schema = z.array(z.object({
54
56
  'name': z.string(),
@@ -149,7 +151,7 @@ export const TYPERT_REMOTE = {
149
151
  typeSymbol: 'dsh-plugin-shop/types#ShopCatalogResult',
150
152
  schema: dsh_plugin_shop_shop_catalog_result$schema,
151
153
  },
152
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":525,"column":9},
154
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":543,"column":9},
153
155
  },
154
156
  {
155
157
  id: 'dsh-plugin-shop#shop/installed',
@@ -164,7 +166,7 @@ export const TYPERT_REMOTE = {
164
166
  typeSymbol: 'dsh-plugin-shop#shop/installed:result',
165
167
  schema: dsh_plugin_shop_shop_installed_result$schema,
166
168
  },
167
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":681,"column":9},
169
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":723,"column":9},
168
170
  },
169
171
  {
170
172
  id: 'dsh-plugin-shop#shop/installStart',
@@ -190,7 +192,7 @@ export const TYPERT_REMOTE = {
190
192
  typeSymbol: 'dsh-plugin-shop/types#ShopInstallResult',
191
193
  schema: dsh_plugin_shop_shop_installStart_result$schema,
192
194
  },
193
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":551,"column":9},
195
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":593,"column":9},
194
196
  },
195
197
  {
196
198
  id: 'dsh-plugin-shop#shop/installStatus',
@@ -215,7 +217,7 @@ export const TYPERT_REMOTE = {
215
217
  typeSymbol: 'dsh-plugin-shop/types#ShopInstallStatusResult',
216
218
  schema: dsh_plugin_shop_shop_installStatus_result$schema,
217
219
  },
218
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":670,"column":3},
220
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":712,"column":3},
219
221
  },
220
222
  {
221
223
  id: 'dsh-plugin-shop#shop/restart',
@@ -230,7 +232,7 @@ export const TYPERT_REMOTE = {
230
232
  typeSymbol: 'dsh-plugin-shop/types#ShopRestartResult',
231
233
  schema: dsh_plugin_shop_shop_restart_result$schema,
232
234
  },
233
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":831,"column":9},
235
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":873,"column":9},
234
236
  },
235
237
  {
236
238
  id: 'dsh-plugin-shop#shop/setEnabled',
@@ -255,7 +257,7 @@ export const TYPERT_REMOTE = {
255
257
  typeSymbol: 'dsh-plugin-shop/types#ShopSetEnabledResult',
256
258
  schema: dsh_plugin_shop_shop_setEnabled_result$schema,
257
259
  },
258
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":442,"column":9},
260
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":460,"column":9},
259
261
  },
260
262
  {
261
263
  id: 'dsh-plugin-shop#shop/uninstallStart',
@@ -281,7 +283,7 @@ export const TYPERT_REMOTE = {
281
283
  typeSymbol: 'dsh-plugin-shop/types#ShopUninstallResult',
282
284
  schema: dsh_plugin_shop_shop_uninstallStart_result$schema,
283
285
  },
284
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":773,"column":9},
286
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":815,"column":9},
285
287
  },
286
288
  {
287
289
  id: 'dsh-plugin-shop#shop/updateStart',
@@ -306,7 +308,7 @@ export const TYPERT_REMOTE = {
306
308
  typeSymbol: 'dsh-plugin-shop/types#ShopUpdateResult',
307
309
  schema: dsh_plugin_shop_shop_updateStart_result$schema,
308
310
  },
309
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":891,"column":9},
311
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":933,"column":9},
310
312
  },
311
313
  {
312
314
  id: 'dsh-plugin-shop#shop/version',
@@ -321,7 +323,7 @@ export const TYPERT_REMOTE = {
321
323
  typeSymbol: 'dsh-plugin-shop/types#ShopVersionResult',
322
324
  schema: dsh_plugin_shop_shop_version_result$schema,
323
325
  },
324
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":875,"column":9},
326
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":917,"column":9},
325
327
  },
326
328
  ],
327
329
  }
@@ -30,6 +30,10 @@ export declare const zh: {
30
30
  acknowledgementBody: string;
31
31
  installing: string;
32
32
  installedRestartNotice: string;
33
+ incompatibleBadge: string;
34
+ incompatibleDetail: string;
35
+ incompatibleInstallWarning: string;
36
+ incompatibleUpdateDetail: string;
33
37
  hotNoPatchNotice: string;
34
38
  hotNotSimpleNotice: string;
35
39
  hotHostUnsupportedNotice: string;
@@ -43,7 +47,7 @@ export declare const zh: {
43
47
  showing: string;
44
48
  repository: string;
45
49
  npmPage: string;
46
- publishedBy: string;
50
+ authorLine: string;
47
51
  license: string;
48
52
  customLicense: string;
49
53
  reviewedVersionLine: string;
@@ -116,6 +120,10 @@ export declare const en: {
116
120
  acknowledgementBody: string;
117
121
  installing: string;
118
122
  installedRestartNotice: string;
123
+ incompatibleBadge: string;
124
+ incompatibleDetail: string;
125
+ incompatibleInstallWarning: string;
126
+ incompatibleUpdateDetail: string;
119
127
  hotNoPatchNotice: string;
120
128
  hotNotSimpleNotice: string;
121
129
  hotHostUnsupportedNotice: string;
@@ -129,7 +137,7 @@ export declare const en: {
129
137
  showing: string;
130
138
  repository: string;
131
139
  npmPage: string;
132
- publishedBy: string;
140
+ authorLine: string;
133
141
  license: string;
134
142
  customLicense: string;
135
143
  reviewedVersionLine: string;
@@ -9,6 +9,10 @@ import type { CatalogEntry, HotRestartReason, InstallRejectionCode } from '../ho
9
9
  export declare function restartReasonKey(reason: HotRestartReason | undefined): ShopLocaleKey;
10
10
  /** Tier → locale key, for the entry-card tier badge (§6.2). */
11
11
  export declare function tierKey(tier: CatalogEntry['tier']): ShopLocaleKey;
12
+ /** The peers the Host said this installation does not provide, or none when it
13
+ * said nothing — a plugin that runs here and one the Host could not judge are
14
+ * both rendered as no warning at all. */
15
+ export declare function missingPeersOf(incompatible: Record<string, string[]>, name: string): string[];
12
16
  /** Spec §9.3 verbatim — the community-tier acknowledgement. The zh dictionary
13
17
  * states the same facts in its own register; never soften this wording. */
14
18
  export declare const ACKNOWLEDGEMENT_EN = "Once installed, this plugin holds the same privileges as a built-in one: reading and writing your files, running shell commands, and reading and modifying the requests sent to the model. It has not been reviewed.";
@@ -145,6 +149,24 @@ export declare function categoryLocaleKey(category: Category): ShopLocaleKey;
145
149
  * A github entry answers null: it is installed from a repo and has no npm
146
150
  * package page.
147
151
  */
152
+ /**
153
+ * Who put this entry where it is — the fact a reader wants when two listings
154
+ * carry the same text from different hands.
155
+ *
156
+ * The two sources answer it differently and neither is npm's `author` field:
157
+ * that one is free text the publisher writes, and a clone inherits it
158
+ * verbatim. An npm entry answers with the account npm recorded for the
159
+ * version (`publisher`); a github entry answers with its repository owner,
160
+ * which is already half of the `owner/slug` that IS its identity — so nothing
161
+ * new has to be harvested or emitted for it.
162
+ *
163
+ * `repo` reaches the client as an unvalidated string, so the owner is taken
164
+ * only from a value that really is `owner/slug` with an owner GitHub would
165
+ * accept (1-39 chars of alphanumerics and hyphens). Anything else yields
166
+ * null: a misleading fragment is worse than saying nothing, the same rule
167
+ * npmPageUrl applies to a package name.
168
+ */
169
+ export declare function authorOf(entry: CatalogEntry): string | null;
148
170
  export declare function npmPageUrl(entry: CatalogEntry): string | null;
149
171
  /**
150
172
  * One entry's star count from the sidecar, or undefined when it has none.
@@ -4,8 +4,10 @@ import type { CatalogEntry, DeniedEntry } from './types.ts';
4
4
  * 3 adds `source` and repo entries (github install channel); 4 adds `subdir`
5
5
  * for monorepo-subpackage entries (2026-08-31 hub-borrowings A); 5 adds
6
6
  * `added`, `tarball` (release rescue), the `theme` category, and
7
- * `denied[].replacement` (2026-08-31 market borrowings). */
8
- export declare const SUPPORTED_SCHEMA_VERSION = 5;
7
+ * `denied[].replacement` (2026-08-31 market borrowings);
8
+ * 6 adds `peers`, the package's declared peer dependency names
9
+ * (2026-09-01 harness compatibility). */
10
+ export declare const SUPPORTED_SCHEMA_VERSION = 6;
9
11
  export interface CatalogSnapshot {
10
12
  schemaVersion: number;
11
13
  builtAt: string;
@@ -8,6 +8,7 @@ import { type InstallStatus } from './executor.ts';
8
8
  import { hotMount, hotUnmount } from './hot.ts';
9
9
  import { type RestartOutcome } from './restart.ts';
10
10
  import { type RepoPinFs } from './repo-pins.ts';
11
+ import { type PeerResolver } from './peers.ts';
11
12
  export type { InstallArgs, InstallRejectionCode } from './install.ts';
12
13
  export type { HotRestartReason } from './hot.ts';
13
14
  export type { CatalogEntry } from './types.ts';
@@ -94,6 +95,9 @@ export interface ShopGatewayOptions {
94
95
  /** Test-only injection: how the release-tarball integrity check fetches
95
96
  * the release asset; production uses global fetch. */
96
97
  fetchTarball?: (url: string) => Promise<Response>;
98
+ /** Test-only injection: answers whether a peer resolves. Production builds
99
+ * one from the profile anchor. */
100
+ resolvePeer?: PeerResolver;
97
101
  }
98
102
  /** `shop/installStart` result (§7.3): rejections are typed wire values with an
99
103
  * author-readable `detail`, not thrown RPC errors. */
@@ -186,6 +190,10 @@ export interface ShopCatalogResult {
186
190
  /** GitHub star counts by package name; {} when the pointer names no sidecar
187
191
  * or the sidecar could not be fetched/verified (§5). */
188
192
  stars: Record<string, number>;
193
+ /** Package name → the declared peers this installation does not provide
194
+ * (design 2026-09-01). A name is absent when the plugin runs here or when
195
+ * no verdict could be formed; the client renders nothing for both. */
196
+ incompatible: Record<string, string[]>;
189
197
  }
190
198
  /** Remote-only service exposing the shop Remote methods of §7.3.
191
199
  *
@@ -230,6 +238,16 @@ export declare class ShopGateway extends TypertRemoteService {
230
238
  /** The install gate runs against the last loaded snapshot, never a fresh
231
239
  * fetch per request (§7.2: the Host's cached snapshot is the truth). */
232
240
  private lastSnapshot;
241
+ /** The incompatibility map already computed for `lastSnapshot`, keyed by
242
+ * that snapshot's own object identity. Design §3 asks for the verdict
243
+ * once per loaded snapshot, not once per RPC call: `loadCatalog` serves
244
+ * the same snapshot from its on-disk cache for minutes at a time, so
245
+ * without this, reopening the tab within that window would re-walk
246
+ * `node_modules` for every distinct peer name again — and Node only
247
+ * caches a SUCCESSFUL resolution, so a genuinely missing peer pays a full
248
+ * failed walk on every single call. Recomputed only when `catalog()`
249
+ * loads a snapshot that is not this exact object. */
250
+ private incompatibleCache;
233
251
  /** Install records, running and finished; a poll finds one here or reports not found. */
234
252
  private readonly installs;
235
253
  /** Every install id in insertion order, oldest first; finished-record eviction walks this from the front. */
@@ -0,0 +1,25 @@
1
+ /** Harness compatibility: which declared peers the running installation does
2
+ * not provide (design 2026-09-01-harness-compatibility). */
3
+ /** Answers "can this installation provide `spec`?" — injected so fixtures
4
+ * drive every verdict and exactly one call site touches the filesystem. */
5
+ export type PeerResolver = (spec: string) => boolean;
6
+ /**
7
+ * The production resolver: the same question the harness's own
8
+ * ClientModuleRegistry asks, through a require anchored at the profile. Asking
9
+ * what the loader asks is what keeps this verdict and the runtime's behaviour
10
+ * from drifting apart.
11
+ */
12
+ export declare function nodeResolver(baseUrl: string): PeerResolver;
13
+ /**
14
+ * Package name → the peer names that did not resolve. A key is present only
15
+ * when at least one peer is missing, so an absent key means "runs here, or we
16
+ * could not tell" — the client renders nothing for either.
17
+ *
18
+ * A resolver that throws yields NO verdict at all: an unavailable fact must
19
+ * never read as an accusation, because one false warning teaches a reader to
20
+ * ignore every warning.
21
+ */
22
+ export declare function incompatibilityMap(entries: readonly {
23
+ name: string;
24
+ peers?: string[];
25
+ }[], resolve: PeerResolver): Record<string, string[]>;
@@ -52,6 +52,8 @@ export interface CatalogEntry {
52
52
  };
53
53
  /** The date this entry first appeared in the catalog (YYYY-MM-DD). */
54
54
  added?: string;
55
+ /** The package's declared peer dependency names (schemaVersion 6). */
56
+ peers?: string[];
55
57
  }
56
58
  export interface DeniedEntry {
57
59
  name: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-shop",
3
- "version": "0.5.4",
3
+ "version": "0.6.0-beta.1",
4
4
  "description": "The DeepSeek Harness plugin shop: browse, install, enable, and update dsh plugins from a git-auditable catalog.",
5
5
  "repository": {
6
6
  "type": "git",