@1e0zj/dsh-plugin-mall 0.3.4 → 0.4.0
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 +23 -7
- package/package.json +1 -1
- package/src/cli.js +160 -10
- package/src/guard.js +1576 -76
- package/src/index.js +75 -4
- package/src/installer.js +779 -35
package/src/index.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
import z from "@deepseek-ai/schemastery";
|
|
17
17
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
18
|
-
import { existsSync, readFileSync, realpathSync, mkdirSync, mkdtempSync, writeFileSync, rmSync } from "node:fs";
|
|
18
|
+
import { existsSync, readFileSync, realpathSync, mkdirSync, mkdtempSync, writeFileSync, rmSync, openSync, closeSync, writeSync } from "node:fs";
|
|
19
19
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
20
20
|
import { spawn } from "node:child_process";
|
|
21
21
|
import { createHash, randomBytes } from "node:crypto";
|
|
@@ -888,7 +888,16 @@ export function resolveRestartLaunchPlan({ profile, config = {}, isWindows }) {
|
|
|
888
888
|
|
|
889
889
|
const nodePath = process.execPath;
|
|
890
890
|
const originalDshArgs = process.argv.slice(2);
|
|
891
|
-
|
|
891
|
+
// A restart is always requested from a page that is open and that reconnects
|
|
892
|
+
// to the successor on its own, so dsh's start-up browser handoff would only
|
|
893
|
+
// add a second window onto the one already showing the result. Suppressing it
|
|
894
|
+
// is safe HERE and nowhere else: this route is gated on a live browser
|
|
895
|
+
// session. If the flag is already in the original argv, leave it alone.
|
|
896
|
+
const suppressOpen = !originalDshArgs.includes("--no-open");
|
|
897
|
+
const dshArgs = suppressOpen ? [...originalDshArgs, "--no-open"] : [...originalDshArgs];
|
|
898
|
+
// The outgoing host names itself so `guard launch` can wait for it to be
|
|
899
|
+
// gone before binding the port — see --await-exit in cli.js.
|
|
900
|
+
const args = [cliPath, "guard", "launch", "--profile", name, "--await-exit", String(process.pid), "--", nodePath, dshEntry, ...dshArgs];
|
|
892
901
|
|
|
893
902
|
return {
|
|
894
903
|
ok: true,
|
|
@@ -897,9 +906,23 @@ export function resolveRestartLaunchPlan({ profile, config = {}, isWindows }) {
|
|
|
897
906
|
cliPath,
|
|
898
907
|
dshEntry,
|
|
899
908
|
profile: name,
|
|
909
|
+
suppressedBrowserOpen: suppressOpen,
|
|
910
|
+
awaitExitPid: process.pid,
|
|
900
911
|
};
|
|
901
912
|
}
|
|
902
913
|
|
|
914
|
+
/**
|
|
915
|
+
* Where a restart's output goes. The successor is spawned from a process that
|
|
916
|
+
* is about to exit, so it cannot inherit anything that outlives the handoff —
|
|
917
|
+
* and on Windows the console it does get is not attached to its stdio, which
|
|
918
|
+
* is how `[guard] … rolled back` used to vanish. Append to a per-profile file
|
|
919
|
+
* instead, so whatever the restart says survives it.
|
|
920
|
+
*/
|
|
921
|
+
export function restartLogPath(profileDir, profile) {
|
|
922
|
+
// <home>/profiles/<name> → <home>/guard/, beside the pending markers.
|
|
923
|
+
return join(dirname(dirname(profileDir)), "guard", `restart-${profile}.log`);
|
|
924
|
+
}
|
|
925
|
+
|
|
903
926
|
// ── in-process job tracker for browser RPC ───────────────────────────────────
|
|
904
927
|
|
|
905
928
|
let trackerCounter = 0;
|
|
@@ -1706,16 +1729,33 @@ async function rpcDispatch(ctx, endpoint, payload, config, token, tracker) {
|
|
|
1706
1729
|
if (!plan.ok) {
|
|
1707
1730
|
return rpcFail(new Error(plan.error));
|
|
1708
1731
|
}
|
|
1732
|
+
// Everything the restart prints goes to a file. `stdio: "ignore"` used to
|
|
1733
|
+
// send it to the void — including the one line that explains a rollback —
|
|
1734
|
+
// and the successor inherited those dead handles, which is why the
|
|
1735
|
+
// console Windows allocates for it comes up blank.
|
|
1736
|
+
let logFd;
|
|
1737
|
+
let logPath;
|
|
1738
|
+
try {
|
|
1739
|
+
logPath = restartLogPath(resolveProfileDir(profile), profile);
|
|
1740
|
+
mkdirSync(dirname(logPath), { recursive: true });
|
|
1741
|
+
logFd = openSync(logPath, "a");
|
|
1742
|
+
writeSync(logFd, `\n=== ${new Date().toISOString()} restart requested (pid ${process.pid} → guard launch) ===\n`);
|
|
1743
|
+
} catch (error) {
|
|
1744
|
+
// A log we cannot open is not a reason to refuse the restart.
|
|
1745
|
+
console.error(`[dsh-plugin-mall] restart log unavailable (${error.message}); continuing without it`);
|
|
1746
|
+
logFd = undefined;
|
|
1747
|
+
}
|
|
1709
1748
|
const child = spawn(plan.nodePath, plan.args, {
|
|
1710
1749
|
shell: false,
|
|
1711
1750
|
detached: true,
|
|
1712
|
-
stdio: "ignore",
|
|
1751
|
+
stdio: logFd === undefined ? "ignore" : ["ignore", logFd, logFd],
|
|
1713
1752
|
cwd: process.cwd(),
|
|
1714
1753
|
windowsHide: true,
|
|
1715
1754
|
});
|
|
1716
1755
|
child.unref();
|
|
1756
|
+
if (logFd !== undefined) closeSync(logFd); // the child holds its own duplicate
|
|
1717
1757
|
setTimeout(() => process.exit(0), 1000);
|
|
1718
|
-
return rpcOk({ restarting: true });
|
|
1758
|
+
return rpcOk({ restarting: true, logPath });
|
|
1719
1759
|
}
|
|
1720
1760
|
case "jobCancel": {
|
|
1721
1761
|
try {
|
|
@@ -2556,6 +2596,37 @@ export async function runSelfTests() {
|
|
|
2556
2596
|
const restartDotPlan = resolveRestartLaunchPlan({ profile: "web.", config: { allowRestart: true }, isWindows: true });
|
|
2557
2597
|
check("尾随点 profile 重启 plan fail-closed", !restartDotPlan.ok && /dot or space/.test(restartDotPlan.error));
|
|
2558
2598
|
|
|
2599
|
+
// 重启 argv 的三条硬约束。真实重启只有完整重启 dsh 才验得到,所以 plan 的
|
|
2600
|
+
// 形状必须在这里钉死:少一条就是那三个现象里的一个回来了。
|
|
2601
|
+
{
|
|
2602
|
+
const argvBefore = process.argv;
|
|
2603
|
+
try {
|
|
2604
|
+
process.argv = [process.execPath, "/x/bin.js", "--profile", "web"];
|
|
2605
|
+
const plan = resolveRestartLaunchPlan({ profile: "web", config: { allowRestart: true } });
|
|
2606
|
+
if (plan.ok) {
|
|
2607
|
+
const dashDash = plan.args.indexOf("--");
|
|
2608
|
+
const dshArgs = plan.args.slice(dashDash + 3); // -- node <dshEntry> …
|
|
2609
|
+
check("重启带 --await-exit 且是本进程 pid", plan.args[plan.args.indexOf("--await-exit") + 1] === String(process.pid) && plan.awaitExitPid === process.pid);
|
|
2610
|
+
check("--await-exit 排在 `--` 之前(是 guard 的参数,不是 dsh 的)", plan.args.indexOf("--await-exit") < dashDash);
|
|
2611
|
+
check("重启给 dsh 补 --no-open", dshArgs.includes("--no-open") && plan.suppressedBrowserOpen === true);
|
|
2612
|
+
check("原始 dsh 参数原样保留", dshArgs.slice(0, 2).join(" ") === "--profile web");
|
|
2613
|
+
|
|
2614
|
+
// 用户自己已经写了 --no-open 时不重复追加。
|
|
2615
|
+
process.argv = [process.execPath, "/x/bin.js", "--profile", "web", "--no-open"];
|
|
2616
|
+
const already = resolveRestartLaunchPlan({ profile: "web", config: { allowRestart: true } });
|
|
2617
|
+
const alreadyArgs = already.ok ? already.args.slice(already.args.indexOf("--") + 3) : [];
|
|
2618
|
+
check("已有 --no-open 则不重复追加", already.ok && alreadyArgs.filter((a) => a === "--no-open").length === 1 && already.suppressedBrowserOpen === false);
|
|
2619
|
+
} else {
|
|
2620
|
+
// 裸检出里解析不到官方 dsh 入口,plan 只能 fail——说清楚,别假装验过。
|
|
2621
|
+
console.log(` SKIP 重启 argv fixture(${plan.error})`);
|
|
2622
|
+
}
|
|
2623
|
+
} finally {
|
|
2624
|
+
process.argv = argvBefore;
|
|
2625
|
+
}
|
|
2626
|
+
}
|
|
2627
|
+
|
|
2628
|
+
check("重启日志落在 <home>/guard/ 下", restartLogPath(join("/h", "profiles", "web"), "web").replace(/\\/g, "/").endsWith("/h/guard/restart-web.log"));
|
|
2629
|
+
|
|
2559
2630
|
// ── 7. Tracker isolation: producer.done rejection handling ───────────────
|
|
2560
2631
|
let settledOutcome = null;
|
|
2561
2632
|
const rejectingProducer = {
|