@co0ontty/wand 4.72.2 → 4.75.0-beta.g1d477a2
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 +4 -4
- package/dist/claude-sdk-runner.d.ts +2 -3
- package/dist/config.d.ts +1 -1
- package/dist/config.js +48 -0
- package/dist/daemon-connection.d.ts +9 -0
- package/dist/daemon-connection.js +86 -0
- package/dist/distribution-manager.d.ts +5 -0
- package/dist/distribution-manager.js +79 -1
- package/dist/git-quick-commit.js +12 -0
- package/dist/message-truncator.js +9 -3
- package/dist/models.d.ts +39 -2
- package/dist/models.js +302 -9
- package/dist/native/darwin-arm64/wand-render +0 -0
- package/dist/native/darwin-arm64/wand-render.sha256 +1 -0
- package/dist/native/darwin-arm64/wand-render.version +1 -0
- package/dist/native/darwin-arm64/wand-structured-renderd +0 -0
- package/dist/native/darwin-arm64/wand-structured-renderd.sha256 +1 -0
- package/dist/native/darwin-arm64/wand-structured-renderd.version +1 -0
- package/dist/native/darwin-x64/wand-render +0 -0
- package/dist/native/darwin-x64/wand-render.sha256 +1 -0
- package/dist/native/darwin-x64/wand-render.version +1 -0
- package/dist/native/darwin-x64/wand-structured-renderd +0 -0
- package/dist/native/darwin-x64/wand-structured-renderd.sha256 +1 -0
- package/dist/native/darwin-x64/wand-structured-renderd.version +1 -0
- package/dist/native/linux-arm64/wand-render +0 -0
- package/dist/native/linux-arm64/wand-render.sha256 +1 -0
- package/dist/native/linux-arm64/wand-render.version +1 -0
- package/dist/native/linux-arm64/wand-structured-renderd +0 -0
- package/dist/native/linux-arm64/wand-structured-renderd.sha256 +1 -0
- package/dist/native/linux-arm64/wand-structured-renderd.version +1 -0
- package/dist/native/linux-x64/wand-render +0 -0
- package/dist/native/linux-x64/wand-render.sha256 +1 -0
- package/dist/native/linux-x64/wand-render.version +1 -0
- package/dist/native/linux-x64/wand-structured-renderd +0 -0
- package/dist/native/linux-x64/wand-structured-renderd.sha256 +1 -0
- package/dist/native/linux-x64/wand-structured-renderd.version +1 -0
- package/dist/process-manager.d.ts +12 -1
- package/dist/process-manager.js +88 -7
- package/dist/pty-terminal-state.d.ts +17 -0
- package/dist/pty-terminal-state.js +51 -1
- package/dist/render-binary.d.ts +61 -0
- package/dist/render-binary.js +204 -0
- package/dist/render-daemon-client.d.ts +202 -0
- package/dist/render-daemon-client.js +1164 -0
- package/dist/render-host.d.ts +88 -0
- package/dist/render-host.js +440 -0
- package/dist/render-protocol.d.ts +128 -0
- package/dist/render-protocol.js +99 -0
- package/dist/render-structured-client.d.ts +80 -0
- package/dist/render-structured-client.js +513 -0
- package/dist/render-structured-host.d.ts +31 -0
- package/dist/render-structured-host.js +218 -0
- package/dist/render-structured-protocol.d.ts +101 -0
- package/dist/render-structured-protocol.js +35 -0
- package/dist/server-session-routes.js +114 -12
- package/dist/server-task-routes.js +55 -6
- package/dist/server-update-routes.d.ts +2 -0
- package/dist/server-update-routes.js +44 -0
- package/dist/server.js +43 -13
- package/dist/session-ai-context.d.ts +1 -1
- package/dist/session-ai-context.js +15 -4
- package/dist/session-logger.d.ts +2 -0
- package/dist/session-logger.js +44 -1
- package/dist/session-transport.js +1 -1
- package/dist/storage.d.ts +2 -1
- package/dist/storage.js +14 -5
- package/dist/structured-client-protocol.d.ts +3 -1
- package/dist/structured-client-protocol.js +85 -25
- package/dist/structured-content.d.ts +20 -3
- package/dist/structured-content.js +80 -1
- package/dist/structured-exec-host.js +7 -1
- package/dist/structured-exec-pump.js +6 -2
- package/dist/structured-grok-adapter.js +22 -0
- package/dist/structured-pi-adapter.js +18 -2
- package/dist/structured-provider-common.d.ts +11 -9
- package/dist/structured-provider-common.js +60 -18
- package/dist/structured-session-manager.d.ts +2 -0
- package/dist/structured-session-manager.js +27 -3
- package/dist/task-types.d.ts +4 -1
- package/dist/terminal-daemon-client.d.ts +9 -2
- package/dist/terminal-daemon-client.js +119 -46
- package/dist/terminal-daemon-server.js +47 -8
- package/dist/terminal-host.d.ts +6 -0
- package/dist/terminal-host.js +3 -0
- package/dist/tui/commands.js +183 -10
- package/dist/tui/ipc-server.js +12 -0
- package/dist/types.d.ts +30 -10
- package/dist/unix-socket-keepalive.d.ts +3 -0
- package/dist/unix-socket-keepalive.js +94 -0
- package/dist/wand-task-sync.js +2 -1
- package/dist/web-ui/content/scripts.js +79 -73
- package/dist/web-ui/content/styles.css +1 -1
- package/dist/web-ui/content/tailwind.css +21 -0
- package/dist/web-ui/embedded-assets.js +2 -2
- package/dist/web-ui/thinking-efforts.d.ts +24 -0
- package/dist/web-ui/thinking-efforts.js +81 -0
- package/dist/ws-broadcast.d.ts +1 -1
- package/dist/ws-broadcast.js +28 -9
- package/package.json +8 -3
- package/scripts/install-render-binary.js +499 -0
- package/scripts/stage-render-binaries.js +626 -0
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* 把平台专用的 Rust `wand-render` 二进制就位到 `<configDir>/bin/wand-render`。
|
|
4
|
+
*
|
|
5
|
+
* 为什么是一个独立脚本,而不是在 Server 里顺手 copy:
|
|
6
|
+
* - npm 全局包升级后,新包里的 `dist/native/<triple>/wand-render` 取代 configDir 里的旧二进制
|
|
7
|
+
* 是**升级链路**的一环(老 Server 重启时必须换成新二进制才会说同一版协议),把它做成
|
|
8
|
+
* 可单独执行、可 `--dry-run` 的工具,install.sh / CI / 手工排障 / Server 启动自检
|
|
9
|
+
* 就都能复用同一套规则,而不是各自实现一遍。
|
|
10
|
+
* - 幂等是硬要求:版本一致时连 mtime 都不动(Server 每次启动都会 check 一次,
|
|
11
|
+
* 不能每次都重写磁盘、更不能打断正在跑的 Render)。
|
|
12
|
+
* - 不读 config.json 内容(只取其所在目录):既避免把配置里的密钥带进日志,
|
|
13
|
+
* 也允许「配置还没生成」时就能把二进制放好。
|
|
14
|
+
*
|
|
15
|
+
* 用法见 `--help`;设计与回滚见 `docs/render-upgrade-path.md`。
|
|
16
|
+
*
|
|
17
|
+
* 依赖的外部约定(与 Node 侧集成方共享,改这里要同步改文档):
|
|
18
|
+
* `wand-render --version` 必须打印含语义化版本号的一行(形如 `wand-render 0.1.0`)并以 0 退出,
|
|
19
|
+
* **不得**因此启动守护进程。读不出可解析版本时视为「需要替换」。
|
|
20
|
+
*/
|
|
21
|
+
import { spawnSync } from "node:child_process";
|
|
22
|
+
import { randomBytes } from "node:crypto";
|
|
23
|
+
import {
|
|
24
|
+
chmodSync,
|
|
25
|
+
copyFileSync,
|
|
26
|
+
mkdirSync,
|
|
27
|
+
readFileSync,
|
|
28
|
+
renameSync,
|
|
29
|
+
rmSync,
|
|
30
|
+
statSync,
|
|
31
|
+
writeFileSync,
|
|
32
|
+
} from "node:fs";
|
|
33
|
+
import os from "node:os";
|
|
34
|
+
import path from "node:path";
|
|
35
|
+
import process from "node:process";
|
|
36
|
+
import { fileURLToPath } from "node:url";
|
|
37
|
+
|
|
38
|
+
/** 本脚本所在仓库 / npm 包的根目录(脚本随包一起分发)。 */
|
|
39
|
+
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
40
|
+
|
|
41
|
+
export const RENDER_BINARY_NAME = "wand-render";
|
|
42
|
+
/** 安装后的二进制旁边记录版本的 sidecar(纯版本字符串 + 换行)。 */
|
|
43
|
+
export const RENDER_VERSION_FILE_NAME = "wand-render.version";
|
|
44
|
+
/** 安装子目录:`<configDir>/bin/`。 */
|
|
45
|
+
export const RENDER_BIN_SUBDIR = "bin";
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 支持的平台三元组。Node 的 `platform` + `arch`
|
|
49
|
+
* (`darwin`/`linux` × `arm64`/`x64`)拼接后正好等于后缀,无需额外映射表。
|
|
50
|
+
* win32 明确不实现:协议第一阶段不做命名管道(render/docs/render-protocol.md §2)。
|
|
51
|
+
*/
|
|
52
|
+
export const SUPPORTED_TRIPLES = ["darwin-arm64", "darwin-x64", "linux-x64", "linux-arm64"];
|
|
53
|
+
|
|
54
|
+
/** 退出码:可被 Server 启动自检与 CI 直接判定,不要随意改动语义。 */
|
|
55
|
+
export const EXIT_OK = 0;
|
|
56
|
+
export const EXIT_UNSUPPORTED = 1;
|
|
57
|
+
export const EXIT_NO_SOURCE = 2;
|
|
58
|
+
export const EXIT_INSTALL_FAILED = 3;
|
|
59
|
+
export const EXIT_NEEDS_ACTION = 4;
|
|
60
|
+
|
|
61
|
+
const VERSION_PROBE_TIMEOUT_MS = 5_000;
|
|
62
|
+
const SEMVER = /\d+\.\d+\.\d+(?:-[0-9A-Za-z.]+)?/;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 解析平台三元组。
|
|
66
|
+
* @param {string} platform `process.platform`
|
|
67
|
+
* @param {string} arch `process.arch`
|
|
68
|
+
* @returns {string | null} 支持时返回三元组,否则 null(调用方给出明确报错,不静默降级)
|
|
69
|
+
*/
|
|
70
|
+
export function resolvePlatformTriple(platform, arch) {
|
|
71
|
+
const triple = `${platform}-${arch}`;
|
|
72
|
+
return SUPPORTED_TRIPLES.includes(triple) ? triple : null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 默认 config 路径,与 `src/config.ts` 的 `resolveConfigPath()` 保持一致。
|
|
77
|
+
* @returns {string}
|
|
78
|
+
*/
|
|
79
|
+
export function defaultConfigPath() {
|
|
80
|
+
return path.resolve(process.env.HOME || os.homedir(), ".wand", "config.json");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 源二进制的候选路径,按优先级排列(先命中先用)。
|
|
85
|
+
*
|
|
86
|
+
* 新分发布局下只有一个正式位置:`<pkg>/dist/native/<triple>/wand-render`
|
|
87
|
+
* ——它由 `scripts/stage-render-binaries.js` 从 `render-bin` 子模块 stage 进来,
|
|
88
|
+
* 而 `dist/` 在 package.json 的 `files` 里,所以全局安装后确实存在。
|
|
89
|
+
* 开发态仍然优先 `render/target/<profile>/`(刚 `cargo build` 的产物要立刻生效)。
|
|
90
|
+
* @param {{ packageRoot?: string, triple: string, from?: string | null, envBinary?: string }} options
|
|
91
|
+
* @returns {{ path: string, source: "from" | "env" | "cargo" | "cargo-debug" | "dist" }[]}
|
|
92
|
+
*/
|
|
93
|
+
export function renderBinarySourceCandidates(options) {
|
|
94
|
+
const packageRoot = options.packageRoot ?? PACKAGE_ROOT;
|
|
95
|
+
const candidates = [];
|
|
96
|
+
if (options.from) {
|
|
97
|
+
candidates.push({ path: path.resolve(options.from), source: "from" });
|
|
98
|
+
}
|
|
99
|
+
const fromEnv = (options.envBinary ?? process.env.WAND_RENDER_BIN ?? "").trim();
|
|
100
|
+
if (fromEnv) {
|
|
101
|
+
candidates.push({ path: path.resolve(fromEnv), source: "env" });
|
|
102
|
+
}
|
|
103
|
+
candidates.push({
|
|
104
|
+
path: path.join(packageRoot, "render", "target", "release", RENDER_BINARY_NAME),
|
|
105
|
+
source: "cargo",
|
|
106
|
+
});
|
|
107
|
+
candidates.push({
|
|
108
|
+
path: path.join(packageRoot, "render", "target", "debug", RENDER_BINARY_NAME),
|
|
109
|
+
source: "cargo-debug",
|
|
110
|
+
});
|
|
111
|
+
candidates.push({
|
|
112
|
+
path: path.join(packageRoot, "dist", "native", options.triple, RENDER_BINARY_NAME),
|
|
113
|
+
source: "dist",
|
|
114
|
+
});
|
|
115
|
+
return candidates;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** 候选来源的中文说明,只用于日志。 */
|
|
119
|
+
const SOURCE_LABELS = {
|
|
120
|
+
from: "--from",
|
|
121
|
+
env: "WAND_RENDER_BIN",
|
|
122
|
+
cargo: "cargo target/release",
|
|
123
|
+
"cargo-debug": "cargo target/debug",
|
|
124
|
+
dist: "dist/native/",
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
/** 已安装版本的来源,只用于日志。 */
|
|
128
|
+
const INSTALLED_ORIGIN_LABELS = {
|
|
129
|
+
sidecar: "读自 version 文件",
|
|
130
|
+
binary: "问自二进制",
|
|
131
|
+
missing: "未知",
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* 读版本 sidecar(`wand-render.version`)。容忍手工编辑出的空行 / 额外空格,
|
|
136
|
+
* 但不做模糊匹配:读不出合法语义化版本就当没有。
|
|
137
|
+
* @param {string} versionPath
|
|
138
|
+
* @returns {string | null}
|
|
139
|
+
*/
|
|
140
|
+
export function readVersionSidecar(versionPath) {
|
|
141
|
+
try {
|
|
142
|
+
const raw = readFileSync(versionPath, "utf8");
|
|
143
|
+
const match = raw.trim().split("\n")[0]?.match(SEMVER);
|
|
144
|
+
return match ? match[0] : null;
|
|
145
|
+
} catch {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* 问二进制自己要版本。
|
|
152
|
+
*
|
|
153
|
+
* 超时 / 非 0 退出 / 输出里没有语义化版本号,一律返回 null(调用方视为需要替换)。
|
|
154
|
+
* 不吞掉 stderr 也解析它,是因为部分 CLI 把版本打到 stderr。
|
|
155
|
+
* @param {string} binaryPath
|
|
156
|
+
* @returns {string | null}
|
|
157
|
+
*/
|
|
158
|
+
export function readRenderBinaryVersion(binaryPath) {
|
|
159
|
+
const result = spawnSync(binaryPath, ["--version"], {
|
|
160
|
+
encoding: "utf8",
|
|
161
|
+
timeout: VERSION_PROBE_TIMEOUT_MS,
|
|
162
|
+
// 不接 stdin:万一对方是「不认 --version 就直接当守护进程跑」的旧二进制,
|
|
163
|
+
// 也不会因为等 TTY 输入而挂满超时窗口。
|
|
164
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
165
|
+
});
|
|
166
|
+
if (result.error || result.status !== 0) return null;
|
|
167
|
+
const output = `${result.stdout ?? ""}\n${result.stderr ?? ""}`;
|
|
168
|
+
const match = output.match(SEMVER);
|
|
169
|
+
return match ? match[0] : null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* 已安装二进制的版本:优先读 sidecar(快,不 spawn 进程),
|
|
174
|
+
* sidecar 缺失/损坏时回落到问二进制本身。
|
|
175
|
+
* @param {{ targetPath: string, versionPath: string }} options
|
|
176
|
+
* @returns {{ version: string | null, origin: "sidecar" | "binary" | "missing" }}
|
|
177
|
+
*/
|
|
178
|
+
export function readInstalledRenderVersion(options) {
|
|
179
|
+
const fromFile = readVersionSidecar(options.versionPath);
|
|
180
|
+
if (fromFile) return { version: fromFile, origin: "sidecar" };
|
|
181
|
+
const fromBinary = readRenderBinaryVersion(options.targetPath);
|
|
182
|
+
if (fromBinary) return { version: fromBinary, origin: "binary" };
|
|
183
|
+
return { version: null, origin: "missing" };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function isFile(candidate) {
|
|
187
|
+
try {
|
|
188
|
+
return statSync(candidate).isFile();
|
|
189
|
+
} catch {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* 原子替换:临时文件(同目录,保证 rename 不跨设备)→ chmod → rename 覆盖。
|
|
196
|
+
* 任何一步失败都清掉临时文件,绝不留半截的 `wand-render`。
|
|
197
|
+
* @param {{ sourcePath: string, targetPath: string }} options
|
|
198
|
+
* @returns {void}
|
|
199
|
+
*/
|
|
200
|
+
export function replaceFileAtomically(options) {
|
|
201
|
+
const tempPath = `${options.targetPath}.tmp-${process.pid}-${randomBytes(4).toString("hex")}`;
|
|
202
|
+
try {
|
|
203
|
+
copyFileSync(options.sourcePath, tempPath);
|
|
204
|
+
chmodSync(tempPath, 0o755);
|
|
205
|
+
renameSync(tempPath, options.targetPath);
|
|
206
|
+
} catch (error) {
|
|
207
|
+
try {
|
|
208
|
+
rmSync(tempPath, { force: true });
|
|
209
|
+
} catch {
|
|
210
|
+
// 清理失败不该掩盖真正的错误
|
|
211
|
+
}
|
|
212
|
+
throw error;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* 原子写一个小文本文件(同样 tmp + rename),失败时不留半截内容。
|
|
218
|
+
* @param {{ filePath: string, content: string }} options
|
|
219
|
+
* @returns {void}
|
|
220
|
+
*/
|
|
221
|
+
function writeTextAtomically(options) {
|
|
222
|
+
const tempPath = `${options.filePath}.tmp-${process.pid}-${randomBytes(4).toString("hex")}`;
|
|
223
|
+
try {
|
|
224
|
+
writeFileSync(tempPath, options.content, { mode: 0o644 });
|
|
225
|
+
renameSync(tempPath, options.filePath);
|
|
226
|
+
} catch (error) {
|
|
227
|
+
try {
|
|
228
|
+
rmSync(tempPath, { force: true });
|
|
229
|
+
} catch {
|
|
230
|
+
// 同上
|
|
231
|
+
}
|
|
232
|
+
throw error;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const NO_OP_LOGGER = () => {};
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* 就位动作的核心实现(无隐式 IO 之外的行为:不联网、不读 config 内容、不杀任何进程)。
|
|
240
|
+
*
|
|
241
|
+
* @param {{
|
|
242
|
+
* configPath?: string,
|
|
243
|
+
* from?: string | null,
|
|
244
|
+
* dryRun?: boolean,
|
|
245
|
+
* check?: boolean,
|
|
246
|
+
* force?: boolean,
|
|
247
|
+
* packageRoot?: string,
|
|
248
|
+
* platform?: string,
|
|
249
|
+
* arch?: string,
|
|
250
|
+
* logger?: (message: string) => void,
|
|
251
|
+
* }} [options]
|
|
252
|
+
* @returns {{
|
|
253
|
+
* exitCode: number,
|
|
254
|
+
* action: "skip" | "install" | "would-install" | "check-needs-action" | "unsupported" | "no-source" | "failed",
|
|
255
|
+
* triple: string | null,
|
|
256
|
+
* sourcePath: string | null,
|
|
257
|
+
* sourceVersion: string | null,
|
|
258
|
+
* targetPath: string | null,
|
|
259
|
+
* versionPath: string | null,
|
|
260
|
+
* installedVersion: string | null,
|
|
261
|
+
* warnings: string[],
|
|
262
|
+
* }}
|
|
263
|
+
*/
|
|
264
|
+
export function installRenderBinary(options = {}) {
|
|
265
|
+
const dryRun = options.dryRun === true;
|
|
266
|
+
const check = options.check === true;
|
|
267
|
+
const platform = options.platform ?? process.platform;
|
|
268
|
+
const arch = options.arch ?? process.arch;
|
|
269
|
+
const configPath = path.resolve(options.configPath ?? defaultConfigPath());
|
|
270
|
+
const configDir = path.dirname(configPath);
|
|
271
|
+
const logger = options.logger ?? NO_OP_LOGGER;
|
|
272
|
+
const base = {
|
|
273
|
+
triple: null,
|
|
274
|
+
sourcePath: null,
|
|
275
|
+
sourceVersion: null,
|
|
276
|
+
targetPath: null,
|
|
277
|
+
versionPath: null,
|
|
278
|
+
installedVersion: null,
|
|
279
|
+
warnings: [],
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const triple = resolvePlatformTriple(platform, arch);
|
|
283
|
+
if (!triple) {
|
|
284
|
+
logger(`wand-render 暂不支持 ${platform}-${arch},支持 ${SUPPORTED_TRIPLES.join(" / ")}`);
|
|
285
|
+
if (platform === "win32") {
|
|
286
|
+
logger("Windows 需要命名管道,协议第一阶段不实现(见 docs/render-protocol.md §2)。");
|
|
287
|
+
}
|
|
288
|
+
logger("保持 legacy terminald 运行即可;升级链路不做任何改动。");
|
|
289
|
+
return { ...base, exitCode: EXIT_UNSUPPORTED, action: "unsupported" };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const targetPath = path.join(configDir, RENDER_BIN_SUBDIR, RENDER_BINARY_NAME);
|
|
293
|
+
const versionPath = path.join(configDir, RENDER_BIN_SUBDIR, RENDER_VERSION_FILE_NAME);
|
|
294
|
+
const result = { ...base, triple, targetPath, versionPath };
|
|
295
|
+
|
|
296
|
+
logger(`平台 ${triple},配置目录 ${configDir}`);
|
|
297
|
+
|
|
298
|
+
// 源:先命中先用。`--from` 是显式指定,指错了要报错而不是悄悄回落;
|
|
299
|
+
// `WAND_RENDER_BIN` 与 Node 侧 `resolveRenderBinaryPath` 一致:指错了告警后继续按约定查找。
|
|
300
|
+
const candidates = renderBinarySourceCandidates({
|
|
301
|
+
packageRoot: options.packageRoot,
|
|
302
|
+
triple,
|
|
303
|
+
from: options.from ?? null,
|
|
304
|
+
});
|
|
305
|
+
const explicitFrom = options.from ? candidates[0] : null;
|
|
306
|
+
if (explicitFrom && !isFile(explicitFrom.path)) {
|
|
307
|
+
logger(`--from 指定的源二进制不存在或是目录:${explicitFrom.path}`);
|
|
308
|
+
return { ...result, exitCode: EXIT_NO_SOURCE, action: "no-source" };
|
|
309
|
+
}
|
|
310
|
+
const envCandidate = candidates.find((candidate) => candidate.source === "env");
|
|
311
|
+
if (envCandidate && !isFile(envCandidate.path)) {
|
|
312
|
+
logger(`warning: WAND_RENDER_BIN=${envCandidate.path} 不是可执行文件,忽略它继续按约定查找`);
|
|
313
|
+
}
|
|
314
|
+
const source = candidates.find((candidate) => isFile(candidate.path)) ?? null;
|
|
315
|
+
|
|
316
|
+
const targetExists = isFile(targetPath);
|
|
317
|
+
const installed = targetExists
|
|
318
|
+
? readInstalledRenderVersion({ targetPath, versionPath })
|
|
319
|
+
: { version: null, origin: /** @type {"missing"} */ ("missing") };
|
|
320
|
+
result.installedVersion = installed.version;
|
|
321
|
+
|
|
322
|
+
if (!source) {
|
|
323
|
+
if (targetExists) {
|
|
324
|
+
// 有可用的旧二进制、但包里没有该平台的源:保持现状,不动磁盘。
|
|
325
|
+
logger(`已安装 ${targetPath}${installed.version ? ` (v${installed.version})` : ""},但找不到源二进制(本平台可能未随包分发)。`);
|
|
326
|
+
logger("不动磁盘,继续使用已安装版本。");
|
|
327
|
+
return { ...result, exitCode: EXIT_OK, action: "skip" };
|
|
328
|
+
}
|
|
329
|
+
logger(`未找到 wand-render 源二进制(triple ${triple})。`);
|
|
330
|
+
logger(`期望路径:${candidates.map((candidate) => candidate.path).join(" 或 ")}`);
|
|
331
|
+
logger("开发态可先跑 `npm run build:render-native`(cargo build --release);升级态可先跑 `npm run build:render-bin` 把 render-bin 的产物 stage 到 dist/native/。");
|
|
332
|
+
return { ...result, exitCode: EXIT_NO_SOURCE, action: "no-source" };
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
result.sourcePath = source.path;
|
|
336
|
+
result.sourceVersion = readRenderBinaryVersion(source.path);
|
|
337
|
+
if (!result.sourceVersion) {
|
|
338
|
+
// 版本读不出来时不能拿它当「一致」,否则会把坏二进制装上去还得不到提示。
|
|
339
|
+
result.warnings.push(`无法从 ${source.path} 读出 --version,视为需要替换`);
|
|
340
|
+
}
|
|
341
|
+
const sourceLabel = SOURCE_LABELS[source.source] ?? source.source;
|
|
342
|
+
const sourceVersionLabel = result.sourceVersion ? ` v${result.sourceVersion}` : " 版本未知";
|
|
343
|
+
logger(`源二进制:${source.path}(${sourceLabel})${sourceVersionLabel}`);
|
|
344
|
+
|
|
345
|
+
const installedLabel = targetExists
|
|
346
|
+
? `${targetPath} (v${installed.version ?? "未知"},${INSTALLED_ORIGIN_LABELS[installed.origin]})`
|
|
347
|
+
: "无";
|
|
348
|
+
logger(`已安装:${installedLabel}`);
|
|
349
|
+
|
|
350
|
+
const matches = !!installed.version && installed.version === result.sourceVersion;
|
|
351
|
+
const needsAction = !targetExists || !matches || options.force === true;
|
|
352
|
+
|
|
353
|
+
if (!needsAction) {
|
|
354
|
+
logger(`版本一致(v${installed.version}),跳过:不写磁盘、不重启任何进程。`);
|
|
355
|
+
return { ...result, exitCode: EXIT_OK, action: "skip" };
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const reason = !targetExists
|
|
359
|
+
? "目标不存在"
|
|
360
|
+
: matches
|
|
361
|
+
? "版本一致但指定了 --force"
|
|
362
|
+
: `版本不一致(已装 ${installed.version ?? "未知"} → 源 ${result.sourceVersion ?? "未知"})`;
|
|
363
|
+
logger(`需要就位(${reason})。`);
|
|
364
|
+
|
|
365
|
+
if (check) {
|
|
366
|
+
logger("--check 只报告不改动;去掉 --check 再跑一次即会就位。");
|
|
367
|
+
return { ...result, exitCode: EXIT_NEEDS_ACTION, action: "check-needs-action" };
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (dryRun) {
|
|
371
|
+
logger(`将写入 ${targetPath},并写版本文件 ${versionPath}`);
|
|
372
|
+
return { ...result, exitCode: EXIT_OK, action: "would-install" };
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
try {
|
|
376
|
+
mkdirSync(path.dirname(targetPath), { recursive: true, mode: 0o755 });
|
|
377
|
+
replaceFileAtomically({ sourcePath: source.path, targetPath });
|
|
378
|
+
// sidecar 与二进制一起原子替换;先写二进制再写版本号,读到旧版本号最多导致下次重装,
|
|
379
|
+
// 反过来则会「版本说新的、二进制是旧的」——那才是难查的错。
|
|
380
|
+
writeTextAtomically({
|
|
381
|
+
filePath: versionPath,
|
|
382
|
+
content: `${result.sourceVersion ?? "unknown"}\n`,
|
|
383
|
+
});
|
|
384
|
+
} catch (error) {
|
|
385
|
+
const code = error && typeof error === "object" && "code" in error ? ` (${error.code})` : "";
|
|
386
|
+
logger(`就位失败${code}:${error instanceof Error ? error.message : String(error)}`);
|
|
387
|
+
logger("常见原因:目标目录只读 / 无写权限 / 磁盘满。已安装的旧二进制未被破坏(原子替换)。");
|
|
388
|
+
return { ...result, exitCode: EXIT_INSTALL_FAILED, action: "failed" };
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
logger(`已就位 ${targetPath}${result.sourceVersion ? ` v${result.sourceVersion}` : ""}`);
|
|
392
|
+
logger(`版本文件 ${versionPath}`);
|
|
393
|
+
|
|
394
|
+
// 装完再问一次:确认放下去的确实是能跑的二进制(架构不匹配 / 动态库缺失都会在这里露出来)。
|
|
395
|
+
const installedNow = readRenderBinaryVersion(targetPath);
|
|
396
|
+
if (!installedNow) {
|
|
397
|
+
result.warnings.push(`安装后无法执行 ${targetPath} --version,请检查架构与权限`);
|
|
398
|
+
} else if (result.sourceVersion && installedNow !== result.sourceVersion) {
|
|
399
|
+
result.warnings.push(`安装后版本为 ${installedNow},与源 ${result.sourceVersion} 不一致`);
|
|
400
|
+
}
|
|
401
|
+
for (const warning of result.warnings) logger(`warning: ${warning}`);
|
|
402
|
+
|
|
403
|
+
return { ...result, exitCode: EXIT_OK, action: "install" };
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* @param {string[]} argv
|
|
408
|
+
* @returns {{ configPath?: string, from?: string | null, dryRun: boolean, check: boolean, force: boolean, help: boolean, error?: string }}
|
|
409
|
+
*/
|
|
410
|
+
export function parseArgs(argv) {
|
|
411
|
+
const options = { dryRun: false, check: false, force: false, help: false, from: null };
|
|
412
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
413
|
+
const arg = argv[index];
|
|
414
|
+
const [name, inlineValue] = arg.startsWith("--") && arg.includes("=")
|
|
415
|
+
? [arg.slice(0, arg.indexOf("=")), arg.slice(arg.indexOf("=") + 1)]
|
|
416
|
+
: [arg, null];
|
|
417
|
+
const takeValue = () => {
|
|
418
|
+
if (inlineValue !== null) return inlineValue;
|
|
419
|
+
index += 1;
|
|
420
|
+
return argv[index];
|
|
421
|
+
};
|
|
422
|
+
if (name === "--dry-run") options.dryRun = true;
|
|
423
|
+
else if (name === "--check") options.check = true;
|
|
424
|
+
else if (name === "--force") options.force = true;
|
|
425
|
+
else if (name === "--help" || name === "-h") options.help = true;
|
|
426
|
+
else if (name === "--from") {
|
|
427
|
+
const value = takeValue();
|
|
428
|
+
if (!value) return { ...options, error: "--from 需要一个路径" };
|
|
429
|
+
options.from = value;
|
|
430
|
+
} else if (name === "--config" || name === "-c") {
|
|
431
|
+
const value = takeValue();
|
|
432
|
+
if (!value) return { ...options, error: `${name} 需要一个路径` };
|
|
433
|
+
options.configPath = value;
|
|
434
|
+
} else {
|
|
435
|
+
return { ...options, error: `未知参数 ${arg}` };
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return options;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const USAGE = `用法:node scripts/install-render-binary.js [选项]
|
|
442
|
+
|
|
443
|
+
把平台专用的 wand-render 二进制就位到 <configDir>/bin/wand-render(原子替换 + 版本幂等)。
|
|
444
|
+
|
|
445
|
+
选项:
|
|
446
|
+
--config <path>, -c <path> 配置文件路径(默认 ~/.wand/config.json);只用其所在目录
|
|
447
|
+
--from <path> 显式指定源二进制(优先级最高;路径不存在直接报错)
|
|
448
|
+
--dry-run 只报告将要做什么,不写磁盘
|
|
449
|
+
--check 只报告现状与是否需要就位,不写磁盘
|
|
450
|
+
--force 版本一致也强制替换(本地同版本重新构建后刷盘用)
|
|
451
|
+
-h, --help 显示本帮助
|
|
452
|
+
|
|
453
|
+
源优先级(先命中先用):
|
|
454
|
+
1) --from <path>
|
|
455
|
+
2) $WAND_RENDER_BIN
|
|
456
|
+
3) <pkg>/render/target/release/wand-render(开发态 cargo build)
|
|
457
|
+
4) <pkg>/render/target/debug/wand-render(开发态)
|
|
458
|
+
5) <pkg>/dist/native/<triple>/wand-render(npm 包内嵌;由 npm run build:render-bin 写入)
|
|
459
|
+
安装目标:<configDir>/bin/wand-render(mode 0755)+ <configDir>/bin/wand-render.version
|
|
460
|
+
|
|
461
|
+
退出码:
|
|
462
|
+
0 已就位 / 已是最新 / dry-run 判定无需或可以就位
|
|
463
|
+
1 平台不支持(darwin/linux 之外)或参数错误
|
|
464
|
+
2 找不到源二进制
|
|
465
|
+
3 就位失败(只读盘 / 无写权限 / 磁盘满)
|
|
466
|
+
4 --check 判定需要就位
|
|
467
|
+
|
|
468
|
+
设计说明与升级/回滚流程:docs/render-upgrade-path.md`;
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* @param {string[]} argv
|
|
472
|
+
* @returns {number} 退出码
|
|
473
|
+
*/
|
|
474
|
+
export function main(argv) {
|
|
475
|
+
const options = parseArgs(argv);
|
|
476
|
+
if (options.help) {
|
|
477
|
+
console.log(USAGE);
|
|
478
|
+
return EXIT_OK;
|
|
479
|
+
}
|
|
480
|
+
if (options.error) {
|
|
481
|
+
console.error(`error: ${options.error}`);
|
|
482
|
+
console.error(USAGE);
|
|
483
|
+
return EXIT_UNSUPPORTED;
|
|
484
|
+
}
|
|
485
|
+
const result = installRenderBinary({
|
|
486
|
+
configPath: options.configPath,
|
|
487
|
+
from: options.from,
|
|
488
|
+
dryRun: options.dryRun,
|
|
489
|
+
check: options.check,
|
|
490
|
+
force: options.force,
|
|
491
|
+
logger: (message) => console.log(`[wand-render] ${message}`),
|
|
492
|
+
});
|
|
493
|
+
return result.exitCode;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// 只有被直接执行时才跑 main;被 import 时只暴露函数(Server 自检复用同一份实现)。
|
|
497
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url))) {
|
|
498
|
+
process.exitCode = main(process.argv.slice(2));
|
|
499
|
+
}
|