dsh-plugin-shop 0.4.9 → 0.4.10

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.10
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.10
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.10
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
  }
@@ -4636,8 +4640,13 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4636
4640
  case "not-in-catalog": return "notInCatalogCode";
4637
4641
  case "version-mismatch": return "versionMismatchCode";
4638
4642
  case "needs-acknowledgement": return "needsAcknowledgementCode";
4643
+ case "git-missing": return "gitMissingCode";
4639
4644
  }
4640
4645
  }
4646
+ /** The version a card displays: full for npm, the short commit for github. */
4647
+ function displayVersion(entry) {
4648
+ return entry.source === "github" ? entry.version.slice(0, 7) : entry.version;
4649
+ }
4641
4650
  /** §7.2 once-per-second poll cadence, as a named constant. */
4642
4651
  const INSTALL_POLL_MS = 1e3;
4643
4652
  /** How long the check-update button reports "up to date" after a re-check
@@ -4695,10 +4704,6 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4695
4704
  "shop",
4696
4705
  "marketplace"
4697
4706
  ];
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
4707
  /**
4703
4708
  * Competing plugin marketplaces whose names carry no store/market keyword
4704
4709
  * and so escape the pattern rules below (spec §7.2). Each is a real npm
@@ -4827,6 +4832,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4827
4832
  notInCatalogCode: "不在目录中",
4828
4833
  versionMismatchCode: "版本不匹配",
4829
4834
  needsAcknowledgementCode: "需要确认",
4835
+ gitMissingCode: "缺少 git",
4836
+ githubSource: "来自 GitHub 仓库安装",
4830
4837
  installedSection: "已安装",
4831
4838
  installedVersion: "已安装 v{version}",
4832
4839
  latestVersion: "最新 v{version}",
@@ -4898,6 +4905,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4898
4905
  notInCatalogCode: "Not in the catalog",
4899
4906
  versionMismatchCode: "Version mismatch",
4900
4907
  needsAcknowledgementCode: "Acknowledgement required",
4908
+ gitMissingCode: "git missing",
4909
+ githubSource: "installed from a GitHub repository",
4901
4910
  installedSection: "Installed",
4902
4911
  installedVersion: "installed v{version}",
4903
4912
  latestVersion: "latest v{version}",
@@ -5074,7 +5083,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5074
5083
  }
5075
5084
  //#endregion
5076
5085
  //#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";
5086
+ 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/* Single-line rows: one full-width column, one plugin per line. */\n.sf657f5_cards {\n display: grid;\n grid-template-columns: 1fr;\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: row;\n align-items: center;\n flex-wrap: wrap;\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 52px;\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 /* Basis 0: the header takes only the space the action buttons leave,\n * so long names truncate instead of wrapping the buttons off the row. */\n flex: 1 1 0;\n min-width: 0;\n padding: 8px 12px;\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: nowrap;\n gap: 6px;\n}\n\n/* The action area is virtual: its children lay out as direct card rows. */\n.s9b5048_cardActions {\n display: contents;\n}\n\n/* The first action owns the right edge; the rest follow it. */\n.s9b5048_cardActions > :first-child {\n margin-left: auto;\n}\n\n/* A flow that outgrows the row — install/uninstall gate, log, notices, the\n * expanded body — breaks onto its own full-width line below it. Nested\n * blocks inside those panels ignore the basis. */\n.s65f383_card > .sc4f2ac_gate,\n.s65f383_card > .s4d374c_installPanel,\n.s65f383_card > .sf53ca0_installedActions,\n.s65f383_card > .s820ebf_body {\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.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 /* A wrapping pill would stretch the single-line card to two lines. */\n white-space: nowrap;\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 margin-top: 2px;\n padding: 10px 12px 12px;\n border-top: 1px solid var(--dsw-alias-border-l1);\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
5087
  if (typeof document !== "undefined" && !document.querySelector("style[data-plugin-css=\"e3675a89\"]")) {
5079
5088
  const tag = document.createElement("style");
5080
5089
  tag.dataset.pluginCss = "e3675a89";
@@ -5118,17 +5127,22 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5118
5127
  "cardsSentry": "sd7ee01_cardsSentry",
5119
5128
  "showingLine": "s705cc6_showingLine",
5120
5129
  "cardSpine": "s4ce25d_cardSpine",
5121
- "cardCover": "s9e2bec_cardCover",
5122
- "coverLabel": "s1a7c5b_coverLabel",
5130
+ "categoryBadge": "s9c8eaa_categoryBadge",
5123
5131
  "entryHeader": "sa97234_entryHeader",
5124
5132
  "name": "sc3f149_name",
5125
5133
  "badges": "s513026_badges",
5134
+ "cardActions": "s9b5048_cardActions",
5135
+ "gate": "sc4f2ac_gate",
5136
+ "installPanel": "s4d374c_installPanel",
5137
+ "installedActions": "sf53ca0_installedActions",
5138
+ "body": "s820ebf_body",
5139
+ "cardVersion": "sc6ec6e_cardVersion",
5140
+ "sourceBadge": "s71f10f_sourceBadge",
5126
5141
  "tierBadge": "s5e82cb_tierBadge",
5127
5142
  "unclaimedBadge": "s23e3e1_unclaimedBadge",
5128
5143
  "starsBadge": "sff43b4_starsBadge",
5129
5144
  "chevron": "s73f7fb_chevron",
5130
5145
  "chevronOpen": "s61393c_chevronOpen",
5131
- "body": "s820ebf_body",
5132
5146
  "summary": "sb8fbc8_summary",
5133
5147
  "summaryZh": "se507db_summaryZh",
5134
5148
  "summaryExpanded": "s10baf0_summaryExpanded",
@@ -5140,15 +5154,12 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5140
5154
  "detailRows": "se98179_detailRows",
5141
5155
  "detailRow": "sfb4d2f_detailRow",
5142
5156
  "reviewedLine": "sd58c6d_reviewedLine",
5143
- "installPanel": "s4d374c_installPanel",
5144
5157
  "installButton": "se7aaed_installButton",
5145
5158
  "confirmButton": "sb43a49_confirmButton",
5146
5159
  "cancelButton": "s6e45d1_cancelButton",
5147
5160
  "installedLabel": "sb6fc74_installedLabel",
5148
- "installedActions": "sf53ca0_installedActions",
5149
5161
  "uninstallButton": "s96967f_uninstallButton",
5150
5162
  "restartButton": "s6ac237_restartButton",
5151
- "gate": "sc4f2ac_gate",
5152
5163
  "gateTitle": "s559c38_gateTitle",
5153
5164
  "gateBody": "s3c0ace_gateBody",
5154
5165
  "gateActions": "s4b2373_gateActions",
@@ -5212,14 +5223,6 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5212
5223
  className: _dsh_plugin_shop_css_e3675a89_default.cardSpine,
5213
5224
  "aria-hidden": "true"
5214
5225
  }),
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
5226
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
5224
5227
  type: "button",
5225
5228
  className: _dsh_plugin_shop_css_e3675a89_default.entryHeader,
@@ -5232,6 +5235,28 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5232
5235
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
5233
5236
  className: _dsh_plugin_shop_css_e3675a89_default.badges,
5234
5237
  children: [
5238
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5239
+ className: _dsh_plugin_shop_css_e3675a89_default.categoryBadge,
5240
+ children: t(categoryKey(entry))
5241
+ }),
5242
+ entry.source === "github" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5243
+ className: _dsh_plugin_shop_css_e3675a89_default.sourceBadge,
5244
+ role: "img",
5245
+ "aria-label": t("githubSource"),
5246
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
5247
+ viewBox: "0 0 16 16",
5248
+ width: "12",
5249
+ height: "12",
5250
+ "aria-hidden": "true",
5251
+ fill: "currentColor",
5252
+ 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" })
5253
+ })
5254
+ }),
5255
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
5256
+ className: _dsh_plugin_shop_css_e3675a89_default.cardVersion,
5257
+ "data-card-version": true,
5258
+ children: ["v", displayVersion(entry)]
5259
+ }),
5235
5260
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5236
5261
  className: _dsh_plugin_shop_css_e3675a89_default.tierBadge,
5237
5262
  "data-tier": entry.tier,
@@ -5251,15 +5276,46 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5251
5276
  ]
5252
5277
  })]
5253
5278
  }),
5254
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5279
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5280
+ className: _dsh_plugin_shop_css_e3675a89_default.cardActions,
5281
+ children: installed === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InstallPanel, {
5282
+ name: entry.name,
5283
+ version: entry.version,
5284
+ tier: entry.tier,
5285
+ t,
5286
+ install,
5287
+ installStatus,
5288
+ restart
5289
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [installed.outdated ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InstallPanel, {
5290
+ name: entry.name,
5291
+ version: entry.version,
5292
+ tier: entry.tier,
5293
+ variant: "update",
5294
+ t,
5295
+ install,
5296
+ installStatus,
5297
+ restart
5298
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5299
+ className: _dsh_plugin_shop_css_e3675a89_default.installedLabel,
5300
+ "data-shop-installed": true,
5301
+ children: t("installed")
5302
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UninstallPanel, {
5303
+ name: entry.name,
5304
+ t,
5305
+ uninstall,
5306
+ installStatus,
5307
+ restart
5308
+ })] })
5309
+ }),
5310
+ open && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5255
5311
  className: _dsh_plugin_shop_css_e3675a89_default.body,
5256
5312
  children: [
5257
5313
  summary !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5258
- className: open ? `${_dsh_plugin_shop_css_e3675a89_default.summary} ${_dsh_plugin_shop_css_e3675a89_default.summaryExpanded}` : _dsh_plugin_shop_css_e3675a89_default.summary,
5314
+ className: `${_dsh_plugin_shop_css_e3675a89_default.summary} ${_dsh_plugin_shop_css_e3675a89_default.summaryExpanded}`,
5259
5315
  children: summary.en
5260
5316
  }),
5261
5317
  summary?.zh !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5262
- className: open ? `${_dsh_plugin_shop_css_e3675a89_default.summaryZh} ${_dsh_plugin_shop_css_e3675a89_default.summaryZhExpanded}` : _dsh_plugin_shop_css_e3675a89_default.summaryZh,
5318
+ className: `${_dsh_plugin_shop_css_e3675a89_default.summaryZh} ${_dsh_plugin_shop_css_e3675a89_default.summaryZhExpanded}`,
5263
5319
  children: summary.zh
5264
5320
  }),
5265
5321
  entry.catalog !== void 0 && entry.catalog.capabilities.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -5272,7 +5328,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5272
5328
  children: entry.catalog.capabilities.map((capability) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: capability }, capability))
5273
5329
  })]
5274
5330
  }),
5275
- open && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
5331
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
5276
5332
  id: detailId,
5277
5333
  className: _dsh_plugin_shop_css_e3675a89_default.detail,
5278
5334
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dl", {
@@ -5303,37 +5359,6 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5303
5359
  current: entry.version
5304
5360
  })
5305
5361
  })]
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
5362
  })
5338
5363
  ]
5339
5364
  })
@@ -5616,7 +5641,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5616
5641
  * installed-state RPC behind it — the tab assumes an installed plugin is on,
5617
5642
  * so the first toggle sends the inverted value. After a successful `setEnabled`
5618
5643
  * the §8 hot note renders. */
5619
- function OutdatedRow({ row, tier, t, setEnabled, install, installStatus, restart }) {
5644
+ function OutdatedRow({ row, tier, source, t, setEnabled, install, installStatus, restart }) {
5620
5645
  const [enabled, setEnabledState] = (0, react.useState)(true);
5621
5646
  const [toggle, setToggle] = (0, react.useState)({ kind: "idle" });
5622
5647
  const onToggle = async () => {
@@ -5655,7 +5680,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5655
5680
  className: _dsh_plugin_shop_css_e3675a89_default.outdatedVersions,
5656
5681
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5657
5682
  className: _dsh_plugin_shop_css_e3675a89_default.outdatedVersion,
5658
- children: t("installedVersion", { version: row.installed })
5683
+ children: t("installedVersion", { version: source === "github" ? row.installed.slice(0, 7) : row.installed })
5659
5684
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5660
5685
  className: _dsh_plugin_shop_css_e3675a89_default.outdatedVersion,
5661
5686
  children: t("latestVersion", { version: row.latest })
@@ -5705,7 +5730,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5705
5730
  * tier for the update gate is looked up from the catalog by name (community →
5706
5731
  * acknowledgement); an entry absent from the catalog defaults to the
5707
5732
  * community gate (the safer read). */
5708
- function OutdatedSection({ state, tiers, t, setEnabled, install, installStatus, restart }) {
5733
+ function OutdatedSection({ state, tiers, sources, t, setEnabled, install, installStatus, restart }) {
5709
5734
  if (state.kind === "loading") return null;
5710
5735
  if (state.kind === "error") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5711
5736
  className: _dsh_plugin_shop_css_e3675a89_default.stateLine,
@@ -5725,6 +5750,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5725
5750
  children: outdated.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OutdatedRow, {
5726
5751
  row,
5727
5752
  tier: tiers.get(row.name) ?? "community",
5753
+ source: sources.get(row.name) ?? "npm",
5728
5754
  t,
5729
5755
  setEnabled,
5730
5756
  install,
@@ -5827,7 +5853,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5827
5853
  if (catalogState.kind !== "ready") return [];
5828
5854
  const q = query.trim().toLowerCase();
5829
5855
  return catalogState.result.plugins.filter((entry) => {
5830
- if (isShopLike(entry.name)) return false;
5856
+ if (isShopLike(entry.name) || entry.repo !== void 0 && isShopLike(entry.repo)) return false;
5831
5857
  if (category === "installed") {
5832
5858
  if (!installedByName.has(entry.name)) return false;
5833
5859
  } else if (category !== null && categoryKey(entry) !== categoryLocaleKey(category)) return false;
@@ -5863,7 +5889,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5863
5889
  const categoryCounts = (0, react.useMemo)(() => {
5864
5890
  const counts = /* @__PURE__ */ new Map();
5865
5891
  if (catalogState.kind === "ready") for (const entry of catalogState.result.plugins) {
5866
- if (isShopLike(entry.name)) continue;
5892
+ if (isShopLike(entry.name) || entry.repo !== void 0 && isShopLike(entry.repo)) continue;
5867
5893
  const key = categoryKey(entry);
5868
5894
  const bare = CATEGORY_ORDER.find((c) => categoryLocaleKey(c) === key);
5869
5895
  if (bare !== void 0) counts.set(bare, (counts.get(bare) ?? 0) + 1);
@@ -5879,6 +5905,11 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5879
5905
  if (catalogState.kind === "ready") for (const entry of catalogState.result.plugins) map.set(entry.name, entry.tier);
5880
5906
  return map;
5881
5907
  }, [catalogState]);
5908
+ const sources = (0, react.useMemo)(() => {
5909
+ const map = /* @__PURE__ */ new Map();
5910
+ if (catalogState.kind === "ready") for (const entry of catalogState.result.plugins) map.set(entry.name, entry.source);
5911
+ return map;
5912
+ }, [catalogState]);
5882
5913
  if (catalogState.kind === "loading") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5883
5914
  className: _dsh_plugin_shop_css_e3675a89_default.panel,
5884
5915
  "data-shop-tab": true,
@@ -6107,7 +6138,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6107
6138
  className: _dsh_plugin_shop_css_e3675a89_default.cards,
6108
6139
  children: [visible.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EntryCard, {
6109
6140
  entry,
6110
- stars: stars[entry.name],
6141
+ stars: stars[entry.repo ?? entry.name],
6111
6142
  installed: installedByName.get(entry.name),
6112
6143
  t,
6113
6144
  install,
@@ -6131,6 +6162,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6131
6162
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OutdatedSection, {
6132
6163
  state: installedState,
6133
6164
  tiers,
6165
+ sources,
6134
6166
  t,
6135
6167
  setEnabled,
6136
6168
  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.10",
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",