dsh-plugin-shop 0.4.2 → 0.4.3
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 +3 -2
- package/lib/client.js +159 -29
- package/lib/index.js +120 -0
- package/lib/typert.host.js +43 -6
- package/lib/typert.remote-client.d.ts +3 -1
- package/lib/typert.remote-client.js +28 -6
- package/lib/types/client/ShopTab.d.ts +2 -1
- package/lib/types/client/index.d.ts +3 -3
- package/lib/types/client/locales.d.ts +12 -0
- package/lib/types/host/index.d.ts +30 -0
- package/lib/types/host/restart.d.ts +35 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -115,6 +115,7 @@ bundle is not.
|
|
|
115
115
|
| **Install** | One confirmation. An unreviewed plugin requires an explicit acknowledgement first — an installed plugin holds the same privileges as a built-in one |
|
|
116
116
|
| **Enable / disable** | Applies to an installed plugin without a restart |
|
|
117
117
|
| **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
|
+
| **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 |
|
|
118
119
|
|
|
119
120
|
## 🧩 How it is put together
|
|
120
121
|
|
|
@@ -124,9 +125,9 @@ boundary:
|
|
|
124
125
|
| Half | Entry | Can reach | Cannot reach |
|
|
125
126
|
|---|---|---|---|
|
|
126
127
|
| **Host** | `dsh-plugin-shop` | The network (catalog fetch and sha256 verify), the filesystem (cache), `dsh plugin add` under a per-profile mutex | — |
|
|
127
|
-
| **Client** | `dsh-plugin-shop/client` | Exactly
|
|
128
|
+
| **Client** | `dsh-plugin-shop/client` | Exactly seven `shop/*` Remote methods | The network, the filesystem |
|
|
128
129
|
|
|
129
|
-
Compromising the browser half buys an attacker those
|
|
130
|
+
Compromising the browser half buys an attacker those seven calls and nothing more.
|
|
130
131
|
|
|
131
132
|
## ⚙️ Configuration
|
|
132
133
|
|
package/lib/client.js
CHANGED
|
@@ -4350,6 +4350,13 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4350
4350
|
]).optional(),
|
|
4351
4351
|
"detail": union([_undefined(), string()]).optional()
|
|
4352
4352
|
});
|
|
4353
|
+
const dsh_plugin_shop_shop_restart_result$schema = union([object({
|
|
4354
|
+
"ok": literal(true),
|
|
4355
|
+
"url": string()
|
|
4356
|
+
}), object({
|
|
4357
|
+
"ok": literal(false),
|
|
4358
|
+
"detail": string()
|
|
4359
|
+
})]);
|
|
4353
4360
|
const dsh_plugin_shop_shop_setEnabled_parameter_0$schema = object({
|
|
4354
4361
|
"name": string(),
|
|
4355
4362
|
"enabled": boolean()
|
|
@@ -4393,7 +4400,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4393
4400
|
},
|
|
4394
4401
|
sourceLocation: {
|
|
4395
4402
|
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4396
|
-
"line":
|
|
4403
|
+
"line": 218,
|
|
4397
4404
|
"column": 9
|
|
4398
4405
|
}
|
|
4399
4406
|
},
|
|
@@ -4411,7 +4418,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4411
4418
|
},
|
|
4412
4419
|
sourceLocation: {
|
|
4413
4420
|
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4414
|
-
"line":
|
|
4421
|
+
"line": 293,
|
|
4415
4422
|
"column": 9
|
|
4416
4423
|
}
|
|
4417
4424
|
},
|
|
@@ -4439,7 +4446,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4439
4446
|
},
|
|
4440
4447
|
sourceLocation: {
|
|
4441
4448
|
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4442
|
-
"line":
|
|
4449
|
+
"line": 244,
|
|
4443
4450
|
"column": 9
|
|
4444
4451
|
}
|
|
4445
4452
|
},
|
|
@@ -4466,10 +4473,28 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4466
4473
|
},
|
|
4467
4474
|
sourceLocation: {
|
|
4468
4475
|
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4469
|
-
"line":
|
|
4476
|
+
"line": 282,
|
|
4470
4477
|
"column": 3
|
|
4471
4478
|
}
|
|
4472
4479
|
},
|
|
4480
|
+
{
|
|
4481
|
+
id: "dsh-plugin-shop#shop/restart",
|
|
4482
|
+
service: "shop",
|
|
4483
|
+
namespace: "shop",
|
|
4484
|
+
method: "restart",
|
|
4485
|
+
invocation: { kind: "direct" },
|
|
4486
|
+
parameters: [],
|
|
4487
|
+
result: {
|
|
4488
|
+
mode: "strict",
|
|
4489
|
+
typeSymbol: "dsh-plugin-shop/types#ShopRestartResult",
|
|
4490
|
+
schema: dsh_plugin_shop_shop_restart_result$schema
|
|
4491
|
+
},
|
|
4492
|
+
sourceLocation: {
|
|
4493
|
+
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4494
|
+
"line": 367,
|
|
4495
|
+
"column": 9
|
|
4496
|
+
}
|
|
4497
|
+
},
|
|
4473
4498
|
{
|
|
4474
4499
|
id: "dsh-plugin-shop#shop/setEnabled",
|
|
4475
4500
|
service: "shop",
|
|
@@ -4493,7 +4518,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4493
4518
|
},
|
|
4494
4519
|
sourceLocation: {
|
|
4495
4520
|
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4496
|
-
"line":
|
|
4521
|
+
"line": 187,
|
|
4497
4522
|
"column": 3
|
|
4498
4523
|
}
|
|
4499
4524
|
},
|
|
@@ -4521,7 +4546,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4521
4546
|
},
|
|
4522
4547
|
sourceLocation: {
|
|
4523
4548
|
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4524
|
-
"line":
|
|
4549
|
+
"line": 333,
|
|
4525
4550
|
"column": 9
|
|
4526
4551
|
}
|
|
4527
4552
|
}
|
|
@@ -4734,6 +4759,12 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4734
4759
|
uninstallFailed: "卸载失败",
|
|
4735
4760
|
uninstallTransportFailed: "卸载请求未能送达。请稍后重试。",
|
|
4736
4761
|
uninstalledRestartNotice: "已卸载;重启 dsh 后生效",
|
|
4762
|
+
restart: "重启 dsh",
|
|
4763
|
+
restartTitle: "确认重启 dsh",
|
|
4764
|
+
restartBody: "重启会断开当前页面与服务器的连接,正在进行的对话和任务会中断;浏览器会自动跳转到重启后的地址,稍等片刻即可继续使用。",
|
|
4765
|
+
restartConfirm: "重启",
|
|
4766
|
+
restarting: "正在重启,浏览器即将跳转…",
|
|
4767
|
+
restartTransportFailed: "重启请求未能送达。请稍后重试。",
|
|
4737
4768
|
enabledSwitch: "启用",
|
|
4738
4769
|
toggleFailed: "设置失败,请重试。",
|
|
4739
4770
|
hotApplyNote: "无需重启,立即生效"
|
|
@@ -4793,6 +4824,12 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4793
4824
|
uninstallFailed: "Uninstall failed",
|
|
4794
4825
|
uninstallTransportFailed: "The uninstall request could not be delivered. Please retry.",
|
|
4795
4826
|
uninstalledRestartNotice: "uninstalled; restart dsh to activate",
|
|
4827
|
+
restart: "Restart dsh",
|
|
4828
|
+
restartTitle: "Restart dsh?",
|
|
4829
|
+
restartBody: "Restarting disconnects this page and interrupts any conversation or task in progress; the browser will jump to the restarted address automatically, so give it a few seconds.",
|
|
4830
|
+
restartConfirm: "Restart",
|
|
4831
|
+
restarting: "Restarting; the browser will jump in a moment…",
|
|
4832
|
+
restartTransportFailed: "The restart request could not be delivered. Please retry.",
|
|
4796
4833
|
enabledSwitch: "Enable",
|
|
4797
4834
|
toggleFailed: "Failed to update. Please retry.",
|
|
4798
4835
|
hotApplyNote: "takes effect without a restart"
|
|
@@ -4905,7 +4942,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4905
4942
|
}
|
|
4906
4943
|
//#endregion
|
|
4907
4944
|
//#region \0dsh-plugin-shop-css:e3675a89
|
|
4908
|
-
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.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";
|
|
4945
|
+
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";
|
|
4909
4946
|
if (typeof document !== "undefined" && !document.querySelector("style[data-plugin-css=\"e3675a89\"]")) {
|
|
4910
4947
|
const tag = document.createElement("style");
|
|
4911
4948
|
tag.dataset.pluginCss = "e3675a89";
|
|
@@ -4971,6 +5008,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4971
5008
|
"installedLabel": "sb6fc74_installedLabel",
|
|
4972
5009
|
"installedActions": "sf53ca0_installedActions",
|
|
4973
5010
|
"uninstallButton": "s96967f_uninstallButton",
|
|
5011
|
+
"restartButton": "s6ac237_restartButton",
|
|
4974
5012
|
"gate": "sc4f2ac_gate",
|
|
4975
5013
|
"gateTitle": "s559c38_gateTitle",
|
|
4976
5014
|
"gateBody": "s3c0ace_gateBody",
|
|
@@ -5019,7 +5057,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
5019
5057
|
* install controls. An installed plugin's card carries its installed row:
|
|
5020
5058
|
* current → the non-interactive installed label, behind → the update button;
|
|
5021
5059
|
* uninstalled → the install button. */
|
|
5022
|
-
const EntryCard = (0, react.memo)(function EntryCard({ entry, stars, installed, t, install, installStatus, uninstall }) {
|
|
5060
|
+
const EntryCard = (0, react.memo)(function EntryCard({ entry, stars, installed, t, install, installStatus, uninstall, restart }) {
|
|
5023
5061
|
const [open, setOpen] = (0, react.useState)(false);
|
|
5024
5062
|
const detailId = (0, react.useId)();
|
|
5025
5063
|
const summary = entry.catalog?.summary;
|
|
@@ -5131,7 +5169,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
5131
5169
|
tier: entry.tier,
|
|
5132
5170
|
t,
|
|
5133
5171
|
install,
|
|
5134
|
-
installStatus
|
|
5172
|
+
installStatus,
|
|
5173
|
+
restart
|
|
5135
5174
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5136
5175
|
className: _dsh_plugin_shop_css_e3675a89_default.installedActions,
|
|
5137
5176
|
children: [installed.outdated ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InstallPanel, {
|
|
@@ -5141,7 +5180,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
5141
5180
|
variant: "update",
|
|
5142
5181
|
t,
|
|
5143
5182
|
install,
|
|
5144
|
-
installStatus
|
|
5183
|
+
installStatus,
|
|
5184
|
+
restart
|
|
5145
5185
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5146
5186
|
className: _dsh_plugin_shop_css_e3675a89_default.installedLabel,
|
|
5147
5187
|
"data-shop-installed": true,
|
|
@@ -5150,7 +5190,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
5150
5190
|
name: entry.name,
|
|
5151
5191
|
t,
|
|
5152
5192
|
uninstall,
|
|
5153
|
-
installStatus
|
|
5193
|
+
installStatus,
|
|
5194
|
+
restart
|
|
5154
5195
|
})]
|
|
5155
5196
|
})
|
|
5156
5197
|
]
|
|
@@ -5163,7 +5204,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
5163
5204
|
* failure detail, rejection detail — driven by `useInstall`. Shared by the
|
|
5164
5205
|
* catalog cards (`variant: 'install'`) and the outdated rows' update button
|
|
5165
5206
|
* (`variant: 'update'`, which drives the same install flow for `name@latest`). */
|
|
5166
|
-
function InstallPanel({ name, version, tier, variant = "install", t, install, installStatus }) {
|
|
5207
|
+
function InstallPanel({ name, version, tier, variant = "install", t, install, installStatus, restart }) {
|
|
5167
5208
|
const [gateOpen, setGateOpen] = (0, react.useState)(false);
|
|
5168
5209
|
const { view, start } = useInstall(install, installStatus);
|
|
5169
5210
|
if (view.kind === "running") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -5179,10 +5220,16 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
5179
5220
|
}, index))
|
|
5180
5221
|
})]
|
|
5181
5222
|
});
|
|
5182
|
-
if (view.kind === "done") return /* @__PURE__ */ (0, react_jsx_runtime.
|
|
5183
|
-
className: _dsh_plugin_shop_css_e3675a89_default.
|
|
5184
|
-
"
|
|
5185
|
-
|
|
5223
|
+
if (view.kind === "done") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5224
|
+
className: _dsh_plugin_shop_css_e3675a89_default.installedActions,
|
|
5225
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5226
|
+
className: _dsh_plugin_shop_css_e3675a89_default.notice,
|
|
5227
|
+
"data-shop-restart-notice": true,
|
|
5228
|
+
children: view.needsRestart ? t("installedRestartNotice") : t("installedNoRestartNotice")
|
|
5229
|
+
}), view.needsRestart && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RestartPanel, {
|
|
5230
|
+
t,
|
|
5231
|
+
restart
|
|
5232
|
+
})]
|
|
5186
5233
|
});
|
|
5187
5234
|
if (view.kind === "failed") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5188
5235
|
className: _dsh_plugin_shop_css_e3675a89_default.installPanel,
|
|
@@ -5271,7 +5318,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
5271
5318
|
* §9.3 is about granting. A business failure (not in the catalog / not
|
|
5272
5319
|
* installed) lands in the failed view with the host's published detail; a
|
|
5273
5320
|
* transport failure carries the empty detail and the localized fallback. */
|
|
5274
|
-
function UninstallPanel({ name, t, uninstall, installStatus }) {
|
|
5321
|
+
function UninstallPanel({ name, t, uninstall, installStatus, restart }) {
|
|
5275
5322
|
const { view, start } = useUninstall(uninstall, installStatus);
|
|
5276
5323
|
if (view.kind === "running") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5277
5324
|
className: _dsh_plugin_shop_css_e3675a89_default.installPanel,
|
|
@@ -5286,10 +5333,16 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
5286
5333
|
}, index))
|
|
5287
5334
|
})]
|
|
5288
5335
|
});
|
|
5289
|
-
if (view.kind === "done") return /* @__PURE__ */ (0, react_jsx_runtime.
|
|
5290
|
-
className: _dsh_plugin_shop_css_e3675a89_default.
|
|
5291
|
-
"
|
|
5292
|
-
|
|
5336
|
+
if (view.kind === "done") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5337
|
+
className: _dsh_plugin_shop_css_e3675a89_default.installedActions,
|
|
5338
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5339
|
+
className: _dsh_plugin_shop_css_e3675a89_default.notice,
|
|
5340
|
+
"data-shop-uninstall-done": true,
|
|
5341
|
+
children: t("uninstalledRestartNotice")
|
|
5342
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RestartPanel, {
|
|
5343
|
+
t,
|
|
5344
|
+
restart
|
|
5345
|
+
})]
|
|
5293
5346
|
});
|
|
5294
5347
|
if (view.kind === "failed") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5295
5348
|
className: _dsh_plugin_shop_css_e3675a89_default.installPanel,
|
|
@@ -5320,13 +5373,85 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
5320
5373
|
children: t("uninstall")
|
|
5321
5374
|
});
|
|
5322
5375
|
}
|
|
5376
|
+
/** The §8 restart flow (amendment 2026-08-27): after an install, update, or
|
|
5377
|
+
* uninstall reports done, this panel offers a restart of dsh. The
|
|
5378
|
+
* confirmation gate states the cost — the page disconnects and in-flight
|
|
5379
|
+
* conversations/tasks are interrupted — and on confirm the restart RPC runs;
|
|
5380
|
+
* the returned URL is where the browser jumps. A failed restart renders the
|
|
5381
|
+
* host's published detail: the old server is still up, nothing was lost. */
|
|
5382
|
+
function RestartPanel({ t, restart }) {
|
|
5383
|
+
const [gateOpen, setGateOpen] = (0, react.useState)(false);
|
|
5384
|
+
const [state, setState] = (0, react.useState)({ kind: "idle" });
|
|
5385
|
+
const onConfirm = async () => {
|
|
5386
|
+
setGateOpen(false);
|
|
5387
|
+
setState({ kind: "restarting" });
|
|
5388
|
+
try {
|
|
5389
|
+
const result = await restart();
|
|
5390
|
+
if (result.ok) window.location.href = result.url;
|
|
5391
|
+
else setState({
|
|
5392
|
+
kind: "failed",
|
|
5393
|
+
detail: result.detail
|
|
5394
|
+
});
|
|
5395
|
+
} catch {
|
|
5396
|
+
setState({
|
|
5397
|
+
kind: "failed",
|
|
5398
|
+
detail: t("restartTransportFailed")
|
|
5399
|
+
});
|
|
5400
|
+
}
|
|
5401
|
+
};
|
|
5402
|
+
if (state.kind === "restarting") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5403
|
+
className: _dsh_plugin_shop_css_e3675a89_default.notice,
|
|
5404
|
+
"data-shop-restarting": true,
|
|
5405
|
+
children: t("restarting")
|
|
5406
|
+
});
|
|
5407
|
+
if (state.kind === "failed") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5408
|
+
className: _dsh_plugin_shop_css_e3675a89_default.failedDetail,
|
|
5409
|
+
"data-shop-restart-error": true,
|
|
5410
|
+
children: state.detail
|
|
5411
|
+
});
|
|
5412
|
+
if (gateOpen) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5413
|
+
className: _dsh_plugin_shop_css_e3675a89_default.gate,
|
|
5414
|
+
children: [
|
|
5415
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5416
|
+
className: _dsh_plugin_shop_css_e3675a89_default.gateTitle,
|
|
5417
|
+
children: t("restartTitle")
|
|
5418
|
+
}),
|
|
5419
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5420
|
+
className: _dsh_plugin_shop_css_e3675a89_default.gateBody,
|
|
5421
|
+
children: t("restartBody")
|
|
5422
|
+
}),
|
|
5423
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5424
|
+
className: _dsh_plugin_shop_css_e3675a89_default.gateActions,
|
|
5425
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5426
|
+
type: "button",
|
|
5427
|
+
className: _dsh_plugin_shop_css_e3675a89_default.confirmButton,
|
|
5428
|
+
"data-shop-restart-confirm": true,
|
|
5429
|
+
onClick: () => void onConfirm(),
|
|
5430
|
+
children: t("restartConfirm")
|
|
5431
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5432
|
+
type: "button",
|
|
5433
|
+
className: _dsh_plugin_shop_css_e3675a89_default.cancelButton,
|
|
5434
|
+
onClick: () => setGateOpen(false),
|
|
5435
|
+
children: t("cancel")
|
|
5436
|
+
})]
|
|
5437
|
+
})
|
|
5438
|
+
]
|
|
5439
|
+
});
|
|
5440
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5441
|
+
type: "button",
|
|
5442
|
+
className: _dsh_plugin_shop_css_e3675a89_default.restartButton,
|
|
5443
|
+
"data-shop-restart": true,
|
|
5444
|
+
onClick: () => setGateOpen(true),
|
|
5445
|
+
children: t("restart")
|
|
5446
|
+
});
|
|
5447
|
+
}
|
|
5323
5448
|
/** One outdated install row (§7.3): the name, the installed and latest
|
|
5324
5449
|
* versions, an optimistic enable/disable switch, and the update button (the
|
|
5325
5450
|
* install flow for `name@latest`, reusing `InstallPanel`). The switch has no
|
|
5326
5451
|
* installed-state RPC behind it — the tab assumes an installed plugin is on,
|
|
5327
5452
|
* so the first toggle sends the inverted value. After a successful `setEnabled`
|
|
5328
5453
|
* the §8 hot note renders. */
|
|
5329
|
-
function OutdatedRow({ row, tier, t, setEnabled, install, installStatus }) {
|
|
5454
|
+
function OutdatedRow({ row, tier, t, setEnabled, install, installStatus, restart }) {
|
|
5330
5455
|
const [enabled, setEnabledState] = (0, react.useState)(true);
|
|
5331
5456
|
const [toggle, setToggle] = (0, react.useState)({ kind: "idle" });
|
|
5332
5457
|
const onToggle = async () => {
|
|
@@ -5391,7 +5516,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
5391
5516
|
variant: "update",
|
|
5392
5517
|
t,
|
|
5393
5518
|
install,
|
|
5394
|
-
installStatus
|
|
5519
|
+
installStatus,
|
|
5520
|
+
restart
|
|
5395
5521
|
})]
|
|
5396
5522
|
}),
|
|
5397
5523
|
toggle.kind === "saved" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
@@ -5414,7 +5540,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
5414
5540
|
* tier for the update gate is looked up from the catalog by name (community →
|
|
5415
5541
|
* acknowledgement); an entry absent from the catalog defaults to the
|
|
5416
5542
|
* community gate (the safer read). */
|
|
5417
|
-
function OutdatedSection({ state, tiers, t, setEnabled, install, installStatus }) {
|
|
5543
|
+
function OutdatedSection({ state, tiers, t, setEnabled, install, installStatus, restart }) {
|
|
5418
5544
|
if (state.kind === "loading") return null;
|
|
5419
5545
|
if (state.kind === "error") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5420
5546
|
className: _dsh_plugin_shop_css_e3675a89_default.stateLine,
|
|
@@ -5437,7 +5563,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
5437
5563
|
t,
|
|
5438
5564
|
setEnabled,
|
|
5439
5565
|
install,
|
|
5440
|
-
installStatus
|
|
5566
|
+
installStatus,
|
|
5567
|
+
restart
|
|
5441
5568
|
}) }, row.name))
|
|
5442
5569
|
})]
|
|
5443
5570
|
});
|
|
@@ -5445,7 +5572,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
5445
5572
|
/** The shop tab root: browse, search, refresh, and render one card per
|
|
5446
5573
|
* entry. Data attributes on the e2e-relevant nodes follow the Task 3 list. */
|
|
5447
5574
|
function ShopTab(props) {
|
|
5448
|
-
const { t, catalog, install, installStatus, setEnabled, installed, uninstall } = props;
|
|
5575
|
+
const { t, catalog, install, installStatus, setEnabled, installed, uninstall, restart } = props;
|
|
5449
5576
|
const [catalogState, setCatalogState] = (0, react.useState)({ kind: "loading" });
|
|
5450
5577
|
const [installedState, setInstalledState] = (0, react.useState)({ kind: "loading" });
|
|
5451
5578
|
const [request, setRequest] = (0, react.useState)({ kind: "initial" });
|
|
@@ -5689,7 +5816,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
5689
5816
|
t,
|
|
5690
5817
|
install,
|
|
5691
5818
|
installStatus,
|
|
5692
|
-
uninstall
|
|
5819
|
+
uninstall,
|
|
5820
|
+
restart
|
|
5693
5821
|
}) }, entry.name)), incremental && visibleCount < filtered.length && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
|
|
5694
5822
|
ref: sentinelRef,
|
|
5695
5823
|
className: _dsh_plugin_shop_css_e3675a89_default.cardsSentry,
|
|
@@ -5710,7 +5838,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
5710
5838
|
t,
|
|
5711
5839
|
setEnabled,
|
|
5712
5840
|
install,
|
|
5713
|
-
installStatus
|
|
5841
|
+
installStatus,
|
|
5842
|
+
restart
|
|
5714
5843
|
})
|
|
5715
5844
|
]
|
|
5716
5845
|
});
|
|
@@ -5754,7 +5883,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
5754
5883
|
installStatus: async (args) => unwrap(await ns.installStatus(args)),
|
|
5755
5884
|
setEnabled: async (args) => unwrap(await ns.setEnabled(args)),
|
|
5756
5885
|
installed: async () => unwrap(await ns.installed()),
|
|
5757
|
-
uninstall: async (args) => unwrap(await ns.uninstallStart(args))
|
|
5886
|
+
uninstall: async (args) => unwrap(await ns.uninstallStart(args)),
|
|
5887
|
+
restart: async () => unwrap(await ns.restart())
|
|
5758
5888
|
});
|
|
5759
5889
|
ctx.slots.inject("settings.plugins.tab", () => ctx.slots.register({
|
|
5760
5890
|
name: "settings.plugins.tab",
|
package/lib/index.js
CHANGED
|
@@ -418,6 +418,85 @@ function startUninstall(options) {
|
|
|
418
418
|
});
|
|
419
419
|
}
|
|
420
420
|
//#endregion
|
|
421
|
+
//#region src/host/restart.ts
|
|
422
|
+
/** Restart executor: re-spawn the Host's own command line and hand the
|
|
423
|
+
* browser the new server's URL. The child is detached (its own process
|
|
424
|
+
* group), so it survives both the parent's exit and the launching
|
|
425
|
+
* terminal's. The parent exits only AFTER the child printed its
|
|
426
|
+
* `dsh web: <url>` line — a restart that fails to come up leaves the old
|
|
427
|
+
* process running untouched. */
|
|
428
|
+
/** How long to wait for the child to print its URL before declaring the
|
|
429
|
+
* restart failed and killing the child. dsh web prints the URL once the
|
|
430
|
+
* Loader tree settles; 20s is generous on slow machines. */
|
|
431
|
+
const RESTART_UP_TIMEOUT_MS = 2e4;
|
|
432
|
+
/** Spawn the restarted server and wait for it to announce its URL.
|
|
433
|
+
*
|
|
434
|
+
* The child runs the same `dsh` with the same argv the current process was
|
|
435
|
+
* launched with (`process.argv.slice(2)` — node and the CLI script path
|
|
436
|
+
* stripped), so the profile, port and flags reproduce the user's launch
|
|
437
|
+
* verbatim. `--port 0` therefore yields a NEW port, and the returned URL is
|
|
438
|
+
* how the browser finds it.
|
|
439
|
+
*
|
|
440
|
+
* On success the caller exits the old process — but only after delivering
|
|
441
|
+
* the RPC response carrying `url`, which is the caller's sequencing duty,
|
|
442
|
+
* not this module's. On failure (child exits before announcing, spawn
|
|
443
|
+
* error, or the timeout) the child is killed if still running and the old
|
|
444
|
+
* process is untouched. */
|
|
445
|
+
function startRestart(options) {
|
|
446
|
+
const { dshBin, argv, env, timeoutMs = RESTART_UP_TIMEOUT_MS } = options;
|
|
447
|
+
const stderr = [];
|
|
448
|
+
return new Promise((resolve) => {
|
|
449
|
+
const child = spawn(dshBin, argv, {
|
|
450
|
+
stdio: [
|
|
451
|
+
"ignore",
|
|
452
|
+
"pipe",
|
|
453
|
+
"pipe"
|
|
454
|
+
],
|
|
455
|
+
env: env ?? process.env,
|
|
456
|
+
detached: true
|
|
457
|
+
});
|
|
458
|
+
child.unref();
|
|
459
|
+
const timeout = setTimeout(() => {
|
|
460
|
+
child.kill();
|
|
461
|
+
resolve({
|
|
462
|
+
ok: false,
|
|
463
|
+
detail: "the restarted server did not announce its URL in time"
|
|
464
|
+
});
|
|
465
|
+
}, timeoutMs);
|
|
466
|
+
child.stdout.on("data", (chunk) => {
|
|
467
|
+
for (const line of chunk.toString().split("\n")) {
|
|
468
|
+
if (line === "") continue;
|
|
469
|
+
const match = /dsh web: (http:\/\/\S+)/.exec(line);
|
|
470
|
+
if (match?.[1] !== void 0) {
|
|
471
|
+
clearTimeout(timeout);
|
|
472
|
+
resolve({
|
|
473
|
+
ok: true,
|
|
474
|
+
url: match[1]
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
child.stderr.on("data", (chunk) => {
|
|
480
|
+
for (const line of chunk.toString().split("\n")) if (line !== "") stderr.push(line);
|
|
481
|
+
});
|
|
482
|
+
child.on("error", (error) => {
|
|
483
|
+
const code = error.code;
|
|
484
|
+
clearTimeout(timeout);
|
|
485
|
+
resolve({
|
|
486
|
+
ok: false,
|
|
487
|
+
detail: code === "ENOENT" ? "dsh not found on PATH — restart could not be launched" : `restart spawn failed: ${error.message}`
|
|
488
|
+
});
|
|
489
|
+
});
|
|
490
|
+
child.on("close", () => {
|
|
491
|
+
clearTimeout(timeout);
|
|
492
|
+
resolve({
|
|
493
|
+
ok: false,
|
|
494
|
+
detail: `the restarted server exited during boot — ${stderr[stderr.length - 1] ?? "no output"}`
|
|
495
|
+
});
|
|
496
|
+
});
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
//#endregion
|
|
421
500
|
//#region src/host/profile.ts
|
|
422
501
|
/** Profile directory discovery and user-layer writes (§8: hot enable/disable). */
|
|
423
502
|
/**
|
|
@@ -543,6 +622,7 @@ let ShopGateway = (() => {
|
|
|
543
622
|
let _installStatus_decorators;
|
|
544
623
|
let _installed_decorators;
|
|
545
624
|
let _uninstall_decorators;
|
|
625
|
+
let _restart_decorators;
|
|
546
626
|
return class ShopGateway extends _classSuper {
|
|
547
627
|
static {
|
|
548
628
|
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
|
|
@@ -552,6 +632,7 @@ let ShopGateway = (() => {
|
|
|
552
632
|
_installStatus_decorators = [Remote("installStatus")];
|
|
553
633
|
_installed_decorators = [Remote("installed")];
|
|
554
634
|
_uninstall_decorators = [Remote("uninstallStart")];
|
|
635
|
+
_restart_decorators = [Remote("restart")];
|
|
555
636
|
__esDecorate(this, null, _setEnabled_decorators, {
|
|
556
637
|
kind: "method",
|
|
557
638
|
name: "setEnabled",
|
|
@@ -618,6 +699,17 @@ let ShopGateway = (() => {
|
|
|
618
699
|
},
|
|
619
700
|
metadata: _metadata
|
|
620
701
|
}, null, _instanceExtraInitializers);
|
|
702
|
+
__esDecorate(this, null, _restart_decorators, {
|
|
703
|
+
kind: "method",
|
|
704
|
+
name: "restart",
|
|
705
|
+
static: false,
|
|
706
|
+
private: false,
|
|
707
|
+
access: {
|
|
708
|
+
has: (obj) => "restart" in obj,
|
|
709
|
+
get: (obj) => obj.restart
|
|
710
|
+
},
|
|
711
|
+
metadata: _metadata
|
|
712
|
+
}, null, _instanceExtraInitializers);
|
|
621
713
|
if (_metadata) Object.defineProperty(this, Symbol.metadata, {
|
|
622
714
|
enumerable: true,
|
|
623
715
|
configurable: true,
|
|
@@ -632,11 +724,21 @@ let ShopGateway = (() => {
|
|
|
632
724
|
profileDir;
|
|
633
725
|
inventory;
|
|
634
726
|
dshBin;
|
|
727
|
+
/** The argv `shop/restart` re-spawns: the real process argv minus node and
|
|
728
|
+
* the CLI script path, or a test-provided substitute. */
|
|
729
|
+
restartArgv;
|
|
730
|
+
/** The exit the restart calls once the response is out; `process.exit` in
|
|
731
|
+
* production, a spy in tests. */
|
|
732
|
+
exit;
|
|
733
|
+
restartExitDelayMs;
|
|
635
734
|
/** The install gate runs against the last loaded snapshot, never a fresh
|
|
636
735
|
* fetch per request (§7.2: the Host's cached snapshot is the truth). */
|
|
637
736
|
/** Finished install records retained, so a poll sees the true terminal
|
|
638
737
|
* state (§8: done / needsRestart / failure detail). Oldest evicted on add. */
|
|
639
738
|
static MAX_FINISHED_INSTALLS = 32;
|
|
739
|
+
/** How long the gateway waits after a successful restart response before
|
|
740
|
+
* exiting the old process — the browser must receive the URL first. */
|
|
741
|
+
static RESTART_EXIT_DELAY_MS = 2e3;
|
|
640
742
|
/** The install gate runs against the last loaded snapshot, never a fresh
|
|
641
743
|
* fetch per request (§7.2: the Host's cached snapshot is the truth). */
|
|
642
744
|
lastSnapshot = null;
|
|
@@ -651,6 +753,9 @@ let ShopGateway = (() => {
|
|
|
651
753
|
this.profileDir = options.profileDir;
|
|
652
754
|
this.inventory = options.inventory;
|
|
653
755
|
this.dshBin = options.dshBin ?? "dsh";
|
|
756
|
+
this.restartArgv = options.restartArgv ?? process.argv.slice(2);
|
|
757
|
+
this.exit = options.exit ?? ((code) => process.exit(code));
|
|
758
|
+
this.restartExitDelayMs = options.restartExitDelayMs ?? ShopGateway.RESTART_EXIT_DELAY_MS;
|
|
654
759
|
}
|
|
655
760
|
/** The boot's Loader root directory (the active profile's `cordis.yml`
|
|
656
761
|
* directory, carried on `ctx.baseUrl`), when present. A `link:` install
|
|
@@ -865,6 +970,21 @@ let ShopGateway = (() => {
|
|
|
865
970
|
installId: running.installId
|
|
866
971
|
};
|
|
867
972
|
}
|
|
973
|
+
/** Restart the dsh process the shop runs in (§8 amendment, 2026-08-27):
|
|
974
|
+
* re-spawn this process's own command line, return the new server's URL
|
|
975
|
+
* once it announces itself, and only then exit. A failed restart returns a
|
|
976
|
+
* typed failure and the old process keeps serving — the restart is
|
|
977
|
+
* all-or-nothing. The response must reach the browser before the exit, so
|
|
978
|
+
* the exit is delayed past the RPC round-trip. */
|
|
979
|
+
async restart() {
|
|
980
|
+
const outcome = await startRestart({
|
|
981
|
+
dshBin: this.dshBin,
|
|
982
|
+
argv: this.restartArgv,
|
|
983
|
+
env: process.env
|
|
984
|
+
});
|
|
985
|
+
if (outcome.ok) setTimeout(() => this.exit(0), this.restartExitDelayMs);
|
|
986
|
+
return outcome;
|
|
987
|
+
}
|
|
868
988
|
};
|
|
869
989
|
})();
|
|
870
990
|
//#endregion
|
package/lib/typert.host.js
CHANGED
|
@@ -67,6 +67,13 @@ const dsh_plugin_shop_shop_installStatus_result$schema = z.object({
|
|
|
67
67
|
'needsRestart': z.union([z.undefined(), z.literal(false), z.literal(true)]).optional(),
|
|
68
68
|
'detail': z.union([z.undefined(), z.string()]).optional(),
|
|
69
69
|
})
|
|
70
|
+
const dsh_plugin_shop_shop_restart_result$schema = z.union([z.object({
|
|
71
|
+
'ok': z.literal(true),
|
|
72
|
+
'url': z.string(),
|
|
73
|
+
}), z.object({
|
|
74
|
+
'ok': z.literal(false),
|
|
75
|
+
'detail': z.string(),
|
|
76
|
+
})])
|
|
70
77
|
const dsh_plugin_shop_shop_setEnabled_parameter_0$schema = z.object({
|
|
71
78
|
'name': z.string(),
|
|
72
79
|
'enabled': z.boolean(),
|
|
@@ -116,7 +123,7 @@ export const TYPERT = {
|
|
|
116
123
|
typeSymbol: 'dsh-plugin-shop/types#ShopCatalogResult',
|
|
117
124
|
schema: dsh_plugin_shop_shop_catalog_result$schema,
|
|
118
125
|
},
|
|
119
|
-
sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":
|
|
126
|
+
sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":218,"column":9},
|
|
120
127
|
},
|
|
121
128
|
{
|
|
122
129
|
id: 'dsh-plugin-shop#shop/installed',
|
|
@@ -131,7 +138,7 @@ export const TYPERT = {
|
|
|
131
138
|
typeSymbol: 'dsh-plugin-shop#shop/installed:result',
|
|
132
139
|
schema: dsh_plugin_shop_shop_installed_result$schema,
|
|
133
140
|
},
|
|
134
|
-
sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":
|
|
141
|
+
sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":293,"column":9},
|
|
135
142
|
},
|
|
136
143
|
{
|
|
137
144
|
id: 'dsh-plugin-shop#shop/installStart',
|
|
@@ -157,7 +164,7 @@ export const TYPERT = {
|
|
|
157
164
|
typeSymbol: 'dsh-plugin-shop/types#ShopInstallResult',
|
|
158
165
|
schema: dsh_plugin_shop_shop_installStart_result$schema,
|
|
159
166
|
},
|
|
160
|
-
sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":
|
|
167
|
+
sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":244,"column":9},
|
|
161
168
|
},
|
|
162
169
|
{
|
|
163
170
|
id: 'dsh-plugin-shop#shop/installStatus',
|
|
@@ -182,7 +189,22 @@ export const TYPERT = {
|
|
|
182
189
|
typeSymbol: 'dsh-plugin-shop/types#ShopInstallStatusResult',
|
|
183
190
|
schema: dsh_plugin_shop_shop_installStatus_result$schema,
|
|
184
191
|
},
|
|
185
|
-
sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":
|
|
192
|
+
sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":282,"column":3},
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
id: 'dsh-plugin-shop#shop/restart',
|
|
196
|
+
service: 'shop',
|
|
197
|
+
namespace: 'shop',
|
|
198
|
+
method: 'restart',
|
|
199
|
+
invocation: { kind: 'direct' },
|
|
200
|
+
parameters: [
|
|
201
|
+
],
|
|
202
|
+
result: {
|
|
203
|
+
mode: 'strict',
|
|
204
|
+
typeSymbol: 'dsh-plugin-shop/types#ShopRestartResult',
|
|
205
|
+
schema: dsh_plugin_shop_shop_restart_result$schema,
|
|
206
|
+
},
|
|
207
|
+
sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":367,"column":9},
|
|
186
208
|
},
|
|
187
209
|
{
|
|
188
210
|
id: 'dsh-plugin-shop#shop/setEnabled',
|
|
@@ -207,7 +229,7 @@ export const TYPERT = {
|
|
|
207
229
|
typeSymbol: 'dsh-plugin-shop/types#ShopSetEnabledResult',
|
|
208
230
|
schema: dsh_plugin_shop_shop_setEnabled_result$schema,
|
|
209
231
|
},
|
|
210
|
-
sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":
|
|
232
|
+
sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":187,"column":3},
|
|
211
233
|
},
|
|
212
234
|
{
|
|
213
235
|
id: 'dsh-plugin-shop#shop/uninstallStart',
|
|
@@ -233,7 +255,7 @@ export const TYPERT = {
|
|
|
233
255
|
typeSymbol: 'dsh-plugin-shop/types#ShopUninstallResult',
|
|
234
256
|
schema: dsh_plugin_shop_shop_uninstallStart_result$schema,
|
|
235
257
|
},
|
|
236
|
-
sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":
|
|
258
|
+
sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":333,"column":9},
|
|
237
259
|
},
|
|
238
260
|
],
|
|
239
261
|
model: {
|
|
@@ -293,6 +315,13 @@ export const TYPERT = {
|
|
|
293
315
|
"signature": "@Remote('uninstallStart') async uninstall(args: { name: string }): Promise<ShopUninstallResult>",
|
|
294
316
|
"summary": "Uninstall one installed catalog plugin from the profile (§7.3 follow-up amendment).",
|
|
295
317
|
"jsDoc": "/** Uninstall one installed catalog plugin from the profile (§7.3 follow-up\n * amendment). Removing revokes privilege rather than granting it, so there\n * is no acknowledgement gate. The name must be a catalog entry the profile\n * manifest declares as a dependency — the RPC cannot remove profile\n * dependencies the shop does not manage (the base bundle, the shop\n * itself). The same install records/polling serve the client. */"
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
"kind": "method",
|
|
321
|
+
"name": "restart",
|
|
322
|
+
"signature": "@Remote('restart') async restart(): Promise<ShopRestartResult>",
|
|
323
|
+
"summary": "Restart the dsh process the shop runs in (§8 amendment, 2026-08-27): re-spawn this process's own command line, return the new server's URL once it announces itself, and only then exit.",
|
|
324
|
+
"jsDoc": "/** Restart the dsh process the shop runs in (§8 amendment, 2026-08-27):\n * re-spawn this process's own command line, return the new server's URL\n * once it announces itself, and only then exit. A failed restart returns a\n * typed failure and the old process keeps serving — the restart is\n * all-or-nothing. The response must reach the browser before the exit, so\n * the exit is delayed past the RPC round-trip. */"
|
|
296
325
|
}
|
|
297
326
|
],
|
|
298
327
|
"types": [
|
|
@@ -328,6 +357,10 @@ export const TYPERT = {
|
|
|
328
357
|
"name": "InstallStatus",
|
|
329
358
|
"declaration": "export interface InstallStatus {\n state: InstallState;\n log: string[];\n needsRestart?: boolean;\n detail?: string;\n}"
|
|
330
359
|
},
|
|
360
|
+
{
|
|
361
|
+
"name": "RestartOutcome",
|
|
362
|
+
"declaration": "export type RestartOutcome = { ok: true; url: string; } | { ok: false; detail: string; };"
|
|
363
|
+
},
|
|
331
364
|
{
|
|
332
365
|
"name": "ShopCatalogResult",
|
|
333
366
|
"declaration": "export interface ShopCatalogResult {\n schemaVersion: number;\n builtAt: string;\n stale: boolean;\n plugins: CatalogEntry[];\n denied: DeniedEntry[];\n stars: Record<string, number>;\n}"
|
|
@@ -344,6 +377,10 @@ export const TYPERT = {
|
|
|
344
377
|
"name": "ShopInstallStatusResult",
|
|
345
378
|
"declaration": "export interface ShopInstallStatusResult extends InstallStatus {\n found: boolean;\n}"
|
|
346
379
|
},
|
|
380
|
+
{
|
|
381
|
+
"name": "ShopRestartResult",
|
|
382
|
+
"declaration": "export type ShopRestartResult = RestartOutcome;"
|
|
383
|
+
},
|
|
347
384
|
{
|
|
348
385
|
"name": "ShopSetEnabledResult",
|
|
349
386
|
"declaration": "export interface ShopSetEnabledResult {\n ok: boolean;\n detail?: string;\n}"
|
|
@@ -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, ShopSetEnabledResult, ShopUninstallResult } from 'dsh-plugin-shop/types'
|
|
6
|
+
import type { InstallArgs, ShopCatalogResult, ShopInstalledEntry, ShopInstallResult, ShopInstallStatusResult, ShopRestartResult, ShopSetEnabledResult, ShopUninstallResult } from 'dsh-plugin-shop/types'
|
|
7
7
|
|
|
8
8
|
declare module '@deepseek-ai/dsh-typert-protocol' {
|
|
9
9
|
interface TypertRemoteNamespace$73686f70 {
|
|
@@ -11,6 +11,7 @@ declare module '@deepseek-ai/dsh-typert-protocol' {
|
|
|
11
11
|
installed: () => Promise<RemoteResult<ShopInstalledEntry[]>>
|
|
12
12
|
installStart: (args: InstallArgs) => Promise<RemoteResult<ShopInstallResult>>
|
|
13
13
|
installStatus: (args: { installId: string; }) => Promise<RemoteResult<ShopInstallStatusResult>>
|
|
14
|
+
restart: () => Promise<RemoteResult<ShopRestartResult>>
|
|
14
15
|
setEnabled: (args: { name: string; enabled: boolean; }) => Promise<RemoteResult<ShopSetEnabledResult>>
|
|
15
16
|
uninstallStart: (args: { name: string; }) => Promise<RemoteResult<ShopUninstallResult>>
|
|
16
17
|
}
|
|
@@ -19,6 +20,7 @@ declare module '@deepseek-ai/dsh-typert-protocol' {
|
|
|
19
20
|
'shop/installed': () => Promise<RemoteResult<ShopInstalledEntry[]>>
|
|
20
21
|
'shop/installStart': (args: InstallArgs) => Promise<RemoteResult<ShopInstallResult>>
|
|
21
22
|
'shop/installStatus': (args: { installId: string; }) => Promise<RemoteResult<ShopInstallStatusResult>>
|
|
23
|
+
'shop/restart': () => Promise<RemoteResult<ShopRestartResult>>
|
|
22
24
|
'shop/setEnabled': (args: { name: string; enabled: boolean; }) => Promise<RemoteResult<ShopSetEnabledResult>>
|
|
23
25
|
'shop/uninstallStart': (args: { name: string; }) => Promise<RemoteResult<ShopUninstallResult>>
|
|
24
26
|
}
|
|
@@ -67,6 +67,13 @@ const dsh_plugin_shop_shop_installStatus_result$schema = z.object({
|
|
|
67
67
|
'needsRestart': z.union([z.undefined(), z.literal(false), z.literal(true)]).optional(),
|
|
68
68
|
'detail': z.union([z.undefined(), z.string()]).optional(),
|
|
69
69
|
})
|
|
70
|
+
const dsh_plugin_shop_shop_restart_result$schema = z.union([z.object({
|
|
71
|
+
'ok': z.literal(true),
|
|
72
|
+
'url': z.string(),
|
|
73
|
+
}), z.object({
|
|
74
|
+
'ok': z.literal(false),
|
|
75
|
+
'detail': z.string(),
|
|
76
|
+
})])
|
|
70
77
|
const dsh_plugin_shop_shop_setEnabled_parameter_0$schema = z.object({
|
|
71
78
|
'name': z.string(),
|
|
72
79
|
'enabled': z.boolean(),
|
|
@@ -113,7 +120,7 @@ export const TYPERT_REMOTE = {
|
|
|
113
120
|
typeSymbol: 'dsh-plugin-shop/types#ShopCatalogResult',
|
|
114
121
|
schema: dsh_plugin_shop_shop_catalog_result$schema,
|
|
115
122
|
},
|
|
116
|
-
sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":
|
|
123
|
+
sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":218,"column":9},
|
|
117
124
|
},
|
|
118
125
|
{
|
|
119
126
|
id: 'dsh-plugin-shop#shop/installed',
|
|
@@ -128,7 +135,7 @@ export const TYPERT_REMOTE = {
|
|
|
128
135
|
typeSymbol: 'dsh-plugin-shop#shop/installed:result',
|
|
129
136
|
schema: dsh_plugin_shop_shop_installed_result$schema,
|
|
130
137
|
},
|
|
131
|
-
sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":
|
|
138
|
+
sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":293,"column":9},
|
|
132
139
|
},
|
|
133
140
|
{
|
|
134
141
|
id: 'dsh-plugin-shop#shop/installStart',
|
|
@@ -154,7 +161,7 @@ export const TYPERT_REMOTE = {
|
|
|
154
161
|
typeSymbol: 'dsh-plugin-shop/types#ShopInstallResult',
|
|
155
162
|
schema: dsh_plugin_shop_shop_installStart_result$schema,
|
|
156
163
|
},
|
|
157
|
-
sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":
|
|
164
|
+
sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":244,"column":9},
|
|
158
165
|
},
|
|
159
166
|
{
|
|
160
167
|
id: 'dsh-plugin-shop#shop/installStatus',
|
|
@@ -179,7 +186,22 @@ export const TYPERT_REMOTE = {
|
|
|
179
186
|
typeSymbol: 'dsh-plugin-shop/types#ShopInstallStatusResult',
|
|
180
187
|
schema: dsh_plugin_shop_shop_installStatus_result$schema,
|
|
181
188
|
},
|
|
182
|
-
sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":
|
|
189
|
+
sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":282,"column":3},
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
id: 'dsh-plugin-shop#shop/restart',
|
|
193
|
+
service: 'shop',
|
|
194
|
+
namespace: 'shop',
|
|
195
|
+
method: 'restart',
|
|
196
|
+
invocation: { kind: 'direct' },
|
|
197
|
+
parameters: [
|
|
198
|
+
],
|
|
199
|
+
result: {
|
|
200
|
+
mode: 'strict',
|
|
201
|
+
typeSymbol: 'dsh-plugin-shop/types#ShopRestartResult',
|
|
202
|
+
schema: dsh_plugin_shop_shop_restart_result$schema,
|
|
203
|
+
},
|
|
204
|
+
sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":367,"column":9},
|
|
183
205
|
},
|
|
184
206
|
{
|
|
185
207
|
id: 'dsh-plugin-shop#shop/setEnabled',
|
|
@@ -204,7 +226,7 @@ export const TYPERT_REMOTE = {
|
|
|
204
226
|
typeSymbol: 'dsh-plugin-shop/types#ShopSetEnabledResult',
|
|
205
227
|
schema: dsh_plugin_shop_shop_setEnabled_result$schema,
|
|
206
228
|
},
|
|
207
|
-
sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":
|
|
229
|
+
sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":187,"column":3},
|
|
208
230
|
},
|
|
209
231
|
{
|
|
210
232
|
id: 'dsh-plugin-shop#shop/uninstallStart',
|
|
@@ -230,7 +252,7 @@ export const TYPERT_REMOTE = {
|
|
|
230
252
|
typeSymbol: 'dsh-plugin-shop/types#ShopUninstallResult',
|
|
231
253
|
schema: dsh_plugin_shop_shop_uninstallStart_result$schema,
|
|
232
254
|
},
|
|
233
|
-
sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":
|
|
255
|
+
sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":333,"column":9},
|
|
234
256
|
},
|
|
235
257
|
],
|
|
236
258
|
}
|
|
@@ -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, ShopSetEnabledResult, ShopUninstallResult } from '../host/index.ts';
|
|
8
|
+
import type { InstallArgs, ShopCatalogResult, ShopInstalledEntry, ShopInstallResult, ShopInstallStatusResult, ShopRestartResult, ShopSetEnabledResult, ShopUninstallResult } 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. */
|
|
@@ -25,6 +25,7 @@ export interface ShopTabInjected {
|
|
|
25
25
|
uninstall: (args: {
|
|
26
26
|
name: string;
|
|
27
27
|
}) => Promise<ShopUninstallResult>;
|
|
28
|
+
restart: () => Promise<ShopRestartResult>;
|
|
28
29
|
}
|
|
29
30
|
/** Full component props assembled by the Settings slot renderer. */
|
|
30
31
|
export type ShopTabProps = PropsRuntime<'settings.plugins.tab'> & PropsLocale<'settings.shop'> & InjectFace<ShopTabInjected>;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shop settings surface, browser half — one tab in `settings.plugins.tab`
|
|
3
3
|
* that browses the catalog, installs with acknowledgement, toggles
|
|
4
|
-
* enablement,
|
|
5
|
-
* (the assembly does not know this package)
|
|
6
|
-
* the
|
|
4
|
+
* enablement, lists installed plugins, and restarts dsh after installs.
|
|
5
|
+
* Mounts the shop Remote itself (the assembly does not know this package)
|
|
6
|
+
* and holds no privilege beyond the seven `shop/*` methods (§5.3).
|
|
7
7
|
*/
|
|
8
8
|
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client';
|
|
9
9
|
import type { ShopLocaleKey } from './locales.ts';
|
|
@@ -54,6 +54,12 @@ export declare const zh: {
|
|
|
54
54
|
uninstallFailed: string;
|
|
55
55
|
uninstallTransportFailed: string;
|
|
56
56
|
uninstalledRestartNotice: string;
|
|
57
|
+
restart: string;
|
|
58
|
+
restartTitle: string;
|
|
59
|
+
restartBody: string;
|
|
60
|
+
restartConfirm: string;
|
|
61
|
+
restarting: string;
|
|
62
|
+
restartTransportFailed: string;
|
|
57
63
|
enabledSwitch: string;
|
|
58
64
|
toggleFailed: string;
|
|
59
65
|
hotApplyNote: string;
|
|
@@ -115,6 +121,12 @@ export declare const en: {
|
|
|
115
121
|
uninstallFailed: string;
|
|
116
122
|
uninstallTransportFailed: string;
|
|
117
123
|
uninstalledRestartNotice: string;
|
|
124
|
+
restart: string;
|
|
125
|
+
restartTitle: string;
|
|
126
|
+
restartBody: string;
|
|
127
|
+
restartConfirm: string;
|
|
128
|
+
restarting: string;
|
|
129
|
+
restartTransportFailed: string;
|
|
118
130
|
enabledSwitch: string;
|
|
119
131
|
toggleFailed: string;
|
|
120
132
|
hotApplyNote: string;
|
|
@@ -5,6 +5,7 @@ import { loadCatalog, type LoadCatalogOptions } from './catalog.ts';
|
|
|
5
5
|
import type { CatalogEntry, DeniedEntry } from './types.ts';
|
|
6
6
|
import { type InstallArgs, type InstallRejectionCode } from './install.ts';
|
|
7
7
|
import { type InstallStatus } from './executor.ts';
|
|
8
|
+
import { type RestartOutcome } from './restart.ts';
|
|
8
9
|
export type { InstallArgs, InstallRejectionCode } from './install.ts';
|
|
9
10
|
export type { CatalogEntry } from './types.ts';
|
|
10
11
|
/** One Loader inventory entry, structurally — the shop never depends on
|
|
@@ -29,6 +30,15 @@ export interface ShopGatewayOptions {
|
|
|
29
30
|
list(): InventoryEntry[];
|
|
30
31
|
};
|
|
31
32
|
dshBin?: string;
|
|
33
|
+
/** The dsh argv this process was launched with, for `shop/restart`;
|
|
34
|
+
* defaults to the real `process.argv` minus node and the script path. */
|
|
35
|
+
restartArgv?: string[];
|
|
36
|
+
/** Test-only injection: the exit the restart calls after the response is
|
|
37
|
+
* delivered. Production uses `process.exit`. */
|
|
38
|
+
exit?: (code?: number) => void;
|
|
39
|
+
/** How long the gateway waits after a successful restart response before
|
|
40
|
+
* exiting the old process; test-only shortening, production uses 2s. */
|
|
41
|
+
restartExitDelayMs?: number;
|
|
32
42
|
}
|
|
33
43
|
/** `shop/installStart` result (§7.3): rejections are typed wire values with an
|
|
34
44
|
* author-readable `detail`, not thrown RPC errors. */
|
|
@@ -59,6 +69,9 @@ export type ShopUninstallResult = {
|
|
|
59
69
|
ok: false;
|
|
60
70
|
detail: string;
|
|
61
71
|
};
|
|
72
|
+
/** `shop/restart` result (§7.3): the restarted server's URL, or a typed
|
|
73
|
+
* failure — on failure the old process is still serving. */
|
|
74
|
+
export type ShopRestartResult = RestartOutcome;
|
|
62
75
|
/** `shop/installed` entry (§7.3): one installed catalog plugin. `installed`
|
|
63
76
|
* is the profile manifest's dependency spec verbatim (a range, a tag, or
|
|
64
77
|
* `workspace:*`); `outdated` is the Host's verdict that the installed version
|
|
@@ -91,11 +104,21 @@ export declare class ShopGateway extends TypertRemoteService {
|
|
|
91
104
|
private readonly profileDir?;
|
|
92
105
|
private readonly inventory?;
|
|
93
106
|
private readonly dshBin;
|
|
107
|
+
/** The argv `shop/restart` re-spawns: the real process argv minus node and
|
|
108
|
+
* the CLI script path, or a test-provided substitute. */
|
|
109
|
+
private readonly restartArgv;
|
|
110
|
+
/** The exit the restart calls once the response is out; `process.exit` in
|
|
111
|
+
* production, a spy in tests. */
|
|
112
|
+
private readonly exit;
|
|
113
|
+
private readonly restartExitDelayMs;
|
|
94
114
|
/** The install gate runs against the last loaded snapshot, never a fresh
|
|
95
115
|
* fetch per request (§7.2: the Host's cached snapshot is the truth). */
|
|
96
116
|
/** Finished install records retained, so a poll sees the true terminal
|
|
97
117
|
* state (§8: done / needsRestart / failure detail). Oldest evicted on add. */
|
|
98
118
|
private static readonly MAX_FINISHED_INSTALLS;
|
|
119
|
+
/** How long the gateway waits after a successful restart response before
|
|
120
|
+
* exiting the old process — the browser must receive the URL first. */
|
|
121
|
+
private static readonly RESTART_EXIT_DELAY_MS;
|
|
99
122
|
/** The install gate runs against the last loaded snapshot, never a fresh
|
|
100
123
|
* fetch per request (§7.2: the Host's cached snapshot is the truth). */
|
|
101
124
|
private lastSnapshot;
|
|
@@ -160,5 +183,12 @@ export declare class ShopGateway extends TypertRemoteService {
|
|
|
160
183
|
uninstall(args: {
|
|
161
184
|
name: string;
|
|
162
185
|
}): Promise<ShopUninstallResult>;
|
|
186
|
+
/** Restart the dsh process the shop runs in (§8 amendment, 2026-08-27):
|
|
187
|
+
* re-spawn this process's own command line, return the new server's URL
|
|
188
|
+
* once it announces itself, and only then exit. A failed restart returns a
|
|
189
|
+
* typed failure and the old process keeps serving — the restart is
|
|
190
|
+
* all-or-nothing. The response must reach the browser before the exit, so
|
|
191
|
+
* the exit is delayed past the RPC round-trip. */
|
|
192
|
+
restart(): Promise<ShopRestartResult>;
|
|
163
193
|
}
|
|
164
194
|
export default ShopGateway;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** Restart executor: re-spawn the Host's own command line and hand the
|
|
2
|
+
* browser the new server's URL. The child is detached (its own process
|
|
3
|
+
* group), so it survives both the parent's exit and the launching
|
|
4
|
+
* terminal's. The parent exits only AFTER the child printed its
|
|
5
|
+
* `dsh web: <url>` line — a restart that fails to come up leaves the old
|
|
6
|
+
* process running untouched. */
|
|
7
|
+
/** `shop/restart` outcome: the new server's URL, or a typed failure with an
|
|
8
|
+
* author-readable detail. A failure means the OLD process is still serving —
|
|
9
|
+
* the restart is all-or-nothing. */
|
|
10
|
+
export type RestartOutcome = {
|
|
11
|
+
ok: true;
|
|
12
|
+
url: string;
|
|
13
|
+
} | {
|
|
14
|
+
ok: false;
|
|
15
|
+
detail: string;
|
|
16
|
+
};
|
|
17
|
+
/** Spawn the restarted server and wait for it to announce its URL.
|
|
18
|
+
*
|
|
19
|
+
* The child runs the same `dsh` with the same argv the current process was
|
|
20
|
+
* launched with (`process.argv.slice(2)` — node and the CLI script path
|
|
21
|
+
* stripped), so the profile, port and flags reproduce the user's launch
|
|
22
|
+
* verbatim. `--port 0` therefore yields a NEW port, and the returned URL is
|
|
23
|
+
* how the browser finds it.
|
|
24
|
+
*
|
|
25
|
+
* On success the caller exits the old process — but only after delivering
|
|
26
|
+
* the RPC response carrying `url`, which is the caller's sequencing duty,
|
|
27
|
+
* not this module's. On failure (child exits before announcing, spawn
|
|
28
|
+
* error, or the timeout) the child is killed if still running and the old
|
|
29
|
+
* process is untouched. */
|
|
30
|
+
export declare function startRestart(options: {
|
|
31
|
+
dshBin: string;
|
|
32
|
+
argv: string[];
|
|
33
|
+
env?: NodeJS.ProcessEnv;
|
|
34
|
+
timeoutMs?: number;
|
|
35
|
+
}): Promise<RestartOutcome>;
|
package/package.json
CHANGED