@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/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
- const args = [cliPath, "guard", "launch", "--profile", name, "--", nodePath, dshEntry, ...originalDshArgs];
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 = {