dsh-plugin-shop 0.4.9 → 0.4.11

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/README.md CHANGED
@@ -6,6 +6,8 @@
6
6
  dsh plugins, install one with a single confirmation, and manage what you have, from a tab inside Settings.
7
7
 
8
8
  [![npm](https://img.shields.io/npm/v/dsh-plugin-shop?logo=npm&color=cb3837)](https://www.npmjs.com/package/dsh-plugin-shop)
9
+ [![plugins](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2FLivXue.github.io%2Fdsh-plugin-shop%2Fv1%2Findex.json&query=count&label=plugins&color=blue)](https://LivXue.github.io/dsh-plugin-shop/v1/index.json)
10
+ [![filtered](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2FLivXue.github.io%2Fdsh-plugin-shop%2Fv1%2Findex.json&query=rejected&label=filtered&color=orange)](https://LivXue.github.io/dsh-plugin-shop/v1/index.json)
9
11
  [![license](https://img.shields.io/npm/l/dsh-plugin-shop?color=blue)](https://github.com/LivXue/dsh-plugin-shop/blob/main/LICENSE)
10
12
  [![plugin CI](https://github.com/LivXue/dsh-plugin-shop/actions/workflows/plugin.yml/badge.svg)](https://github.com/LivXue/dsh-plugin-shop/actions/workflows/plugin.yml)
11
13
  [![catalog](https://github.com/LivXue/dsh-plugin-shop/actions/workflows/daily.yml/badge.svg)](https://github.com/LivXue/dsh-plugin-shop/actions/workflows/daily.yml)
@@ -50,9 +52,9 @@ install them once with `npm install -g @deepseek-ai/dsh pnpm` and verify with
50
52
  # holds back very recent releases, so a bare `add dsh-plugin-shop` can hand
51
53
  # you an older version for a while. Pin the current release — refresh it
52
54
  # with `npm view dsh-plugin-shop version`.
53
- dsh plugin --profile web add dsh-plugin-shop@0.4.9
55
+ dsh plugin --profile web add dsh-plugin-shop@0.4.11
54
56
  # or straight through npx, nothing installed:
55
- npx -y @deepseek-ai/dsh plugin --profile web add dsh-plugin-shop@0.4.9
57
+ npx -y @deepseek-ai/dsh plugin --profile web add dsh-plugin-shop@0.4.11
56
58
  ```
57
59
 
58
60
  Replace `web` with your profile if you use another one. Then restart `dsh` once — a
@@ -82,7 +84,7 @@ ls -1 "${DSH_HOME:-$HOME/.dsh}/profiles" | grep -v '^node_modules$'
82
84
  cooldown, and deterministic installs are the point of the agent path.
83
85
 
84
86
  ```sh
85
- dsh plugin --profile <profile> add dsh-plugin-shop@0.4.9
87
+ dsh plugin --profile <profile> add dsh-plugin-shop@0.4.11
86
88
  ```
87
89
 
88
90
  **3. Verify — do not skip this.** A zero exit from step 2 means pnpm resolved the
@@ -144,7 +146,7 @@ Compromising the browser half buys an attacker those nine calls and nothing more
144
146
 
145
147
  Built daily from the public npm registry and published as static JSON:
146
148
 
147
- - [`/v1/index.json`](https://LivXue.github.io/dsh-plugin-shop/v1/index.json) — the pointer, carrying `schemaVersion`, `builtAt`, and the content hash
149
+ - [`/v1/index.json`](https://LivXue.github.io/dsh-plugin-shop/v1/index.json) — the pointer, carrying `schemaVersion`, `builtAt`, the entry `count` and `rejected` totals (the badges above read them live), and the content hash
148
150
  - `/v1/plugins.<sha256>.json` — the data, content-addressed and safe to cache indefinitely
149
151
 
150
152
  The pointer is small enough to poll. The shop verifies the data file's sha256 against
@@ -163,7 +165,9 @@ actually given — so passing review once cannot buy trust for every future rele
163
165
  ## 🏷️ For plugin authors
164
166
 
165
167
  Add a harvest keyword — `"keywords": ["dsh-plugin"]` or `"keywords": ["deepseek-harness"]` — to
166
- `package.json` and publish. The daily build finds you; nothing is submitted to this project. Declare a `dsh.catalog` section to control
168
+ `package.json` and publish. Or, without npm: add the keyword as a GitHub repo *topic* and keep a
169
+ `package.json` at the root with a `name` and `dsh.bundle` — the catalog lists the repo and pins the
170
+ default-branch commit as its version. The daily build finds you; nothing is submitted to this project. Declare a `dsh.catalog` section to control
167
171
  your own category, summary and capabilities — or omit it, and the catalog derives a
168
172
  listing from your npm `description`.
169
173
 
package/lib/client.js CHANGED
@@ -4279,7 +4279,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4279
4279
  ]),
4280
4280
  "metadata": union([literal("declared"), literal("derived")]),
4281
4281
  "review": union([_undefined(), object({
4282
- "reviewedVersion": string(),
4282
+ "reviewedVersion": union([_undefined(), string()]).optional(),
4283
+ "reviewedCommit": union([_undefined(), string()]).optional(),
4283
4284
  "reviewer": string(),
4284
4285
  "reviewCommit": string(),
4285
4286
  "notes": string()
@@ -4298,7 +4299,9 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4298
4299
  "zh": union([_undefined(), string()]).optional()
4299
4300
  }),
4300
4301
  "capabilities": array(string())
4301
- })]).optional()
4302
+ })]).optional(),
4303
+ "source": union([literal("npm"), literal("github")]),
4304
+ "repo": union([_undefined(), string()]).optional()
4302
4305
  })),
4303
4306
  "denied": array(object({
4304
4307
  "name": string(),
@@ -4330,7 +4333,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4330
4333
  literal("not-in-catalog"),
4331
4334
  literal("denied"),
4332
4335
  literal("version-mismatch"),
4333
- literal("needs-acknowledgement")
4336
+ literal("needs-acknowledgement"),
4337
+ literal("git-missing")
4334
4338
  ]),
4335
4339
  "detail": string()
4336
4340
  })]);
@@ -4410,7 +4414,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4410
4414
  },
4411
4415
  sourceLocation: {
4412
4416
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4413
- "line": 244,
4417
+ "line": 270,
4414
4418
  "column": 9
4415
4419
  }
4416
4420
  },
@@ -4428,7 +4432,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4428
4432
  },
4429
4433
  sourceLocation: {
4430
4434
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4431
- "line": 319,
4435
+ "line": 374,
4432
4436
  "column": 9
4433
4437
  }
4434
4438
  },
@@ -4456,7 +4460,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4456
4460
  },
4457
4461
  sourceLocation: {
4458
4462
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4459
- "line": 270,
4463
+ "line": 296,
4460
4464
  "column": 9
4461
4465
  }
4462
4466
  },
@@ -4483,7 +4487,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4483
4487
  },
4484
4488
  sourceLocation: {
4485
4489
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4486
- "line": 308,
4490
+ "line": 363,
4487
4491
  "column": 3
4488
4492
  }
4489
4493
  },
@@ -4501,7 +4505,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4501
4505
  },
4502
4506
  sourceLocation: {
4503
4507
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4504
- "line": 392,
4508
+ "line": 469,
4505
4509
  "column": 9
4506
4510
  }
4507
4511
  },
@@ -4528,7 +4532,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4528
4532
  },
4529
4533
  sourceLocation: {
4530
4534
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4531
- "line": 213,
4535
+ "line": 239,
4532
4536
  "column": 3
4533
4537
  }
4534
4538
  },
@@ -4556,7 +4560,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4556
4560
  },
4557
4561
  sourceLocation: {
4558
4562
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4559
- "line": 359,
4563
+ "line": 429,
4560
4564
  "column": 9
4561
4565
  }
4562
4566
  },
@@ -4583,7 +4587,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4583
4587
  },
4584
4588
  sourceLocation: {
4585
4589
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4586
- "line": 441,
4590
+ "line": 518,
4587
4591
  "column": 9
4588
4592
  }
4589
4593
  },
@@ -4601,7 +4605,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4601
4605
  },
4602
4606
  sourceLocation: {
4603
4607
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4604
- "line": 426,
4608
+ "line": 503,
4605
4609
  "column": 9
4606
4610
  }
4607
4611
  }
@@ -4617,12 +4621,6 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4617
4621
  case "community": return "tierCommunity";
4618
4622
  }
4619
4623
  }
4620
- /** A derived listing has no author-declared catalog section; the shop
4621
- * presents it as unclaimed, which is the signal that prompts an author to add
4622
- * one (§6.1). Never present a derived entry as though the author wrote it. */
4623
- function isUnclaimed(entry) {
4624
- return entry.metadata === "derived";
4625
- }
4626
4624
  /** Spec §9.3 verbatim — the community-tier acknowledgement. The zh dictionary
4627
4625
  * states the same facts in its own register; never soften this wording. */
4628
4626
  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.";
@@ -4636,8 +4634,13 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4636
4634
  case "not-in-catalog": return "notInCatalogCode";
4637
4635
  case "version-mismatch": return "versionMismatchCode";
4638
4636
  case "needs-acknowledgement": return "needsAcknowledgementCode";
4637
+ case "git-missing": return "gitMissingCode";
4639
4638
  }
4640
4639
  }
4640
+ /** The version a card displays: full for npm, the short commit for github. */
4641
+ function displayVersion(entry) {
4642
+ return entry.source === "github" ? entry.version.slice(0, 7) : entry.version;
4643
+ }
4641
4644
  /** §7.2 once-per-second poll cadence, as a named constant. */
4642
4645
  const INSTALL_POLL_MS = 1e3;
4643
4646
  /** How long the check-update button reports "up to date" after a re-check
@@ -4695,10 +4698,6 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4695
4698
  "shop",
4696
4699
  "marketplace"
4697
4700
  ];
4698
- /** Competing marketplaces whose names carry no store/market keyword and so
4699
- * escape the pattern rules below. Named explicitly rather than guessed:
4700
- * `dsh-plugin` is the npm package of github.com/dshplugin/dsh-plugin-hub —
4701
- * a community plugin marketplace for DeepSeek Harness. */
4702
4701
  /**
4703
4702
  * Competing plugin marketplaces whose names carry no store/market keyword
4704
4703
  * and so escape the pattern rules below (spec §7.2). Each is a real npm
@@ -4804,7 +4803,6 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4804
4803
  tierVerified: "已审核",
4805
4804
  tierVerifiedStale: "审核版本过期",
4806
4805
  tierCommunity: "社区",
4807
- unclaimed: "作者未认领",
4808
4806
  capabilitiesNote: "作者自述,无强制约束",
4809
4807
  install: "安装",
4810
4808
  confirm: "确认",
@@ -4827,6 +4825,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4827
4825
  notInCatalogCode: "不在目录中",
4828
4826
  versionMismatchCode: "版本不匹配",
4829
4827
  needsAcknowledgementCode: "需要确认",
4828
+ gitMissingCode: "缺少 git",
4829
+ githubSource: "来自 GitHub 仓库安装",
4830
4830
  installedSection: "已安装",
4831
4831
  installedVersion: "已安装 v{version}",
4832
4832
  latestVersion: "最新 v{version}",
@@ -4875,7 +4875,6 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4875
4875
  tierVerified: "Verified",
4876
4876
  tierVerifiedStale: "Reviewed version stale",
4877
4877
  tierCommunity: "Community",
4878
- unclaimed: "not claimed by its author",
4879
4878
  capabilitiesNote: "self-declared by the author, not enforced",
4880
4879
  install: "Install",
4881
4880
  confirm: "Confirm",
@@ -4898,6 +4897,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4898
4897
  notInCatalogCode: "Not in the catalog",
4899
4898
  versionMismatchCode: "Version mismatch",
4900
4899
  needsAcknowledgementCode: "Acknowledgement required",
4900
+ gitMissingCode: "git missing",
4901
+ githubSource: "installed from a GitHub repository",
4901
4902
  installedSection: "Installed",
4902
4903
  installedVersion: "installed v{version}",
4903
4904
  latestVersion: "latest v{version}",
@@ -5074,7 +5075,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5074
5075
  }
5075
5076
  //#endregion
5076
5077
  //#region \0dsh-plugin-shop-css:e3675a89
5077
- 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 (six fixed hues, not the brand token — the brand resolves to\n * near-black in the light theme, so six brand opacities read as six shades\n * 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}\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: repeat(auto-fill, minmax(260px, 1fr));\n gap: 12px;\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.s30e662_skeletonCover {\n display: block;\n height: 26px;\n background: color-mix(in srgb, var(--dsw-alias-label-primary) 10%, transparent);\n}\n\n.s8c7d6f_skeletonName {\n width: 55%;\n height: 14px;\n margin: 12px 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 14px;\n border-radius: 4px;\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.sf657f5_cards {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));\n gap: 12px;\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 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 240px;\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='other'] { --spine-hue: #8B8E96; }\n\n.s9e2bec_cardCover {\n display: block;\n padding: 5px 12px 5px 12px;\n border-bottom: 1px solid var(--dsw-alias-border-l1);\n background: color-mix(in srgb, var(--spine-hue, #8B8E96) 12%, transparent);\n}\n\n.s1a7c5b_coverLabel {\n display: block;\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n color: var(--spine-hue, #8B8E96);\n}\n\n.sa97234_entryHeader {\n display: flex;\n flex-direction: column;\n align-items: stretch;\n gap: 8px;\n padding: 10px 12px 6px;\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 owns the full header width; badges sit on their own line below. */\n.sc3f149_name {\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 13px;\n font-weight: 600;\n overflow-wrap: anywhere;\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.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.s23e3e1_unclaimedBadge {\n padding: 1px 7px;\n border-radius: 999px;\n border: 1px dashed var(--dsw-alias-border-l2);\n font-size: 11px;\n line-height: 1.6;\n color: var(--dsw-alias-label-secondary);\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 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.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 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";
5078
+ 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 (six fixed hues, not the brand token — the brand resolves to\n * near-black in the light theme, so six brand opacities read as six shades\n * 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='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.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 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";
5078
5079
  if (typeof document !== "undefined" && !document.querySelector("style[data-plugin-css=\"e3675a89\"]")) {
5079
5080
  const tag = document.createElement("style");
5080
5081
  tag.dataset.pluginCss = "e3675a89";
@@ -5091,10 +5092,10 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5091
5092
  "srOnly": "s3a0ece_srOnly",
5092
5093
  "skeletonGrid": "se0c3b4_skeletonGrid",
5093
5094
  "skeletonCard": "s520416_skeletonCard",
5094
- "skeletonCover": "s30e662_skeletonCover",
5095
5095
  "skeletonName": "s8c7d6f_skeletonName",
5096
5096
  "skeletonSummary": "sf5179e_skeletonSummary",
5097
5097
  "skeletonSummaryShort": "sbb0d25_skeletonSummaryShort",
5098
+ "skeletonActions": "s96e910_skeletonActions",
5098
5099
  "actionButton": "s73242a_actionButton",
5099
5100
  "toolbar": "s646509_toolbar",
5100
5101
  "toolbarRight": "s745e3c_toolbarRight",
@@ -5118,13 +5119,17 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5118
5119
  "cardsSentry": "sd7ee01_cardsSentry",
5119
5120
  "showingLine": "s705cc6_showingLine",
5120
5121
  "cardSpine": "s4ce25d_cardSpine",
5121
- "cardCover": "s9e2bec_cardCover",
5122
- "coverLabel": "s1a7c5b_coverLabel",
5122
+ "categoryBadge": "s9c8eaa_categoryBadge",
5123
5123
  "entryHeader": "sa97234_entryHeader",
5124
5124
  "name": "sc3f149_name",
5125
5125
  "badges": "s513026_badges",
5126
+ "cardActions": "s9b5048_cardActions",
5127
+ "gate": "sc4f2ac_gate",
5128
+ "installPanel": "s4d374c_installPanel",
5129
+ "installedActions": "sf53ca0_installedActions",
5130
+ "cardVersion": "sc6ec6e_cardVersion",
5131
+ "sourceBadge": "s71f10f_sourceBadge",
5126
5132
  "tierBadge": "s5e82cb_tierBadge",
5127
- "unclaimedBadge": "s23e3e1_unclaimedBadge",
5128
5133
  "starsBadge": "sff43b4_starsBadge",
5129
5134
  "chevron": "s73f7fb_chevron",
5130
5135
  "chevronOpen": "s61393c_chevronOpen",
@@ -5140,15 +5145,12 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5140
5145
  "detailRows": "se98179_detailRows",
5141
5146
  "detailRow": "sfb4d2f_detailRow",
5142
5147
  "reviewedLine": "sd58c6d_reviewedLine",
5143
- "installPanel": "s4d374c_installPanel",
5144
5148
  "installButton": "se7aaed_installButton",
5145
5149
  "confirmButton": "sb43a49_confirmButton",
5146
5150
  "cancelButton": "s6e45d1_cancelButton",
5147
5151
  "installedLabel": "sb6fc74_installedLabel",
5148
- "installedActions": "sf53ca0_installedActions",
5149
5152
  "uninstallButton": "s96967f_uninstallButton",
5150
5153
  "restartButton": "s6ac237_restartButton",
5151
- "gate": "sc4f2ac_gate",
5152
5154
  "gateTitle": "s559c38_gateTitle",
5153
5155
  "gateBody": "s3c0ace_gateBody",
5154
5156
  "gateActions": "s4b2373_gateActions",
@@ -5193,7 +5195,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5193
5195
  });
5194
5196
  }
5195
5197
  /** One entry card: the category spine and cover band, the expandable header
5196
- * (name, tier, category, unclaimed marker), the plain-text summary in both
5198
+ * (name, tier, category), the plain-text summary in both
5197
5199
  * languages, the self-declared capabilities, the detail section, and the
5198
5200
  * install controls. An installed plugin's card carries its installed row:
5199
5201
  * current → the non-interactive installed label, behind → the update button;
@@ -5212,14 +5214,6 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5212
5214
  className: _dsh_plugin_shop_css_e3675a89_default.cardSpine,
5213
5215
  "aria-hidden": "true"
5214
5216
  }),
5215
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5216
- className: _dsh_plugin_shop_css_e3675a89_default.cardCover,
5217
- "aria-hidden": "true",
5218
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5219
- className: _dsh_plugin_shop_css_e3675a89_default.coverLabel,
5220
- children: t(categoryKey(entry))
5221
- })
5222
- }),
5223
5217
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
5224
5218
  type: "button",
5225
5219
  className: _dsh_plugin_shop_css_e3675a89_default.entryHeader,
@@ -5232,15 +5226,33 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5232
5226
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
5233
5227
  className: _dsh_plugin_shop_css_e3675a89_default.badges,
5234
5228
  children: [
5229
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5230
+ className: _dsh_plugin_shop_css_e3675a89_default.categoryBadge,
5231
+ children: t(categoryKey(entry))
5232
+ }),
5233
+ entry.source === "github" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5234
+ className: _dsh_plugin_shop_css_e3675a89_default.sourceBadge,
5235
+ role: "img",
5236
+ "aria-label": t("githubSource"),
5237
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
5238
+ viewBox: "0 0 16 16",
5239
+ width: "12",
5240
+ height: "12",
5241
+ "aria-hidden": "true",
5242
+ fill: "currentColor",
5243
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8Z" })
5244
+ })
5245
+ }),
5246
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
5247
+ className: _dsh_plugin_shop_css_e3675a89_default.cardVersion,
5248
+ "data-card-version": true,
5249
+ children: ["v", displayVersion(entry)]
5250
+ }),
5235
5251
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5236
5252
  className: _dsh_plugin_shop_css_e3675a89_default.tierBadge,
5237
5253
  "data-tier": entry.tier,
5238
5254
  children: t(tierKey(entry.tier))
5239
5255
  }),
5240
- isUnclaimed(entry) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5241
- className: _dsh_plugin_shop_css_e3675a89_default.unclaimedBadge,
5242
- children: t("unclaimed")
5243
- }),
5244
5256
  stars !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
5245
5257
  className: _dsh_plugin_shop_css_e3675a89_default.starsBadge,
5246
5258
  role: "img",
@@ -5262,7 +5274,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5262
5274
  className: open ? `${_dsh_plugin_shop_css_e3675a89_default.summaryZh} ${_dsh_plugin_shop_css_e3675a89_default.summaryZhExpanded}` : _dsh_plugin_shop_css_e3675a89_default.summaryZh,
5263
5275
  children: summary.zh
5264
5276
  }),
5265
- entry.catalog !== void 0 && entry.catalog.capabilities.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5277
+ open && entry.catalog !== void 0 && entry.catalog.capabilities.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5266
5278
  className: _dsh_plugin_shop_css_e3675a89_default.capabilitiesBlock,
5267
5279
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5268
5280
  className: _dsh_plugin_shop_css_e3675a89_default.capabilitiesNote,
@@ -5303,39 +5315,39 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5303
5315
  current: entry.version
5304
5316
  })
5305
5317
  })]
5306
- }),
5307
- installed === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InstallPanel, {
5308
- name: entry.name,
5309
- version: entry.version,
5310
- tier: entry.tier,
5311
- t,
5312
- install,
5313
- installStatus,
5314
- restart
5315
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5316
- className: _dsh_plugin_shop_css_e3675a89_default.installedActions,
5317
- children: [installed.outdated ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InstallPanel, {
5318
- name: entry.name,
5319
- version: entry.version,
5320
- tier: entry.tier,
5321
- variant: "update",
5322
- t,
5323
- install,
5324
- installStatus,
5325
- restart
5326
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5327
- className: _dsh_plugin_shop_css_e3675a89_default.installedLabel,
5328
- "data-shop-installed": true,
5329
- children: t("installed")
5330
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UninstallPanel, {
5331
- name: entry.name,
5332
- t,
5333
- uninstall,
5334
- installStatus,
5335
- restart
5336
- })]
5337
5318
  })
5338
5319
  ]
5320
+ }),
5321
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5322
+ className: _dsh_plugin_shop_css_e3675a89_default.cardActions,
5323
+ children: installed === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InstallPanel, {
5324
+ name: entry.name,
5325
+ version: entry.version,
5326
+ tier: entry.tier,
5327
+ t,
5328
+ install,
5329
+ installStatus,
5330
+ restart
5331
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [installed.outdated ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InstallPanel, {
5332
+ name: entry.name,
5333
+ version: entry.version,
5334
+ tier: entry.tier,
5335
+ variant: "update",
5336
+ t,
5337
+ install,
5338
+ installStatus,
5339
+ restart
5340
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5341
+ className: _dsh_plugin_shop_css_e3675a89_default.installedLabel,
5342
+ "data-shop-installed": true,
5343
+ children: t("installed")
5344
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UninstallPanel, {
5345
+ name: entry.name,
5346
+ t,
5347
+ uninstall,
5348
+ installStatus,
5349
+ restart
5350
+ })] })
5339
5351
  })
5340
5352
  ]
5341
5353
  });
@@ -5616,7 +5628,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5616
5628
  * installed-state RPC behind it — the tab assumes an installed plugin is on,
5617
5629
  * so the first toggle sends the inverted value. After a successful `setEnabled`
5618
5630
  * the §8 hot note renders. */
5619
- function OutdatedRow({ row, tier, t, setEnabled, install, installStatus, restart }) {
5631
+ function OutdatedRow({ row, tier, source, t, setEnabled, install, installStatus, restart }) {
5620
5632
  const [enabled, setEnabledState] = (0, react.useState)(true);
5621
5633
  const [toggle, setToggle] = (0, react.useState)({ kind: "idle" });
5622
5634
  const onToggle = async () => {
@@ -5655,7 +5667,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5655
5667
  className: _dsh_plugin_shop_css_e3675a89_default.outdatedVersions,
5656
5668
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5657
5669
  className: _dsh_plugin_shop_css_e3675a89_default.outdatedVersion,
5658
- children: t("installedVersion", { version: row.installed })
5670
+ children: t("installedVersion", { version: source === "github" ? row.installed.slice(0, 7) : row.installed })
5659
5671
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5660
5672
  className: _dsh_plugin_shop_css_e3675a89_default.outdatedVersion,
5661
5673
  children: t("latestVersion", { version: row.latest })
@@ -5705,7 +5717,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5705
5717
  * tier for the update gate is looked up from the catalog by name (community →
5706
5718
  * acknowledgement); an entry absent from the catalog defaults to the
5707
5719
  * community gate (the safer read). */
5708
- function OutdatedSection({ state, tiers, t, setEnabled, install, installStatus, restart }) {
5720
+ function OutdatedSection({ state, tiers, sources, t, setEnabled, install, installStatus, restart }) {
5709
5721
  if (state.kind === "loading") return null;
5710
5722
  if (state.kind === "error") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5711
5723
  className: _dsh_plugin_shop_css_e3675a89_default.stateLine,
@@ -5725,6 +5737,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5725
5737
  children: outdated.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OutdatedRow, {
5726
5738
  row,
5727
5739
  tier: tiers.get(row.name) ?? "community",
5740
+ source: sources.get(row.name) ?? "npm",
5728
5741
  t,
5729
5742
  setEnabled,
5730
5743
  install,
@@ -5827,7 +5840,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5827
5840
  if (catalogState.kind !== "ready") return [];
5828
5841
  const q = query.trim().toLowerCase();
5829
5842
  return catalogState.result.plugins.filter((entry) => {
5830
- if (isShopLike(entry.name)) return false;
5843
+ if (isShopLike(entry.name) || entry.repo !== void 0 && isShopLike(entry.repo)) return false;
5831
5844
  if (category === "installed") {
5832
5845
  if (!installedByName.has(entry.name)) return false;
5833
5846
  } else if (category !== null && categoryKey(entry) !== categoryLocaleKey(category)) return false;
@@ -5863,7 +5876,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5863
5876
  const categoryCounts = (0, react.useMemo)(() => {
5864
5877
  const counts = /* @__PURE__ */ new Map();
5865
5878
  if (catalogState.kind === "ready") for (const entry of catalogState.result.plugins) {
5866
- if (isShopLike(entry.name)) continue;
5879
+ if (isShopLike(entry.name) || entry.repo !== void 0 && isShopLike(entry.repo)) continue;
5867
5880
  const key = categoryKey(entry);
5868
5881
  const bare = CATEGORY_ORDER.find((c) => categoryLocaleKey(c) === key);
5869
5882
  if (bare !== void 0) counts.set(bare, (counts.get(bare) ?? 0) + 1);
@@ -5879,6 +5892,11 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5879
5892
  if (catalogState.kind === "ready") for (const entry of catalogState.result.plugins) map.set(entry.name, entry.tier);
5880
5893
  return map;
5881
5894
  }, [catalogState]);
5895
+ const sources = (0, react.useMemo)(() => {
5896
+ const map = /* @__PURE__ */ new Map();
5897
+ if (catalogState.kind === "ready") for (const entry of catalogState.result.plugins) map.set(entry.name, entry.source);
5898
+ return map;
5899
+ }, [catalogState]);
5882
5900
  if (catalogState.kind === "loading") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5883
5901
  className: _dsh_plugin_shop_css_e3675a89_default.panel,
5884
5902
  "data-shop-tab": true,
@@ -5893,10 +5911,10 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5893
5911
  children: Array.from({ length: 8 }, (_, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5894
5912
  className: _dsh_plugin_shop_css_e3675a89_default.skeletonCard,
5895
5913
  children: [
5896
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: _dsh_plugin_shop_css_e3675a89_default.skeletonCover }),
5897
5914
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: _dsh_plugin_shop_css_e3675a89_default.skeletonName }),
5898
5915
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: _dsh_plugin_shop_css_e3675a89_default.skeletonSummary }),
5899
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: _dsh_plugin_shop_css_e3675a89_default.skeletonSummaryShort })
5916
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: _dsh_plugin_shop_css_e3675a89_default.skeletonSummaryShort }),
5917
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: _dsh_plugin_shop_css_e3675a89_default.skeletonActions })
5900
5918
  ]
5901
5919
  }, i))
5902
5920
  })]
@@ -6107,7 +6125,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6107
6125
  className: _dsh_plugin_shop_css_e3675a89_default.cards,
6108
6126
  children: [visible.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EntryCard, {
6109
6127
  entry,
6110
- stars: stars[entry.name],
6128
+ stars: stars[entry.repo ?? entry.name],
6111
6129
  installed: installedByName.get(entry.name),
6112
6130
  t,
6113
6131
  install,
@@ -6131,6 +6149,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6131
6149
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OutdatedSection, {
6132
6150
  state: installedState,
6133
6151
  tiers,
6152
+ sources,
6134
6153
  t,
6135
6154
  setEnabled,
6136
6155
  install,
package/lib/index.js CHANGED
@@ -1,12 +1,12 @@
1
1
  import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
2
2
  import { loadOptionalPatches, readProfileManifest, resolveProfileDir } from "@deepseek-ai/dsh-app-boot";
3
3
  import { lt, minVersion, valid } from "semver";
4
+ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, realpathSync, renameSync, writeFileSync } from "node:fs";
5
+ import { spawn, spawnSync } from "node:child_process";
4
6
  import { fileURLToPath } from "node:url";
5
7
  import { basename, dirname, join } from "node:path";
6
- import { closeSync, existsSync, mkdirSync, openSync, readFileSync, realpathSync, renameSync, writeFileSync } from "node:fs";
7
8
  import { createHash, randomUUID } from "node:crypto";
8
9
  import { z } from "zod";
9
- import { spawn } from "node:child_process";
10
10
  import { dump } from "js-yaml";
11
11
  //#region src/own-version.ts
12
12
  /** The shop's own published version, read from the package.json that ships
@@ -37,7 +37,8 @@ const entrySchema = z.object({
37
37
  ]),
38
38
  metadata: z.enum(["declared", "derived"]),
39
39
  review: z.object({
40
- reviewedVersion: z.string(),
40
+ reviewedVersion: z.string().optional(),
41
+ reviewedCommit: z.string().optional(),
41
42
  reviewer: z.string(),
42
43
  reviewCommit: z.string(),
43
44
  notes: z.string()
@@ -56,7 +57,9 @@ const entrySchema = z.object({
56
57
  zh: z.string().optional()
57
58
  }),
58
59
  capabilities: z.array(z.string())
59
- }).optional()
60
+ }).optional(),
61
+ source: z.enum(["npm", "github"]).default("npm"),
62
+ repo: z.string().optional()
60
63
  });
61
64
  const dataSchema = z.object({
62
65
  schemaVersion: z.number(),
@@ -140,13 +143,13 @@ async function loadCatalog(options) {
140
143
  const readCached = () => {
141
144
  try {
142
145
  const pointer = pointerSchema.parse(JSON.parse(fsImpl.read(indexPath)));
143
- if (pointer.schemaVersion > 2) throw new Error(`catalog schemaVersion ${pointer.schemaVersion} is newer than this build supports (2)`);
146
+ if (pointer.schemaVersion > 3) throw new Error(`catalog schemaVersion ${pointer.schemaVersion} is newer than this build supports (3)`);
144
147
  const dataPath = join(cacheDir, basename(pointer.plugins.url));
145
148
  const dataText = fsImpl.read(dataPath);
146
149
  const actual = createHash("sha256").update(dataText).digest("hex");
147
150
  if (actual !== pointer.plugins.sha256) throw new Error(`cached catalog data failed integrity check: expected ${pointer.plugins.sha256}, got ${actual}`);
148
151
  const data = dataSchema.parse(JSON.parse(dataText));
149
- if (data.schemaVersion > 2) throw new Error(`catalog schemaVersion ${data.schemaVersion} is newer than this build supports (2)`);
152
+ if (data.schemaVersion > 3) throw new Error(`catalog schemaVersion ${data.schemaVersion} is newer than this build supports (3)`);
150
153
  let stars = {};
151
154
  if (pointer.stars !== void 0) try {
152
155
  const starsText = fsImpl.read(join(cacheDir, basename(pointer.stars.url)));
@@ -187,7 +190,7 @@ async function loadCatalog(options) {
187
190
  throw error;
188
191
  }
189
192
  const pointer = pointerSchema.parse(JSON.parse(pointerText));
190
- if (pointer.schemaVersion > 2) throw new Error(`catalog schemaVersion ${pointer.schemaVersion} is newer than this build supports (2)`);
193
+ if (pointer.schemaVersion > 3) throw new Error(`catalog schemaVersion ${pointer.schemaVersion} is newer than this build supports (3)`);
191
194
  const dataUrl = resolveDataUrl(baseUrl, pointer.plugins.url);
192
195
  let dataText;
193
196
  try {
@@ -205,7 +208,7 @@ async function loadCatalog(options) {
205
208
  const actual = createHash("sha256").update(dataText).digest("hex");
206
209
  if (actual !== pointer.plugins.sha256) throw new Error(`catalog data failed integrity check: expected ${pointer.plugins.sha256}, got ${actual}`);
207
210
  const data = dataSchema.parse(JSON.parse(dataText));
208
- if (data.schemaVersion > 2) throw new Error(`catalog schemaVersion ${data.schemaVersion} is newer than this build supports (2)`);
211
+ if (data.schemaVersion > 3) throw new Error(`catalog schemaVersion ${data.schemaVersion} is newer than this build supports (3)`);
209
212
  let stars = {};
210
213
  if (pointer.stars !== void 0) try {
211
214
  const starsResponse = await fetchImpl(resolveDataUrl(baseUrl, pointer.stars.url));
@@ -497,6 +500,25 @@ async function fetchLatestVersion(fetchFn = fetch) {
497
500
  }
498
501
  }
499
502
  //#endregion
503
+ //#region src/host/repo-pins.ts
504
+ /** Read the pins file; any irregularity degrades to an empty record. */
505
+ function readRepoPins(fs, path) {
506
+ if (!fs.exists(path)) return {};
507
+ try {
508
+ const parsed = JSON.parse(fs.read(path));
509
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return {};
510
+ const out = {};
511
+ for (const [name, commit] of Object.entries(parsed)) if (typeof commit === "string" && /^[0-9a-f]{40}$/.test(commit)) out[name] = commit;
512
+ return out;
513
+ } catch {
514
+ return {};
515
+ }
516
+ }
517
+ /** Write the pins file atomically enough for its purpose (a cache). */
518
+ function writeRepoPins(fs, path, pins) {
519
+ fs.write(path, `${JSON.stringify(pins, null, 2)}\n`);
520
+ }
521
+ //#endregion
500
522
  //#region src/host/profile.ts
501
523
  /** Profile directory discovery and user-layer writes (§8: hot enable/disable). */
502
524
  /**
@@ -759,6 +781,8 @@ let ShopGateway = (() => {
759
781
  restartExitDelayMs;
760
782
  restartParentPid;
761
783
  latestVersion;
784
+ hasGit;
785
+ pinFs;
762
786
  /** The install gate runs against the last loaded snapshot, never a fresh
763
787
  * fetch per request (§7.2: the Host's cached snapshot is the truth). */
764
788
  /** Finished install records retained, so a poll sees the true terminal
@@ -786,6 +810,19 @@ let ShopGateway = (() => {
786
810
  this.restartExitDelayMs = options.restartExitDelayMs ?? ShopGateway.RESTART_EXIT_DELAY_MS;
787
811
  this.restartParentPid = options.restartParentPid ?? process.pid;
788
812
  this.latestVersion = options.fetchLatestVersion ?? (() => fetchLatestVersion());
813
+ this.hasGit = options.hasGit ?? (() => spawnSync("git", ["--version"], { stdio: "ignore" }).status === 0);
814
+ this.pinFs = options.pinFs ?? {
815
+ exists: (path) => existsSync(path),
816
+ read: (path) => readFileSync(path, "utf8"),
817
+ write: (path, data) => {
818
+ mkdirSync(dirname(path), { recursive: true });
819
+ writeFileSync(path, data);
820
+ }
821
+ };
822
+ }
823
+ /** The pins file lives in the shop's own cache, next to the catalog cache. */
824
+ pinsPath() {
825
+ return join(this.rowConfig().cacheDir, "github-pins.json");
789
826
  }
790
827
  /** The boot's Loader root directory (the active profile's `cordis.yml`
791
828
  * directory, carried on `ctx.baseUrl`), when present. A `link:` install
@@ -882,12 +919,39 @@ let ShopGateway = (() => {
882
919
  code: verdict.code,
883
920
  detail: verdict.detail
884
921
  };
922
+ const entry = this.lastSnapshot.entries.find((e) => e.name === args.name);
923
+ if (entry === void 0) return {
924
+ ok: false,
925
+ code: "not-in-catalog",
926
+ detail: `dsh-plugin-shop: ${args.name} is not in the catalog`
927
+ };
928
+ let spec;
929
+ if (entry.source === "github") {
930
+ if (entry.repo === void 0 || !/^[0-9a-f]{40}$/.test(args.version)) return {
931
+ ok: false,
932
+ code: "version-mismatch",
933
+ detail: `dsh-plugin-shop: ${args.name} has no installable commit`
934
+ };
935
+ if (!this.hasGit()) return {
936
+ ok: false,
937
+ code: "git-missing",
938
+ detail: "dsh-plugin-shop: git is not on PATH, which github installs require; install git and retry"
939
+ };
940
+ spec = `github:${entry.repo}#${args.version}`;
941
+ } else spec = `${args.name}@${args.version}`;
885
942
  const running = startInstall({
886
943
  profile: this.profile,
887
- spec: `${args.name}@${args.version}`,
944
+ spec,
888
945
  dshBin: this.dshBin,
889
946
  expectedName: args.name
890
947
  });
948
+ if (entry.source === "github") {
949
+ const pins = readRepoPins(this.pinFs, this.pinsPath());
950
+ writeRepoPins(this.pinFs, this.pinsPath(), {
951
+ ...pins,
952
+ [args.name]: args.version
953
+ });
954
+ }
891
955
  this.installs.set(running.installId, running);
892
956
  this.installOrder.push(running.installId);
893
957
  this.evictFinishedInstalls();
@@ -936,11 +1000,20 @@ let ShopGateway = (() => {
936
1000
  this.lastSnapshot = snapshot;
937
1001
  }
938
1002
  const dependencies = readProfileManifest("dsh-plugin-shop", this.profileDirResolved()).dependencies ?? {};
1003
+ const pins = readRepoPins(this.pinFs, this.pinsPath());
939
1004
  const installed = [];
940
1005
  for (const entry of this.lastSnapshot.entries) {
941
1006
  const spec = dependencies[entry.name];
942
1007
  if (spec === void 0) continue;
943
- installed.push({
1008
+ if (entry.source === "github") {
1009
+ const pin = pins[entry.name];
1010
+ installed.push({
1011
+ name: entry.name,
1012
+ installed: pin ?? spec,
1013
+ latest: entry.version,
1014
+ outdated: pin !== void 0 && pin !== entry.version
1015
+ });
1016
+ } else installed.push({
944
1017
  name: entry.name,
945
1018
  installed: spec,
946
1019
  latest: entry.version,
@@ -992,6 +1065,11 @@ let ShopGateway = (() => {
992
1065
  dshBin: this.dshBin,
993
1066
  expectedName: args.name
994
1067
  });
1068
+ const pins = readRepoPins(this.pinFs, this.pinsPath());
1069
+ if (pins[args.name] !== void 0) {
1070
+ delete pins[args.name];
1071
+ writeRepoPins(this.pinFs, this.pinsPath(), pins);
1072
+ }
995
1073
  this.installs.set(running.installId, running);
996
1074
  this.installOrder.push(running.installId);
997
1075
  this.evictFinishedInstalls();
@@ -18,7 +18,8 @@ const dsh_plugin_shop_shop_catalog_result$schema = z.object({
18
18
  'tier': z.union([z.literal("verified"), z.literal("verified-stale"), z.literal("community")]),
19
19
  'metadata': z.union([z.literal("declared"), z.literal("derived")]),
20
20
  'review': z.union([z.undefined(), z.object({
21
- 'reviewedVersion': z.string(),
21
+ 'reviewedVersion': z.union([z.undefined(), z.string()]).optional(),
22
+ 'reviewedCommit': z.union([z.undefined(), z.string()]).optional(),
22
23
  'reviewer': z.string(),
23
24
  'reviewCommit': z.string(),
24
25
  'notes': z.string(),
@@ -31,6 +32,8 @@ const dsh_plugin_shop_shop_catalog_result$schema = z.object({
31
32
  }),
32
33
  'capabilities': z.array(z.string()),
33
34
  })]).optional(),
35
+ 'source': z.union([z.literal("npm"), z.literal("github")]),
36
+ 'repo': z.union([z.undefined(), z.string()]).optional(),
34
37
  })),
35
38
  'denied': z.array(z.object({
36
39
  'name': z.string(),
@@ -54,7 +57,7 @@ const dsh_plugin_shop_shop_installStart_result$schema = z.union([z.object({
54
57
  'installId': z.string(),
55
58
  }), z.object({
56
59
  'ok': z.literal(false),
57
- 'code': z.union([z.literal("not-in-catalog"), z.literal("denied"), z.literal("version-mismatch"), z.literal("needs-acknowledgement")]),
60
+ 'code': z.union([z.literal("not-in-catalog"), z.literal("denied"), z.literal("version-mismatch"), z.literal("needs-acknowledgement"), z.literal("git-missing")]),
58
61
  'detail': z.string(),
59
62
  })])
60
63
  const dsh_plugin_shop_shop_installStatus_parameter_0$schema = z.object({
@@ -137,7 +140,7 @@ export const TYPERT = {
137
140
  typeSymbol: 'dsh-plugin-shop/types#ShopCatalogResult',
138
141
  schema: dsh_plugin_shop_shop_catalog_result$schema,
139
142
  },
140
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":244,"column":9},
143
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":270,"column":9},
141
144
  },
142
145
  {
143
146
  id: 'dsh-plugin-shop#shop/installed',
@@ -152,7 +155,7 @@ export const TYPERT = {
152
155
  typeSymbol: 'dsh-plugin-shop#shop/installed:result',
153
156
  schema: dsh_plugin_shop_shop_installed_result$schema,
154
157
  },
155
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":319,"column":9},
158
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":374,"column":9},
156
159
  },
157
160
  {
158
161
  id: 'dsh-plugin-shop#shop/installStart',
@@ -178,7 +181,7 @@ export const TYPERT = {
178
181
  typeSymbol: 'dsh-plugin-shop/types#ShopInstallResult',
179
182
  schema: dsh_plugin_shop_shop_installStart_result$schema,
180
183
  },
181
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":270,"column":9},
184
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":296,"column":9},
182
185
  },
183
186
  {
184
187
  id: 'dsh-plugin-shop#shop/installStatus',
@@ -203,7 +206,7 @@ export const TYPERT = {
203
206
  typeSymbol: 'dsh-plugin-shop/types#ShopInstallStatusResult',
204
207
  schema: dsh_plugin_shop_shop_installStatus_result$schema,
205
208
  },
206
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":308,"column":3},
209
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":363,"column":3},
207
210
  },
208
211
  {
209
212
  id: 'dsh-plugin-shop#shop/restart',
@@ -218,7 +221,7 @@ export const TYPERT = {
218
221
  typeSymbol: 'dsh-plugin-shop/types#ShopRestartResult',
219
222
  schema: dsh_plugin_shop_shop_restart_result$schema,
220
223
  },
221
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":392,"column":9},
224
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":469,"column":9},
222
225
  },
223
226
  {
224
227
  id: 'dsh-plugin-shop#shop/setEnabled',
@@ -243,7 +246,7 @@ export const TYPERT = {
243
246
  typeSymbol: 'dsh-plugin-shop/types#ShopSetEnabledResult',
244
247
  schema: dsh_plugin_shop_shop_setEnabled_result$schema,
245
248
  },
246
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":213,"column":3},
249
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":239,"column":3},
247
250
  },
248
251
  {
249
252
  id: 'dsh-plugin-shop#shop/uninstallStart',
@@ -269,7 +272,7 @@ export const TYPERT = {
269
272
  typeSymbol: 'dsh-plugin-shop/types#ShopUninstallResult',
270
273
  schema: dsh_plugin_shop_shop_uninstallStart_result$schema,
271
274
  },
272
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":359,"column":9},
275
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":429,"column":9},
273
276
  },
274
277
  {
275
278
  id: 'dsh-plugin-shop#shop/updateStart',
@@ -294,7 +297,7 @@ export const TYPERT = {
294
297
  typeSymbol: 'dsh-plugin-shop/types#ShopUpdateResult',
295
298
  schema: dsh_plugin_shop_shop_updateStart_result$schema,
296
299
  },
297
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":441,"column":9},
300
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":518,"column":9},
298
301
  },
299
302
  {
300
303
  id: 'dsh-plugin-shop#shop/version',
@@ -309,7 +312,7 @@ export const TYPERT = {
309
312
  typeSymbol: 'dsh-plugin-shop/types#ShopVersionResult',
310
313
  schema: dsh_plugin_shop_shop_version_result$schema,
311
314
  },
312
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":426,"column":9},
315
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":503,"column":9},
313
316
  },
314
317
  ],
315
318
  model: {
@@ -395,7 +398,7 @@ export const TYPERT = {
395
398
  "types": [
396
399
  {
397
400
  "name": "CatalogEntry",
398
- "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; reviewer: string; reviewCommit: string; notes: string; };\n catalog?: CatalogSection;\n}"
401
+ "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; reviewer: string; reviewCommit: string; notes: string; };\n catalog?: CatalogSection;\n source: 'npm' | 'github';\n repo?: string;\n}"
399
402
  },
400
403
  {
401
404
  "name": "CatalogSection",
@@ -415,7 +418,7 @@ export const TYPERT = {
415
418
  },
416
419
  {
417
420
  "name": "InstallRejectionCode",
418
- "declaration": "export type InstallRejectionCode = 'not-in-catalog' | 'denied' | 'version-mismatch' | 'needs-acknowledgement';"
421
+ "declaration": "export type InstallRejectionCode = 'not-in-catalog' | 'denied' | 'version-mismatch' | 'needs-acknowledgement' | 'git-missing';"
419
422
  },
420
423
  {
421
424
  "name": "InstallState",
@@ -18,7 +18,8 @@ const dsh_plugin_shop_shop_catalog_result$schema = z.object({
18
18
  'tier': z.union([z.literal("verified"), z.literal("verified-stale"), z.literal("community")]),
19
19
  'metadata': z.union([z.literal("declared"), z.literal("derived")]),
20
20
  'review': z.union([z.undefined(), z.object({
21
- 'reviewedVersion': z.string(),
21
+ 'reviewedVersion': z.union([z.undefined(), z.string()]).optional(),
22
+ 'reviewedCommit': z.union([z.undefined(), z.string()]).optional(),
22
23
  'reviewer': z.string(),
23
24
  'reviewCommit': z.string(),
24
25
  'notes': z.string(),
@@ -31,6 +32,8 @@ const dsh_plugin_shop_shop_catalog_result$schema = z.object({
31
32
  }),
32
33
  'capabilities': z.array(z.string()),
33
34
  })]).optional(),
35
+ 'source': z.union([z.literal("npm"), z.literal("github")]),
36
+ 'repo': z.union([z.undefined(), z.string()]).optional(),
34
37
  })),
35
38
  'denied': z.array(z.object({
36
39
  'name': z.string(),
@@ -54,7 +57,7 @@ const dsh_plugin_shop_shop_installStart_result$schema = z.union([z.object({
54
57
  'installId': z.string(),
55
58
  }), z.object({
56
59
  'ok': z.literal(false),
57
- 'code': z.union([z.literal("not-in-catalog"), z.literal("denied"), z.literal("version-mismatch"), z.literal("needs-acknowledgement")]),
60
+ 'code': z.union([z.literal("not-in-catalog"), z.literal("denied"), z.literal("version-mismatch"), z.literal("needs-acknowledgement"), z.literal("git-missing")]),
58
61
  'detail': z.string(),
59
62
  })])
60
63
  const dsh_plugin_shop_shop_installStatus_parameter_0$schema = z.object({
@@ -134,7 +137,7 @@ export const TYPERT_REMOTE = {
134
137
  typeSymbol: 'dsh-plugin-shop/types#ShopCatalogResult',
135
138
  schema: dsh_plugin_shop_shop_catalog_result$schema,
136
139
  },
137
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":244,"column":9},
140
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":270,"column":9},
138
141
  },
139
142
  {
140
143
  id: 'dsh-plugin-shop#shop/installed',
@@ -149,7 +152,7 @@ export const TYPERT_REMOTE = {
149
152
  typeSymbol: 'dsh-plugin-shop#shop/installed:result',
150
153
  schema: dsh_plugin_shop_shop_installed_result$schema,
151
154
  },
152
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":319,"column":9},
155
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":374,"column":9},
153
156
  },
154
157
  {
155
158
  id: 'dsh-plugin-shop#shop/installStart',
@@ -175,7 +178,7 @@ export const TYPERT_REMOTE = {
175
178
  typeSymbol: 'dsh-plugin-shop/types#ShopInstallResult',
176
179
  schema: dsh_plugin_shop_shop_installStart_result$schema,
177
180
  },
178
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":270,"column":9},
181
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":296,"column":9},
179
182
  },
180
183
  {
181
184
  id: 'dsh-plugin-shop#shop/installStatus',
@@ -200,7 +203,7 @@ export const TYPERT_REMOTE = {
200
203
  typeSymbol: 'dsh-plugin-shop/types#ShopInstallStatusResult',
201
204
  schema: dsh_plugin_shop_shop_installStatus_result$schema,
202
205
  },
203
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":308,"column":3},
206
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":363,"column":3},
204
207
  },
205
208
  {
206
209
  id: 'dsh-plugin-shop#shop/restart',
@@ -215,7 +218,7 @@ export const TYPERT_REMOTE = {
215
218
  typeSymbol: 'dsh-plugin-shop/types#ShopRestartResult',
216
219
  schema: dsh_plugin_shop_shop_restart_result$schema,
217
220
  },
218
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":392,"column":9},
221
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":469,"column":9},
219
222
  },
220
223
  {
221
224
  id: 'dsh-plugin-shop#shop/setEnabled',
@@ -240,7 +243,7 @@ export const TYPERT_REMOTE = {
240
243
  typeSymbol: 'dsh-plugin-shop/types#ShopSetEnabledResult',
241
244
  schema: dsh_plugin_shop_shop_setEnabled_result$schema,
242
245
  },
243
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":213,"column":3},
246
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":239,"column":3},
244
247
  },
245
248
  {
246
249
  id: 'dsh-plugin-shop#shop/uninstallStart',
@@ -266,7 +269,7 @@ export const TYPERT_REMOTE = {
266
269
  typeSymbol: 'dsh-plugin-shop/types#ShopUninstallResult',
267
270
  schema: dsh_plugin_shop_shop_uninstallStart_result$schema,
268
271
  },
269
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":359,"column":9},
272
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":429,"column":9},
270
273
  },
271
274
  {
272
275
  id: 'dsh-plugin-shop#shop/updateStart',
@@ -291,7 +294,7 @@ export const TYPERT_REMOTE = {
291
294
  typeSymbol: 'dsh-plugin-shop/types#ShopUpdateResult',
292
295
  schema: dsh_plugin_shop_shop_updateStart_result$schema,
293
296
  },
294
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":441,"column":9},
297
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":518,"column":9},
295
298
  },
296
299
  {
297
300
  id: 'dsh-plugin-shop#shop/version',
@@ -306,7 +309,7 @@ export const TYPERT_REMOTE = {
306
309
  typeSymbol: 'dsh-plugin-shop/types#ShopVersionResult',
307
310
  schema: dsh_plugin_shop_shop_version_result$schema,
308
311
  },
309
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":426,"column":9},
312
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":503,"column":9},
310
313
  },
311
314
  ],
312
315
  }
@@ -44,6 +44,8 @@ export declare const zh: {
44
44
  notInCatalogCode: string;
45
45
  versionMismatchCode: string;
46
46
  needsAcknowledgementCode: string;
47
+ gitMissingCode: string;
48
+ githubSource: string;
47
49
  installedSection: string;
48
50
  installedVersion: string;
49
51
  latestVersion: string;
@@ -65,6 +67,7 @@ export declare const zh: {
65
67
  updateTransportFailed: string;
66
68
  checkUpdate: string;
67
69
  upToDate: string;
70
+ github: string;
68
71
  enabledSwitch: string;
69
72
  toggleFailed: string;
70
73
  hotApplyNote: string;
@@ -116,6 +119,8 @@ export declare const en: {
116
119
  notInCatalogCode: string;
117
120
  versionMismatchCode: string;
118
121
  needsAcknowledgementCode: string;
122
+ gitMissingCode: string;
123
+ githubSource: string;
119
124
  installedSection: string;
120
125
  installedVersion: string;
121
126
  latestVersion: string;
@@ -137,6 +142,7 @@ export declare const en: {
137
142
  updateTransportFailed: string;
138
143
  checkUpdate: string;
139
144
  upToDate: string;
145
+ github: string;
140
146
  enabledSwitch: string;
141
147
  toggleFailed: string;
142
148
  hotApplyNote: string;
@@ -17,6 +17,11 @@ export declare const ACKNOWLEDGEMENT_ZH = "\u5B89\u88C5\u540E\uFF0C\u6B64\u63D2\
17
17
  /** Install rejection code → locale key, for the rejected-state code label
18
18
  * (§7.2): every rejection the host can return has a human label here. */
19
19
  export declare function rejectionCodeKey(code: InstallRejectionCode): ShopLocaleKey;
20
+ /** The version a card displays: full for npm, the short commit for github. */
21
+ export declare function displayVersion(entry: {
22
+ source: 'npm' | 'github';
23
+ version: string;
24
+ }): string;
20
25
  /** One polled install status (§7.3 wire data), structural. */
21
26
  export interface InstallStatusShape {
22
27
  found: boolean;
@@ -1,7 +1,8 @@
1
1
  /** Catalog fetch, verification, and disk cache — the Host's only network path. */
2
2
  import type { CatalogEntry, DeniedEntry } from './types.ts';
3
- /** Highest schemaVersion this build understands; a higher one is refused (§10). */
4
- export declare const SUPPORTED_SCHEMA_VERSION = 2;
3
+ /** Highest schemaVersion this build understands; a higher one is refused (§10).
4
+ * 3 adds `source` and repo entries (github install channel). */
5
+ export declare const SUPPORTED_SCHEMA_VERSION = 3;
5
6
  export interface CatalogSnapshot {
6
7
  schemaVersion: number;
7
8
  builtAt: string;
@@ -6,6 +6,7 @@ import type { CatalogEntry, DeniedEntry } from './types.ts';
6
6
  import { type InstallArgs, type InstallRejectionCode } from './install.ts';
7
7
  import { type InstallStatus } from './executor.ts';
8
8
  import { type RestartOutcome } from './restart.ts';
9
+ import { type RepoPinFs } from './repo-pins.ts';
9
10
  export type { InstallArgs, InstallRejectionCode } from './install.ts';
10
11
  export type { CatalogEntry } from './types.ts';
11
12
  /** One Loader inventory entry, structurally — the shop never depends on
@@ -46,6 +47,11 @@ export interface ShopGatewayOptions {
46
47
  /** How long the gateway waits after a successful restart response before
47
48
  * exiting the old process; test-only shortening, production uses 2s. */
48
49
  restartExitDelayMs?: number;
50
+ /** Whether `git` is on PATH — a github entry cannot install without it;
51
+ * test-only injection, production probes the real binary. */
52
+ hasGit?: () => boolean;
53
+ /** Test-only injection: the pins file's filesystem; production uses node:fs. */
54
+ pinFs?: RepoPinFs;
49
55
  }
50
56
  /** `shop/installStart` result (§7.3): rejections are typed wire values with an
51
57
  * author-readable `detail`, not thrown RPC errors. */
@@ -138,6 +144,8 @@ export declare class ShopGateway extends TypertRemoteService {
138
144
  private readonly restartExitDelayMs;
139
145
  private readonly restartParentPid;
140
146
  private readonly latestVersion;
147
+ private readonly hasGit;
148
+ private readonly pinFs;
141
149
  /** The install gate runs against the last loaded snapshot, never a fresh
142
150
  * fetch per request (§7.2: the Host's cached snapshot is the truth). */
143
151
  /** Finished install records retained, so a poll sees the true terminal
@@ -154,6 +162,8 @@ export declare class ShopGateway extends TypertRemoteService {
154
162
  /** Every install id in insertion order, oldest first; finished-record eviction walks this from the front. */
155
163
  private readonly installOrder;
156
164
  constructor(ctx: Context, options?: ShopGatewayOptions);
165
+ /** The pins file lives in the shop's own cache, next to the catalog cache. */
166
+ private pinsPath;
157
167
  /** The boot's Loader root directory (the active profile's `cordis.yml`
158
168
  * directory, carried on `ctx.baseUrl`), when present. A `link:` install
159
169
  * keeps this package at its source location, so the walk-up from
@@ -1,6 +1,6 @@
1
1
  /** Install gate: the four rejection paths of §7.2, as a pure function. */
2
2
  import type { CatalogSnapshot } from './catalog.ts';
3
- export type InstallRejectionCode = 'not-in-catalog' | 'denied' | 'version-mismatch' | 'needs-acknowledgement';
3
+ export type InstallRejectionCode = 'not-in-catalog' | 'denied' | 'version-mismatch' | 'needs-acknowledgement' | 'git-missing';
4
4
  export interface InstallArgs {
5
5
  name: string;
6
6
  version: string;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The Host's memory of which commit it installed for each github entry.
3
+ *
4
+ * pnpm writes the manifest dependency as `github:owner/slug` — no commit —
5
+ * and the resolved commit lives only in the lockfile. The shop records the
6
+ * commit it pinned at install time in its own cache so `installed()` can
7
+ * report `outdated` honestly without parsing pnpm's lockfile. The profile
8
+ * manifest remains the source of truth for *presence*: a pin whose bundle
9
+ * is not a manifest dependency is a leftover from a failed install and is
10
+ * never reported.
11
+ */
12
+ export interface RepoPinFs {
13
+ exists: (path: string) => boolean;
14
+ read: (path: string) => string;
15
+ write: (path: string, data: string) => void;
16
+ }
17
+ /** Bundle name to the pinned commit. */
18
+ export type RepoPins = Record<string, string>;
19
+ /** Read the pins file; any irregularity degrades to an empty record. */
20
+ export declare function readRepoPins(fs: RepoPinFs, path: string): RepoPins;
21
+ /** Write the pins file atomically enough for its purpose (a cache). */
22
+ export declare function writeRepoPins(fs: RepoPinFs, path: string, pins: RepoPins): void;
@@ -18,12 +18,17 @@ export interface CatalogEntry {
18
18
  tier: 'verified' | 'verified-stale' | 'community';
19
19
  metadata: 'declared' | 'derived';
20
20
  review?: {
21
- reviewedVersion: string;
21
+ reviewedVersion?: string;
22
+ reviewedCommit?: string;
22
23
  reviewer: string;
23
24
  reviewCommit: string;
24
25
  notes: string;
25
26
  };
26
27
  catalog?: CatalogSection;
28
+ /** Where the entry installs from. */
29
+ source: 'npm' | 'github';
30
+ /** `owner/slug`; present exactly when `source` is github. */
31
+ repo?: string;
27
32
  }
28
33
  export interface DeniedEntry {
29
34
  name: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-shop",
3
- "version": "0.4.9",
3
+ "version": "0.4.11",
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",