@co0ontty/wand 4.38.0 → 4.39.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/dist/build-info.json +3 -3
- package/dist/cli.js +6 -0
- package/dist/process-manager.d.ts +12 -4
- package/dist/process-manager.js +314 -258
- package/dist/server-session-routes.js +9 -9
- package/dist/server.d.ts +2 -0
- package/dist/server.js +10 -6
- package/dist/session-transport.d.ts +1 -1
- package/dist/storage.d.ts +2 -2
- package/dist/storage.js +31 -6
- package/dist/terminal-daemon-client.d.ts +29 -0
- package/dist/terminal-daemon-client.js +406 -0
- package/dist/terminal-daemon-protocol.d.ts +45 -0
- package/dist/terminal-daemon-protocol.js +31 -0
- package/dist/terminal-daemon-server.d.ts +2 -0
- package/dist/terminal-daemon-server.js +349 -0
- package/dist/terminal-host.d.ts +79 -0
- package/dist/terminal-host.js +123 -0
- package/dist/tui/commands.js +220 -33
- package/dist/types.d.ts +4 -0
- package/dist/web-ui/content/scripts.js +65 -65
- package/dist/web-ui/content/styles.css +1 -1
- package/dist/web-ui/embedded-assets.d.ts +1 -1
- package/dist/web-ui/embedded-assets.js +3 -3
- package/package.json +1 -1
package/dist/tui/commands.js
CHANGED
|
@@ -14,6 +14,8 @@ import { whichSync } from "../path-repair.js";
|
|
|
14
14
|
import { computeRelaunch } from "../relaunch.js";
|
|
15
15
|
import { ensureDatabaseFile, resolveDatabasePath, WandStorage } from "../storage.js";
|
|
16
16
|
import { getErrorMessage } from "../error-utils.js";
|
|
17
|
+
import { readTerminalDaemonPid } from "../terminal-daemon-server.js";
|
|
18
|
+
import { isPidAlive } from "../pidfile.js";
|
|
17
19
|
// ─── 重启 ────────────────────────────────────────────────────────────────
|
|
18
20
|
/**
|
|
19
21
|
* 重启当前进程。
|
|
@@ -146,6 +148,7 @@ function clipboardCandidates() {
|
|
|
146
148
|
}
|
|
147
149
|
// ─── 系统服务(systemd system / user / launchd) ─────────────────────────
|
|
148
150
|
const LAUNCHD_LABEL = "com.wand.web";
|
|
151
|
+
const LAUNCHD_TERMINAL_LABEL = "com.wand.terminald";
|
|
149
152
|
export const DEFAULT_SERVICE_SCOPE = "system";
|
|
150
153
|
/** 当前 process 是不是 root(POSIX)。Windows 永远返回 false。 */
|
|
151
154
|
function isRoot() {
|
|
@@ -389,8 +392,8 @@ function launchdRestart(scope) {
|
|
|
389
392
|
function launchdDomain(scope) {
|
|
390
393
|
return scope === "user" ? `gui/${currentUid()}` : "system";
|
|
391
394
|
}
|
|
392
|
-
function launchdTarget(scope) {
|
|
393
|
-
return `${launchdDomain(scope)}/${
|
|
395
|
+
function launchdTarget(scope, label = LAUNCHD_LABEL) {
|
|
396
|
+
return `${launchdDomain(scope)}/${label}`;
|
|
394
397
|
}
|
|
395
398
|
function currentUid() {
|
|
396
399
|
const fn = process.getuid;
|
|
@@ -409,39 +412,105 @@ function matchLaunchdField(text, field) {
|
|
|
409
412
|
const match = text.match(new RegExp(`^\\s*${escaped} = (.+)$`, "m"));
|
|
410
413
|
return match?.[1]?.trim() ?? null;
|
|
411
414
|
}
|
|
412
|
-
|
|
415
|
+
const LAUNCHD_POLL_INTERVAL_MS = 100;
|
|
416
|
+
function sleepSync(ms) {
|
|
417
|
+
const signal = new Int32Array(new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT));
|
|
418
|
+
Atomics.wait(signal, 0, 0, ms);
|
|
419
|
+
}
|
|
420
|
+
function waitForLaunchdCondition(check, timeoutMs) {
|
|
421
|
+
const deadline = Date.now() + timeoutMs;
|
|
422
|
+
while (true) {
|
|
423
|
+
if (check())
|
|
424
|
+
return true;
|
|
425
|
+
if (Date.now() >= deadline)
|
|
426
|
+
return false;
|
|
427
|
+
sleepSync(Math.min(LAUNCHD_POLL_INTERVAL_MS, Math.max(1, deadline - Date.now())));
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
function launchdTargetIsLoaded(scope, label = LAUNCHD_LABEL) {
|
|
431
|
+
const printed = spawnSync("launchctl", ["print", launchdTarget(scope, label)], {
|
|
432
|
+
encoding: "utf8",
|
|
433
|
+
timeout: 10_000,
|
|
434
|
+
});
|
|
435
|
+
return printed.status === 0;
|
|
436
|
+
}
|
|
437
|
+
function waitForLaunchdTarget(scope, loaded, timeoutMs, label = LAUNCHD_LABEL) {
|
|
438
|
+
return waitForLaunchdCondition(() => launchdTargetIsLoaded(scope, label) === loaded, timeoutMs);
|
|
439
|
+
}
|
|
440
|
+
function waitForLaunchdActive(scope, timeoutMs, label = LAUNCHD_LABEL) {
|
|
441
|
+
return waitForLaunchdCondition(() => {
|
|
442
|
+
const printed = spawnSync("launchctl", ["print", launchdTarget(scope, label)], {
|
|
443
|
+
encoding: "utf8",
|
|
444
|
+
timeout: 10_000,
|
|
445
|
+
});
|
|
446
|
+
if (printed.status !== 0)
|
|
447
|
+
return false;
|
|
448
|
+
const text = printed.stdout || "";
|
|
449
|
+
return Number(matchLaunchdField(text, "pid") || "0") > 0
|
|
450
|
+
|| matchLaunchdField(text, "state") === "running";
|
|
451
|
+
}, timeoutMs);
|
|
452
|
+
}
|
|
453
|
+
function launchdBootstrap(scope, plist, successMessage, label = LAUNCHD_LABEL) {
|
|
413
454
|
const domain = launchdDomain(scope);
|
|
414
|
-
const target = launchdTarget(scope);
|
|
455
|
+
const target = launchdTarget(scope, label);
|
|
415
456
|
const bootout = spawnSync("launchctl", ["bootout", domain, plist], { encoding: "utf8", timeout: 10_000 });
|
|
416
|
-
const bootstrap = spawnSync("launchctl", ["bootstrap", domain, plist], { encoding: "utf8", timeout: 10_000 });
|
|
417
|
-
const enable = spawnSync("launchctl", ["enable", target], { encoding: "utf8", timeout: 10_000 });
|
|
418
|
-
const kickstart = spawnSync("launchctl", ["kickstart", "-k", target], { encoding: "utf8", timeout: 10_000 });
|
|
419
457
|
const detail = [
|
|
420
458
|
`target: ${target}`,
|
|
421
459
|
`bootout: ${formatSpawnResult(bootout)}`,
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
if (
|
|
427
|
-
return {
|
|
460
|
+
];
|
|
461
|
+
// launchctl can return before the old job has fully disappeared. Starting a
|
|
462
|
+
// new job in that window often produces the unhelpful "Bootstrap failed: 5"
|
|
463
|
+
// error even though the plist is valid.
|
|
464
|
+
if (!waitForLaunchdTarget(scope, false, 2_000, label)) {
|
|
465
|
+
return {
|
|
466
|
+
ok: false,
|
|
467
|
+
message: `launchctl bootout 未生效 (${target})`,
|
|
468
|
+
detail: detail.join("\n"),
|
|
469
|
+
};
|
|
428
470
|
}
|
|
471
|
+
const bootstrapAttempts = [];
|
|
472
|
+
let loaded = false;
|
|
473
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
474
|
+
const bootstrap = spawnSync("launchctl", ["bootstrap", domain, plist], {
|
|
475
|
+
encoding: "utf8",
|
|
476
|
+
timeout: 10_000,
|
|
477
|
+
});
|
|
478
|
+
bootstrapAttempts.push(bootstrap);
|
|
479
|
+
loaded = waitForLaunchdTarget(scope, true, 750, label);
|
|
480
|
+
if (loaded)
|
|
481
|
+
break;
|
|
482
|
+
if (!launchdIsTransientBootstrapFailure(bootstrap))
|
|
483
|
+
break;
|
|
484
|
+
sleepSync(250);
|
|
485
|
+
}
|
|
486
|
+
detail.push(...bootstrapAttempts.map((result, index) => `bootstrap${bootstrapAttempts.length > 1 ? ` #${index + 1}` : ""}: ${formatSpawnResult(result)}`));
|
|
487
|
+
if (!loaded) {
|
|
488
|
+
return { ok: false, message: `launchctl bootstrap 失败 (${target})`, detail: detail.join("\n") };
|
|
489
|
+
}
|
|
490
|
+
const enable = spawnSync("launchctl", ["enable", target], { encoding: "utf8", timeout: 10_000 });
|
|
491
|
+
detail.push(`enable: ${formatSpawnResult(enable)}`);
|
|
429
492
|
if (enable.status !== 0) {
|
|
430
|
-
return { ok: false, message: `launchctl enable 失败 (${target})`, detail };
|
|
493
|
+
return { ok: false, message: `launchctl enable 失败 (${target})`, detail: detail.join("\n") };
|
|
431
494
|
}
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
495
|
+
// RunAtLoad normally starts a freshly bootstrapped job. Avoid immediately
|
|
496
|
+
// killing that still-starting process with kickstart -k; only kick an idle
|
|
497
|
+
// job after giving launchd a short settling window.
|
|
498
|
+
if (waitForLaunchdActive(scope, 2_000, label)) {
|
|
499
|
+
detail.push("kickstart: skipped (already active)");
|
|
500
|
+
return { ok: true, message: successMessage, detail: detail.join("\n") };
|
|
501
|
+
}
|
|
502
|
+
const kickstart = spawnSync("launchctl", ["kickstart", target], { encoding: "utf8", timeout: 10_000 });
|
|
503
|
+
detail.push(`kickstart: ${formatSpawnResult(kickstart)}`);
|
|
504
|
+
if (!waitForLaunchdActive(scope, 2_000, label)) {
|
|
505
|
+
return { ok: false, message: `launchctl kickstart 失败 (${target})`, detail: detail.join("\n") };
|
|
437
506
|
}
|
|
438
|
-
return { ok: true, message: successMessage, detail };
|
|
507
|
+
return { ok: true, message: successMessage, detail: detail.join("\n") };
|
|
439
508
|
}
|
|
440
|
-
function launchdBootout(scope, plist, successMessage) {
|
|
509
|
+
function launchdBootout(scope, plist, successMessage, label = LAUNCHD_LABEL) {
|
|
441
510
|
const domain = launchdDomain(scope);
|
|
442
|
-
const target = launchdTarget(scope);
|
|
511
|
+
const target = launchdTarget(scope, label);
|
|
443
512
|
const bootout = spawnSync("launchctl", ["bootout", domain, plist], { encoding: "utf8", timeout: 10_000 });
|
|
444
|
-
if (
|
|
513
|
+
if (waitForLaunchdTarget(scope, false, 2_000, label)) {
|
|
445
514
|
return {
|
|
446
515
|
ok: true,
|
|
447
516
|
message: successMessage,
|
|
@@ -454,13 +523,9 @@ function launchdBootout(scope, plist, successMessage) {
|
|
|
454
523
|
detail: formatSpawnResult(bootout),
|
|
455
524
|
};
|
|
456
525
|
}
|
|
457
|
-
function
|
|
526
|
+
function launchdIsTransientBootstrapFailure(result) {
|
|
458
527
|
const text = `${result.stdout || ""}\n${result.stderr || ""}`;
|
|
459
|
-
return /
|
|
460
|
-
}
|
|
461
|
-
function launchdIsNotBootstrapped(result) {
|
|
462
|
-
const text = `${result.stdout || ""}\n${result.stderr || ""}`;
|
|
463
|
-
return /No such process|service is not loaded|Bootstrap failed:\s*3/i.test(text);
|
|
528
|
+
return /Bootstrap failed:\s*(?:5|37)|Input\/output error|resource busy|operation (?:now |in )?progress/i.test(text);
|
|
464
529
|
}
|
|
465
530
|
function formatSpawnResult(result) {
|
|
466
531
|
const text = ((result.stdout || "") + "\n" + (result.stderr || "")).trim();
|
|
@@ -525,6 +590,23 @@ function servicePathFor(scope) {
|
|
|
525
590
|
}
|
|
526
591
|
return "";
|
|
527
592
|
}
|
|
593
|
+
function terminalServicePathFor(scope) {
|
|
594
|
+
if (process.platform === "linux") {
|
|
595
|
+
return scope === "user"
|
|
596
|
+
? path.join(os.homedir(), ".config/systemd/user/wand-terminald.service")
|
|
597
|
+
: "/etc/systemd/system/wand-terminald.service";
|
|
598
|
+
}
|
|
599
|
+
if (process.platform === "darwin") {
|
|
600
|
+
return scope === "user"
|
|
601
|
+
? path.join(os.homedir(), "Library/LaunchAgents/com.wand.terminald.plist")
|
|
602
|
+
: "/Library/LaunchDaemons/com.wand.terminald.plist";
|
|
603
|
+
}
|
|
604
|
+
return "";
|
|
605
|
+
}
|
|
606
|
+
function hasLiveTerminalDaemon(configPath) {
|
|
607
|
+
const pid = readTerminalDaemonPid(configPath);
|
|
608
|
+
return pid !== null && isPidAlive(pid);
|
|
609
|
+
}
|
|
528
610
|
function readServiceEntrypointForUpdate(scope) {
|
|
529
611
|
const servicePath = servicePathFor(scope);
|
|
530
612
|
if (process.platform === "darwin") {
|
|
@@ -678,6 +760,7 @@ function readFileStat(targetPath) {
|
|
|
678
760
|
}
|
|
679
761
|
function installSystemdService(ctx, scope) {
|
|
680
762
|
const unitPath = servicePathFor(scope);
|
|
763
|
+
const terminalUnitPath = terminalServicePathFor(scope);
|
|
681
764
|
const wandBin = resolveWandBin(ctx);
|
|
682
765
|
const nodeBin = process.execPath;
|
|
683
766
|
const nodeBinDir = path.dirname(nodeBin);
|
|
@@ -693,7 +776,7 @@ function installSystemdService(ctx, scope) {
|
|
|
693
776
|
const commonExec = [
|
|
694
777
|
"[Unit]",
|
|
695
778
|
"Description=wand web console",
|
|
696
|
-
"After=network-online.target",
|
|
779
|
+
"After=network-online.target wand-terminald.service",
|
|
697
780
|
"Wants=network-online.target",
|
|
698
781
|
"",
|
|
699
782
|
"[Service]",
|
|
@@ -733,8 +816,40 @@ function installSystemdService(ctx, scope) {
|
|
|
733
816
|
];
|
|
734
817
|
}
|
|
735
818
|
const unit = unitLines.join("\n");
|
|
819
|
+
const terminalCommon = [
|
|
820
|
+
"[Unit]",
|
|
821
|
+
"Description=wand persistent terminal host",
|
|
822
|
+
"After=network-online.target",
|
|
823
|
+
"Wants=network-online.target",
|
|
824
|
+
"",
|
|
825
|
+
"[Service]",
|
|
826
|
+
"Type=simple",
|
|
827
|
+
`ExecStart=${nodeBin} ${wandBin} terminald -c ${ctx.configPath}`,
|
|
828
|
+
`Environment=PATH=${servicePath}`,
|
|
829
|
+
"Restart=always",
|
|
830
|
+
"RestartSec=3",
|
|
831
|
+
"StandardOutput=journal",
|
|
832
|
+
"StandardError=journal",
|
|
833
|
+
"SyslogIdentifier=wand-terminald",
|
|
834
|
+
];
|
|
835
|
+
const terminalUnit = [
|
|
836
|
+
...terminalCommon,
|
|
837
|
+
...(scope === "system"
|
|
838
|
+
? [
|
|
839
|
+
`User=${runUser}`,
|
|
840
|
+
`WorkingDirectory=${runHome}`,
|
|
841
|
+
`Environment=HOME=${runHome}`,
|
|
842
|
+
"OOMScoreAdjust=-500",
|
|
843
|
+
"",
|
|
844
|
+
"[Install]",
|
|
845
|
+
"WantedBy=multi-user.target",
|
|
846
|
+
]
|
|
847
|
+
: ["", "[Install]", "WantedBy=default.target"]),
|
|
848
|
+
"",
|
|
849
|
+
].join("\n");
|
|
736
850
|
try {
|
|
737
851
|
mkdirSync(path.dirname(unitPath), { recursive: true });
|
|
852
|
+
writeFileSync(terminalUnitPath, terminalUnit, "utf8");
|
|
738
853
|
writeFileSync(unitPath, unit, "utf8");
|
|
739
854
|
}
|
|
740
855
|
catch (err) {
|
|
@@ -742,6 +857,8 @@ function installSystemdService(ctx, scope) {
|
|
|
742
857
|
}
|
|
743
858
|
const base = systemctlBaseArgs(scope);
|
|
744
859
|
const reload = spawnSync("systemctl", [...base, "daemon-reload"], { encoding: "utf8" });
|
|
860
|
+
const terminalAlreadyLive = hasLiveTerminalDaemon(ctx.configPath);
|
|
861
|
+
const enableTerminal = spawnSync("systemctl", [...base, "enable", ...(terminalAlreadyLive ? [] : ["--now"]), "wand-terminald.service"], { encoding: "utf8" });
|
|
745
862
|
const enable = spawnSync("systemctl", [...base, "enable", "--now", "wand.service"], { encoding: "utf8" });
|
|
746
863
|
const hints = scope === "user"
|
|
747
864
|
? "提示: 若需登出后保持运行,请运行 `loginctl enable-linger $USER`"
|
|
@@ -749,12 +866,14 @@ function installSystemdService(ctx, scope) {
|
|
|
749
866
|
const detail = [
|
|
750
867
|
`scope: ${scope}`,
|
|
751
868
|
`unit: ${unitPath}`,
|
|
869
|
+
`terminal unit: ${terminalUnitPath}`,
|
|
752
870
|
`daemon-reload: ${reload.status === 0 ? "ok" : `failed (${reload.stderr.trim()})`}`,
|
|
871
|
+
`terminal enable${terminalAlreadyLive ? " (kept existing daemon)" : " --now"}: ${enableTerminal.status === 0 ? "ok" : `failed (${enableTerminal.stderr.trim()})`}`,
|
|
753
872
|
`enable --now: ${enable.status === 0 ? "ok" : `failed (${enable.stderr.trim()})`}`,
|
|
754
873
|
"",
|
|
755
874
|
hints,
|
|
756
875
|
].join("\n");
|
|
757
|
-
if (enable.status !== 0) {
|
|
876
|
+
if (enableTerminal.status !== 0 || enable.status !== 0) {
|
|
758
877
|
return {
|
|
759
878
|
ok: false,
|
|
760
879
|
message: `已写入 unit,但 systemctl ${scope === "user" ? "--user " : ""}启用失败`,
|
|
@@ -769,13 +888,17 @@ function installSystemdService(ctx, scope) {
|
|
|
769
888
|
}
|
|
770
889
|
function uninstallSystemdService(scope) {
|
|
771
890
|
const unitPath = servicePathFor(scope);
|
|
891
|
+
const terminalUnitPath = terminalServicePathFor(scope);
|
|
772
892
|
if (!existsSync(unitPath)) {
|
|
773
893
|
return { ok: false, message: `未检测到已安装的 systemd ${scope} 服务` };
|
|
774
894
|
}
|
|
775
895
|
const base = systemctlBaseArgs(scope);
|
|
776
896
|
const stop = spawnSync("systemctl", [...base, "disable", "--now", "wand.service"], { encoding: "utf8" });
|
|
897
|
+
const stopTerminal = spawnSync("systemctl", [...base, "disable", "--now", "wand-terminald.service"], { encoding: "utf8" });
|
|
777
898
|
try {
|
|
778
899
|
unlinkSync(unitPath);
|
|
900
|
+
if (existsSync(terminalUnitPath))
|
|
901
|
+
unlinkSync(terminalUnitPath);
|
|
779
902
|
}
|
|
780
903
|
catch (err) {
|
|
781
904
|
return { ok: false, message: `删除 unit 失败: ${getErrorMessage(err)}` };
|
|
@@ -784,11 +907,15 @@ function uninstallSystemdService(scope) {
|
|
|
784
907
|
return {
|
|
785
908
|
ok: true,
|
|
786
909
|
message: `已卸载 systemd ${scope === "user" ? "用户" : "系统"}服务`,
|
|
787
|
-
detail:
|
|
910
|
+
detail: [
|
|
911
|
+
stop.status === 0 ? "web disable --now: ok" : `web disable --now: ${stop.stderr.trim()}`,
|
|
912
|
+
stopTerminal.status === 0 ? "terminal disable --now: ok" : `terminal disable --now: ${stopTerminal.stderr.trim()}`,
|
|
913
|
+
].join("\n"),
|
|
788
914
|
};
|
|
789
915
|
}
|
|
790
916
|
function installLaunchdService(ctx, scope) {
|
|
791
917
|
const plistPath = servicePathFor(scope);
|
|
918
|
+
const terminalPlistPath = terminalServicePathFor(scope);
|
|
792
919
|
const wandBin = resolveWandBin(ctx);
|
|
793
920
|
const nodeBin = process.execPath;
|
|
794
921
|
const nodeBinDir = path.dirname(nodeBin);
|
|
@@ -824,14 +951,67 @@ ${userNameField} <key>WorkingDirectory</key><string>${runHome}</string>
|
|
|
824
951
|
<key>KeepAlive</key><true/>
|
|
825
952
|
</dict>
|
|
826
953
|
</plist>
|
|
954
|
+
`;
|
|
955
|
+
const terminalPlist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
956
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
957
|
+
<plist version="1.0">
|
|
958
|
+
<dict>
|
|
959
|
+
<key>Label</key><string>${LAUNCHD_TERMINAL_LABEL}</string>
|
|
960
|
+
${userNameField} <key>WorkingDirectory</key><string>${runHome}</string>
|
|
961
|
+
<key>ProgramArguments</key>
|
|
962
|
+
<array>
|
|
963
|
+
<string>${nodeBin}</string>
|
|
964
|
+
<string>${wandBin}</string>
|
|
965
|
+
<string>terminald</string>
|
|
966
|
+
<string>-c</string>
|
|
967
|
+
<string>${ctx.configPath}</string>
|
|
968
|
+
</array>
|
|
969
|
+
<key>EnvironmentVariables</key>
|
|
970
|
+
<dict>
|
|
971
|
+
<key>PATH</key><string>${servicePath}</string>
|
|
972
|
+
<key>HOME</key><string>${runHome}</string>
|
|
973
|
+
</dict>
|
|
974
|
+
<key>RunAtLoad</key><true/>
|
|
975
|
+
<key>KeepAlive</key><true/>
|
|
976
|
+
</dict>
|
|
977
|
+
</plist>
|
|
827
978
|
`;
|
|
828
979
|
try {
|
|
829
980
|
mkdirSync(path.dirname(plistPath), { recursive: true });
|
|
981
|
+
writeFileSync(terminalPlistPath, terminalPlist, "utf8");
|
|
830
982
|
writeFileSync(plistPath, plist, "utf8");
|
|
831
983
|
}
|
|
832
984
|
catch (err) {
|
|
833
985
|
return { ok: false, message: `写入 plist 失败: ${getErrorMessage(err)}` };
|
|
834
986
|
}
|
|
987
|
+
const terminalEnable = spawnSync("launchctl", ["enable", launchdTarget(scope, LAUNCHD_TERMINAL_LABEL)], { encoding: "utf8", timeout: 10_000 });
|
|
988
|
+
if (terminalEnable.status !== 0) {
|
|
989
|
+
return {
|
|
990
|
+
ok: false,
|
|
991
|
+
message: "已写入 plist,但持久终端宿主 enable 失败",
|
|
992
|
+
detail: formatSpawnResult(terminalEnable),
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
let terminalDetail;
|
|
996
|
+
if (launchdTargetIsLoaded(scope, LAUNCHD_TERMINAL_LABEL)) {
|
|
997
|
+
terminalDetail = `terminal: kept running (${launchdTarget(scope, LAUNCHD_TERMINAL_LABEL)})`;
|
|
998
|
+
}
|
|
999
|
+
else if (hasLiveTerminalDaemon(ctx.configPath)) {
|
|
1000
|
+
// A daemon started by an older/manual web process already owns live PTYs.
|
|
1001
|
+
// Keep it untouched; the new plist will take ownership on the next boot.
|
|
1002
|
+
terminalDetail = "terminal: existing standalone daemon kept running; plist enabled for next boot";
|
|
1003
|
+
}
|
|
1004
|
+
else {
|
|
1005
|
+
const terminalStarted = launchdBootstrap(scope, terminalPlistPath, `已启动持久终端宿主: ${terminalPlistPath}`, LAUNCHD_TERMINAL_LABEL);
|
|
1006
|
+
if (!terminalStarted.ok) {
|
|
1007
|
+
return {
|
|
1008
|
+
ok: false,
|
|
1009
|
+
message: "已写入 plist,但持久终端宿主启动失败",
|
|
1010
|
+
detail: terminalStarted.detail,
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
terminalDetail = terminalStarted.detail ?? terminalStarted.message;
|
|
1014
|
+
}
|
|
835
1015
|
const started = launchdBootstrap(scope, plistPath, `已注册 launchd ${scope === "user" ? "用户代理" : "系统守护"}: ${plistPath}`);
|
|
836
1016
|
if (!started.ok) {
|
|
837
1017
|
return {
|
|
@@ -843,18 +1023,23 @@ ${userNameField} <key>WorkingDirectory</key><string>${runHome}</string>
|
|
|
843
1023
|
return {
|
|
844
1024
|
ok: true,
|
|
845
1025
|
message: `已注册 launchd ${scope === "user" ? "用户代理" : "系统守护"}: ${plistPath}`,
|
|
846
|
-
detail: started.detail,
|
|
1026
|
+
detail: [terminalDetail, started.detail].filter(Boolean).join("\n"),
|
|
847
1027
|
};
|
|
848
1028
|
}
|
|
849
1029
|
function uninstallLaunchdService(scope) {
|
|
850
1030
|
const plistPath = servicePathFor(scope);
|
|
1031
|
+
const terminalPlistPath = terminalServicePathFor(scope);
|
|
851
1032
|
if (!existsSync(plistPath)) {
|
|
852
1033
|
return { ok: false, message: `未检测到已安装的 launchd ${scope} 服务` };
|
|
853
1034
|
}
|
|
854
1035
|
const stopped = launchdBootout(scope, plistPath, `已停止 launchd ${scope}`);
|
|
1036
|
+
const stoppedTerminal = launchdBootout(scope, terminalPlistPath, `已停止持久终端宿主 ${scope}`, LAUNCHD_TERMINAL_LABEL);
|
|
855
1037
|
const disabled = spawnSync("launchctl", ["disable", launchdTarget(scope)], { encoding: "utf8", timeout: 10_000 });
|
|
1038
|
+
const disabledTerminal = spawnSync("launchctl", ["disable", launchdTarget(scope, LAUNCHD_TERMINAL_LABEL)], { encoding: "utf8", timeout: 10_000 });
|
|
856
1039
|
try {
|
|
857
1040
|
unlinkSync(plistPath);
|
|
1041
|
+
if (existsSync(terminalPlistPath))
|
|
1042
|
+
unlinkSync(terminalPlistPath);
|
|
858
1043
|
}
|
|
859
1044
|
catch (err) {
|
|
860
1045
|
return { ok: false, message: `删除 plist 失败: ${getErrorMessage(err)}` };
|
|
@@ -864,7 +1049,9 @@ function uninstallLaunchdService(scope) {
|
|
|
864
1049
|
message: `已卸载 launchd ${scope === "user" ? "用户代理" : "系统守护"}`,
|
|
865
1050
|
detail: [
|
|
866
1051
|
stopped.detail ?? stopped.message,
|
|
1052
|
+
stoppedTerminal.detail ?? stoppedTerminal.message,
|
|
867
1053
|
`disable: ${formatSpawnResult(disabled)}`,
|
|
1054
|
+
`terminal disable: ${formatSpawnResult(disabledTerminal)}`,
|
|
868
1055
|
].join("\n"),
|
|
869
1056
|
};
|
|
870
1057
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -569,6 +569,10 @@ export interface SessionSnapshot {
|
|
|
569
569
|
ptyCols?: number;
|
|
570
570
|
/** 当前 PTY 行数,由最近一次 resize 决定。 */
|
|
571
571
|
ptyRows?: number;
|
|
572
|
+
/** terminal host 已处理并原子持久化的最后一个输出块序号;不进入客户端传输。 */
|
|
573
|
+
ptyOutputSeq?: number;
|
|
574
|
+
/** Internal shell-wrapper marker needed to keep parsing a daemon-owned PTY after reattach. */
|
|
575
|
+
ptyLaunchMarkerToken?: string | null;
|
|
572
576
|
}
|
|
573
577
|
/** Unified event type emitted by ClaudePtyBridge for WebSocket broadcast */
|
|
574
578
|
export type SessionEventType = "output.raw" | "output.chat" | "chat.turn" | "permission.prompt" | "permission.resolved" | "session.id" | "task" | "ended";
|