dsh-plugin-shop 0.4.3 → 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/lib/client.js +51 -22
- package/lib/index.js +66 -82
- package/lib/typert.host.js +10 -11
- package/lib/typert.remote-client.js +7 -8
- package/lib/types/client/locales.d.ts +2 -0
- package/lib/types/client/present.d.ts +7 -0
- package/lib/types/host/index.d.ts +9 -5
- package/lib/types/host/restart.d.ts +27 -24
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -4350,10 +4350,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4350
4350
|
]).optional(),
|
|
4351
4351
|
"detail": union([_undefined(), string()]).optional()
|
|
4352
4352
|
});
|
|
4353
|
-
const dsh_plugin_shop_shop_restart_result$schema = union([object({
|
|
4354
|
-
"ok": literal(true),
|
|
4355
|
-
"url": string()
|
|
4356
|
-
}), object({
|
|
4353
|
+
const dsh_plugin_shop_shop_restart_result$schema = union([object({ "ok": literal(true) }), object({
|
|
4357
4354
|
"ok": literal(false),
|
|
4358
4355
|
"detail": string()
|
|
4359
4356
|
})]);
|
|
@@ -4400,7 +4397,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4400
4397
|
},
|
|
4401
4398
|
sourceLocation: {
|
|
4402
4399
|
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4403
|
-
"line":
|
|
4400
|
+
"line": 225,
|
|
4404
4401
|
"column": 9
|
|
4405
4402
|
}
|
|
4406
4403
|
},
|
|
@@ -4418,7 +4415,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4418
4415
|
},
|
|
4419
4416
|
sourceLocation: {
|
|
4420
4417
|
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4421
|
-
"line":
|
|
4418
|
+
"line": 300,
|
|
4422
4419
|
"column": 9
|
|
4423
4420
|
}
|
|
4424
4421
|
},
|
|
@@ -4446,7 +4443,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4446
4443
|
},
|
|
4447
4444
|
sourceLocation: {
|
|
4448
4445
|
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4449
|
-
"line":
|
|
4446
|
+
"line": 251,
|
|
4450
4447
|
"column": 9
|
|
4451
4448
|
}
|
|
4452
4449
|
},
|
|
@@ -4473,7 +4470,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4473
4470
|
},
|
|
4474
4471
|
sourceLocation: {
|
|
4475
4472
|
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4476
|
-
"line":
|
|
4473
|
+
"line": 289,
|
|
4477
4474
|
"column": 3
|
|
4478
4475
|
}
|
|
4479
4476
|
},
|
|
@@ -4491,7 +4488,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4491
4488
|
},
|
|
4492
4489
|
sourceLocation: {
|
|
4493
4490
|
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4494
|
-
"line":
|
|
4491
|
+
"line": 373,
|
|
4495
4492
|
"column": 9
|
|
4496
4493
|
}
|
|
4497
4494
|
},
|
|
@@ -4518,7 +4515,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4518
4515
|
},
|
|
4519
4516
|
sourceLocation: {
|
|
4520
4517
|
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4521
|
-
"line":
|
|
4518
|
+
"line": 194,
|
|
4522
4519
|
"column": 3
|
|
4523
4520
|
}
|
|
4524
4521
|
},
|
|
@@ -4546,7 +4543,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4546
4543
|
},
|
|
4547
4544
|
sourceLocation: {
|
|
4548
4545
|
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4549
|
-
"line":
|
|
4546
|
+
"line": 340,
|
|
4550
4547
|
"column": 9
|
|
4551
4548
|
}
|
|
4552
4549
|
}
|
|
@@ -4637,6 +4634,11 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4637
4634
|
"shop",
|
|
4638
4635
|
"marketplace"
|
|
4639
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"];
|
|
4640
4642
|
/**
|
|
4641
4643
|
* Whether a package name reads as a plugin shop (a marketplace for dsh
|
|
4642
4644
|
* plugins, e.g. `dsh-plugin-shop`, `dsh-store`, `pluginstore`). The shop
|
|
@@ -4649,6 +4651,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4649
4651
|
* it matches other people's package names, not ours.
|
|
4650
4652
|
*/
|
|
4651
4653
|
function isShopLike(name) {
|
|
4654
|
+
if (SHOP_LIKE_NAMES.includes(name.toLowerCase())) return true;
|
|
4652
4655
|
const segments = name.toLowerCase().split(/[-_.]+/);
|
|
4653
4656
|
const hasPlugin = segments.includes("plugin");
|
|
4654
4657
|
if (segments.some((segment) => /plugin(store|market|mall|shop|marketplace)/.test(segment) || /(store|market|mall|shop|marketplace)plugin/.test(segment))) return true;
|
|
@@ -4763,7 +4766,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4763
4766
|
restartTitle: "确认重启 dsh",
|
|
4764
4767
|
restartBody: "重启会断开当前页面与服务器的连接,正在进行的对话和任务会中断;浏览器会自动跳转到重启后的地址,稍等片刻即可继续使用。",
|
|
4765
4768
|
restartConfirm: "重启",
|
|
4766
|
-
restarting: "
|
|
4769
|
+
restarting: "正在重启,服务器就绪后页面会自动刷新…",
|
|
4770
|
+
restartFailedNotice: "重启后服务器没有回来。请手动启动:dsh web(新进程的日志在商店缓存目录的 restart.log)",
|
|
4767
4771
|
restartTransportFailed: "重启请求未能送达。请稍后重试。",
|
|
4768
4772
|
enabledSwitch: "启用",
|
|
4769
4773
|
toggleFailed: "设置失败,请重试。",
|
|
@@ -4828,7 +4832,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4828
4832
|
restartTitle: "Restart dsh?",
|
|
4829
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.",
|
|
4830
4834
|
restartConfirm: "Restart",
|
|
4831
|
-
restarting: "Restarting; the
|
|
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)",
|
|
4832
4837
|
restartTransportFailed: "The restart request could not be delivered. Please retry.",
|
|
4833
4838
|
enabledSwitch: "Enable",
|
|
4834
4839
|
toggleFailed: "Failed to update. Please retry.",
|
|
@@ -5376,22 +5381,27 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
5376
5381
|
/** The §8 restart flow (amendment 2026-08-27): after an install, update, or
|
|
5377
5382
|
* uninstall reports done, this panel offers a restart of dsh. The
|
|
5378
5383
|
* confirmation gate states the cost — the page disconnects and in-flight
|
|
5379
|
-
* conversations/tasks are interrupted — and on confirm the restart RPC
|
|
5380
|
-
* the
|
|
5381
|
-
*
|
|
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. */
|
|
5382
5390
|
function RestartPanel({ t, restart }) {
|
|
5383
5391
|
const [gateOpen, setGateOpen] = (0, react.useState)(false);
|
|
5384
5392
|
const [state, setState] = (0, react.useState)({ kind: "idle" });
|
|
5385
5393
|
const onConfirm = async () => {
|
|
5386
5394
|
setGateOpen(false);
|
|
5387
|
-
setState({ kind: "restarting" });
|
|
5388
5395
|
try {
|
|
5389
5396
|
const result = await restart();
|
|
5390
|
-
if (result.ok)
|
|
5391
|
-
|
|
5392
|
-
|
|
5393
|
-
|
|
5394
|
-
|
|
5397
|
+
if (!result.ok) {
|
|
5398
|
+
setState({
|
|
5399
|
+
kind: "failed",
|
|
5400
|
+
detail: result.detail
|
|
5401
|
+
});
|
|
5402
|
+
return;
|
|
5403
|
+
}
|
|
5404
|
+
setState({ kind: "restarting" });
|
|
5395
5405
|
} catch {
|
|
5396
5406
|
setState({
|
|
5397
5407
|
kind: "failed",
|
|
@@ -5399,6 +5409,25 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
5399
5409
|
});
|
|
5400
5410
|
}
|
|
5401
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]);
|
|
5402
5431
|
if (state.kind === "restarting") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5403
5432
|
className: _dsh_plugin_shop_css_e3675a89_default.notice,
|
|
5404
5433
|
"data-shop-restarting": true,
|
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";
|
|
@@ -419,82 +419,50 @@ function startUninstall(options) {
|
|
|
419
419
|
}
|
|
420
420
|
//#endregion
|
|
421
421
|
//#region src/host/restart.ts
|
|
422
|
-
/** Restart executor:
|
|
423
|
-
* browser the new server's URL. The child is detached (its own process
|
|
424
|
-
* group), so it survives both the parent's exit and the launching
|
|
425
|
-
* terminal's. The parent exits only AFTER the child printed its
|
|
426
|
-
* `dsh web: <url>` line — a restart that fails to come up leaves the old
|
|
427
|
-
* process running untouched. */
|
|
428
|
-
/** How long to wait for the child to print its URL before declaring the
|
|
429
|
-
* restart failed and killing the child. dsh web prints the URL once the
|
|
430
|
-
* Loader tree settles; 20s is generous on slow machines. */
|
|
431
|
-
const RESTART_UP_TIMEOUT_MS = 2e4;
|
|
432
|
-
/** Spawn the restarted server and wait for it to announce its URL.
|
|
422
|
+
/** Restart executor: hand the port to a new dsh instance, two-phase.
|
|
433
423
|
*
|
|
434
|
-
* The
|
|
435
|
-
*
|
|
436
|
-
*
|
|
437
|
-
*
|
|
438
|
-
*
|
|
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
439
|
*
|
|
440
|
-
*
|
|
441
|
-
* the
|
|
442
|
-
*
|
|
443
|
-
* error, or the timeout) the child is killed if still running and the old
|
|
444
|
-
* process is untouched. */
|
|
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. */
|
|
445
443
|
function startRestart(options) {
|
|
446
|
-
const { dshBin, argv,
|
|
447
|
-
const
|
|
448
|
-
|
|
449
|
-
|
|
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
|
+
], {
|
|
450
455
|
stdio: [
|
|
451
456
|
"ignore",
|
|
452
|
-
|
|
453
|
-
|
|
457
|
+
logFd,
|
|
458
|
+
logFd
|
|
454
459
|
],
|
|
455
460
|
env: env ?? process.env,
|
|
456
461
|
detached: true
|
|
457
|
-
});
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
resolve({
|
|
462
|
-
ok: false,
|
|
463
|
-
detail: "the restarted server did not announce its URL in time"
|
|
464
|
-
});
|
|
465
|
-
}, timeoutMs);
|
|
466
|
-
child.stdout.on("data", (chunk) => {
|
|
467
|
-
for (const line of chunk.toString().split("\n")) {
|
|
468
|
-
if (line === "") continue;
|
|
469
|
-
const match = /dsh web: (http:\/\/\S+)/.exec(line);
|
|
470
|
-
if (match?.[1] !== void 0) {
|
|
471
|
-
clearTimeout(timeout);
|
|
472
|
-
resolve({
|
|
473
|
-
ok: true,
|
|
474
|
-
url: match[1]
|
|
475
|
-
});
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
});
|
|
479
|
-
child.stderr.on("data", (chunk) => {
|
|
480
|
-
for (const line of chunk.toString().split("\n")) if (line !== "") stderr.push(line);
|
|
481
|
-
});
|
|
482
|
-
child.on("error", (error) => {
|
|
483
|
-
const code = error.code;
|
|
484
|
-
clearTimeout(timeout);
|
|
485
|
-
resolve({
|
|
486
|
-
ok: false,
|
|
487
|
-
detail: code === "ENOENT" ? "dsh not found on PATH — restart could not be launched" : `restart spawn failed: ${error.message}`
|
|
488
|
-
});
|
|
489
|
-
});
|
|
490
|
-
child.on("close", () => {
|
|
491
|
-
clearTimeout(timeout);
|
|
492
|
-
resolve({
|
|
493
|
-
ok: false,
|
|
494
|
-
detail: `the restarted server exited during boot — ${stderr[stderr.length - 1] ?? "no output"}`
|
|
495
|
-
});
|
|
496
|
-
});
|
|
497
|
-
});
|
|
462
|
+
}).unref();
|
|
463
|
+
} finally {
|
|
464
|
+
closeSync(logFd);
|
|
465
|
+
}
|
|
498
466
|
}
|
|
499
467
|
//#endregion
|
|
500
468
|
//#region src/host/profile.ts
|
|
@@ -731,6 +699,7 @@ let ShopGateway = (() => {
|
|
|
731
699
|
* production, a spy in tests. */
|
|
732
700
|
exit;
|
|
733
701
|
restartExitDelayMs;
|
|
702
|
+
restartParentPid;
|
|
734
703
|
/** The install gate runs against the last loaded snapshot, never a fresh
|
|
735
704
|
* fetch per request (§7.2: the Host's cached snapshot is the truth). */
|
|
736
705
|
/** Finished install records retained, so a poll sees the true terminal
|
|
@@ -756,6 +725,7 @@ let ShopGateway = (() => {
|
|
|
756
725
|
this.restartArgv = options.restartArgv ?? process.argv.slice(2);
|
|
757
726
|
this.exit = options.exit ?? ((code) => process.exit(code));
|
|
758
727
|
this.restartExitDelayMs = options.restartExitDelayMs ?? ShopGateway.RESTART_EXIT_DELAY_MS;
|
|
728
|
+
this.restartParentPid = options.restartParentPid ?? process.pid;
|
|
759
729
|
}
|
|
760
730
|
/** The boot's Loader root directory (the active profile's `cordis.yml`
|
|
761
731
|
* directory, carried on `ctx.baseUrl`), when present. A `link:` install
|
|
@@ -971,19 +941,33 @@ let ShopGateway = (() => {
|
|
|
971
941
|
};
|
|
972
942
|
}
|
|
973
943
|
/** Restart the dsh process the shop runs in (§8 amendment, 2026-08-27):
|
|
974
|
-
*
|
|
975
|
-
*
|
|
976
|
-
*
|
|
977
|
-
*
|
|
978
|
-
* the exit is delayed past the RPC round-trip. */
|
|
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. */
|
|
979
948
|
async restart() {
|
|
980
|
-
const
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
|
|
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 };
|
|
987
971
|
}
|
|
988
972
|
};
|
|
989
973
|
})();
|
package/lib/typert.host.js
CHANGED
|
@@ -69,7 +69,6 @@ const dsh_plugin_shop_shop_installStatus_result$schema = z.object({
|
|
|
69
69
|
})
|
|
70
70
|
const dsh_plugin_shop_shop_restart_result$schema = z.union([z.object({
|
|
71
71
|
'ok': z.literal(true),
|
|
72
|
-
'url': z.string(),
|
|
73
72
|
}), z.object({
|
|
74
73
|
'ok': z.literal(false),
|
|
75
74
|
'detail': z.string(),
|
|
@@ -123,7 +122,7 @@ export const TYPERT = {
|
|
|
123
122
|
typeSymbol: 'dsh-plugin-shop/types#ShopCatalogResult',
|
|
124
123
|
schema: dsh_plugin_shop_shop_catalog_result$schema,
|
|
125
124
|
},
|
|
126
|
-
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},
|
|
127
126
|
},
|
|
128
127
|
{
|
|
129
128
|
id: 'dsh-plugin-shop#shop/installed',
|
|
@@ -138,7 +137,7 @@ export const TYPERT = {
|
|
|
138
137
|
typeSymbol: 'dsh-plugin-shop#shop/installed:result',
|
|
139
138
|
schema: dsh_plugin_shop_shop_installed_result$schema,
|
|
140
139
|
},
|
|
141
|
-
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},
|
|
142
141
|
},
|
|
143
142
|
{
|
|
144
143
|
id: 'dsh-plugin-shop#shop/installStart',
|
|
@@ -164,7 +163,7 @@ export const TYPERT = {
|
|
|
164
163
|
typeSymbol: 'dsh-plugin-shop/types#ShopInstallResult',
|
|
165
164
|
schema: dsh_plugin_shop_shop_installStart_result$schema,
|
|
166
165
|
},
|
|
167
|
-
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},
|
|
168
167
|
},
|
|
169
168
|
{
|
|
170
169
|
id: 'dsh-plugin-shop#shop/installStatus',
|
|
@@ -189,7 +188,7 @@ export const TYPERT = {
|
|
|
189
188
|
typeSymbol: 'dsh-plugin-shop/types#ShopInstallStatusResult',
|
|
190
189
|
schema: dsh_plugin_shop_shop_installStatus_result$schema,
|
|
191
190
|
},
|
|
192
|
-
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},
|
|
193
192
|
},
|
|
194
193
|
{
|
|
195
194
|
id: 'dsh-plugin-shop#shop/restart',
|
|
@@ -204,7 +203,7 @@ export const TYPERT = {
|
|
|
204
203
|
typeSymbol: 'dsh-plugin-shop/types#ShopRestartResult',
|
|
205
204
|
schema: dsh_plugin_shop_shop_restart_result$schema,
|
|
206
205
|
},
|
|
207
|
-
sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":
|
|
206
|
+
sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":373,"column":9},
|
|
208
207
|
},
|
|
209
208
|
{
|
|
210
209
|
id: 'dsh-plugin-shop#shop/setEnabled',
|
|
@@ -229,7 +228,7 @@ export const TYPERT = {
|
|
|
229
228
|
typeSymbol: 'dsh-plugin-shop/types#ShopSetEnabledResult',
|
|
230
229
|
schema: dsh_plugin_shop_shop_setEnabled_result$schema,
|
|
231
230
|
},
|
|
232
|
-
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},
|
|
233
232
|
},
|
|
234
233
|
{
|
|
235
234
|
id: 'dsh-plugin-shop#shop/uninstallStart',
|
|
@@ -255,7 +254,7 @@ export const TYPERT = {
|
|
|
255
254
|
typeSymbol: 'dsh-plugin-shop/types#ShopUninstallResult',
|
|
256
255
|
schema: dsh_plugin_shop_shop_uninstallStart_result$schema,
|
|
257
256
|
},
|
|
258
|
-
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},
|
|
259
258
|
},
|
|
260
259
|
],
|
|
261
260
|
model: {
|
|
@@ -320,8 +319,8 @@ export const TYPERT = {
|
|
|
320
319
|
"kind": "method",
|
|
321
320
|
"name": "restart",
|
|
322
321
|
"signature": "@Remote('restart') async restart(): Promise<ShopRestartResult>",
|
|
323
|
-
"summary": "Restart the dsh process the shop runs in (§8 amendment, 2026-08-27): re-
|
|
324
|
-
"jsDoc": "/** Restart the dsh process the shop runs in (§8 amendment, 2026-08-27):\n *
|
|
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. */"
|
|
325
324
|
}
|
|
326
325
|
],
|
|
327
326
|
"types": [
|
|
@@ -359,7 +358,7 @@ export const TYPERT = {
|
|
|
359
358
|
},
|
|
360
359
|
{
|
|
361
360
|
"name": "RestartOutcome",
|
|
362
|
-
"declaration": "export type RestartOutcome = { ok: true;
|
|
361
|
+
"declaration": "export type RestartOutcome = { ok: true; } | { ok: false; detail: string; };"
|
|
363
362
|
},
|
|
364
363
|
{
|
|
365
364
|
"name": "ShopCatalogResult",
|
|
@@ -69,7 +69,6 @@ const dsh_plugin_shop_shop_installStatus_result$schema = z.object({
|
|
|
69
69
|
})
|
|
70
70
|
const dsh_plugin_shop_shop_restart_result$schema = z.union([z.object({
|
|
71
71
|
'ok': z.literal(true),
|
|
72
|
-
'url': z.string(),
|
|
73
72
|
}), z.object({
|
|
74
73
|
'ok': z.literal(false),
|
|
75
74
|
'detail': z.string(),
|
|
@@ -120,7 +119,7 @@ export const TYPERT_REMOTE = {
|
|
|
120
119
|
typeSymbol: 'dsh-plugin-shop/types#ShopCatalogResult',
|
|
121
120
|
schema: dsh_plugin_shop_shop_catalog_result$schema,
|
|
122
121
|
},
|
|
123
|
-
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},
|
|
124
123
|
},
|
|
125
124
|
{
|
|
126
125
|
id: 'dsh-plugin-shop#shop/installed',
|
|
@@ -135,7 +134,7 @@ export const TYPERT_REMOTE = {
|
|
|
135
134
|
typeSymbol: 'dsh-plugin-shop#shop/installed:result',
|
|
136
135
|
schema: dsh_plugin_shop_shop_installed_result$schema,
|
|
137
136
|
},
|
|
138
|
-
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},
|
|
139
138
|
},
|
|
140
139
|
{
|
|
141
140
|
id: 'dsh-plugin-shop#shop/installStart',
|
|
@@ -161,7 +160,7 @@ export const TYPERT_REMOTE = {
|
|
|
161
160
|
typeSymbol: 'dsh-plugin-shop/types#ShopInstallResult',
|
|
162
161
|
schema: dsh_plugin_shop_shop_installStart_result$schema,
|
|
163
162
|
},
|
|
164
|
-
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},
|
|
165
164
|
},
|
|
166
165
|
{
|
|
167
166
|
id: 'dsh-plugin-shop#shop/installStatus',
|
|
@@ -186,7 +185,7 @@ export const TYPERT_REMOTE = {
|
|
|
186
185
|
typeSymbol: 'dsh-plugin-shop/types#ShopInstallStatusResult',
|
|
187
186
|
schema: dsh_plugin_shop_shop_installStatus_result$schema,
|
|
188
187
|
},
|
|
189
|
-
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},
|
|
190
189
|
},
|
|
191
190
|
{
|
|
192
191
|
id: 'dsh-plugin-shop#shop/restart',
|
|
@@ -201,7 +200,7 @@ export const TYPERT_REMOTE = {
|
|
|
201
200
|
typeSymbol: 'dsh-plugin-shop/types#ShopRestartResult',
|
|
202
201
|
schema: dsh_plugin_shop_shop_restart_result$schema,
|
|
203
202
|
},
|
|
204
|
-
sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":
|
|
203
|
+
sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":373,"column":9},
|
|
205
204
|
},
|
|
206
205
|
{
|
|
207
206
|
id: 'dsh-plugin-shop#shop/setEnabled',
|
|
@@ -226,7 +225,7 @@ export const TYPERT_REMOTE = {
|
|
|
226
225
|
typeSymbol: 'dsh-plugin-shop/types#ShopSetEnabledResult',
|
|
227
226
|
schema: dsh_plugin_shop_shop_setEnabled_result$schema,
|
|
228
227
|
},
|
|
229
|
-
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},
|
|
230
229
|
},
|
|
231
230
|
{
|
|
232
231
|
id: 'dsh-plugin-shop#shop/uninstallStart',
|
|
@@ -252,7 +251,7 @@ export const TYPERT_REMOTE = {
|
|
|
252
251
|
typeSymbol: 'dsh-plugin-shop/types#ShopUninstallResult',
|
|
253
252
|
schema: dsh_plugin_shop_shop_uninstallStart_result$schema,
|
|
254
253
|
},
|
|
255
|
-
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},
|
|
256
255
|
},
|
|
257
256
|
],
|
|
258
257
|
}
|
|
@@ -59,6 +59,7 @@ export declare const zh: {
|
|
|
59
59
|
restartBody: string;
|
|
60
60
|
restartConfirm: string;
|
|
61
61
|
restarting: string;
|
|
62
|
+
restartFailedNotice: string;
|
|
62
63
|
restartTransportFailed: string;
|
|
63
64
|
enabledSwitch: string;
|
|
64
65
|
toggleFailed: string;
|
|
@@ -126,6 +127,7 @@ export declare const en: {
|
|
|
126
127
|
restartBody: string;
|
|
127
128
|
restartConfirm: string;
|
|
128
129
|
restarting: string;
|
|
130
|
+
restartFailedNotice: string;
|
|
129
131
|
restartTransportFailed: string;
|
|
130
132
|
enabledSwitch: string;
|
|
131
133
|
toggleFailed: 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. */
|
|
@@ -36,6 +36,10 @@ export interface ShopGatewayOptions {
|
|
|
36
36
|
/** Test-only injection: the exit the restart calls after the response is
|
|
37
37
|
* delivered. Production uses `process.exit`. */
|
|
38
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;
|
|
39
43
|
/** How long the gateway waits after a successful restart response before
|
|
40
44
|
* exiting the old process; test-only shortening, production uses 2s. */
|
|
41
45
|
restartExitDelayMs?: number;
|
|
@@ -111,6 +115,7 @@ export declare class ShopGateway extends TypertRemoteService {
|
|
|
111
115
|
* production, a spy in tests. */
|
|
112
116
|
private readonly exit;
|
|
113
117
|
private readonly restartExitDelayMs;
|
|
118
|
+
private readonly restartParentPid;
|
|
114
119
|
/** The install gate runs against the last loaded snapshot, never a fresh
|
|
115
120
|
* fetch per request (§7.2: the Host's cached snapshot is the truth). */
|
|
116
121
|
/** Finished install records retained, so a poll sees the true terminal
|
|
@@ -184,11 +189,10 @@ export declare class ShopGateway extends TypertRemoteService {
|
|
|
184
189
|
name: string;
|
|
185
190
|
}): Promise<ShopUninstallResult>;
|
|
186
191
|
/** Restart the dsh process the shop runs in (§8 amendment, 2026-08-27):
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
* the exit is delayed past the RPC round-trip. */
|
|
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. */
|
|
192
196
|
restart(): Promise<ShopRestartResult>;
|
|
193
197
|
}
|
|
194
198
|
export default ShopGateway;
|
|
@@ -1,35 +1,38 @@
|
|
|
1
|
-
/** Restart executor:
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
|
|
8
|
-
*
|
|
9
|
-
* the
|
|
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. */
|
|
10
16
|
export type RestartOutcome = {
|
|
11
17
|
ok: true;
|
|
12
|
-
url: string;
|
|
13
18
|
} | {
|
|
14
19
|
ok: false;
|
|
15
20
|
detail: string;
|
|
16
21
|
};
|
|
17
|
-
/** Spawn the
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* how the browser finds it.
|
|
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).
|
|
24
28
|
*
|
|
25
|
-
*
|
|
26
|
-
* the
|
|
27
|
-
*
|
|
28
|
-
* error, or the timeout) the child is killed if still running and the old
|
|
29
|
-
* process is untouched. */
|
|
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. */
|
|
30
32
|
export declare function startRestart(options: {
|
|
31
33
|
dshBin: string;
|
|
32
34
|
argv: string[];
|
|
35
|
+
parentPid: number;
|
|
36
|
+
logFile: string;
|
|
33
37
|
env?: NodeJS.ProcessEnv;
|
|
34
|
-
|
|
35
|
-
}): Promise<RestartOutcome>;
|
|
38
|
+
}): void;
|
package/package.json
CHANGED