dsh-plugin-shop 0.4.2 → 0.4.4
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 +188 -29
- package/lib/index.js +106 -2
- package/lib/typert.host.js +42 -6
- package/lib/typert.remote-client.d.ts +3 -1
- package/lib/typert.remote-client.js +27 -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 +14 -0
- package/lib/types/client/present.d.ts +7 -0
- package/lib/types/host/index.d.ts +34 -0
- package/lib/types/host/restart.d.ts +38 -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,10 @@ 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({ "ok": literal(true) }), object({
|
|
4354
|
+
"ok": literal(false),
|
|
4355
|
+
"detail": string()
|
|
4356
|
+
})]);
|
|
4353
4357
|
const dsh_plugin_shop_shop_setEnabled_parameter_0$schema = object({
|
|
4354
4358
|
"name": string(),
|
|
4355
4359
|
"enabled": boolean()
|
|
@@ -4393,7 +4397,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4393
4397
|
},
|
|
4394
4398
|
sourceLocation: {
|
|
4395
4399
|
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4396
|
-
"line":
|
|
4400
|
+
"line": 225,
|
|
4397
4401
|
"column": 9
|
|
4398
4402
|
}
|
|
4399
4403
|
},
|
|
@@ -4411,7 +4415,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4411
4415
|
},
|
|
4412
4416
|
sourceLocation: {
|
|
4413
4417
|
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4414
|
-
"line":
|
|
4418
|
+
"line": 300,
|
|
4415
4419
|
"column": 9
|
|
4416
4420
|
}
|
|
4417
4421
|
},
|
|
@@ -4439,7 +4443,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4439
4443
|
},
|
|
4440
4444
|
sourceLocation: {
|
|
4441
4445
|
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4442
|
-
"line":
|
|
4446
|
+
"line": 251,
|
|
4443
4447
|
"column": 9
|
|
4444
4448
|
}
|
|
4445
4449
|
},
|
|
@@ -4466,10 +4470,28 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4466
4470
|
},
|
|
4467
4471
|
sourceLocation: {
|
|
4468
4472
|
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4469
|
-
"line":
|
|
4473
|
+
"line": 289,
|
|
4470
4474
|
"column": 3
|
|
4471
4475
|
}
|
|
4472
4476
|
},
|
|
4477
|
+
{
|
|
4478
|
+
id: "dsh-plugin-shop#shop/restart",
|
|
4479
|
+
service: "shop",
|
|
4480
|
+
namespace: "shop",
|
|
4481
|
+
method: "restart",
|
|
4482
|
+
invocation: { kind: "direct" },
|
|
4483
|
+
parameters: [],
|
|
4484
|
+
result: {
|
|
4485
|
+
mode: "strict",
|
|
4486
|
+
typeSymbol: "dsh-plugin-shop/types#ShopRestartResult",
|
|
4487
|
+
schema: dsh_plugin_shop_shop_restart_result$schema
|
|
4488
|
+
},
|
|
4489
|
+
sourceLocation: {
|
|
4490
|
+
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4491
|
+
"line": 373,
|
|
4492
|
+
"column": 9
|
|
4493
|
+
}
|
|
4494
|
+
},
|
|
4473
4495
|
{
|
|
4474
4496
|
id: "dsh-plugin-shop#shop/setEnabled",
|
|
4475
4497
|
service: "shop",
|
|
@@ -4493,7 +4515,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4493
4515
|
},
|
|
4494
4516
|
sourceLocation: {
|
|
4495
4517
|
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4496
|
-
"line":
|
|
4518
|
+
"line": 194,
|
|
4497
4519
|
"column": 3
|
|
4498
4520
|
}
|
|
4499
4521
|
},
|
|
@@ -4521,7 +4543,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4521
4543
|
},
|
|
4522
4544
|
sourceLocation: {
|
|
4523
4545
|
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4524
|
-
"line":
|
|
4546
|
+
"line": 340,
|
|
4525
4547
|
"column": 9
|
|
4526
4548
|
}
|
|
4527
4549
|
}
|
|
@@ -4612,6 +4634,11 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4612
4634
|
"shop",
|
|
4613
4635
|
"marketplace"
|
|
4614
4636
|
];
|
|
4637
|
+
/** Competing marketplaces whose names carry no store/market keyword and so
|
|
4638
|
+
* escape the pattern rules below. Named explicitly rather than guessed:
|
|
4639
|
+
* `dsh-plugin` is the npm package of github.com/dshplugin/dsh-plugin-hub —
|
|
4640
|
+
* a community plugin marketplace for DeepSeek Harness. */
|
|
4641
|
+
const SHOP_LIKE_NAMES = ["dsh-plugin"];
|
|
4615
4642
|
/**
|
|
4616
4643
|
* Whether a package name reads as a plugin shop (a marketplace for dsh
|
|
4617
4644
|
* plugins, e.g. `dsh-plugin-shop`, `dsh-store`, `pluginstore`). The shop
|
|
@@ -4624,6 +4651,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4624
4651
|
* it matches other people's package names, not ours.
|
|
4625
4652
|
*/
|
|
4626
4653
|
function isShopLike(name) {
|
|
4654
|
+
if (SHOP_LIKE_NAMES.includes(name.toLowerCase())) return true;
|
|
4627
4655
|
const segments = name.toLowerCase().split(/[-_.]+/);
|
|
4628
4656
|
const hasPlugin = segments.includes("plugin");
|
|
4629
4657
|
if (segments.some((segment) => /plugin(store|market|mall|shop|marketplace)/.test(segment) || /(store|market|mall|shop|marketplace)plugin/.test(segment))) return true;
|
|
@@ -4734,6 +4762,13 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4734
4762
|
uninstallFailed: "卸载失败",
|
|
4735
4763
|
uninstallTransportFailed: "卸载请求未能送达。请稍后重试。",
|
|
4736
4764
|
uninstalledRestartNotice: "已卸载;重启 dsh 后生效",
|
|
4765
|
+
restart: "重启 dsh",
|
|
4766
|
+
restartTitle: "确认重启 dsh",
|
|
4767
|
+
restartBody: "重启会断开当前页面与服务器的连接,正在进行的对话和任务会中断;浏览器会自动跳转到重启后的地址,稍等片刻即可继续使用。",
|
|
4768
|
+
restartConfirm: "重启",
|
|
4769
|
+
restarting: "正在重启,服务器就绪后页面会自动刷新…",
|
|
4770
|
+
restartFailedNotice: "重启后服务器没有回来。请手动启动:dsh web(新进程的日志在商店缓存目录的 restart.log)",
|
|
4771
|
+
restartTransportFailed: "重启请求未能送达。请稍后重试。",
|
|
4737
4772
|
enabledSwitch: "启用",
|
|
4738
4773
|
toggleFailed: "设置失败,请重试。",
|
|
4739
4774
|
hotApplyNote: "无需重启,立即生效"
|
|
@@ -4793,6 +4828,13 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4793
4828
|
uninstallFailed: "Uninstall failed",
|
|
4794
4829
|
uninstallTransportFailed: "The uninstall request could not be delivered. Please retry.",
|
|
4795
4830
|
uninstalledRestartNotice: "uninstalled; restart dsh to activate",
|
|
4831
|
+
restart: "Restart dsh",
|
|
4832
|
+
restartTitle: "Restart dsh?",
|
|
4833
|
+
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.",
|
|
4834
|
+
restartConfirm: "Restart",
|
|
4835
|
+
restarting: "Restarting; the page will refresh once the server is back…",
|
|
4836
|
+
restartFailedNotice: "The server did not come back after the restart. Start it manually: dsh web (see restart.log in the shop cache directory)",
|
|
4837
|
+
restartTransportFailed: "The restart request could not be delivered. Please retry.",
|
|
4796
4838
|
enabledSwitch: "Enable",
|
|
4797
4839
|
toggleFailed: "Failed to update. Please retry.",
|
|
4798
4840
|
hotApplyNote: "takes effect without a restart"
|
|
@@ -4905,7 +4947,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4905
4947
|
}
|
|
4906
4948
|
//#endregion
|
|
4907
4949
|
//#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";
|
|
4950
|
+
const css = "/* Shop tab styles — a market shelf inside the settings surface. The visual\n * language is the dsh theme's alias tokens ONLY (the four tokens the previous\n * styles referenced that do not exist — bg-layer-3, interactive-bg-hover,\n * state-error, state-success — are corrected to bg-layer-2 and the\n * state-*-primary set). The signature is the category spine: one HUE per\n * category (six fixed hues, not the brand token — the brand resolves to\n * near-black in the light theme, so six brand opacities read as six shades\n * of gray), with the category name as the cover label in the same hue. Class names are mapped to content-derived names at bundle time\n * (tsdown.s8583e3_client.s942a79_config.s5f67aa_ts) and stubbed in tests; only the keys are relied\n * on.\n *\n * One token rule the theme forces: the background LAYERS do not carry\n * contrast. In the light theme bg-base, bg-layer-1, bg-layer-2 and\n * bg-layer-3 all resolve to the same white; only the dark theme spreads them\n * (950/875/850/800). So a layer-2 fill on a layer-1 ground is invisible for\n * half of all users, and anything whose fill is its ONLY affordance — the\n * loading skeleton, the borderless capability chips — derives its fill from\n * label-primary instead, which inverts with the theme by construction.\n * Elements that also carry a border or text may keep a layer fill.\n * `css-tokens.s8583e3_client.sfd9d8a_spec.s5f67aa_ts` pins this. */\n\n.s255e5a_panel {\n display: flex;\n flex-direction: column;\n gap: 14px;\n}\n\n.s44a3ec_stateLine {\n margin: 0;\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n}\n\n/* Visually hidden but screen-reader readable (the loading copy while the\n * skeleton stands in visually). */\n.s3a0ece_srOnly {\n position: absolute;\n width: 1px;\n height: 1px;\n margin: -1px;\n padding: 0;\n border: 0;\n clip: rect(0 0 0 0);\n clip-path: inset(50%);\n overflow: hidden;\n white-space: nowrap;\n}\n\n/* The loading shelf: ghost cards with the same geometry as the real grid, so\n * the swap to content is a same-shape handoff instead of a jump. */\n.se0c3b4_skeletonGrid {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));\n gap: 12px;\n}\n\n.s520416_skeletonCard {\n position: relative;\n display: flex;\n flex-direction: column;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 10px;\n background: var(--dsw-alias-bg-layer-1);\n overflow: hidden;\n}\n\n/* The shimmer sweep: one gradient band travels across each ghost card while\n * the bars breathe, so the loading state reads as \"the shelf is scanning\". */\n.s520416_skeletonCard::after {\n content: '';\n position: absolute;\n inset: 0;\n background: linear-gradient(\n 100deg,\n transparent 25%,\n color-mix(in srgb, var(--dsw-alias-label-primary) 14%, transparent) 50%,\n transparent 75%\n );\n transform: translateX(-100%);\n animation: dshShopSkeletonSweep 1.4s ease-in-out infinite;\n pointer-events: none;\n}\n\n@keyframes dshShopSkeletonSweep {\n to { transform: translateX(100%); }\n}\n\n.s30e662_skeletonCover {\n display: block;\n height: 26px;\n background: color-mix(in srgb, var(--dsw-alias-label-primary) 10%, transparent);\n}\n\n.s8c7d6f_skeletonName {\n width: 55%;\n height: 14px;\n margin: 12px 12px 0;\n border-radius: 4px;\n background: color-mix(in srgb, var(--dsw-alias-label-primary) 8%, transparent);\n}\n\n.sf5179e_skeletonSummary {\n width: 92%;\n height: 12px;\n margin: 10px 12px 0;\n border-radius: 4px;\n background: color-mix(in srgb, var(--dsw-alias-label-primary) 8%, transparent);\n}\n\n.sbb0d25_skeletonSummaryShort {\n width: 68%;\n height: 12px;\n margin: 6px 12px 14px;\n border-radius: 4px;\n background: color-mix(in srgb, var(--dsw-alias-label-primary) 8%, transparent);\n}\n\n.s520416_skeletonCard span {\n animation: dshShopSkeletonPulse 1.6s ease-in-out infinite;\n}\n\n@keyframes dshShopSkeletonPulse {\n 0%, 100% { opacity: 0.5; }\n 50% { opacity: 1; }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .s520416_skeletonCard span { animation: none; opacity: 0.75; }\n .s520416_skeletonCard::after { content: none; }\n}\n\n.s73242a_actionButton {\n align-self: flex-start;\n padding: 5px 14px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 6px;\n background: var(--dsw-alias-bg-layer-2);\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n cursor: pointer;\n}\n\n.s73242a_actionButton:hover {\n border-color: color-mix(in srgb, var(--dsw-alias-brand-primary) 45%, var(--dsw-alias-border-l2));\n color: var(--dsw-alias-brand-primary);\n}\n\n.s73242a_actionButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.s646509_toolbar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n flex-wrap: wrap;\n}\n\n.s232ec4_searchInput {\n width: min(280px, 100%);\n padding: 6px 10px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 6px;\n background: var(--dsw-alias-bg-layer-1);\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n}\n\n.s232ec4_searchInput:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.s1555aa_staleBlock {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.s3946f1_staleBadge {\n padding: 2px 8px;\n border-radius: 999px;\n background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 14%, transparent);\n border: 1px solid color-mix(in srgb, var(--dsw-alias-state-warn-primary) 40%, transparent);\n color: var(--dsw-alias-state-warn-primary);\n font-size: 12px;\n}\n\n.sb2c859_categoryBar {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n margin-top: 6px;\n}\n\n.s26d58e_categoryButton {\n padding: 3px 10px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 999px;\n background: var(--dsw-alias-bg-layer-1);\n color: var(--dsw-alias-label-secondary);\n font-size: 12px;\n cursor: pointer;\n}\n\n.s26d58e_categoryButton:hover {\n border-color: color-mix(in srgb, var(--dsw-alias-brand-primary) 45%, var(--dsw-alias-border-l2));\n color: var(--dsw-alias-brand-primary);\n}\n\n.s5f68c4_categoryButtonOn {\n border-color: var(--dsw-alias-brand-primary);\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent);\n color: var(--dsw-alias-brand-primary);\n font-weight: 600;\n}\n\n.s26d58e_categoryButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.s668774_catalogHeading {\n margin: 2px 0 0;\n font-size: 16px;\n font-weight: 600;\n letter-spacing: 0.02em;\n color: var(--dsw-alias-label-primary);\n}\n\n.sccc4d1_catalogStats {\n margin: 6px 0 0;\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 12px;\n letter-spacing: 0.02em;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s6de1ab_emptyLine {\n margin: 0;\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n}\n\n/* The shelf: a responsive grid of cards. */\n.sf657f5_cards {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));\n gap: 12px;\n list-style: none;\n margin: 0;\n padding: 0;\n animation: dshShopCardsIn 220ms ease;\n}\n\n@keyframes dshShopCardsIn {\n from { opacity: 0; transform: translateY(3px); }\n to { opacity: 1; transform: none; }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .sf657f5_cards { animation: none; }\n}\n\n.s65f383_card {\n position: relative;\n display: flex;\n flex-direction: column;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 10px;\n background: var(--dsw-alias-bg-layer-1);\n overflow: hidden;\n transition: border-color 120ms ease, transform 120ms ease, box-shadow 120ms ease;\n /* Off-screen cards skip layout and paint; the placeholder keeps scrollbar\n * estimation sane before the browser has measured the real height. */\n content-visibility: auto;\n contain-intrinsic-size: auto 240px;\n}\n\n/* The sentinel that triggers the next shelf batch: zero visual footprint. */\n.sd7ee01_cardsSentry {\n height: 1px;\n margin: 0;\n padding: 0;\n}\n\n.s705cc6_showingLine {\n margin: 0;\n font-size: 12px;\n letter-spacing: 0.02em;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s65f383_card:hover {\n border-color: color-mix(in srgb, var(--dsw-alias-brand-primary) 45%, var(--dsw-alias-border-l1));\n transform: translateY(-1px);\n box-shadow: 0 2px 8px color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .s65f383_card { transition: none; }\n .s65f383_card:hover { transform: none; }\n}\n\n/* The signature: a spine and cover in the category's own hue. The hues are\n * fixed mid-bright colors that read on both themes; `other` deliberately\n * stays a neutral gray so the fallback category does not compete. */\n.s4ce25d_cardSpine {\n position: absolute;\n top: 0;\n bottom: 0;\n left: 0;\n width: 3px;\n background: var(--spine-hue, #8B8E96);\n}\n\n.s65f383_card[data-category='tool'] { --spine-hue: #4C8DFF; }\n.s65f383_card[data-category='provider'] { --spine-hue: #A78BFA; }\n.s65f383_card[data-category='ui'] { --spine-hue: #2DD4BF; }\n.s65f383_card[data-category='workflow'] { --spine-hue: #F59E0B; }\n.s65f383_card[data-category='integration'] { --spine-hue: #34D399; }\n.s65f383_card[data-category='other'] { --spine-hue: #8B8E96; }\n\n.s9e2bec_cardCover {\n display: block;\n padding: 5px 12px 5px 12px;\n border-bottom: 1px solid var(--dsw-alias-border-l1);\n background: color-mix(in srgb, var(--spine-hue, #8B8E96) 12%, transparent);\n}\n\n.s1a7c5b_coverLabel {\n display: block;\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n color: var(--spine-hue, #8B8E96);\n}\n\n.sa97234_entryHeader {\n display: flex;\n flex-direction: column;\n align-items: stretch;\n gap: 8px;\n padding: 10px 12px 6px;\n border: none;\n background: transparent;\n color: inherit;\n font: inherit;\n text-align: left;\n cursor: pointer;\n}\n\n.sa97234_entryHeader:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: -2px;\n}\n\n/* The name owns the full header width; badges sit on their own line below. */\n.sc3f149_name {\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 13px;\n font-weight: 600;\n overflow-wrap: anywhere;\n color: var(--dsw-alias-label-primary);\n}\n\n.s513026_badges {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 6px;\n}\n\n.s5e82cb_tierBadge {\n padding: 1px 7px;\n border-radius: 999px;\n font-size: 11px;\n line-height: 1.6;\n}\n\n.s5e82cb_tierBadge[data-tier='verified'] {\n color: var(--dsw-alias-state-success-primary);\n background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 14%, transparent);\n border: 1px solid color-mix(in srgb, var(--dsw-alias-state-success-primary) 40%, transparent);\n}\n\n.s5e82cb_tierBadge[data-tier='verified-stale'] {\n color: var(--dsw-alias-state-warn-primary);\n background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 14%, transparent);\n border: 1px solid color-mix(in srgb, var(--dsw-alias-state-warn-primary) 40%, transparent);\n}\n\n.s5e82cb_tierBadge[data-tier='community'] {\n color: var(--dsw-alias-label-secondary);\n background: var(--dsw-alias-bg-layer-2);\n border: 1px solid var(--dsw-alias-border-l2);\n}\n\n.s23e3e1_unclaimedBadge {\n padding: 1px 7px;\n border-radius: 999px;\n border: 1px dashed var(--dsw-alias-border-l2);\n font-size: 11px;\n line-height: 1.6;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sff43b4_starsBadge {\n font-size: 11px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s73f7fb_chevron, .s61393c_chevronOpen {\n color: var(--dsw-alias-label-secondary);\n transition: transform 120ms ease;\n}\n\n.s61393c_chevronOpen { transform: rotate(180deg); }\n\n@media (prefers-reduced-motion: reduce) {\n .s73f7fb_chevron, .s61393c_chevronOpen { transition: none; }\n}\n\n.s820ebf_body {\n display: flex;\n flex-direction: column;\n gap: 8px;\n padding: 0 12px 12px;\n}\n\n.sb8fbc8_summary {\n margin: 0;\n font-size: 13px;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n overflow: hidden;\n}\n\n.se507db_summaryZh {\n margin: -4px 0 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-secondary);\n display: -webkit-box;\n -webkit-line-clamp: 1;\n -webkit-box-orient: vertical;\n overflow: hidden;\n}\n\n/* Expanded cards show the full summary: the clamp is lifted, never raised —\n * a higher number would still truncate the author's text, just later. */\n.s10baf0_summaryExpanded {\n -webkit-line-clamp: unset;\n display: block;\n}\n\n.sb9dba9_summaryZhExpanded {\n -webkit-line-clamp: unset;\n display: block;\n}\n\n.s9d2029_capabilitiesBlock {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n.s3bdbd5_capabilitiesNote {\n margin: 0;\n font-size: 11px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s441702_capabilities {\n display: flex;\n flex-wrap: wrap;\n gap: 4px;\n list-style: none;\n margin: 0;\n padding: 0;\n}\n\n.s441702_capabilities li {\n padding: 1px 6px;\n border-radius: 4px;\n background: color-mix(in srgb, var(--dsw-alias-label-primary) 6%, transparent);\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 11px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sde3f0c_detail {\n border-top: 1px solid var(--dsw-alias-border-l1);\n padding-top: 8px;\n}\n\n.se98179_detailRows {\n display: flex;\n flex-direction: column;\n gap: 4px;\n margin: 0;\n}\n\n.sfb4d2f_detailRow {\n display: flex;\n justify-content: space-between;\n gap: 12px;\n font-size: 12px;\n}\n\n.sfb4d2f_detailRow dt {\n color: var(--dsw-alias-label-secondary);\n}\n\n.sfb4d2f_detailRow dd {\n margin: 0;\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n overflow-wrap: anywhere;\n text-align: right;\n color: var(--dsw-alias-label-primary);\n}\n\n.sfb4d2f_detailRow a {\n color: var(--dsw-alias-brand-primary);\n text-decoration: none;\n}\n\n.sfb4d2f_detailRow a:hover {\n text-decoration: underline;\n}\n\n.sfb4d2f_detailRow a:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.sd58c6d_reviewedLine {\n margin: 8px 0 0;\n padding: 6px 8px;\n border-radius: 6px;\n background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 12%, transparent);\n font-size: 12px;\n color: var(--dsw-alias-state-warn-primary);\n}\n\n/* Install flow. */\n.s4d374c_installPanel {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-top: 2px;\n}\n\n.se7aaed_installButton, .sb43a49_confirmButton, .s6e45d1_cancelButton {\n align-self: flex-start;\n padding: 5px 14px;\n border-radius: 6px;\n font-size: 13px;\n cursor: pointer;\n}\n\n.se7aaed_installButton {\n border: 1px solid var(--dsw-alias-brand-primary);\n background: var(--dsw-alias-brand-primary);\n color: var(--dsw-alias-bg-base);\n}\n\n.se7aaed_installButton:hover {\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 88%, var(--dsw-alias-label-primary));\n}\n\n.sb43a49_confirmButton {\n border: 1px solid var(--dsw-alias-brand-primary);\n background: var(--dsw-alias-brand-primary);\n color: var(--dsw-alias-bg-base);\n}\n\n.sb43a49_confirmButton:hover {\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 88%, var(--dsw-alias-label-primary));\n}\n\n.s6e45d1_cancelButton {\n border: 1px solid var(--dsw-alias-border-l2);\n background: var(--dsw-alias-bg-layer-2);\n color: var(--dsw-alias-label-primary);\n}\n\n.s6e45d1_cancelButton:hover {\n border-color: var(--dsw-alias-label-secondary);\n}\n\n.se7aaed_installButton:focus-visible, .sb43a49_confirmButton:focus-visible, .s6e45d1_cancelButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* The shelf card of a plugin that is installed and current carries this\n * non-interactive label instead of an install button. Border + text do the\n * work in the light theme, where every bg-layer token resolves to white. */\n.sb6fc74_installedLabel {\n align-self: flex-start;\n margin: 0;\n padding: 5px 14px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 6px;\n font-size: 13px;\n color: var(--dsw-alias-label-secondary);\n background: var(--dsw-alias-bg-layer-2);\n}\n\n/* An installed card's controls: the installed label or the update button,\n * plus the uninstall button. The install panel's running/failed states are\n * column layouts and sit as one item of this row. */\n.sf53ca0_installedActions {\n display: flex;\n align-items: flex-start;\n flex-wrap: wrap;\n gap: 8px;\n}\n\n/* Uninstall revokes privilege; the quiet outline keeps it from competing\n * with the primary action, and the error tint says \"removes\". */\n.s96967f_uninstallButton {\n align-self: flex-start;\n padding: 5px 14px;\n border: 1px solid color-mix(in srgb, var(--dsw-alias-state-error-primary) 55%, transparent);\n border-radius: 6px;\n background: transparent;\n color: var(--dsw-alias-state-error-primary);\n font-size: 13px;\n cursor: pointer;\n}\n\n.s96967f_uninstallButton:hover {\n background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent);\n}\n\n.s96967f_uninstallButton:focus-visible {\n outline: 2px solid var(--dsw-alias-state-error-primary);\n outline-offset: 1px;\n}\n\n/* The restart dsh button on a done install: quiet outline, same rank as\n * the other secondary actions. */\n.s6ac237_restartButton {\n align-self: flex-start;\n padding: 5px 14px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 6px;\n background: var(--dsw-alias-bg-layer-2);\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n cursor: pointer;\n}\n\n.s6ac237_restartButton:hover {\n border-color: var(--dsw-alias-label-secondary);\n}\n\n.s6ac237_restartButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.sc4f2ac_gate {\n display: flex;\n flex-direction: column;\n gap: 8px;\n padding: 10px;\n border: 1px solid var(--dsw-alias-state-warn-primary);\n border-radius: 8px;\n background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 8%, transparent);\n}\n\n.s559c38_gateTitle {\n margin: 0;\n font-size: 13px;\n font-weight: 600;\n color: var(--dsw-alias-state-warn-primary);\n}\n\n.s3c0ace_gateBody {\n margin: 0;\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-primary);\n}\n\n.s4b2373_gateActions {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n}\n\n.s52b5a5_installing {\n margin: 0;\n font-size: 12px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s64ab71_log {\n max-height: 160px;\n overflow: auto;\n margin: 0;\n padding: 8px;\n border-radius: 6px;\n background: var(--dsw-alias-bg-base);\n border: 1px solid var(--dsw-alias-border-l1);\n list-style: none;\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 11px;\n line-height: 1.5;\n}\n\n.s1531a8_logLine {\n white-space: pre-wrap;\n word-break: break-all;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sf20dda_notice {\n margin: 0;\n font-size: 12px;\n color: var(--dsw-alias-state-success-primary);\n}\n\n.seb9b08_failedHeading {\n margin: 0;\n font-size: 12px;\n font-weight: 600;\n color: var(--dsw-alias-state-error-primary);\n}\n\n.sfd3021_failedDetail {\n margin: 0;\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-primary);\n}\n\n.s50ba35_rejectedCode {\n margin: 0;\n font-size: 12px;\n font-weight: 600;\n color: var(--dsw-alias-state-error-primary);\n}\n\n.se75489_rejectedDetail {\n margin: 0;\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-primary);\n}\n\n/* Installed section. */\n.sa276cf_outdatedSection {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-top: 6px;\n}\n\n.sceae6e_outdatedList {\n display: flex;\n flex-direction: column;\n gap: 8px;\n list-style: none;\n margin: 0;\n padding: 0;\n}\n\n.s8d2072_outdatedRow {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n flex-wrap: wrap;\n padding: 10px 12px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 8px;\n background: var(--dsw-alias-bg-layer-1);\n}\n\n.s5c2545_outdatedInfo {\n display: flex;\n flex-direction: column;\n gap: 2px;\n min-width: 0;\n}\n\n.s5c2545_outdatedInfo strong {\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 13px;\n word-break: break-all;\n color: var(--dsw-alias-label-primary);\n}\n\n.s877c68_outdatedVersions {\n font-size: 12px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sa1f556_outdatedActions {\n display: flex;\n align-items: center;\n gap: 10px;\n}\n\n/* The enable/disable switch. */\n.s11c019_switch {\n position: relative;\n width: 32px;\n height: 18px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 999px;\n background: var(--dsw-alias-bg-layer-2);\n cursor: pointer;\n transition: background 120ms ease, border-color 120ms ease;\n}\n\n.s7580e8_switchOn {\n border-color: var(--dsw-alias-state-success-primary);\n background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 55%, transparent);\n}\n\n.saab2b3_switchKnob {\n position: absolute;\n top: 2px;\n left: 2px;\n width: 12px;\n height: 12px;\n border-radius: 999px;\n background: var(--dsw-alias-label-primary);\n transition: transform 120ms ease, background 120ms ease;\n}\n\n.s7580e8_switchOn .saab2b3_switchKnob {\n transform: translateX(14px);\n background: var(--dsw-alias-bg-base);\n}\n\n.s11c019_switch:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .s11c019_switch, .saab2b3_switchKnob { transition: none; }\n}\n";
|
|
4909
4951
|
if (typeof document !== "undefined" && !document.querySelector("style[data-plugin-css=\"e3675a89\"]")) {
|
|
4910
4952
|
const tag = document.createElement("style");
|
|
4911
4953
|
tag.dataset.pluginCss = "e3675a89";
|
|
@@ -4971,6 +5013,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4971
5013
|
"installedLabel": "sb6fc74_installedLabel",
|
|
4972
5014
|
"installedActions": "sf53ca0_installedActions",
|
|
4973
5015
|
"uninstallButton": "s96967f_uninstallButton",
|
|
5016
|
+
"restartButton": "s6ac237_restartButton",
|
|
4974
5017
|
"gate": "sc4f2ac_gate",
|
|
4975
5018
|
"gateTitle": "s559c38_gateTitle",
|
|
4976
5019
|
"gateBody": "s3c0ace_gateBody",
|
|
@@ -5019,7 +5062,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
5019
5062
|
* install controls. An installed plugin's card carries its installed row:
|
|
5020
5063
|
* current → the non-interactive installed label, behind → the update button;
|
|
5021
5064
|
* uninstalled → the install button. */
|
|
5022
|
-
const EntryCard = (0, react.memo)(function EntryCard({ entry, stars, installed, t, install, installStatus, uninstall }) {
|
|
5065
|
+
const EntryCard = (0, react.memo)(function EntryCard({ entry, stars, installed, t, install, installStatus, uninstall, restart }) {
|
|
5023
5066
|
const [open, setOpen] = (0, react.useState)(false);
|
|
5024
5067
|
const detailId = (0, react.useId)();
|
|
5025
5068
|
const summary = entry.catalog?.summary;
|
|
@@ -5131,7 +5174,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
5131
5174
|
tier: entry.tier,
|
|
5132
5175
|
t,
|
|
5133
5176
|
install,
|
|
5134
|
-
installStatus
|
|
5177
|
+
installStatus,
|
|
5178
|
+
restart
|
|
5135
5179
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5136
5180
|
className: _dsh_plugin_shop_css_e3675a89_default.installedActions,
|
|
5137
5181
|
children: [installed.outdated ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InstallPanel, {
|
|
@@ -5141,7 +5185,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
5141
5185
|
variant: "update",
|
|
5142
5186
|
t,
|
|
5143
5187
|
install,
|
|
5144
|
-
installStatus
|
|
5188
|
+
installStatus,
|
|
5189
|
+
restart
|
|
5145
5190
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5146
5191
|
className: _dsh_plugin_shop_css_e3675a89_default.installedLabel,
|
|
5147
5192
|
"data-shop-installed": true,
|
|
@@ -5150,7 +5195,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
5150
5195
|
name: entry.name,
|
|
5151
5196
|
t,
|
|
5152
5197
|
uninstall,
|
|
5153
|
-
installStatus
|
|
5198
|
+
installStatus,
|
|
5199
|
+
restart
|
|
5154
5200
|
})]
|
|
5155
5201
|
})
|
|
5156
5202
|
]
|
|
@@ -5163,7 +5209,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
5163
5209
|
* failure detail, rejection detail — driven by `useInstall`. Shared by the
|
|
5164
5210
|
* catalog cards (`variant: 'install'`) and the outdated rows' update button
|
|
5165
5211
|
* (`variant: 'update'`, which drives the same install flow for `name@latest`). */
|
|
5166
|
-
function InstallPanel({ name, version, tier, variant = "install", t, install, installStatus }) {
|
|
5212
|
+
function InstallPanel({ name, version, tier, variant = "install", t, install, installStatus, restart }) {
|
|
5167
5213
|
const [gateOpen, setGateOpen] = (0, react.useState)(false);
|
|
5168
5214
|
const { view, start } = useInstall(install, installStatus);
|
|
5169
5215
|
if (view.kind === "running") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -5179,10 +5225,16 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
5179
5225
|
}, index))
|
|
5180
5226
|
})]
|
|
5181
5227
|
});
|
|
5182
|
-
if (view.kind === "done") return /* @__PURE__ */ (0, react_jsx_runtime.
|
|
5183
|
-
className: _dsh_plugin_shop_css_e3675a89_default.
|
|
5184
|
-
"
|
|
5185
|
-
|
|
5228
|
+
if (view.kind === "done") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5229
|
+
className: _dsh_plugin_shop_css_e3675a89_default.installedActions,
|
|
5230
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5231
|
+
className: _dsh_plugin_shop_css_e3675a89_default.notice,
|
|
5232
|
+
"data-shop-restart-notice": true,
|
|
5233
|
+
children: view.needsRestart ? t("installedRestartNotice") : t("installedNoRestartNotice")
|
|
5234
|
+
}), view.needsRestart && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RestartPanel, {
|
|
5235
|
+
t,
|
|
5236
|
+
restart
|
|
5237
|
+
})]
|
|
5186
5238
|
});
|
|
5187
5239
|
if (view.kind === "failed") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5188
5240
|
className: _dsh_plugin_shop_css_e3675a89_default.installPanel,
|
|
@@ -5271,7 +5323,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
5271
5323
|
* §9.3 is about granting. A business failure (not in the catalog / not
|
|
5272
5324
|
* installed) lands in the failed view with the host's published detail; a
|
|
5273
5325
|
* transport failure carries the empty detail and the localized fallback. */
|
|
5274
|
-
function UninstallPanel({ name, t, uninstall, installStatus }) {
|
|
5326
|
+
function UninstallPanel({ name, t, uninstall, installStatus, restart }) {
|
|
5275
5327
|
const { view, start } = useUninstall(uninstall, installStatus);
|
|
5276
5328
|
if (view.kind === "running") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5277
5329
|
className: _dsh_plugin_shop_css_e3675a89_default.installPanel,
|
|
@@ -5286,10 +5338,16 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
5286
5338
|
}, index))
|
|
5287
5339
|
})]
|
|
5288
5340
|
});
|
|
5289
|
-
if (view.kind === "done") return /* @__PURE__ */ (0, react_jsx_runtime.
|
|
5290
|
-
className: _dsh_plugin_shop_css_e3675a89_default.
|
|
5291
|
-
"
|
|
5292
|
-
|
|
5341
|
+
if (view.kind === "done") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5342
|
+
className: _dsh_plugin_shop_css_e3675a89_default.installedActions,
|
|
5343
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5344
|
+
className: _dsh_plugin_shop_css_e3675a89_default.notice,
|
|
5345
|
+
"data-shop-uninstall-done": true,
|
|
5346
|
+
children: t("uninstalledRestartNotice")
|
|
5347
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RestartPanel, {
|
|
5348
|
+
t,
|
|
5349
|
+
restart
|
|
5350
|
+
})]
|
|
5293
5351
|
});
|
|
5294
5352
|
if (view.kind === "failed") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5295
5353
|
className: _dsh_plugin_shop_css_e3675a89_default.installPanel,
|
|
@@ -5320,13 +5378,109 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
5320
5378
|
children: t("uninstall")
|
|
5321
5379
|
});
|
|
5322
5380
|
}
|
|
5381
|
+
/** The §8 restart flow (amendment 2026-08-27): after an install, update, or
|
|
5382
|
+
* uninstall reports done, this panel offers a restart of dsh. The
|
|
5383
|
+
* confirmation gate states the cost — the page disconnects and in-flight
|
|
5384
|
+
* conversations/tasks are interrupted — and on confirm the restart RPC
|
|
5385
|
+
* commits the two-phase handoff: the host exits, a helper re-runs dsh, and
|
|
5386
|
+
* this panel polls the origin after a grace period, refreshing the page
|
|
5387
|
+
* once the NEW server answers. A refused restart renders the host's
|
|
5388
|
+
* published detail; a server that never comes back names the manual
|
|
5389
|
+
* command. */
|
|
5390
|
+
function RestartPanel({ t, restart }) {
|
|
5391
|
+
const [gateOpen, setGateOpen] = (0, react.useState)(false);
|
|
5392
|
+
const [state, setState] = (0, react.useState)({ kind: "idle" });
|
|
5393
|
+
const onConfirm = async () => {
|
|
5394
|
+
setGateOpen(false);
|
|
5395
|
+
try {
|
|
5396
|
+
const result = await restart();
|
|
5397
|
+
if (!result.ok) {
|
|
5398
|
+
setState({
|
|
5399
|
+
kind: "failed",
|
|
5400
|
+
detail: result.detail
|
|
5401
|
+
});
|
|
5402
|
+
return;
|
|
5403
|
+
}
|
|
5404
|
+
setState({ kind: "restarting" });
|
|
5405
|
+
} catch {
|
|
5406
|
+
setState({
|
|
5407
|
+
kind: "failed",
|
|
5408
|
+
detail: t("restartTransportFailed")
|
|
5409
|
+
});
|
|
5410
|
+
}
|
|
5411
|
+
};
|
|
5412
|
+
(0, react.useEffect)(() => {
|
|
5413
|
+
if (state.kind !== "restarting") return;
|
|
5414
|
+
const started = Date.now();
|
|
5415
|
+
const timer = setInterval(() => {
|
|
5416
|
+
const elapsed = Date.now() - started;
|
|
5417
|
+
if (elapsed < 3e3) return;
|
|
5418
|
+
fetch(window.location.href, { cache: "no-store" }).then(() => {
|
|
5419
|
+
window.location.reload();
|
|
5420
|
+
}, () => {});
|
|
5421
|
+
if (elapsed > 3e4) {
|
|
5422
|
+
clearInterval(timer);
|
|
5423
|
+
setState({
|
|
5424
|
+
kind: "failed",
|
|
5425
|
+
detail: t("restartFailedNotice")
|
|
5426
|
+
});
|
|
5427
|
+
}
|
|
5428
|
+
}, INSTALL_POLL_MS);
|
|
5429
|
+
return () => clearInterval(timer);
|
|
5430
|
+
}, [state, t]);
|
|
5431
|
+
if (state.kind === "restarting") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5432
|
+
className: _dsh_plugin_shop_css_e3675a89_default.notice,
|
|
5433
|
+
"data-shop-restarting": true,
|
|
5434
|
+
children: t("restarting")
|
|
5435
|
+
});
|
|
5436
|
+
if (state.kind === "failed") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5437
|
+
className: _dsh_plugin_shop_css_e3675a89_default.failedDetail,
|
|
5438
|
+
"data-shop-restart-error": true,
|
|
5439
|
+
children: state.detail
|
|
5440
|
+
});
|
|
5441
|
+
if (gateOpen) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5442
|
+
className: _dsh_plugin_shop_css_e3675a89_default.gate,
|
|
5443
|
+
children: [
|
|
5444
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5445
|
+
className: _dsh_plugin_shop_css_e3675a89_default.gateTitle,
|
|
5446
|
+
children: t("restartTitle")
|
|
5447
|
+
}),
|
|
5448
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5449
|
+
className: _dsh_plugin_shop_css_e3675a89_default.gateBody,
|
|
5450
|
+
children: t("restartBody")
|
|
5451
|
+
}),
|
|
5452
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5453
|
+
className: _dsh_plugin_shop_css_e3675a89_default.gateActions,
|
|
5454
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5455
|
+
type: "button",
|
|
5456
|
+
className: _dsh_plugin_shop_css_e3675a89_default.confirmButton,
|
|
5457
|
+
"data-shop-restart-confirm": true,
|
|
5458
|
+
onClick: () => void onConfirm(),
|
|
5459
|
+
children: t("restartConfirm")
|
|
5460
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5461
|
+
type: "button",
|
|
5462
|
+
className: _dsh_plugin_shop_css_e3675a89_default.cancelButton,
|
|
5463
|
+
onClick: () => setGateOpen(false),
|
|
5464
|
+
children: t("cancel")
|
|
5465
|
+
})]
|
|
5466
|
+
})
|
|
5467
|
+
]
|
|
5468
|
+
});
|
|
5469
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5470
|
+
type: "button",
|
|
5471
|
+
className: _dsh_plugin_shop_css_e3675a89_default.restartButton,
|
|
5472
|
+
"data-shop-restart": true,
|
|
5473
|
+
onClick: () => setGateOpen(true),
|
|
5474
|
+
children: t("restart")
|
|
5475
|
+
});
|
|
5476
|
+
}
|
|
5323
5477
|
/** One outdated install row (§7.3): the name, the installed and latest
|
|
5324
5478
|
* versions, an optimistic enable/disable switch, and the update button (the
|
|
5325
5479
|
* install flow for `name@latest`, reusing `InstallPanel`). The switch has no
|
|
5326
5480
|
* installed-state RPC behind it — the tab assumes an installed plugin is on,
|
|
5327
5481
|
* so the first toggle sends the inverted value. After a successful `setEnabled`
|
|
5328
5482
|
* the §8 hot note renders. */
|
|
5329
|
-
function OutdatedRow({ row, tier, t, setEnabled, install, installStatus }) {
|
|
5483
|
+
function OutdatedRow({ row, tier, t, setEnabled, install, installStatus, restart }) {
|
|
5330
5484
|
const [enabled, setEnabledState] = (0, react.useState)(true);
|
|
5331
5485
|
const [toggle, setToggle] = (0, react.useState)({ kind: "idle" });
|
|
5332
5486
|
const onToggle = async () => {
|
|
@@ -5391,7 +5545,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
5391
5545
|
variant: "update",
|
|
5392
5546
|
t,
|
|
5393
5547
|
install,
|
|
5394
|
-
installStatus
|
|
5548
|
+
installStatus,
|
|
5549
|
+
restart
|
|
5395
5550
|
})]
|
|
5396
5551
|
}),
|
|
5397
5552
|
toggle.kind === "saved" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
@@ -5414,7 +5569,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
5414
5569
|
* tier for the update gate is looked up from the catalog by name (community →
|
|
5415
5570
|
* acknowledgement); an entry absent from the catalog defaults to the
|
|
5416
5571
|
* community gate (the safer read). */
|
|
5417
|
-
function OutdatedSection({ state, tiers, t, setEnabled, install, installStatus }) {
|
|
5572
|
+
function OutdatedSection({ state, tiers, t, setEnabled, install, installStatus, restart }) {
|
|
5418
5573
|
if (state.kind === "loading") return null;
|
|
5419
5574
|
if (state.kind === "error") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5420
5575
|
className: _dsh_plugin_shop_css_e3675a89_default.stateLine,
|
|
@@ -5437,7 +5592,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
5437
5592
|
t,
|
|
5438
5593
|
setEnabled,
|
|
5439
5594
|
install,
|
|
5440
|
-
installStatus
|
|
5595
|
+
installStatus,
|
|
5596
|
+
restart
|
|
5441
5597
|
}) }, row.name))
|
|
5442
5598
|
})]
|
|
5443
5599
|
});
|
|
@@ -5445,7 +5601,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
5445
5601
|
/** The shop tab root: browse, search, refresh, and render one card per
|
|
5446
5602
|
* entry. Data attributes on the e2e-relevant nodes follow the Task 3 list. */
|
|
5447
5603
|
function ShopTab(props) {
|
|
5448
|
-
const { t, catalog, install, installStatus, setEnabled, installed, uninstall } = props;
|
|
5604
|
+
const { t, catalog, install, installStatus, setEnabled, installed, uninstall, restart } = props;
|
|
5449
5605
|
const [catalogState, setCatalogState] = (0, react.useState)({ kind: "loading" });
|
|
5450
5606
|
const [installedState, setInstalledState] = (0, react.useState)({ kind: "loading" });
|
|
5451
5607
|
const [request, setRequest] = (0, react.useState)({ kind: "initial" });
|
|
@@ -5689,7 +5845,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
5689
5845
|
t,
|
|
5690
5846
|
install,
|
|
5691
5847
|
installStatus,
|
|
5692
|
-
uninstall
|
|
5848
|
+
uninstall,
|
|
5849
|
+
restart
|
|
5693
5850
|
}) }, entry.name)), incremental && visibleCount < filtered.length && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
|
|
5694
5851
|
ref: sentinelRef,
|
|
5695
5852
|
className: _dsh_plugin_shop_css_e3675a89_default.cardsSentry,
|
|
@@ -5710,7 +5867,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
5710
5867
|
t,
|
|
5711
5868
|
setEnabled,
|
|
5712
5869
|
install,
|
|
5713
|
-
installStatus
|
|
5870
|
+
installStatus,
|
|
5871
|
+
restart
|
|
5714
5872
|
})
|
|
5715
5873
|
]
|
|
5716
5874
|
});
|
|
@@ -5754,7 +5912,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
5754
5912
|
installStatus: async (args) => unwrap(await ns.installStatus(args)),
|
|
5755
5913
|
setEnabled: async (args) => unwrap(await ns.setEnabled(args)),
|
|
5756
5914
|
installed: async () => unwrap(await ns.installed()),
|
|
5757
|
-
uninstall: async (args) => unwrap(await ns.uninstallStart(args))
|
|
5915
|
+
uninstall: async (args) => unwrap(await ns.uninstallStart(args)),
|
|
5916
|
+
restart: async () => unwrap(await ns.restart())
|
|
5758
5917
|
});
|
|
5759
5918
|
ctx.slots.inject("settings.plugins.tab", () => ctx.slots.register({
|
|
5760
5919
|
name: "settings.plugins.tab",
|
package/lib/index.js
CHANGED
|
@@ -2,9 +2,9 @@ import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
|
|
|
2
2
|
import { loadOptionalPatches, readProfileManifest, resolveProfileDir } from "@deepseek-ai/dsh-app-boot";
|
|
3
3
|
import { lt, minVersion } from "semver";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { createHash, randomUUID } from "node:crypto";
|
|
6
|
-
import { existsSync, mkdirSync, readFileSync, realpathSync, renameSync, writeFileSync } from "node:fs";
|
|
7
5
|
import { basename, dirname, join } from "node:path";
|
|
6
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
7
|
+
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, realpathSync, renameSync, writeFileSync } from "node:fs";
|
|
8
8
|
import { z } from "zod";
|
|
9
9
|
import { spawn } from "node:child_process";
|
|
10
10
|
import { dump } from "js-yaml";
|
|
@@ -418,6 +418,53 @@ function startUninstall(options) {
|
|
|
418
418
|
});
|
|
419
419
|
}
|
|
420
420
|
//#endregion
|
|
421
|
+
//#region src/host/restart.ts
|
|
422
|
+
/** Restart executor: hand the port to a new dsh instance, two-phase.
|
|
423
|
+
*
|
|
424
|
+
* The old process cannot wait for the new one: the new one must bind the
|
|
425
|
+
* port the old one still holds, and two live processes cannot bind it at
|
|
426
|
+
* once — the first implementation spawned the child and waited for its URL,
|
|
427
|
+
* and the child crashed in boot with EADDRINUSE every time. The handoff is
|
|
428
|
+
* therefore inverted: the parent commits and exits FIRST, and a detached
|
|
429
|
+
* helper waits for the parent's pid to disappear before exec'ing the same
|
|
430
|
+
* dsh command line. The browser monitors the origin and refreshes once the
|
|
431
|
+
* new server answers; a boot that fails is diagnosed from the log file,
|
|
432
|
+
* since nobody is attached to the child's pipes. */
|
|
433
|
+
/** Spawn the two-phase handoff. The helper is a POSIX shell wrapper that
|
|
434
|
+
* polls the parent pid until it is gone, then replaces itself with the dsh
|
|
435
|
+
* command — `exec "$@"` keeps the argv verbatim, so no argument quoting is
|
|
436
|
+
* involved. The child's stdout/stderr go to `logFile`, opened here in
|
|
437
|
+
* append mode; opening throws on failure, and the caller treats a throw as
|
|
438
|
+
* a refusal (the restart is never committed without its log).
|
|
439
|
+
*
|
|
440
|
+
* The pid-poll has the usual tiny reuse race — if the parent's pid is
|
|
441
|
+
* recycled within the 0.2s polling gap the helper waits for the unrelated
|
|
442
|
+
* process too. Harmless: it only delays the boot. */
|
|
443
|
+
function startRestart(options) {
|
|
444
|
+
const { dshBin, argv, parentPid, logFile, env } = options;
|
|
445
|
+
const logFd = openSync(logFile, "a");
|
|
446
|
+
try {
|
|
447
|
+
spawn("sh", [
|
|
448
|
+
"-c",
|
|
449
|
+
"while kill -0 \"$1\" 2>/dev/null; do sleep 0.2; done; shift; exec \"$@\"",
|
|
450
|
+
"sh",
|
|
451
|
+
String(parentPid),
|
|
452
|
+
dshBin,
|
|
453
|
+
...argv
|
|
454
|
+
], {
|
|
455
|
+
stdio: [
|
|
456
|
+
"ignore",
|
|
457
|
+
logFd,
|
|
458
|
+
logFd
|
|
459
|
+
],
|
|
460
|
+
env: env ?? process.env,
|
|
461
|
+
detached: true
|
|
462
|
+
}).unref();
|
|
463
|
+
} finally {
|
|
464
|
+
closeSync(logFd);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
//#endregion
|
|
421
468
|
//#region src/host/profile.ts
|
|
422
469
|
/** Profile directory discovery and user-layer writes (§8: hot enable/disable). */
|
|
423
470
|
/**
|
|
@@ -543,6 +590,7 @@ let ShopGateway = (() => {
|
|
|
543
590
|
let _installStatus_decorators;
|
|
544
591
|
let _installed_decorators;
|
|
545
592
|
let _uninstall_decorators;
|
|
593
|
+
let _restart_decorators;
|
|
546
594
|
return class ShopGateway extends _classSuper {
|
|
547
595
|
static {
|
|
548
596
|
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
|
|
@@ -552,6 +600,7 @@ let ShopGateway = (() => {
|
|
|
552
600
|
_installStatus_decorators = [Remote("installStatus")];
|
|
553
601
|
_installed_decorators = [Remote("installed")];
|
|
554
602
|
_uninstall_decorators = [Remote("uninstallStart")];
|
|
603
|
+
_restart_decorators = [Remote("restart")];
|
|
555
604
|
__esDecorate(this, null, _setEnabled_decorators, {
|
|
556
605
|
kind: "method",
|
|
557
606
|
name: "setEnabled",
|
|
@@ -618,6 +667,17 @@ let ShopGateway = (() => {
|
|
|
618
667
|
},
|
|
619
668
|
metadata: _metadata
|
|
620
669
|
}, null, _instanceExtraInitializers);
|
|
670
|
+
__esDecorate(this, null, _restart_decorators, {
|
|
671
|
+
kind: "method",
|
|
672
|
+
name: "restart",
|
|
673
|
+
static: false,
|
|
674
|
+
private: false,
|
|
675
|
+
access: {
|
|
676
|
+
has: (obj) => "restart" in obj,
|
|
677
|
+
get: (obj) => obj.restart
|
|
678
|
+
},
|
|
679
|
+
metadata: _metadata
|
|
680
|
+
}, null, _instanceExtraInitializers);
|
|
621
681
|
if (_metadata) Object.defineProperty(this, Symbol.metadata, {
|
|
622
682
|
enumerable: true,
|
|
623
683
|
configurable: true,
|
|
@@ -632,11 +692,22 @@ let ShopGateway = (() => {
|
|
|
632
692
|
profileDir;
|
|
633
693
|
inventory;
|
|
634
694
|
dshBin;
|
|
695
|
+
/** The argv `shop/restart` re-spawns: the real process argv minus node and
|
|
696
|
+
* the CLI script path, or a test-provided substitute. */
|
|
697
|
+
restartArgv;
|
|
698
|
+
/** The exit the restart calls once the response is out; `process.exit` in
|
|
699
|
+
* production, a spy in tests. */
|
|
700
|
+
exit;
|
|
701
|
+
restartExitDelayMs;
|
|
702
|
+
restartParentPid;
|
|
635
703
|
/** The install gate runs against the last loaded snapshot, never a fresh
|
|
636
704
|
* fetch per request (§7.2: the Host's cached snapshot is the truth). */
|
|
637
705
|
/** Finished install records retained, so a poll sees the true terminal
|
|
638
706
|
* state (§8: done / needsRestart / failure detail). Oldest evicted on add. */
|
|
639
707
|
static MAX_FINISHED_INSTALLS = 32;
|
|
708
|
+
/** How long the gateway waits after a successful restart response before
|
|
709
|
+
* exiting the old process — the browser must receive the URL first. */
|
|
710
|
+
static RESTART_EXIT_DELAY_MS = 2e3;
|
|
640
711
|
/** The install gate runs against the last loaded snapshot, never a fresh
|
|
641
712
|
* fetch per request (§7.2: the Host's cached snapshot is the truth). */
|
|
642
713
|
lastSnapshot = null;
|
|
@@ -651,6 +722,10 @@ let ShopGateway = (() => {
|
|
|
651
722
|
this.profileDir = options.profileDir;
|
|
652
723
|
this.inventory = options.inventory;
|
|
653
724
|
this.dshBin = options.dshBin ?? "dsh";
|
|
725
|
+
this.restartArgv = options.restartArgv ?? process.argv.slice(2);
|
|
726
|
+
this.exit = options.exit ?? ((code) => process.exit(code));
|
|
727
|
+
this.restartExitDelayMs = options.restartExitDelayMs ?? ShopGateway.RESTART_EXIT_DELAY_MS;
|
|
728
|
+
this.restartParentPid = options.restartParentPid ?? process.pid;
|
|
654
729
|
}
|
|
655
730
|
/** The boot's Loader root directory (the active profile's `cordis.yml`
|
|
656
731
|
* directory, carried on `ctx.baseUrl`), when present. A `link:` install
|
|
@@ -865,6 +940,35 @@ let ShopGateway = (() => {
|
|
|
865
940
|
installId: running.installId
|
|
866
941
|
};
|
|
867
942
|
}
|
|
943
|
+
/** Restart the dsh process the shop runs in (§8 amendment, 2026-08-27):
|
|
944
|
+
* commit a two-phase handoff — a detached helper waits for this pid to
|
|
945
|
+
* exit, then re-runs this process's own command line — and exit once the
|
|
946
|
+
* response is out. The browser monitors the origin and refreshes when the
|
|
947
|
+
* new server answers. Refusals are issued before anything is torn down. */
|
|
948
|
+
async restart() {
|
|
949
|
+
const portIndex = this.restartArgv.indexOf("--port");
|
|
950
|
+
if (portIndex !== -1 && this.restartArgv[portIndex + 1] === "0") return {
|
|
951
|
+
ok: false,
|
|
952
|
+
detail: "dsh-plugin-shop: restart is not supported when dsh was launched with --port 0; restart dsh manually"
|
|
953
|
+
};
|
|
954
|
+
try {
|
|
955
|
+
const { cacheDir } = this.rowConfig();
|
|
956
|
+
startRestart({
|
|
957
|
+
dshBin: this.dshBin,
|
|
958
|
+
argv: this.restartArgv,
|
|
959
|
+
parentPid: this.restartParentPid,
|
|
960
|
+
logFile: join(cacheDir, "restart.log"),
|
|
961
|
+
env: process.env
|
|
962
|
+
});
|
|
963
|
+
} catch (error) {
|
|
964
|
+
return {
|
|
965
|
+
ok: false,
|
|
966
|
+
detail: `dsh-plugin-shop: restart could not be started: ${error.message}`
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
setTimeout(() => this.exit(0), this.restartExitDelayMs);
|
|
970
|
+
return { ok: true };
|
|
971
|
+
}
|
|
868
972
|
};
|
|
869
973
|
})();
|
|
870
974
|
//#endregion
|
package/lib/typert.host.js
CHANGED
|
@@ -67,6 +67,12 @@ 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
|
+
}), z.object({
|
|
73
|
+
'ok': z.literal(false),
|
|
74
|
+
'detail': z.string(),
|
|
75
|
+
})])
|
|
70
76
|
const dsh_plugin_shop_shop_setEnabled_parameter_0$schema = z.object({
|
|
71
77
|
'name': z.string(),
|
|
72
78
|
'enabled': z.boolean(),
|
|
@@ -116,7 +122,7 @@ export const TYPERT = {
|
|
|
116
122
|
typeSymbol: 'dsh-plugin-shop/types#ShopCatalogResult',
|
|
117
123
|
schema: dsh_plugin_shop_shop_catalog_result$schema,
|
|
118
124
|
},
|
|
119
|
-
sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":
|
|
125
|
+
sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":225,"column":9},
|
|
120
126
|
},
|
|
121
127
|
{
|
|
122
128
|
id: 'dsh-plugin-shop#shop/installed',
|
|
@@ -131,7 +137,7 @@ export const TYPERT = {
|
|
|
131
137
|
typeSymbol: 'dsh-plugin-shop#shop/installed:result',
|
|
132
138
|
schema: dsh_plugin_shop_shop_installed_result$schema,
|
|
133
139
|
},
|
|
134
|
-
sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":
|
|
140
|
+
sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":300,"column":9},
|
|
135
141
|
},
|
|
136
142
|
{
|
|
137
143
|
id: 'dsh-plugin-shop#shop/installStart',
|
|
@@ -157,7 +163,7 @@ export const TYPERT = {
|
|
|
157
163
|
typeSymbol: 'dsh-plugin-shop/types#ShopInstallResult',
|
|
158
164
|
schema: dsh_plugin_shop_shop_installStart_result$schema,
|
|
159
165
|
},
|
|
160
|
-
sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":
|
|
166
|
+
sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":251,"column":9},
|
|
161
167
|
},
|
|
162
168
|
{
|
|
163
169
|
id: 'dsh-plugin-shop#shop/installStatus',
|
|
@@ -182,7 +188,22 @@ export const TYPERT = {
|
|
|
182
188
|
typeSymbol: 'dsh-plugin-shop/types#ShopInstallStatusResult',
|
|
183
189
|
schema: dsh_plugin_shop_shop_installStatus_result$schema,
|
|
184
190
|
},
|
|
185
|
-
sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":
|
|
191
|
+
sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":289,"column":3},
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
id: 'dsh-plugin-shop#shop/restart',
|
|
195
|
+
service: 'shop',
|
|
196
|
+
namespace: 'shop',
|
|
197
|
+
method: 'restart',
|
|
198
|
+
invocation: { kind: 'direct' },
|
|
199
|
+
parameters: [
|
|
200
|
+
],
|
|
201
|
+
result: {
|
|
202
|
+
mode: 'strict',
|
|
203
|
+
typeSymbol: 'dsh-plugin-shop/types#ShopRestartResult',
|
|
204
|
+
schema: dsh_plugin_shop_shop_restart_result$schema,
|
|
205
|
+
},
|
|
206
|
+
sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":373,"column":9},
|
|
186
207
|
},
|
|
187
208
|
{
|
|
188
209
|
id: 'dsh-plugin-shop#shop/setEnabled',
|
|
@@ -207,7 +228,7 @@ export const TYPERT = {
|
|
|
207
228
|
typeSymbol: 'dsh-plugin-shop/types#ShopSetEnabledResult',
|
|
208
229
|
schema: dsh_plugin_shop_shop_setEnabled_result$schema,
|
|
209
230
|
},
|
|
210
|
-
sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":
|
|
231
|
+
sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":194,"column":3},
|
|
211
232
|
},
|
|
212
233
|
{
|
|
213
234
|
id: 'dsh-plugin-shop#shop/uninstallStart',
|
|
@@ -233,7 +254,7 @@ export const TYPERT = {
|
|
|
233
254
|
typeSymbol: 'dsh-plugin-shop/types#ShopUninstallResult',
|
|
234
255
|
schema: dsh_plugin_shop_shop_uninstallStart_result$schema,
|
|
235
256
|
},
|
|
236
|
-
sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":
|
|
257
|
+
sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":340,"column":9},
|
|
237
258
|
},
|
|
238
259
|
],
|
|
239
260
|
model: {
|
|
@@ -293,6 +314,13 @@ export const TYPERT = {
|
|
|
293
314
|
"signature": "@Remote('uninstallStart') async uninstall(args: { name: string }): Promise<ShopUninstallResult>",
|
|
294
315
|
"summary": "Uninstall one installed catalog plugin from the profile (§7.3 follow-up amendment).",
|
|
295
316
|
"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. */"
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
"kind": "method",
|
|
320
|
+
"name": "restart",
|
|
321
|
+
"signature": "@Remote('restart') async restart(): Promise<ShopRestartResult>",
|
|
322
|
+
"summary": "Restart the dsh process the shop runs in (§8 amendment, 2026-08-27): commit a two-phase handoff — a detached helper waits for this pid to exit, then re-runs this process's own command line — and exit once the response is out.",
|
|
323
|
+
"jsDoc": "/** Restart the dsh process the shop runs in (§8 amendment, 2026-08-27):\n * commit a two-phase handoff — a detached helper waits for this pid to\n * exit, then re-runs this process's own command line — and exit once the\n * response is out. The browser monitors the origin and refreshes when the\n * new server answers. Refusals are issued before anything is torn down. */"
|
|
296
324
|
}
|
|
297
325
|
],
|
|
298
326
|
"types": [
|
|
@@ -328,6 +356,10 @@ export const TYPERT = {
|
|
|
328
356
|
"name": "InstallStatus",
|
|
329
357
|
"declaration": "export interface InstallStatus {\n state: InstallState;\n log: string[];\n needsRestart?: boolean;\n detail?: string;\n}"
|
|
330
358
|
},
|
|
359
|
+
{
|
|
360
|
+
"name": "RestartOutcome",
|
|
361
|
+
"declaration": "export type RestartOutcome = { ok: true; } | { ok: false; detail: string; };"
|
|
362
|
+
},
|
|
331
363
|
{
|
|
332
364
|
"name": "ShopCatalogResult",
|
|
333
365
|
"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 +376,10 @@ export const TYPERT = {
|
|
|
344
376
|
"name": "ShopInstallStatusResult",
|
|
345
377
|
"declaration": "export interface ShopInstallStatusResult extends InstallStatus {\n found: boolean;\n}"
|
|
346
378
|
},
|
|
379
|
+
{
|
|
380
|
+
"name": "ShopRestartResult",
|
|
381
|
+
"declaration": "export type ShopRestartResult = RestartOutcome;"
|
|
382
|
+
},
|
|
347
383
|
{
|
|
348
384
|
"name": "ShopSetEnabledResult",
|
|
349
385
|
"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,12 @@ 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
|
+
}), z.object({
|
|
73
|
+
'ok': z.literal(false),
|
|
74
|
+
'detail': z.string(),
|
|
75
|
+
})])
|
|
70
76
|
const dsh_plugin_shop_shop_setEnabled_parameter_0$schema = z.object({
|
|
71
77
|
'name': z.string(),
|
|
72
78
|
'enabled': z.boolean(),
|
|
@@ -113,7 +119,7 @@ export const TYPERT_REMOTE = {
|
|
|
113
119
|
typeSymbol: 'dsh-plugin-shop/types#ShopCatalogResult',
|
|
114
120
|
schema: dsh_plugin_shop_shop_catalog_result$schema,
|
|
115
121
|
},
|
|
116
|
-
sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":
|
|
122
|
+
sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":225,"column":9},
|
|
117
123
|
},
|
|
118
124
|
{
|
|
119
125
|
id: 'dsh-plugin-shop#shop/installed',
|
|
@@ -128,7 +134,7 @@ export const TYPERT_REMOTE = {
|
|
|
128
134
|
typeSymbol: 'dsh-plugin-shop#shop/installed:result',
|
|
129
135
|
schema: dsh_plugin_shop_shop_installed_result$schema,
|
|
130
136
|
},
|
|
131
|
-
sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":
|
|
137
|
+
sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":300,"column":9},
|
|
132
138
|
},
|
|
133
139
|
{
|
|
134
140
|
id: 'dsh-plugin-shop#shop/installStart',
|
|
@@ -154,7 +160,7 @@ export const TYPERT_REMOTE = {
|
|
|
154
160
|
typeSymbol: 'dsh-plugin-shop/types#ShopInstallResult',
|
|
155
161
|
schema: dsh_plugin_shop_shop_installStart_result$schema,
|
|
156
162
|
},
|
|
157
|
-
sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":
|
|
163
|
+
sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":251,"column":9},
|
|
158
164
|
},
|
|
159
165
|
{
|
|
160
166
|
id: 'dsh-plugin-shop#shop/installStatus',
|
|
@@ -179,7 +185,22 @@ export const TYPERT_REMOTE = {
|
|
|
179
185
|
typeSymbol: 'dsh-plugin-shop/types#ShopInstallStatusResult',
|
|
180
186
|
schema: dsh_plugin_shop_shop_installStatus_result$schema,
|
|
181
187
|
},
|
|
182
|
-
sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":
|
|
188
|
+
sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":289,"column":3},
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
id: 'dsh-plugin-shop#shop/restart',
|
|
192
|
+
service: 'shop',
|
|
193
|
+
namespace: 'shop',
|
|
194
|
+
method: 'restart',
|
|
195
|
+
invocation: { kind: 'direct' },
|
|
196
|
+
parameters: [
|
|
197
|
+
],
|
|
198
|
+
result: {
|
|
199
|
+
mode: 'strict',
|
|
200
|
+
typeSymbol: 'dsh-plugin-shop/types#ShopRestartResult',
|
|
201
|
+
schema: dsh_plugin_shop_shop_restart_result$schema,
|
|
202
|
+
},
|
|
203
|
+
sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":373,"column":9},
|
|
183
204
|
},
|
|
184
205
|
{
|
|
185
206
|
id: 'dsh-plugin-shop#shop/setEnabled',
|
|
@@ -204,7 +225,7 @@ export const TYPERT_REMOTE = {
|
|
|
204
225
|
typeSymbol: 'dsh-plugin-shop/types#ShopSetEnabledResult',
|
|
205
226
|
schema: dsh_plugin_shop_shop_setEnabled_result$schema,
|
|
206
227
|
},
|
|
207
|
-
sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":
|
|
228
|
+
sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":194,"column":3},
|
|
208
229
|
},
|
|
209
230
|
{
|
|
210
231
|
id: 'dsh-plugin-shop#shop/uninstallStart',
|
|
@@ -230,7 +251,7 @@ export const TYPERT_REMOTE = {
|
|
|
230
251
|
typeSymbol: 'dsh-plugin-shop/types#ShopUninstallResult',
|
|
231
252
|
schema: dsh_plugin_shop_shop_uninstallStart_result$schema,
|
|
232
253
|
},
|
|
233
|
-
sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":
|
|
254
|
+
sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":340,"column":9},
|
|
234
255
|
},
|
|
235
256
|
],
|
|
236
257
|
}
|
|
@@ -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,13 @@ 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
|
+
restartFailedNotice: string;
|
|
63
|
+
restartTransportFailed: string;
|
|
57
64
|
enabledSwitch: string;
|
|
58
65
|
toggleFailed: string;
|
|
59
66
|
hotApplyNote: string;
|
|
@@ -115,6 +122,13 @@ export declare const en: {
|
|
|
115
122
|
uninstallFailed: string;
|
|
116
123
|
uninstallTransportFailed: string;
|
|
117
124
|
uninstalledRestartNotice: string;
|
|
125
|
+
restart: string;
|
|
126
|
+
restartTitle: string;
|
|
127
|
+
restartBody: string;
|
|
128
|
+
restartConfirm: string;
|
|
129
|
+
restarting: string;
|
|
130
|
+
restartFailedNotice: string;
|
|
131
|
+
restartTransportFailed: string;
|
|
118
132
|
enabledSwitch: string;
|
|
119
133
|
toggleFailed: string;
|
|
120
134
|
hotApplyNote: string;
|
|
@@ -58,6 +58,13 @@ export type InstallEvent = {
|
|
|
58
58
|
};
|
|
59
59
|
/** §7.2 once-per-second poll cadence, as a named constant. */
|
|
60
60
|
export declare const INSTALL_POLL_MS = 1000;
|
|
61
|
+
/** §8 restart handoff: the client polls the origin only after this grace
|
|
62
|
+
* period (the host's exit delay plus margin), so an origin that answers is
|
|
63
|
+
* the NEW server, never the dying old one. */
|
|
64
|
+
export declare const RESTART_GRACE_MS = 3000;
|
|
65
|
+
/** §8 restart handoff: how long the client waits for the new server to
|
|
66
|
+
* answer before reporting the manual restart command. */
|
|
67
|
+
export declare const RESTART_WAIT_MS = 30000;
|
|
61
68
|
/** How many shelf cards mount at a time. The shelf holds ~1900 entries; one
|
|
62
69
|
* commit of the whole grid is ~28k DOM nodes, so the list renders in batches
|
|
63
70
|
* behind a sentinel (§A1) and grows on scroll. 48 is ~3-4 rows. */
|
|
@@ -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,19 @@ 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
|
+
/** The pid the restart helper waits on before exec'ing the new dsh;
|
|
40
|
+
* defaults to this process. Tests point it at a dead pid so the fixture
|
|
41
|
+
* runs immediately instead of waiting for the vitest worker to exit. */
|
|
42
|
+
restartParentPid?: number;
|
|
43
|
+
/** How long the gateway waits after a successful restart response before
|
|
44
|
+
* exiting the old process; test-only shortening, production uses 2s. */
|
|
45
|
+
restartExitDelayMs?: number;
|
|
32
46
|
}
|
|
33
47
|
/** `shop/installStart` result (§7.3): rejections are typed wire values with an
|
|
34
48
|
* author-readable `detail`, not thrown RPC errors. */
|
|
@@ -59,6 +73,9 @@ export type ShopUninstallResult = {
|
|
|
59
73
|
ok: false;
|
|
60
74
|
detail: string;
|
|
61
75
|
};
|
|
76
|
+
/** `shop/restart` result (§7.3): the restarted server's URL, or a typed
|
|
77
|
+
* failure — on failure the old process is still serving. */
|
|
78
|
+
export type ShopRestartResult = RestartOutcome;
|
|
62
79
|
/** `shop/installed` entry (§7.3): one installed catalog plugin. `installed`
|
|
63
80
|
* is the profile manifest's dependency spec verbatim (a range, a tag, or
|
|
64
81
|
* `workspace:*`); `outdated` is the Host's verdict that the installed version
|
|
@@ -91,11 +108,22 @@ export declare class ShopGateway extends TypertRemoteService {
|
|
|
91
108
|
private readonly profileDir?;
|
|
92
109
|
private readonly inventory?;
|
|
93
110
|
private readonly dshBin;
|
|
111
|
+
/** The argv `shop/restart` re-spawns: the real process argv minus node and
|
|
112
|
+
* the CLI script path, or a test-provided substitute. */
|
|
113
|
+
private readonly restartArgv;
|
|
114
|
+
/** The exit the restart calls once the response is out; `process.exit` in
|
|
115
|
+
* production, a spy in tests. */
|
|
116
|
+
private readonly exit;
|
|
117
|
+
private readonly restartExitDelayMs;
|
|
118
|
+
private readonly restartParentPid;
|
|
94
119
|
/** The install gate runs against the last loaded snapshot, never a fresh
|
|
95
120
|
* fetch per request (§7.2: the Host's cached snapshot is the truth). */
|
|
96
121
|
/** Finished install records retained, so a poll sees the true terminal
|
|
97
122
|
* state (§8: done / needsRestart / failure detail). Oldest evicted on add. */
|
|
98
123
|
private static readonly MAX_FINISHED_INSTALLS;
|
|
124
|
+
/** How long the gateway waits after a successful restart response before
|
|
125
|
+
* exiting the old process — the browser must receive the URL first. */
|
|
126
|
+
private static readonly RESTART_EXIT_DELAY_MS;
|
|
99
127
|
/** The install gate runs against the last loaded snapshot, never a fresh
|
|
100
128
|
* fetch per request (§7.2: the Host's cached snapshot is the truth). */
|
|
101
129
|
private lastSnapshot;
|
|
@@ -160,5 +188,11 @@ export declare class ShopGateway extends TypertRemoteService {
|
|
|
160
188
|
uninstall(args: {
|
|
161
189
|
name: string;
|
|
162
190
|
}): Promise<ShopUninstallResult>;
|
|
191
|
+
/** Restart the dsh process the shop runs in (§8 amendment, 2026-08-27):
|
|
192
|
+
* commit a two-phase handoff — a detached helper waits for this pid to
|
|
193
|
+
* exit, then re-runs this process's own command line — and exit once the
|
|
194
|
+
* response is out. The browser monitors the origin and refreshes when the
|
|
195
|
+
* new server answers. Refusals are issued before anything is torn down. */
|
|
196
|
+
restart(): Promise<ShopRestartResult>;
|
|
163
197
|
}
|
|
164
198
|
export default ShopGateway;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** Restart executor: hand the port to a new dsh instance, two-phase.
|
|
2
|
+
*
|
|
3
|
+
* The old process cannot wait for the new one: the new one must bind the
|
|
4
|
+
* port the old one still holds, and two live processes cannot bind it at
|
|
5
|
+
* once — the first implementation spawned the child and waited for its URL,
|
|
6
|
+
* and the child crashed in boot with EADDRINUSE every time. The handoff is
|
|
7
|
+
* therefore inverted: the parent commits and exits FIRST, and a detached
|
|
8
|
+
* helper waits for the parent's pid to disappear before exec'ing the same
|
|
9
|
+
* dsh command line. The browser monitors the origin and refreshes once the
|
|
10
|
+
* new server answers; a boot that fails is diagnosed from the log file,
|
|
11
|
+
* since nobody is attached to the child's pipes. */
|
|
12
|
+
/** `shop/restart` result: committed, or a typed refusal issued BEFORE
|
|
13
|
+
* anything is torn down. Once `ok` is returned the old process WILL exit —
|
|
14
|
+
* the client monitors the new server and reports a failed boot with the
|
|
15
|
+
* manual command. */
|
|
16
|
+
export type RestartOutcome = {
|
|
17
|
+
ok: true;
|
|
18
|
+
} | {
|
|
19
|
+
ok: false;
|
|
20
|
+
detail: string;
|
|
21
|
+
};
|
|
22
|
+
/** Spawn the two-phase handoff. The helper is a POSIX shell wrapper that
|
|
23
|
+
* polls the parent pid until it is gone, then replaces itself with the dsh
|
|
24
|
+
* command — `exec "$@"` keeps the argv verbatim, so no argument quoting is
|
|
25
|
+
* involved. The child's stdout/stderr go to `logFile`, opened here in
|
|
26
|
+
* append mode; opening throws on failure, and the caller treats a throw as
|
|
27
|
+
* a refusal (the restart is never committed without its log).
|
|
28
|
+
*
|
|
29
|
+
* The pid-poll has the usual tiny reuse race — if the parent's pid is
|
|
30
|
+
* recycled within the 0.2s polling gap the helper waits for the unrelated
|
|
31
|
+
* process too. Harmless: it only delays the boot. */
|
|
32
|
+
export declare function startRestart(options: {
|
|
33
|
+
dshBin: string;
|
|
34
|
+
argv: string[];
|
|
35
|
+
parentPid: number;
|
|
36
|
+
logFile: string;
|
|
37
|
+
env?: NodeJS.ProcessEnv;
|
|
38
|
+
}): void;
|
package/package.json
CHANGED