@co0ontty/wand 4.56.0 → 4.56.1
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 +2 -0
- package/dist/ensure-node-pty-helper.d.ts +9 -0
- package/dist/ensure-node-pty-helper.js +118 -31
- package/dist/process-manager.js +15 -6
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"commit": "
|
|
3
|
-
"builtAt": "2026-09-
|
|
4
|
-
"version": "4.56.
|
|
2
|
+
"commit": "477c21d747e18e9f4df6942ab0f47bd817f66ff0",
|
|
3
|
+
"builtAt": "2026-09-10T03:53:28.222Z",
|
|
4
|
+
"version": "4.56.1",
|
|
5
5
|
"channel": "stable"
|
|
6
6
|
}
|
package/dist/cli.js
CHANGED
|
@@ -93,6 +93,8 @@ async function main() {
|
|
|
93
93
|
break;
|
|
94
94
|
}
|
|
95
95
|
case "terminald": {
|
|
96
|
+
const { ensureNodePtyHelperExecutable } = await import("./ensure-node-pty-helper.js");
|
|
97
|
+
ensureNodePtyHelperExecutable();
|
|
96
98
|
const { runTerminalDaemon } = await import("./terminal-daemon-server.js");
|
|
97
99
|
await runTerminalDaemon(configPath);
|
|
98
100
|
break;
|
|
@@ -1 +1,10 @@
|
|
|
1
|
+
export interface SpawnHelperRepair {
|
|
2
|
+
vendorPath: string;
|
|
3
|
+
helperPath: string;
|
|
4
|
+
copied: boolean;
|
|
5
|
+
}
|
|
6
|
+
/** Repair one spawn-helper file. Exported for tests. */
|
|
7
|
+
export declare function repairSpawnHelperFile(vendorPath: string): SpawnHelperRepair;
|
|
1
8
|
export declare function ensureNodePtyHelperExecutable(): void;
|
|
9
|
+
export declare function resetNodePtyHelperStateForTests(): void;
|
|
10
|
+
export declare function describePtySpawnFailure(error: unknown): string;
|
|
@@ -5,64 +5,151 @@
|
|
|
5
5
|
//
|
|
6
6
|
// Locates node-pty wherever it actually lives (dev repo, hoisted global,
|
|
7
7
|
// pnpm store …) via require.resolve, finds the per-arch prebuilds dir the
|
|
8
|
-
// loaded binary is in, and ensures the helper
|
|
9
|
-
// and best-effort: silent on the happy path, warns on failure but never
|
|
10
|
-
// blocks startup.
|
|
8
|
+
// loaded binary is in, and ensures the helper is actually executable.
|
|
11
9
|
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
10
|
+
// Two repair paths:
|
|
11
|
+
// 1. chmod +x the vendor helper (works when we own the file).
|
|
12
|
+
// 2. If chmod is denied (root-owned global install), copy the helper to a
|
|
13
|
+
// user-writable cache and rewrite node-pty's native `fork` helperPath.
|
|
14
|
+
// In-place chmod cannot fix a root-owned 644 file, and the native addon
|
|
15
|
+
// receives the helper path as an argument, so a writable copy is enough.
|
|
16
|
+
//
|
|
17
|
+
// Called both once at server/daemon startup AND right before every PTY spawn.
|
|
18
|
+
// The per-spawn call is what makes this self-heal: a self-update (`npm install
|
|
14
19
|
// -g`) reinstalls node-pty and re-drops the bit, and that extraction can
|
|
15
|
-
// land *after* the relaunched server already ran its startup chmod
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
// cached, the happy path is a single stat + early return).
|
|
19
|
-
import { chmodSync, existsSync, statSync } from "node:fs";
|
|
20
|
+
// land *after* the relaunched server already ran its startup chmod.
|
|
21
|
+
import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, statSync, } from "node:fs";
|
|
22
|
+
import { createHash } from "node:crypto";
|
|
20
23
|
import { createRequire } from "node:module";
|
|
24
|
+
import os from "node:os";
|
|
21
25
|
import path from "node:path";
|
|
22
26
|
import process from "node:process";
|
|
23
27
|
import { getErrorMessage } from "./error-utils.js";
|
|
24
28
|
const requireFromHere = createRequire(import.meta.url);
|
|
25
|
-
// Cache the resolved helper path: `null` = not yet resolved, `""` =
|
|
26
|
-
// to "no helper on this platform/install" (don't retry).
|
|
27
|
-
let
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
// Cache the resolved vendor helper path: `null` = not yet resolved, `""` =
|
|
30
|
+
// resolved to "no helper on this platform/install" (don't retry).
|
|
31
|
+
let cachedVendorHelperPath = null;
|
|
32
|
+
let nativeForkPatched = false;
|
|
33
|
+
let patchedVendorHelperPath = null;
|
|
34
|
+
let patchedExecutableHelperPath = null;
|
|
35
|
+
function resolveVendorHelperPath() {
|
|
36
|
+
if (cachedVendorHelperPath !== null)
|
|
37
|
+
return cachedVendorHelperPath;
|
|
31
38
|
// spawn-helper is only used on Unix-likes. Windows uses winpty / conpty.
|
|
32
39
|
if (process.platform === "win32")
|
|
33
|
-
return (
|
|
40
|
+
return (cachedVendorHelperPath = "");
|
|
34
41
|
let nodePtyEntry;
|
|
35
42
|
try {
|
|
36
43
|
nodePtyEntry = requireFromHere.resolve("node-pty");
|
|
37
44
|
}
|
|
38
45
|
catch {
|
|
39
|
-
return (
|
|
46
|
+
return (cachedVendorHelperPath = "");
|
|
40
47
|
}
|
|
41
48
|
// node-pty's lib/index.js sits at <pkg>/lib/index.js; helper lives at
|
|
42
49
|
// <pkg>/prebuilds/<platform>-<arch>/spawn-helper.
|
|
43
50
|
const pkgRoot = path.resolve(path.dirname(nodePtyEntry), "..");
|
|
44
51
|
const arch = `${process.platform}-${process.arch}`;
|
|
45
|
-
return (
|
|
52
|
+
return (cachedVendorHelperPath = path.join(pkgRoot, "prebuilds", arch, "spawn-helper"));
|
|
46
53
|
}
|
|
47
|
-
|
|
48
|
-
const helper = resolveHelperPath();
|
|
49
|
-
if (!helper || !existsSync(helper))
|
|
50
|
-
return;
|
|
51
|
-
let mode;
|
|
54
|
+
function isExecutable(filePath) {
|
|
52
55
|
try {
|
|
53
|
-
|
|
56
|
+
return (statSync(filePath).mode & 0o111) === 0o111;
|
|
54
57
|
}
|
|
55
58
|
catch {
|
|
56
|
-
return;
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function helperCacheDir() {
|
|
63
|
+
return path.join(os.tmpdir(), "wand-pty-helpers");
|
|
64
|
+
}
|
|
65
|
+
function copyHelperToWritableCache(source) {
|
|
66
|
+
const digest = createHash("sha256").update(readFileSync(source)).digest("hex").slice(0, 16);
|
|
67
|
+
const dir = helperCacheDir();
|
|
68
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
69
|
+
const dest = path.join(dir, `${digest}-spawn-helper`);
|
|
70
|
+
if (!existsSync(dest)) {
|
|
71
|
+
copyFileSync(source, dest);
|
|
72
|
+
}
|
|
73
|
+
if (!isExecutable(dest)) {
|
|
74
|
+
chmodSync(dest, 0o755);
|
|
57
75
|
}
|
|
58
|
-
|
|
76
|
+
return dest;
|
|
77
|
+
}
|
|
78
|
+
function installNativeHelperPathPatch(vendorPath, executablePath) {
|
|
79
|
+
patchedVendorHelperPath = vendorPath;
|
|
80
|
+
patchedExecutableHelperPath = executablePath;
|
|
81
|
+
if (nativeForkPatched || process.platform === "win32" || vendorPath === executablePath)
|
|
59
82
|
return;
|
|
83
|
+
const utils = requireFromHere("node-pty/lib/utils.js");
|
|
84
|
+
const native = utils.loadNativeModule("pty");
|
|
85
|
+
const originalFork = native.module.fork.bind(native.module);
|
|
86
|
+
native.module.fork = (...args) => {
|
|
87
|
+
if (typeof args[9] === "string" && patchedVendorHelperPath && patchedExecutableHelperPath) {
|
|
88
|
+
const requested = args[9];
|
|
89
|
+
const resolvedRequested = path.resolve(requested);
|
|
90
|
+
if (requested === patchedVendorHelperPath
|
|
91
|
+
|| resolvedRequested === patchedVendorHelperPath
|
|
92
|
+
|| resolvedRequested === path.resolve(patchedVendorHelperPath)) {
|
|
93
|
+
args[9] = patchedExecutableHelperPath;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return originalFork(...args);
|
|
97
|
+
};
|
|
98
|
+
nativeForkPatched = true;
|
|
99
|
+
process.stderr.write(`[wand] Using writable node-pty spawn-helper copy at ${executablePath}\n`);
|
|
100
|
+
}
|
|
101
|
+
/** Repair one spawn-helper file. Exported for tests. */
|
|
102
|
+
export function repairSpawnHelperFile(vendorPath) {
|
|
103
|
+
if (!vendorPath || !existsSync(vendorPath)) {
|
|
104
|
+
return { vendorPath, helperPath: vendorPath, copied: false };
|
|
105
|
+
}
|
|
106
|
+
if (isExecutable(vendorPath)) {
|
|
107
|
+
return { vendorPath, helperPath: vendorPath, copied: false };
|
|
108
|
+
}
|
|
60
109
|
try {
|
|
61
|
-
|
|
62
|
-
|
|
110
|
+
const mode = statSync(vendorPath).mode & 0o777;
|
|
111
|
+
chmodSync(vendorPath, mode | 0o755);
|
|
112
|
+
process.stderr.write(`[wand] Restored +x on ${vendorPath} (npm dropped the bit on install)\n`);
|
|
113
|
+
return { vendorPath, helperPath: vendorPath, copied: false };
|
|
63
114
|
}
|
|
64
115
|
catch (err) {
|
|
65
|
-
|
|
66
|
-
|
|
116
|
+
try {
|
|
117
|
+
const copiedPath = copyHelperToWritableCache(vendorPath);
|
|
118
|
+
process.stderr.write(`[wand] Warning: could not chmod +x ${vendorPath}: ${getErrorMessage(err)}\n`
|
|
119
|
+
+ `[wand] Copied spawn-helper to ${copiedPath}\n`);
|
|
120
|
+
return { vendorPath, helperPath: copiedPath, copied: true };
|
|
121
|
+
}
|
|
122
|
+
catch (copyErr) {
|
|
123
|
+
process.stderr.write(`[wand] Warning: could not chmod +x ${vendorPath}: ${getErrorMessage(err)}\n`
|
|
124
|
+
+ `[wand] Also failed to copy spawn-helper: ${getErrorMessage(copyErr)}\n`
|
|
125
|
+
+ `[wand] PTY sessions may fail to start. Run: chmod +x ${JSON.stringify(vendorPath)}\n`);
|
|
126
|
+
return { vendorPath, helperPath: vendorPath, copied: false };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
export function ensureNodePtyHelperExecutable() {
|
|
131
|
+
const vendorPath = resolveVendorHelperPath();
|
|
132
|
+
if (!vendorPath)
|
|
133
|
+
return;
|
|
134
|
+
const repaired = repairSpawnHelperFile(vendorPath);
|
|
135
|
+
if (repaired.copied) {
|
|
136
|
+
try {
|
|
137
|
+
installNativeHelperPathPatch(repaired.vendorPath, repaired.helperPath);
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
process.stderr.write(`[wand] Warning: could not redirect node-pty spawn-helper: ${getErrorMessage(err)}\n`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
export function resetNodePtyHelperStateForTests() {
|
|
145
|
+
cachedVendorHelperPath = null;
|
|
146
|
+
patchedVendorHelperPath = null;
|
|
147
|
+
patchedExecutableHelperPath = null;
|
|
148
|
+
}
|
|
149
|
+
export function describePtySpawnFailure(error) {
|
|
150
|
+
const message = getErrorMessage(error, "无法启动终端。");
|
|
151
|
+
if (/posix_spawnp failed/i.test(message)) {
|
|
152
|
+
return "无法启动终端:node-pty 的 spawn-helper 不可执行。请重新安装 Wand,或对该文件执行 chmod +x。";
|
|
67
153
|
}
|
|
154
|
+
return message;
|
|
68
155
|
}
|
package/dist/process-manager.js
CHANGED
|
@@ -16,6 +16,7 @@ import { getProviderCommandSessionId, getProviderResumeCommandSessionId } from "
|
|
|
16
16
|
import { normalizeThinkingEffort, thinkingEffortToClaudeCliEffort, thinkingEffortToClaudeSlashEffort, thinkingEffortToCodexReasoningEffort, thinkingEffortToOpenCodeVariant, thinkingEffortToPiLevel } from "./structured-provider-common.js";
|
|
17
17
|
import { consumePtyInputForTopic, createPtyTopicLineBuffer, provisionalSessionTopic, SessionTopicCoordinator, sessionTopicBlocklistForSnapshot, shouldAcceptGeneratedSessionTitle, } from "./session-topic.js";
|
|
18
18
|
import { getErrorMessage } from "./error-utils.js";
|
|
19
|
+
import { describePtySpawnFailure } from "./ensure-node-pty-helper.js";
|
|
19
20
|
import { resolveSystemAiContext } from "./session-ai-context.js";
|
|
20
21
|
import { resolveSessionCwd } from "./session-cwd.js";
|
|
21
22
|
import { PtyTerminalState } from "./pty-terminal-state.js";
|
|
@@ -1144,12 +1145,20 @@ export class ProcessManager extends EventEmitter {
|
|
|
1144
1145
|
}
|
|
1145
1146
|
catch (err) {
|
|
1146
1147
|
console.error("[ProcessManager] terminal spawn failed", { sessionId: id, error: String(err) });
|
|
1147
|
-
record
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
this.
|
|
1152
|
-
|
|
1148
|
+
this.cleanupRecord(record, false);
|
|
1149
|
+
this.sessions.delete(id);
|
|
1150
|
+
this.lastPersistedMessageState.delete(id);
|
|
1151
|
+
this.dirtySessions.delete(id);
|
|
1152
|
+
this.terminalHost.forget(id);
|
|
1153
|
+
try {
|
|
1154
|
+
this.storage.deleteSession(id);
|
|
1155
|
+
}
|
|
1156
|
+
catch { /* best-effort spawn-failure cleanup */ }
|
|
1157
|
+
try {
|
|
1158
|
+
this.logger.deleteSession(id);
|
|
1159
|
+
}
|
|
1160
|
+
catch { /* best-effort spawn-failure cleanup */ }
|
|
1161
|
+
throw new Error(describePtySpawnFailure(err));
|
|
1153
1162
|
}
|
|
1154
1163
|
record.processId = child.pid;
|
|
1155
1164
|
record.ptyProcess = child;
|