dsh-plugin-shop 0.4.4 → 0.4.6

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
@@ -46,10 +46,13 @@ install them once with `npm install -g @deepseek-ai/dsh pnpm` and verify with
46
46
  `dsh --version` and `pnpm --version`.
47
47
 
48
48
  ```sh
49
- # dsh on PATH (global install):
50
- dsh plugin --profile web add dsh-plugin-shop
49
+ # dsh on PATH (global install). The explicit version pin matters: pnpm 11
50
+ # holds back very recent releases, so a bare `add dsh-plugin-shop` can hand
51
+ # you an older version for a while. Pin the current release — refresh it
52
+ # with `npm view dsh-plugin-shop version`.
53
+ dsh plugin --profile web add dsh-plugin-shop@0.4.6
51
54
  # or straight through npx, nothing installed:
52
- npx -y @deepseek-ai/dsh plugin --profile web add dsh-plugin-shop
55
+ npx -y @deepseek-ai/dsh plugin --profile web add dsh-plugin-shop@0.4.6
53
56
  ```
54
57
 
55
58
  Replace `web` with your profile if you use another one. Then restart `dsh` once — a
@@ -75,10 +78,11 @@ profile.
75
78
  ls -1 "${DSH_HOME:-$HOME/.dsh}/profiles" | grep -v '^node_modules$'
76
79
  ```
77
80
 
78
- **2. Install.**
81
+ **2. Install.** Pin the version — the explicit pin bypasses pnpm's release
82
+ cooldown, and deterministic installs are the point of the agent path.
79
83
 
80
84
  ```sh
81
- dsh plugin --profile <profile> add dsh-plugin-shop
85
+ dsh plugin --profile <profile> add dsh-plugin-shop@0.4.6
82
86
  ```
83
87
 
84
88
  **3. Verify — do not skip this.** A zero exit from step 2 means pnpm resolved the
@@ -101,7 +105,7 @@ bundle is not.
101
105
  | What you see | What it means | What to do |
102
106
  |---|---|---|
103
107
  | `error: required option '--profile <name>' not specified` | `--profile` was omitted | Pass it; there is no default |
104
- | A version older than npm's `latest` gets installed | pnpm 11 holds back very recently published versions | Expected, not an error; the newest lands once its cooldown passes |
108
+ | A version older than npm's `latest` gets installed | pnpm 11 holds back very recently published versions | Pin the version explicitly: `dsh plugin --profile <p> add dsh-plugin-shop@<version>` |
105
109
  | `client bundles not found ... lib/client.js` | the copy on disk was built without its browser half | Install from npm rather than from a source checkout, or run `pnpm build` in that checkout |
106
110
  | `dsh: pnpm not found on PATH — install pnpm to manage profile plugins` | `dsh plugin` forwards to pnpm, and pnpm is missing | `npm install -g pnpm` |
107
111
  | `no profile directory found above <path>` | the plugin could not locate its profile | Please report it — this is resolved from `ctx.baseUrl` and should not fail |
@@ -116,6 +120,7 @@ bundle is not.
116
120
  | **Enable / disable** | Applies to an installed plugin without a restart |
117
121
  | **Installed state** | An installed plugin shows an Installed label on its card — or an Update button when the catalog has a newer version — plus an Uninstall button; the Installed filter in the category bar shows only installed plugins |
118
122
  | **Restart** | After an install, update, or uninstall, the shop offers to restart dsh — stating the cost first: the page disconnects and in-flight work is interrupted |
123
+ | **Self-update** | The shop shows its own version next to the search box, checks npm for a newer release, and updates itself with the pinned version — then the usual restart |
119
124
 
120
125
  ## 🧩 How it is put together
121
126
 
@@ -125,9 +130,9 @@ boundary:
125
130
  | Half | Entry | Can reach | Cannot reach |
126
131
  |---|---|---|---|
127
132
  | **Host** | `dsh-plugin-shop` | The network (catalog fetch and sha256 verify), the filesystem (cache), `dsh plugin add` under a per-profile mutex | — |
128
- | **Client** | `dsh-plugin-shop/client` | Exactly seven `shop/*` Remote methods | The network, the filesystem |
133
+ | **Client** | `dsh-plugin-shop/client` | Exactly nine `shop/*` Remote methods | The network, the filesystem |
129
134
 
130
- Compromising the browser half buys an attacker those seven calls and nothing more.
135
+ Compromising the browser half buys an attacker those nine calls and nothing more.
131
136
 
132
137
  ## ⚙️ Configuration
133
138
 
package/lib/client.js CHANGED
@@ -4370,6 +4370,19 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4370
4370
  "ok": literal(false),
4371
4371
  "detail": string()
4372
4372
  })]);
4373
+ const dsh_plugin_shop_shop_updateStart_parameter_0$schema = object({ "version": string() });
4374
+ const dsh_plugin_shop_shop_updateStart_result$schema = union([object({
4375
+ "ok": literal(true),
4376
+ "installId": string()
4377
+ }), object({
4378
+ "ok": literal(false),
4379
+ "detail": string()
4380
+ })]);
4381
+ const dsh_plugin_shop_shop_version_result$schema = object({
4382
+ "installed": string(),
4383
+ "latest": union([literal(null), string()]),
4384
+ "outdated": boolean()
4385
+ });
4373
4386
  const TYPERT_REMOTE = {
4374
4387
  package: "dsh-plugin-shop",
4375
4388
  descriptors: [
@@ -4397,7 +4410,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4397
4410
  },
4398
4411
  sourceLocation: {
4399
4412
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4400
- "line": 225,
4413
+ "line": 244,
4401
4414
  "column": 9
4402
4415
  }
4403
4416
  },
@@ -4415,7 +4428,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4415
4428
  },
4416
4429
  sourceLocation: {
4417
4430
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4418
- "line": 300,
4431
+ "line": 319,
4419
4432
  "column": 9
4420
4433
  }
4421
4434
  },
@@ -4443,7 +4456,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4443
4456
  },
4444
4457
  sourceLocation: {
4445
4458
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4446
- "line": 251,
4459
+ "line": 270,
4447
4460
  "column": 9
4448
4461
  }
4449
4462
  },
@@ -4470,7 +4483,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4470
4483
  },
4471
4484
  sourceLocation: {
4472
4485
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4473
- "line": 289,
4486
+ "line": 308,
4474
4487
  "column": 3
4475
4488
  }
4476
4489
  },
@@ -4488,7 +4501,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4488
4501
  },
4489
4502
  sourceLocation: {
4490
4503
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4491
- "line": 373,
4504
+ "line": 392,
4492
4505
  "column": 9
4493
4506
  }
4494
4507
  },
@@ -4515,7 +4528,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4515
4528
  },
4516
4529
  sourceLocation: {
4517
4530
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4518
- "line": 194,
4531
+ "line": 213,
4519
4532
  "column": 3
4520
4533
  }
4521
4534
  },
@@ -4543,7 +4556,52 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4543
4556
  },
4544
4557
  sourceLocation: {
4545
4558
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4546
- "line": 340,
4559
+ "line": 359,
4560
+ "column": 9
4561
+ }
4562
+ },
4563
+ {
4564
+ id: "dsh-plugin-shop#shop/updateStart",
4565
+ service: "shop",
4566
+ namespace: "shop",
4567
+ method: "updateStart",
4568
+ invocation: { kind: "direct" },
4569
+ parameters: [{
4570
+ name: "args",
4571
+ wire: "args",
4572
+ source: "json",
4573
+ codec: {
4574
+ mode: "strict",
4575
+ typeSymbol: "dsh-plugin-shop#shop/updateStart:args",
4576
+ schema: dsh_plugin_shop_shop_updateStart_parameter_0$schema
4577
+ }
4578
+ }],
4579
+ result: {
4580
+ mode: "strict",
4581
+ typeSymbol: "dsh-plugin-shop/types#ShopUpdateResult",
4582
+ schema: dsh_plugin_shop_shop_updateStart_result$schema
4583
+ },
4584
+ sourceLocation: {
4585
+ "file": "packages/dsh-plugin-shop/src/host/index.ts",
4586
+ "line": 441,
4587
+ "column": 9
4588
+ }
4589
+ },
4590
+ {
4591
+ id: "dsh-plugin-shop#shop/version",
4592
+ service: "shop",
4593
+ namespace: "shop",
4594
+ method: "version",
4595
+ invocation: { kind: "direct" },
4596
+ parameters: [],
4597
+ result: {
4598
+ mode: "strict",
4599
+ typeSymbol: "dsh-plugin-shop/types#ShopVersionResult",
4600
+ schema: dsh_plugin_shop_shop_version_result$schema
4601
+ },
4602
+ sourceLocation: {
4603
+ "file": "packages/dsh-plugin-shop/src/host/index.ts",
4604
+ "line": 426,
4547
4605
  "column": 9
4548
4606
  }
4549
4607
  }
@@ -4769,6 +4827,9 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4769
4827
  restarting: "正在重启,服务器就绪后页面会自动刷新…",
4770
4828
  restartFailedNotice: "重启后服务器没有回来。请手动启动:dsh web(新进程的日志在商店缓存目录的 restart.log)",
4771
4829
  restartTransportFailed: "重启请求未能送达。请稍后重试。",
4830
+ updateFailed: "更新失败",
4831
+ updateTransportFailed: "更新请求未能送达。请稍后重试。",
4832
+ checkUpdate: "检查更新",
4772
4833
  enabledSwitch: "启用",
4773
4834
  toggleFailed: "设置失败,请重试。",
4774
4835
  hotApplyNote: "无需重启,立即生效"
@@ -4835,6 +4896,9 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4835
4896
  restarting: "Restarting; the page will refresh once the server is back…",
4836
4897
  restartFailedNotice: "The server did not come back after the restart. Start it manually: dsh web (see restart.log in the shop cache directory)",
4837
4898
  restartTransportFailed: "The restart request could not be delivered. Please retry.",
4899
+ updateFailed: "Update failed",
4900
+ updateTransportFailed: "The update request could not be delivered. Please retry.",
4901
+ checkUpdate: "Check for updates",
4838
4902
  enabledSwitch: "Enable",
4839
4903
  toggleFailed: "Failed to update. Please retry.",
4840
4904
  hotApplyNote: "takes effect without a restart"
@@ -4946,8 +5010,50 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4946
5010
  };
4947
5011
  }
4948
5012
  //#endregion
5013
+ //#region src/client/useUpdateSelf.ts
5014
+ /** Self-update driving hook for the shop itself: start, poll to terminal.
5015
+ * Shares the install view machine and poll loop; the start mapping is the
5016
+ * uninstall one — a business refusal (a non-semver version) lands in the
5017
+ * `failed` view with the host's detail, never in the install `rejected`
5018
+ * codes. */
5019
+ /** Drive one self-update: the host's business union, the shared polling
5020
+ * loop, and terminal states. The `rejected` install state never occurs. */
5021
+ function useUpdateSelf(updateStart, installStatus) {
5022
+ const [view, setView] = (0, react.useState)({ kind: "idle" });
5023
+ const start = (0, react.useCallback)(async (args) => {
5024
+ setView({ kind: "idle" });
5025
+ try {
5026
+ const result = await updateStart(args);
5027
+ if (!result.ok) {
5028
+ setView({
5029
+ kind: "failed",
5030
+ detail: result.detail,
5031
+ log: []
5032
+ });
5033
+ return;
5034
+ }
5035
+ setView({
5036
+ kind: "running",
5037
+ installId: result.installId,
5038
+ log: []
5039
+ });
5040
+ } catch {
5041
+ setView({
5042
+ kind: "failed",
5043
+ detail: "",
5044
+ log: []
5045
+ });
5046
+ }
5047
+ }, [updateStart]);
5048
+ usePollStatus(view, setView, installStatus);
5049
+ return {
5050
+ view,
5051
+ start
5052
+ };
5053
+ }
5054
+ //#endregion
4949
5055
  //#region \0dsh-plugin-shop-css:e3675a89
4950
- 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.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";
5056
+ 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 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";
4951
5057
  if (typeof document !== "undefined" && !document.querySelector("style[data-plugin-css=\"e3675a89\"]")) {
4952
5058
  const tag = document.createElement("style");
4953
5059
  tag.dataset.pluginCss = "e3675a89";
@@ -4970,6 +5076,12 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4970
5076
  "skeletonSummaryShort": "sbb0d25_skeletonSummaryShort",
4971
5077
  "actionButton": "s73242a_actionButton",
4972
5078
  "toolbar": "s646509_toolbar",
5079
+ "toolbarRight": "s745e3c_toolbarRight",
5080
+ "versionBlock": "s4fe61f_versionBlock",
5081
+ "versionText": "sb9b924_versionText",
5082
+ "updateSelfButton": "sa04455_updateSelfButton",
5083
+ "checkUpdateButton": "s614a38_checkUpdateButton",
5084
+ "selfUpdatePanel": "sb81ba5_selfUpdatePanel",
4973
5085
  "searchInput": "s232ec4_searchInput",
4974
5086
  "staleBlock": "s1555aa_staleBlock",
4975
5087
  "staleBadge": "s3946f1_staleBadge",
@@ -5601,9 +5713,11 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5601
5713
  /** The shop tab root: browse, search, refresh, and render one card per
5602
5714
  * entry. Data attributes on the e2e-relevant nodes follow the Task 3 list. */
5603
5715
  function ShopTab(props) {
5604
- const { t, catalog, install, installStatus, setEnabled, installed, uninstall, restart } = props;
5716
+ const { t, catalog, install, installStatus, setEnabled, installed, uninstall, restart, version, updateStart } = props;
5605
5717
  const [catalogState, setCatalogState] = (0, react.useState)({ kind: "loading" });
5606
5718
  const [installedState, setInstalledState] = (0, react.useState)({ kind: "loading" });
5719
+ const [selfVersion, setSelfVersion] = (0, react.useState)(null);
5720
+ const selfUpdate = useUpdateSelf(updateStart, installStatus);
5607
5721
  const [request, setRequest] = (0, react.useState)({ kind: "initial" });
5608
5722
  const [query, setQuery] = (0, react.useState)("");
5609
5723
  const [category, setCategory] = (0, react.useState)(null);
@@ -5630,6 +5744,28 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5630
5744
  cancelled = true;
5631
5745
  };
5632
5746
  }, [catalog, request]);
5747
+ (0, react.useEffect)(() => {
5748
+ let cancelled = false;
5749
+ const load = async () => {
5750
+ try {
5751
+ const result = await version();
5752
+ if (!cancelled) setSelfVersion(result);
5753
+ } catch {}
5754
+ };
5755
+ load();
5756
+ return () => {
5757
+ cancelled = true;
5758
+ };
5759
+ }, [version, request]);
5760
+ const [checkingVersion, setCheckingVersion] = (0, react.useState)(false);
5761
+ const checkVersion = async () => {
5762
+ setCheckingVersion(true);
5763
+ try {
5764
+ setSelfVersion(await version());
5765
+ } catch {} finally {
5766
+ setCheckingVersion(false);
5767
+ }
5768
+ };
5633
5769
  (0, react.useEffect)(() => {
5634
5770
  let cancelled = false;
5635
5771
  const load = async () => {
@@ -5761,19 +5897,97 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5761
5897
  setQuery(event.target.value);
5762
5898
  setVisibleCount(48);
5763
5899
  }
5764
- }), result.stale && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5765
- className: _dsh_plugin_shop_css_e3675a89_default.staleBlock,
5766
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5767
- className: _dsh_plugin_shop_css_e3675a89_default.staleBadge,
5768
- children: t("staleLabel", { date: result.builtAt.slice(0, 10) })
5769
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5770
- type: "button",
5771
- className: _dsh_plugin_shop_css_e3675a89_default.actionButton,
5772
- onClick: () => setRequest({ kind: "refresh" }),
5773
- children: t("refresh")
5900
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5901
+ className: _dsh_plugin_shop_css_e3675a89_default.toolbarRight,
5902
+ children: [result.stale && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5903
+ className: _dsh_plugin_shop_css_e3675a89_default.staleBlock,
5904
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5905
+ className: _dsh_plugin_shop_css_e3675a89_default.staleBadge,
5906
+ children: t("staleLabel", { date: result.builtAt.slice(0, 10) })
5907
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5908
+ type: "button",
5909
+ className: _dsh_plugin_shop_css_e3675a89_default.actionButton,
5910
+ onClick: () => setRequest({ kind: "refresh" }),
5911
+ children: t("refresh")
5912
+ })]
5913
+ }), selfVersion !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5914
+ className: _dsh_plugin_shop_css_e3675a89_default.versionBlock,
5915
+ children: [
5916
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
5917
+ className: _dsh_plugin_shop_css_e3675a89_default.versionText,
5918
+ "data-shop-version": true,
5919
+ children: ["v", selfVersion.installed]
5920
+ }),
5921
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5922
+ type: "button",
5923
+ className: _dsh_plugin_shop_css_e3675a89_default.checkUpdateButton,
5924
+ "data-shop-check-update": true,
5925
+ disabled: checkingVersion,
5926
+ onClick: () => void checkVersion(),
5927
+ children: t("checkUpdate")
5928
+ }),
5929
+ selfVersion.outdated && selfVersion.latest !== null && selfUpdate.view.kind === "idle" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5930
+ type: "button",
5931
+ className: _dsh_plugin_shop_css_e3675a89_default.updateSelfButton,
5932
+ "data-shop-update-self": true,
5933
+ onClick: () => {
5934
+ if (selfVersion.latest !== null) selfUpdate.start({ version: selfVersion.latest });
5935
+ },
5936
+ children: t("update")
5937
+ })
5938
+ ]
5774
5939
  })]
5775
5940
  })]
5776
5941
  }),
5942
+ selfUpdate.view.kind === "running" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5943
+ className: _dsh_plugin_shop_css_e3675a89_default.selfUpdatePanel,
5944
+ "data-shop-self-updating": true,
5945
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5946
+ className: _dsh_plugin_shop_css_e3675a89_default.installing,
5947
+ children: t("installing")
5948
+ }), selfUpdate.view.log.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5949
+ className: _dsh_plugin_shop_css_e3675a89_default.log,
5950
+ children: selfUpdate.view.log.map((line, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5951
+ className: _dsh_plugin_shop_css_e3675a89_default.logLine,
5952
+ children: line
5953
+ }, index))
5954
+ })]
5955
+ }),
5956
+ selfUpdate.view.kind === "done" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5957
+ className: _dsh_plugin_shop_css_e3675a89_default.selfUpdatePanel,
5958
+ "data-shop-self-update-done": true,
5959
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5960
+ className: _dsh_plugin_shop_css_e3675a89_default.installedActions,
5961
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5962
+ className: _dsh_plugin_shop_css_e3675a89_default.notice,
5963
+ children: t("installedRestartNotice")
5964
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RestartPanel, {
5965
+ t,
5966
+ restart
5967
+ })]
5968
+ })
5969
+ }),
5970
+ selfUpdate.view.kind === "failed" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5971
+ className: _dsh_plugin_shop_css_e3675a89_default.selfUpdatePanel,
5972
+ "data-shop-self-update-failed": true,
5973
+ children: [
5974
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5975
+ className: _dsh_plugin_shop_css_e3675a89_default.failedHeading,
5976
+ children: t("updateFailed")
5977
+ }),
5978
+ selfUpdate.view.log.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5979
+ className: _dsh_plugin_shop_css_e3675a89_default.log,
5980
+ children: selfUpdate.view.log.map((line, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5981
+ className: _dsh_plugin_shop_css_e3675a89_default.logLine,
5982
+ children: line
5983
+ }, index))
5984
+ }),
5985
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5986
+ className: _dsh_plugin_shop_css_e3675a89_default.failedDetail,
5987
+ children: selfUpdate.view.detail === "" ? t("updateTransportFailed") : selfUpdate.view.detail
5988
+ })
5989
+ ]
5990
+ }),
5777
5991
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5778
5992
  className: _dsh_plugin_shop_css_e3675a89_default.categoryBar,
5779
5993
  role: "group",
@@ -5913,7 +6127,9 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5913
6127
  setEnabled: async (args) => unwrap(await ns.setEnabled(args)),
5914
6128
  installed: async () => unwrap(await ns.installed()),
5915
6129
  uninstall: async (args) => unwrap(await ns.uninstallStart(args)),
5916
- restart: async () => unwrap(await ns.restart())
6130
+ restart: async () => unwrap(await ns.restart()),
6131
+ version: async () => unwrap(await ns.version()),
6132
+ updateStart: async (args) => unwrap(await ns.updateStart(args))
5917
6133
  });
5918
6134
  ctx.slots.inject("settings.plugins.tab", () => ctx.slots.register({
5919
6135
  name: "settings.plugins.tab",
package/lib/index.js CHANGED
@@ -1,13 +1,23 @@
1
1
  import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
2
2
  import { loadOptionalPatches, readProfileManifest, resolveProfileDir } from "@deepseek-ai/dsh-app-boot";
3
- import { lt, minVersion } from "semver";
3
+ import { lt, minVersion, valid } from "semver";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { basename, dirname, join } from "node:path";
6
- import { createHash, randomUUID } from "node:crypto";
7
6
  import { closeSync, existsSync, mkdirSync, openSync, readFileSync, realpathSync, renameSync, writeFileSync } from "node:fs";
7
+ import { createHash, randomUUID } from "node:crypto";
8
8
  import { z } from "zod";
9
9
  import { spawn } from "node:child_process";
10
10
  import { dump } from "js-yaml";
11
+ //#region src/own-version.ts
12
+ /** The shop's own published version, read from the package.json that ships
13
+ * next to this package — the RUNNING version, not the manifest's range
14
+ * spec. This lives at the package root (not under src/host) on purpose:
15
+ * both the source tree (tests) and the bundled `lib/index.js` sit exactly
16
+ * one level below the package root, so the same relative URL resolves in
17
+ * both. */
18
+ function ownVersion() {
19
+ return JSON.parse(readFileSync(fileURLToPath(new URL("../package.json", import.meta.url)), "utf8")).version;
20
+ }
11
21
  /** A cached catalog younger than this is served without touching the network. */
12
22
  const FRESH_MS = 3e5;
13
23
  /** Records when the loader itself wrote the cache; the pointer's `builtAt` is
@@ -465,6 +475,28 @@ function startRestart(options) {
465
475
  }
466
476
  }
467
477
  //#endregion
478
+ //#region src/host/self-update.ts
479
+ /** Self-update version check: the shop's latest published version, from the
480
+ * npm packument. Advisory by design — like the stars sidecar, a failed
481
+ * check degrades to `null` and never throws, never blocks a publish. The
482
+ * catalog cannot serve here: the shop is bootstrap-installed and is not
483
+ * harvested into its own catalog. */
484
+ /** The npm package the shop itself is published as. */
485
+ const SHOP_PACKAGE = "dsh-plugin-shop";
486
+ /** Fetch the shop's `latest` dist-tag, or `null` when the registry cannot
487
+ * answer (network failure, unexpected payload — anything). The caller
488
+ * renders `null` as "no update check", never as a failure. */
489
+ async function fetchLatestVersion(fetchFn = fetch) {
490
+ try {
491
+ const response = await fetchFn(`https://registry.npmjs.org/${SHOP_PACKAGE}`);
492
+ if (!response.ok) return null;
493
+ const latest = (await response.json())["dist-tags"]?.latest;
494
+ return typeof latest === "string" ? latest : null;
495
+ } catch {
496
+ return null;
497
+ }
498
+ }
499
+ //#endregion
468
500
  //#region src/host/profile.ts
469
501
  /** Profile directory discovery and user-layer writes (§8: hot enable/disable). */
470
502
  /**
@@ -591,6 +623,8 @@ let ShopGateway = (() => {
591
623
  let _installed_decorators;
592
624
  let _uninstall_decorators;
593
625
  let _restart_decorators;
626
+ let _version_decorators;
627
+ let _updateStart_decorators;
594
628
  return class ShopGateway extends _classSuper {
595
629
  static {
596
630
  const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
@@ -601,6 +635,8 @@ let ShopGateway = (() => {
601
635
  _installed_decorators = [Remote("installed")];
602
636
  _uninstall_decorators = [Remote("uninstallStart")];
603
637
  _restart_decorators = [Remote("restart")];
638
+ _version_decorators = [Remote("version")];
639
+ _updateStart_decorators = [Remote("updateStart")];
604
640
  __esDecorate(this, null, _setEnabled_decorators, {
605
641
  kind: "method",
606
642
  name: "setEnabled",
@@ -678,6 +714,28 @@ let ShopGateway = (() => {
678
714
  },
679
715
  metadata: _metadata
680
716
  }, null, _instanceExtraInitializers);
717
+ __esDecorate(this, null, _version_decorators, {
718
+ kind: "method",
719
+ name: "version",
720
+ static: false,
721
+ private: false,
722
+ access: {
723
+ has: (obj) => "version" in obj,
724
+ get: (obj) => obj.version
725
+ },
726
+ metadata: _metadata
727
+ }, null, _instanceExtraInitializers);
728
+ __esDecorate(this, null, _updateStart_decorators, {
729
+ kind: "method",
730
+ name: "updateStart",
731
+ static: false,
732
+ private: false,
733
+ access: {
734
+ has: (obj) => "updateStart" in obj,
735
+ get: (obj) => obj.updateStart
736
+ },
737
+ metadata: _metadata
738
+ }, null, _instanceExtraInitializers);
681
739
  if (_metadata) Object.defineProperty(this, Symbol.metadata, {
682
740
  enumerable: true,
683
741
  configurable: true,
@@ -700,6 +758,7 @@ let ShopGateway = (() => {
700
758
  exit;
701
759
  restartExitDelayMs;
702
760
  restartParentPid;
761
+ latestVersion;
703
762
  /** The install gate runs against the last loaded snapshot, never a fresh
704
763
  * fetch per request (§7.2: the Host's cached snapshot is the truth). */
705
764
  /** Finished install records retained, so a poll sees the true terminal
@@ -726,6 +785,7 @@ let ShopGateway = (() => {
726
785
  this.exit = options.exit ?? ((code) => process.exit(code));
727
786
  this.restartExitDelayMs = options.restartExitDelayMs ?? ShopGateway.RESTART_EXIT_DELAY_MS;
728
787
  this.restartParentPid = options.restartParentPid ?? process.pid;
788
+ this.latestVersion = options.fetchLatestVersion ?? (() => fetchLatestVersion());
729
789
  }
730
790
  /** The boot's Loader root directory (the active profile's `cordis.yml`
731
791
  * directory, carried on `ctx.baseUrl`), when present. A `link:` install
@@ -969,6 +1029,42 @@ let ShopGateway = (() => {
969
1029
  setTimeout(() => this.exit(0), this.restartExitDelayMs);
970
1030
  return { ok: true };
971
1031
  }
1032
+ /** The shop's own version and whether npm has a newer one (§7.3). The
1033
+ * check is advisory: a registry that cannot answer leaves `latest` null
1034
+ * and the client shows the version alone. `installed` is the RUNNING
1035
+ * version (own-version.ts), not the manifest's range spec. */
1036
+ async version() {
1037
+ const installed = ownVersion();
1038
+ const latest = await this.latestVersion();
1039
+ return {
1040
+ installed,
1041
+ latest,
1042
+ outdated: latest !== null && lt(installed, latest)
1043
+ };
1044
+ }
1045
+ /** Update the shop itself to a published version (§7.3): the explicit pin
1046
+ * is the only install form that bypasses pnpm's release cooldown. The
1047
+ * version is re-validated as plain semver at the boundary — the spec
1048
+ * `dsh-plugin-shop@<version>` is built here, never from the wire. */
1049
+ async updateStart(args) {
1050
+ if (valid(args.version) === null) return {
1051
+ ok: false,
1052
+ detail: `dsh-plugin-shop: ${args.version} is not a valid version`
1053
+ };
1054
+ const running = startInstall({
1055
+ profile: this.profile,
1056
+ spec: `dsh-plugin-shop@${args.version}`,
1057
+ dshBin: this.dshBin,
1058
+ expectedName: "dsh-plugin-shop"
1059
+ });
1060
+ this.installs.set(running.installId, running);
1061
+ this.installOrder.push(running.installId);
1062
+ this.evictFinishedInstalls();
1063
+ return {
1064
+ ok: true,
1065
+ installId: running.installId
1066
+ };
1067
+ }
972
1068
  };
973
1069
  })();
974
1070
  //#endregion
@@ -91,6 +91,21 @@ const dsh_plugin_shop_shop_uninstallStart_result$schema = z.union([z.object({
91
91
  'ok': z.literal(false),
92
92
  'detail': z.string(),
93
93
  })])
94
+ const dsh_plugin_shop_shop_updateStart_parameter_0$schema = z.object({
95
+ 'version': z.string(),
96
+ })
97
+ const dsh_plugin_shop_shop_updateStart_result$schema = z.union([z.object({
98
+ 'ok': z.literal(true),
99
+ 'installId': z.string(),
100
+ }), z.object({
101
+ 'ok': z.literal(false),
102
+ 'detail': z.string(),
103
+ })])
104
+ const dsh_plugin_shop_shop_version_result$schema = z.object({
105
+ 'installed': z.string(),
106
+ 'latest': z.union([z.literal(null), z.string()]),
107
+ 'outdated': z.boolean(),
108
+ })
94
109
 
95
110
  export const TYPERT = {
96
111
  package: 'dsh-plugin-shop',
@@ -122,7 +137,7 @@ export const TYPERT = {
122
137
  typeSymbol: 'dsh-plugin-shop/types#ShopCatalogResult',
123
138
  schema: dsh_plugin_shop_shop_catalog_result$schema,
124
139
  },
125
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":225,"column":9},
140
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":244,"column":9},
126
141
  },
127
142
  {
128
143
  id: 'dsh-plugin-shop#shop/installed',
@@ -137,7 +152,7 @@ export const TYPERT = {
137
152
  typeSymbol: 'dsh-plugin-shop#shop/installed:result',
138
153
  schema: dsh_plugin_shop_shop_installed_result$schema,
139
154
  },
140
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":300,"column":9},
155
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":319,"column":9},
141
156
  },
142
157
  {
143
158
  id: 'dsh-plugin-shop#shop/installStart',
@@ -163,7 +178,7 @@ export const TYPERT = {
163
178
  typeSymbol: 'dsh-plugin-shop/types#ShopInstallResult',
164
179
  schema: dsh_plugin_shop_shop_installStart_result$schema,
165
180
  },
166
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":251,"column":9},
181
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":270,"column":9},
167
182
  },
168
183
  {
169
184
  id: 'dsh-plugin-shop#shop/installStatus',
@@ -188,7 +203,7 @@ export const TYPERT = {
188
203
  typeSymbol: 'dsh-plugin-shop/types#ShopInstallStatusResult',
189
204
  schema: dsh_plugin_shop_shop_installStatus_result$schema,
190
205
  },
191
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":289,"column":3},
206
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":308,"column":3},
192
207
  },
193
208
  {
194
209
  id: 'dsh-plugin-shop#shop/restart',
@@ -203,7 +218,7 @@ export const TYPERT = {
203
218
  typeSymbol: 'dsh-plugin-shop/types#ShopRestartResult',
204
219
  schema: dsh_plugin_shop_shop_restart_result$schema,
205
220
  },
206
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":373,"column":9},
221
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":392,"column":9},
207
222
  },
208
223
  {
209
224
  id: 'dsh-plugin-shop#shop/setEnabled',
@@ -228,7 +243,7 @@ export const TYPERT = {
228
243
  typeSymbol: 'dsh-plugin-shop/types#ShopSetEnabledResult',
229
244
  schema: dsh_plugin_shop_shop_setEnabled_result$schema,
230
245
  },
231
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":194,"column":3},
246
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":213,"column":3},
232
247
  },
233
248
  {
234
249
  id: 'dsh-plugin-shop#shop/uninstallStart',
@@ -254,7 +269,47 @@ export const TYPERT = {
254
269
  typeSymbol: 'dsh-plugin-shop/types#ShopUninstallResult',
255
270
  schema: dsh_plugin_shop_shop_uninstallStart_result$schema,
256
271
  },
257
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":340,"column":9},
272
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":359,"column":9},
273
+ },
274
+ {
275
+ id: 'dsh-plugin-shop#shop/updateStart',
276
+ service: 'shop',
277
+ namespace: 'shop',
278
+ method: 'updateStart',
279
+ invocation: { kind: 'direct' },
280
+ parameters: [
281
+ {
282
+ name: 'args',
283
+ wire: 'args',
284
+ source: 'json',
285
+ codec: {
286
+ mode: 'strict',
287
+ typeSymbol: 'dsh-plugin-shop#shop/updateStart:args',
288
+ schema: dsh_plugin_shop_shop_updateStart_parameter_0$schema,
289
+ },
290
+ },
291
+ ],
292
+ result: {
293
+ mode: 'strict',
294
+ typeSymbol: 'dsh-plugin-shop/types#ShopUpdateResult',
295
+ schema: dsh_plugin_shop_shop_updateStart_result$schema,
296
+ },
297
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":441,"column":9},
298
+ },
299
+ {
300
+ id: 'dsh-plugin-shop#shop/version',
301
+ service: 'shop',
302
+ namespace: 'shop',
303
+ method: 'version',
304
+ invocation: { kind: 'direct' },
305
+ parameters: [
306
+ ],
307
+ result: {
308
+ mode: 'strict',
309
+ typeSymbol: 'dsh-plugin-shop/types#ShopVersionResult',
310
+ schema: dsh_plugin_shop_shop_version_result$schema,
311
+ },
312
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":426,"column":9},
258
313
  },
259
314
  ],
260
315
  model: {
@@ -321,6 +376,20 @@ export const TYPERT = {
321
376
  "signature": "@Remote('restart') async restart(): Promise<ShopRestartResult>",
322
377
  "summary": "Restart the dsh process the shop runs in (§8 amendment, 2026-08-27): commit a two-phase handoff — a detached helper waits for this pid to exit, then re-runs this process's own command line — and exit once the response is out.",
323
378
  "jsDoc": "/** Restart the dsh process the shop runs in (§8 amendment, 2026-08-27):\n * commit a two-phase handoff — a detached helper waits for this pid to\n * exit, then re-runs this process's own command line — and exit once the\n * response is out. The browser monitors the origin and refreshes when the\n * new server answers. Refusals are issued before anything is torn down. */"
379
+ },
380
+ {
381
+ "kind": "method",
382
+ "name": "version",
383
+ "signature": "@Remote('version') async version(): Promise<ShopVersionResult>",
384
+ "summary": "The shop's own version and whether npm has a newer one (§7.3).",
385
+ "jsDoc": "/** The shop's own version and whether npm has a newer one (§7.3). The\n * check is advisory: a registry that cannot answer leaves `latest` null\n * and the client shows the version alone. `installed` is the RUNNING\n * version (own-version.ts), not the manifest's range spec. */"
386
+ },
387
+ {
388
+ "kind": "method",
389
+ "name": "updateStart",
390
+ "signature": "@Remote('updateStart') async updateStart(args: { version: string }): Promise<ShopUpdateResult>",
391
+ "summary": "Update the shop itself to a published version (§7.3): the explicit pin is the only install form that bypasses pnpm's release cooldown.",
392
+ "jsDoc": "/** Update the shop itself to a published version (§7.3): the explicit pin\n * is the only install form that bypasses pnpm's release cooldown. The\n * version is re-validated as plain semver at the boundary — the spec\n * `dsh-plugin-shop@<version>` is built here, never from the wire. */"
324
393
  }
325
394
  ],
326
395
  "types": [
@@ -387,6 +456,14 @@ export const TYPERT = {
387
456
  {
388
457
  "name": "ShopUninstallResult",
389
458
  "declaration": "export type ShopUninstallResult = { ok: true; installId: string; } | { ok: false; detail: string; };"
459
+ },
460
+ {
461
+ "name": "ShopUpdateResult",
462
+ "declaration": "export type ShopUpdateResult = { ok: true; installId: string; } | { ok: false; detail: string; };"
463
+ },
464
+ {
465
+ "name": "ShopVersionResult",
466
+ "declaration": "export interface ShopVersionResult {\n installed: string;\n latest: string | null;\n outdated: boolean;\n}"
390
467
  }
391
468
  ]
392
469
  }
@@ -3,7 +3,7 @@ import type {
3
3
  RemoteResult,
4
4
  TypertRemoteContribution,
5
5
  } from '@deepseek-ai/dsh-typert-protocol'
6
- import type { InstallArgs, ShopCatalogResult, ShopInstalledEntry, ShopInstallResult, ShopInstallStatusResult, ShopRestartResult, ShopSetEnabledResult, ShopUninstallResult } from 'dsh-plugin-shop/types'
6
+ import type { InstallArgs, ShopCatalogResult, ShopInstalledEntry, ShopInstallResult, ShopInstallStatusResult, ShopRestartResult, ShopSetEnabledResult, ShopUninstallResult, ShopUpdateResult, ShopVersionResult } from 'dsh-plugin-shop/types'
7
7
 
8
8
  declare module '@deepseek-ai/dsh-typert-protocol' {
9
9
  interface TypertRemoteNamespace$73686f70 {
@@ -14,6 +14,8 @@ declare module '@deepseek-ai/dsh-typert-protocol' {
14
14
  restart: () => Promise<RemoteResult<ShopRestartResult>>
15
15
  setEnabled: (args: { name: string; enabled: boolean; }) => Promise<RemoteResult<ShopSetEnabledResult>>
16
16
  uninstallStart: (args: { name: string; }) => Promise<RemoteResult<ShopUninstallResult>>
17
+ updateStart: (args: { version: string; }) => Promise<RemoteResult<ShopUpdateResult>>
18
+ version: () => Promise<RemoteResult<ShopVersionResult>>
17
19
  }
18
20
  interface TypertRemoteMap {
19
21
  'shop/catalog': (args?: { refresh?: boolean; }) => Promise<RemoteResult<ShopCatalogResult>>
@@ -23,6 +25,8 @@ declare module '@deepseek-ai/dsh-typert-protocol' {
23
25
  'shop/restart': () => Promise<RemoteResult<ShopRestartResult>>
24
26
  'shop/setEnabled': (args: { name: string; enabled: boolean; }) => Promise<RemoteResult<ShopSetEnabledResult>>
25
27
  'shop/uninstallStart': (args: { name: string; }) => Promise<RemoteResult<ShopUninstallResult>>
28
+ 'shop/updateStart': (args: { version: string; }) => Promise<RemoteResult<ShopUpdateResult>>
29
+ 'shop/version': () => Promise<RemoteResult<ShopVersionResult>>
26
30
  }
27
31
  interface TypertRemoteNamespaceMap {
28
32
  'shop': TypertRemoteNamespace$73686f70
@@ -91,6 +91,21 @@ const dsh_plugin_shop_shop_uninstallStart_result$schema = z.union([z.object({
91
91
  'ok': z.literal(false),
92
92
  'detail': z.string(),
93
93
  })])
94
+ const dsh_plugin_shop_shop_updateStart_parameter_0$schema = z.object({
95
+ 'version': z.string(),
96
+ })
97
+ const dsh_plugin_shop_shop_updateStart_result$schema = z.union([z.object({
98
+ 'ok': z.literal(true),
99
+ 'installId': z.string(),
100
+ }), z.object({
101
+ 'ok': z.literal(false),
102
+ 'detail': z.string(),
103
+ })])
104
+ const dsh_plugin_shop_shop_version_result$schema = z.object({
105
+ 'installed': z.string(),
106
+ 'latest': z.union([z.literal(null), z.string()]),
107
+ 'outdated': z.boolean(),
108
+ })
94
109
 
95
110
  export const TYPERT_REMOTE = {
96
111
  package: 'dsh-plugin-shop',
@@ -119,7 +134,7 @@ export const TYPERT_REMOTE = {
119
134
  typeSymbol: 'dsh-plugin-shop/types#ShopCatalogResult',
120
135
  schema: dsh_plugin_shop_shop_catalog_result$schema,
121
136
  },
122
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":225,"column":9},
137
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":244,"column":9},
123
138
  },
124
139
  {
125
140
  id: 'dsh-plugin-shop#shop/installed',
@@ -134,7 +149,7 @@ export const TYPERT_REMOTE = {
134
149
  typeSymbol: 'dsh-plugin-shop#shop/installed:result',
135
150
  schema: dsh_plugin_shop_shop_installed_result$schema,
136
151
  },
137
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":300,"column":9},
152
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":319,"column":9},
138
153
  },
139
154
  {
140
155
  id: 'dsh-plugin-shop#shop/installStart',
@@ -160,7 +175,7 @@ export const TYPERT_REMOTE = {
160
175
  typeSymbol: 'dsh-plugin-shop/types#ShopInstallResult',
161
176
  schema: dsh_plugin_shop_shop_installStart_result$schema,
162
177
  },
163
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":251,"column":9},
178
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":270,"column":9},
164
179
  },
165
180
  {
166
181
  id: 'dsh-plugin-shop#shop/installStatus',
@@ -185,7 +200,7 @@ export const TYPERT_REMOTE = {
185
200
  typeSymbol: 'dsh-plugin-shop/types#ShopInstallStatusResult',
186
201
  schema: dsh_plugin_shop_shop_installStatus_result$schema,
187
202
  },
188
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":289,"column":3},
203
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":308,"column":3},
189
204
  },
190
205
  {
191
206
  id: 'dsh-plugin-shop#shop/restart',
@@ -200,7 +215,7 @@ export const TYPERT_REMOTE = {
200
215
  typeSymbol: 'dsh-plugin-shop/types#ShopRestartResult',
201
216
  schema: dsh_plugin_shop_shop_restart_result$schema,
202
217
  },
203
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":373,"column":9},
218
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":392,"column":9},
204
219
  },
205
220
  {
206
221
  id: 'dsh-plugin-shop#shop/setEnabled',
@@ -225,7 +240,7 @@ export const TYPERT_REMOTE = {
225
240
  typeSymbol: 'dsh-plugin-shop/types#ShopSetEnabledResult',
226
241
  schema: dsh_plugin_shop_shop_setEnabled_result$schema,
227
242
  },
228
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":194,"column":3},
243
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":213,"column":3},
229
244
  },
230
245
  {
231
246
  id: 'dsh-plugin-shop#shop/uninstallStart',
@@ -251,7 +266,47 @@ export const TYPERT_REMOTE = {
251
266
  typeSymbol: 'dsh-plugin-shop/types#ShopUninstallResult',
252
267
  schema: dsh_plugin_shop_shop_uninstallStart_result$schema,
253
268
  },
254
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":340,"column":9},
269
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":359,"column":9},
270
+ },
271
+ {
272
+ id: 'dsh-plugin-shop#shop/updateStart',
273
+ service: 'shop',
274
+ namespace: 'shop',
275
+ method: 'updateStart',
276
+ invocation: { kind: 'direct' },
277
+ parameters: [
278
+ {
279
+ name: 'args',
280
+ wire: 'args',
281
+ source: 'json',
282
+ codec: {
283
+ mode: 'strict',
284
+ typeSymbol: 'dsh-plugin-shop#shop/updateStart:args',
285
+ schema: dsh_plugin_shop_shop_updateStart_parameter_0$schema,
286
+ },
287
+ },
288
+ ],
289
+ result: {
290
+ mode: 'strict',
291
+ typeSymbol: 'dsh-plugin-shop/types#ShopUpdateResult',
292
+ schema: dsh_plugin_shop_shop_updateStart_result$schema,
293
+ },
294
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":441,"column":9},
295
+ },
296
+ {
297
+ id: 'dsh-plugin-shop#shop/version',
298
+ service: 'shop',
299
+ namespace: 'shop',
300
+ method: 'version',
301
+ invocation: { kind: 'direct' },
302
+ parameters: [
303
+ ],
304
+ result: {
305
+ mode: 'strict',
306
+ typeSymbol: 'dsh-plugin-shop/types#ShopVersionResult',
307
+ schema: dsh_plugin_shop_shop_version_result$schema,
308
+ },
309
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":426,"column":9},
255
310
  },
256
311
  ],
257
312
  }
@@ -5,7 +5,7 @@
5
5
  * §11.3.4): no render path here may ever use dangerouslySetInnerHTML. */
6
6
  import { type ReactNode } from 'react';
7
7
  import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
8
- import type { InstallArgs, ShopCatalogResult, ShopInstalledEntry, ShopInstallResult, ShopInstallStatusResult, ShopRestartResult, ShopSetEnabledResult, ShopUninstallResult } from '../host/index.ts';
8
+ import type { InstallArgs, ShopCatalogResult, ShopInstalledEntry, ShopInstallResult, ShopInstallStatusResult, ShopRestartResult, ShopSetEnabledResult, ShopUninstallResult, ShopUpdateResult, ShopVersionResult } from '../host/index.ts';
9
9
  /** The tab's Remote face: the Host result types, already unwrapped from the
10
10
  * wire envelope by `index.ts`; `catalog` throws on a wire error so the tab's
11
11
  * error state renders. */
@@ -26,6 +26,10 @@ export interface ShopTabInjected {
26
26
  name: string;
27
27
  }) => Promise<ShopUninstallResult>;
28
28
  restart: () => Promise<ShopRestartResult>;
29
+ version: () => Promise<ShopVersionResult>;
30
+ updateStart: (args: {
31
+ version: string;
32
+ }) => Promise<ShopUpdateResult>;
29
33
  }
30
34
  /** Full component props assembled by the Settings slot renderer. */
31
35
  export type ShopTabProps = PropsRuntime<'settings.plugins.tab'> & PropsLocale<'settings.shop'> & InjectFace<ShopTabInjected>;
@@ -61,6 +61,9 @@ export declare const zh: {
61
61
  restarting: string;
62
62
  restartFailedNotice: string;
63
63
  restartTransportFailed: string;
64
+ updateFailed: string;
65
+ updateTransportFailed: string;
66
+ checkUpdate: string;
64
67
  enabledSwitch: string;
65
68
  toggleFailed: string;
66
69
  hotApplyNote: string;
@@ -129,6 +132,9 @@ export declare const en: {
129
132
  restarting: string;
130
133
  restartFailedNotice: string;
131
134
  restartTransportFailed: string;
135
+ updateFailed: string;
136
+ updateTransportFailed: string;
137
+ checkUpdate: string;
132
138
  enabledSwitch: string;
133
139
  toggleFailed: string;
134
140
  hotApplyNote: string;
@@ -0,0 +1,20 @@
1
+ /** Self-update driving hook for the shop itself: start, poll to terminal.
2
+ * Shares the install view machine and poll loop; the start mapping is the
3
+ * uninstall one — a business refusal (a non-semver version) lands in the
4
+ * `failed` view with the host's detail, never in the install `rejected`
5
+ * codes. */
6
+ import type { ShopInstallStatusResult, ShopUpdateResult } from '../host/index.ts';
7
+ import type { InstallView } from './present.ts';
8
+ export interface UseUpdateSelfResult {
9
+ view: InstallView;
10
+ start: (args: {
11
+ version: string;
12
+ }) => Promise<void>;
13
+ }
14
+ /** Drive one self-update: the host's business union, the shared polling
15
+ * loop, and terminal states. The `rejected` install state never occurs. */
16
+ export declare function useUpdateSelf(updateStart: (args: {
17
+ version: string;
18
+ }) => Promise<ShopUpdateResult>, installStatus: (args: {
19
+ installId: string;
20
+ }) => Promise<ShopInstallStatusResult>): UseUpdateSelfResult;
@@ -40,6 +40,9 @@ export interface ShopGatewayOptions {
40
40
  * defaults to this process. Tests point it at a dead pid so the fixture
41
41
  * runs immediately instead of waiting for the vitest worker to exit. */
42
42
  restartParentPid?: number;
43
+ /** Test-only injection: the shop's latest-version lookup; production
44
+ * fetches the npm packument. */
45
+ fetchLatestVersion?: () => Promise<string | null>;
43
46
  /** How long the gateway waits after a successful restart response before
44
47
  * exiting the old process; test-only shortening, production uses 2s. */
45
48
  restartExitDelayMs?: number;
@@ -76,6 +79,24 @@ export type ShopUninstallResult = {
76
79
  /** `shop/restart` result (§7.3): the restarted server's URL, or a typed
77
80
  * failure — on failure the old process is still serving. */
78
81
  export type ShopRestartResult = RestartOutcome;
82
+ /** `shop/version` result (§7.3): the RUNNING shop version (from the shipped
83
+ * package.json, not the manifest's range), the npm latest when the check
84
+ * could answer (`null` = no answer — advisory, never an error), and the
85
+ * comparison verdict. */
86
+ export interface ShopVersionResult {
87
+ installed: string;
88
+ latest: string | null;
89
+ outdated: boolean;
90
+ }
91
+ /** `shop/updateStart` result (§7.3): the self-update spawn, or a typed
92
+ * refusal (a version that is not plain semver). */
93
+ export type ShopUpdateResult = {
94
+ ok: true;
95
+ installId: string;
96
+ } | {
97
+ ok: false;
98
+ detail: string;
99
+ };
79
100
  /** `shop/installed` entry (§7.3): one installed catalog plugin. `installed`
80
101
  * is the profile manifest's dependency spec verbatim (a range, a tag, or
81
102
  * `workspace:*`); `outdated` is the Host's verdict that the installed version
@@ -116,6 +137,7 @@ export declare class ShopGateway extends TypertRemoteService {
116
137
  private readonly exit;
117
138
  private readonly restartExitDelayMs;
118
139
  private readonly restartParentPid;
140
+ private readonly latestVersion;
119
141
  /** The install gate runs against the last loaded snapshot, never a fresh
120
142
  * fetch per request (§7.2: the Host's cached snapshot is the truth). */
121
143
  /** Finished install records retained, so a poll sees the true terminal
@@ -194,5 +216,17 @@ export declare class ShopGateway extends TypertRemoteService {
194
216
  * response is out. The browser monitors the origin and refreshes when the
195
217
  * new server answers. Refusals are issued before anything is torn down. */
196
218
  restart(): Promise<ShopRestartResult>;
219
+ /** The shop's own version and whether npm has a newer one (§7.3). The
220
+ * check is advisory: a registry that cannot answer leaves `latest` null
221
+ * and the client shows the version alone. `installed` is the RUNNING
222
+ * version (own-version.ts), not the manifest's range spec. */
223
+ version(): Promise<ShopVersionResult>;
224
+ /** Update the shop itself to a published version (§7.3): the explicit pin
225
+ * is the only install form that bypasses pnpm's release cooldown. The
226
+ * version is re-validated as plain semver at the boundary — the spec
227
+ * `dsh-plugin-shop@<version>` is built here, never from the wire. */
228
+ updateStart(args: {
229
+ version: string;
230
+ }): Promise<ShopUpdateResult>;
197
231
  }
198
232
  export default ShopGateway;
@@ -0,0 +1,9 @@
1
+ /** Self-update version check: the shop's latest published version, from the
2
+ * npm packument. Advisory by design — like the stars sidecar, a failed
3
+ * check degrades to `null` and never throws, never blocks a publish. The
4
+ * catalog cannot serve here: the shop is bootstrap-installed and is not
5
+ * harvested into its own catalog. */
6
+ /** Fetch the shop's `latest` dist-tag, or `null` when the registry cannot
7
+ * answer (network failure, unexpected payload — anything). The caller
8
+ * renders `null` as "no update check", never as a failure. */
9
+ export declare function fetchLatestVersion(fetchFn?: typeof fetch): Promise<string | null>;
@@ -0,0 +1,7 @@
1
+ /** The shop's own published version, read from the package.json that ships
2
+ * next to this package — the RUNNING version, not the manifest's range
3
+ * spec. This lives at the package root (not under src/host) on purpose:
4
+ * both the source tree (tests) and the bundled `lib/index.js` sit exactly
5
+ * one level below the package root, so the same relative URL resolves in
6
+ * both. */
7
+ export declare function ownVersion(): string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-shop",
3
- "version": "0.4.4",
3
+ "version": "0.4.6",
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",