@co0ontty/wand 4.72.2 → 4.73.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.
Files changed (72) hide show
  1. package/dist/build-info.json +3 -3
  2. package/dist/config.js +20 -0
  3. package/dist/daemon-connection.d.ts +9 -0
  4. package/dist/daemon-connection.js +86 -0
  5. package/dist/distribution-manager.d.ts +5 -0
  6. package/dist/distribution-manager.js +79 -1
  7. package/dist/git-quick-commit.js +12 -0
  8. package/dist/message-truncator.js +6 -0
  9. package/dist/native/darwin-arm64/wand-render +0 -0
  10. package/dist/native/darwin-arm64/wand-render.sha256 +1 -0
  11. package/dist/native/darwin-arm64/wand-render.version +1 -0
  12. package/dist/native/darwin-x64/wand-render +0 -0
  13. package/dist/native/darwin-x64/wand-render.sha256 +1 -0
  14. package/dist/native/darwin-x64/wand-render.version +1 -0
  15. package/dist/native/linux-arm64/wand-render +0 -0
  16. package/dist/native/linux-arm64/wand-render.sha256 +1 -0
  17. package/dist/native/linux-arm64/wand-render.version +1 -0
  18. package/dist/native/linux-x64/wand-render +0 -0
  19. package/dist/native/linux-x64/wand-render.sha256 +1 -0
  20. package/dist/native/linux-x64/wand-render.version +1 -0
  21. package/dist/process-manager.d.ts +10 -0
  22. package/dist/process-manager.js +73 -4
  23. package/dist/render-binary.d.ts +61 -0
  24. package/dist/render-binary.js +204 -0
  25. package/dist/render-daemon-client.d.ts +171 -0
  26. package/dist/render-daemon-client.js +1114 -0
  27. package/dist/render-host.d.ts +64 -0
  28. package/dist/render-host.js +301 -0
  29. package/dist/render-protocol.d.ts +128 -0
  30. package/dist/render-protocol.js +99 -0
  31. package/dist/render-structured-client.d.ts +80 -0
  32. package/dist/render-structured-client.js +513 -0
  33. package/dist/render-structured-host.d.ts +31 -0
  34. package/dist/render-structured-host.js +218 -0
  35. package/dist/render-structured-protocol.d.ts +101 -0
  36. package/dist/render-structured-protocol.js +35 -0
  37. package/dist/server-session-routes.js +72 -9
  38. package/dist/server-task-routes.js +52 -3
  39. package/dist/server-update-routes.d.ts +2 -0
  40. package/dist/server-update-routes.js +44 -0
  41. package/dist/server.js +31 -9
  42. package/dist/session-transport.js +1 -1
  43. package/dist/storage.d.ts +2 -1
  44. package/dist/storage.js +14 -5
  45. package/dist/structured-client-protocol.d.ts +3 -1
  46. package/dist/structured-client-protocol.js +85 -25
  47. package/dist/structured-content.d.ts +20 -3
  48. package/dist/structured-content.js +80 -1
  49. package/dist/structured-exec-host.js +7 -1
  50. package/dist/structured-exec-pump.js +6 -2
  51. package/dist/structured-pi-adapter.js +18 -2
  52. package/dist/structured-session-manager.d.ts +2 -0
  53. package/dist/structured-session-manager.js +27 -3
  54. package/dist/task-types.d.ts +2 -0
  55. package/dist/terminal-daemon-client.d.ts +9 -2
  56. package/dist/terminal-daemon-client.js +119 -46
  57. package/dist/terminal-daemon-server.js +20 -4
  58. package/dist/terminal-host.d.ts +6 -0
  59. package/dist/terminal-host.js +3 -0
  60. package/dist/tui/ipc-server.js +12 -0
  61. package/dist/types.d.ts +19 -1
  62. package/dist/unix-socket-keepalive.d.ts +3 -0
  63. package/dist/unix-socket-keepalive.js +94 -0
  64. package/dist/web-ui/content/scripts.js +68 -68
  65. package/dist/web-ui/content/styles.css +1 -1
  66. package/dist/web-ui/content/tailwind.css +12 -0
  67. package/dist/web-ui/embedded-assets.js +2 -2
  68. package/dist/ws-broadcast.d.ts +1 -1
  69. package/dist/ws-broadcast.js +17 -5
  70. package/package.json +8 -3
  71. package/scripts/install-render-binary.js +499 -0
  72. package/scripts/stage-render-binaries.js +626 -0
@@ -0,0 +1,626 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 把 `render-bin` 子模块里 pin 住的 wand-render 产物 stage 到 `dist/native/<triple>/`,
4
+ * 供 npm 包内嵌(`dist/` 在 package.json 的 `files` 里)。
5
+ *
6
+ * 为什么必须有这一步,而不是运行时去读 `render-bin/`:
7
+ * - npm 全局安装的结果里只有 `dist/` + `browser-extension` + 少量 `scripts/`;
8
+ * `render-bin` 子模块**不会**出现在安装结果里。包要在发布前就把二进制塞进 dist。
9
+ * - 二进制经 npm / git 传输会丢可执行位(legacy 的 node-pty spawn-helper 就栽在这),
10
+ * 所以 stage 完必须显式 chmod 0755,并留下 `.sha256` sidecar 让下游可校验。
11
+ * - 三重校验(sha256 / 可执行且版本一致 / 不是 stub)是硬门槛:曾经有一个只打印
12
+ * `(stub)` 的 302KB 假二进制进过分发目录。校验失败一律**报错退出**,绝不「放行让下游发现」。
13
+ * - `render-bin` 子模块没初始化时要**警告并跳过**(退出码 0):没拉子模块的开发机上
14
+ * `npm run build` 不该因此失败;`--strict` 才是「必须内嵌」的场景(CI 发布链路)。
15
+ *
16
+ * 用法见 `--help`;布局与升级路径见 `docs/render-upgrade-path.md`。
17
+ */
18
+ import { spawnSync } from "node:child_process";
19
+ import { createHash, randomBytes } from "node:crypto";
20
+ import {
21
+ chmodSync,
22
+ closeSync,
23
+ copyFileSync,
24
+ mkdirSync,
25
+ mkdtempSync,
26
+ openSync,
27
+ readFileSync,
28
+ readSync,
29
+ renameSync,
30
+ rmSync,
31
+ statSync,
32
+ writeFileSync,
33
+ } from "node:fs";
34
+ import os from "node:os";
35
+ import path from "node:path";
36
+ import process from "node:process";
37
+ import { fileURLToPath } from "node:url";
38
+
39
+ /** 本脚本所在仓库 / npm 包的根目录(脚本随包一起分发)。 */
40
+ const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
41
+
42
+ /** 已发布产物的子模块目录与索引文件名。 */
43
+ export const RENDER_BIN_DIR_NAME = "render-bin";
44
+ export const MANIFEST_FILE_NAME = "manifest.json";
45
+
46
+ export const BINARY_NAME = "wand-render";
47
+ export const STRUCTURED_BINARY_NAME = "wand-structured-renderd";
48
+ export const VERSION_FILE_NAME = "wand-render.version";
49
+ export const SHA256_FILE_NAME = "wand-render.sha256";
50
+
51
+ /**
52
+ * 支持的平台三元组,必须与 `render-bin/manifest.json` 的键和
53
+ * `src/render-binary.ts` 的 `SUPPORTED_RENDER_TRIPLES` 一致。
54
+ * `src` 侧与 `scripts` 侧的解析逻辑各自独立(TS 不能直接被构建脚本 import),
55
+ * 一致性由 `tests/render-staging.test.ts` 的对照用例守住。
56
+ */
57
+ export const SUPPORTED_TRIPLES = ["darwin-arm64", "darwin-x64", "linux-x64", "linux-arm64"];
58
+
59
+ export const EXIT_OK = 0;
60
+ export const EXIT_FAILED = 1;
61
+
62
+ const VERSION_PROBE_TIMEOUT_MS = 5_000;
63
+ const ARCH_PROBE_TIMEOUT_MS = 1_000;
64
+ const SEMVER = /\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.]+)?/;
65
+
66
+ /** `uname -m` 的架构别名 → Node 的 process.arch 命名。 */
67
+ const MACHINE_ARCH_ALIASES = {
68
+ arm64: "arm64",
69
+ aarch64: "arm64",
70
+ x86_64: "x64",
71
+ amd64: "x64",
72
+ x64: "x64",
73
+ };
74
+
75
+ /** `uname -m` 输出归一化;认不出返回 null。 */
76
+ export function normalizeMachineArch(machine) {
77
+ if (typeof machine !== "string") return null;
78
+ return MACHINE_ARCH_ALIASES[machine.trim().toLowerCase()] ?? null;
79
+ }
80
+
81
+ /**
82
+ * 平台三元组;不在支持列表里返回 null(调用方给出明确结论,不静默降级)。
83
+ *
84
+ * 用 `uname -m` 而不是 `process.arch`:macOS 上 Node 跑在 Rosetta 里时它报 `x64`,
85
+ * 机器其实是 arm64;`rosettaTranslated` 是反向兜底(翻译态下 `uname -m` 也报 x86_64)。
86
+ */
87
+ export function resolvePlatformTriple(platform, arch, unameMachine, rosettaTranslated = false) {
88
+ let normalizedArch = normalizeMachineArch(unameMachine) ?? arch;
89
+ if (platform === "darwin" && normalizedArch === "x64" && rosettaTranslated) normalizedArch = "arm64";
90
+ const triple = `${platform}-${normalizedArch}`;
91
+ return SUPPORTED_TRIPLES.includes(triple) ? triple : null;
92
+ }
93
+
94
+ function runUnameMachine() {
95
+ if (process.platform === "win32") return null;
96
+ try {
97
+ const result = spawnSync("uname", ["-m"], {
98
+ encoding: "utf8",
99
+ timeout: ARCH_PROBE_TIMEOUT_MS,
100
+ stdio: ["ignore", "pipe", "ignore"],
101
+ });
102
+ if (result.error || result.status !== 0) return null;
103
+ return (result.stdout ?? "").trim() || null;
104
+ } catch {
105
+ return null;
106
+ }
107
+ }
108
+
109
+ function isRosettaTranslated() {
110
+ if (process.platform !== "darwin") return false;
111
+ try {
112
+ const result = spawnSync("sysctl", ["-n", "sysctl.proc_translated"], {
113
+ encoding: "utf8",
114
+ timeout: ARCH_PROBE_TIMEOUT_MS,
115
+ stdio: ["ignore", "pipe", "ignore"],
116
+ });
117
+ if (result.error || result.status !== 0) return false;
118
+ return (result.stdout ?? "").trim() === "1";
119
+ } catch {
120
+ return false;
121
+ }
122
+ }
123
+
124
+ /** 文件 sha256(hex,小写)。产物不到 1 MB,整块读即可。 */
125
+ export function sha256File(filePath) {
126
+ return createHash("sha256").update(readFileSync(filePath)).digest("hex");
127
+ }
128
+
129
+ function isFile(candidate) {
130
+ try {
131
+ return statSync(candidate).isFile();
132
+ } catch {
133
+ return false;
134
+ }
135
+ }
136
+
137
+ /**
138
+ * 执行二进制拿版本。
139
+ *
140
+ * `isStub` 单独判一次是为了给出可读的报错(stub 通常连语义化版本号都没有,
141
+ * 只按版本比对会报「版本不一致」,看不出真正原因)。
142
+ */
143
+ export function probeRenderBinary(binaryPath) {
144
+ const result = spawnSync(binaryPath, ["--version"], {
145
+ encoding: "utf8",
146
+ timeout: VERSION_PROBE_TIMEOUT_MS,
147
+ // 不接 stdin:万一对方是「不认 --version 就直接当守护进程跑」的旧二进制,
148
+ // 也不会因为等 TTY 输入而挂满超时窗口。
149
+ stdio: ["ignore", "pipe", "pipe"],
150
+ });
151
+ const output = `${result.stdout ?? ""}\n${result.stderr ?? ""}`.trim();
152
+ const ran = !result.error && result.status === 0;
153
+ return {
154
+ ran,
155
+ output,
156
+ version: ran ? SEMVER.exec(output)?.[0] ?? null : null,
157
+ isStub: /stub/i.test(output),
158
+ };
159
+ }
160
+
161
+ /**
162
+ * 读 manifest。区分「子模块没初始化」(缺文件 → 可跳过)与「文件坏了」(硬错误)。
163
+ */
164
+ export function readManifest(manifestPath) {
165
+ if (!isFile(manifestPath)) return { ok: false, missing: true, error: `${manifestPath} 不存在` };
166
+ let parsed;
167
+ try {
168
+ parsed = JSON.parse(readFileSync(manifestPath, "utf8"));
169
+ } catch (error) {
170
+ return { ok: false, missing: false, error: `${manifestPath} 不是合法 JSON:${error instanceof Error ? error.message : String(error)}` };
171
+ }
172
+ const latest = parsed && typeof parsed.latest === "string" ? parsed.latest : null;
173
+ const entry = latest && parsed.versions && typeof parsed.versions === "object" ? parsed.versions[latest] : null;
174
+ if (!latest || !entry || typeof entry !== "object") {
175
+ return { ok: false, missing: false, error: `${manifestPath} 缺少 latest/versions 结构` };
176
+ }
177
+ const triples = entry.triples && typeof entry.triples === "object" ? entry.triples : null;
178
+ if (!triples) {
179
+ return { ok: false, missing: false, error: `${manifestPath} 的 versions["${latest}"] 缺少 triples` };
180
+ }
181
+ return { ok: true, missing: false, data: { latest, entry, triples } };
182
+ }
183
+
184
+ /** 在临时目录里造一份 0755 的可执行副本再探测(不碰 dist/,也不假设源文件带可执行位)。 */
185
+ function withExecutableCopy(sourcePath, body) {
186
+ const tempDir = mkdtempSync(path.join(os.tmpdir(), "wand-render-probe-"));
187
+ try {
188
+ const tempPath = path.join(tempDir, BINARY_NAME);
189
+ copyFileSync(sourcePath, tempPath);
190
+ chmodSync(tempPath, 0o755);
191
+ return body(tempPath);
192
+ } finally {
193
+ rmSync(tempDir, { recursive: true, force: true });
194
+ }
195
+ }
196
+
197
+ /**
198
+ * 校验单个产物:尺寸、sha256、能否执行、版本是否与 manifest 一致、是否 stub。
199
+ * @returns {string | null} 通过返回 null,否则返回可直接打印的原因
200
+ */
201
+ /** 各平台产物应有的容器格式。用于「不能执行」的跨平台产物做格式级校验。 */
202
+ const TRIPLE_CONTAINER_FORMAT = { darwin: "macho", linux: "elf", win32: "pe" };
203
+
204
+ /**
205
+ * 读文件头判断容器格式。
206
+ *
207
+ * 跨平台校验只能做到这一步:Linux CI 上无法执行 macOS 产物(exec format error),
208
+ * 反过来也一样。但「这个槽位里放的是不是那一类二进制」可以判断 ——
209
+ * 文本 stub、放错平台的文件都能在这里被拦下。
210
+ */
211
+ export function readBinaryContainerFormat(filePath) {
212
+ const header = Buffer.alloc(4);
213
+ const handle = openSync(filePath, "r");
214
+ try {
215
+ readSync(handle, header, 0, 4, 0);
216
+ } finally {
217
+ closeSync(handle);
218
+ }
219
+ const magic = header.toString("hex");
220
+ // Mach-O 64/32 小端与 FAT/Universal,以及大端变体
221
+ if (magic.startsWith("cffaedfe") || magic.startsWith("cefaedfe") || magic === "cafebabe" || magic === "bebafeca") return "macho";
222
+ if (magic === "7f454c46") return "elf";
223
+ if (magic.startsWith("4d5a")) return "pe";
224
+ if (magic.startsWith("2321")) return "script";
225
+ return "unknown";
226
+ }
227
+
228
+ function verifyArtifact({ triple, artifact, sourcePath, expectedVersion, hostTriple, log, protocolVersion = null }) {
229
+ if (!isFile(sourcePath)) return `${triple}: 产物缺失 ${sourcePath}(manifest 指向的路径不对?)`;
230
+
231
+ const actualSize = statSync(sourcePath).size;
232
+ if (typeof artifact.size === "number" && actualSize !== artifact.size) {
233
+ return `${triple}: 尺寸与 manifest 不一致(实际 ${actualSize},manifest ${artifact.size})`;
234
+ }
235
+ const actualSha = sha256File(sourcePath);
236
+ const expectedSha = typeof artifact.sha256 === "string" ? artifact.sha256.trim().toLowerCase() : "";
237
+ if (!expectedSha) return `${triple}: manifest 里没有 sha256,拒绝安装未固定哈希的产物`;
238
+ if (actualSha !== expectedSha) {
239
+ return `${triple}: sha256 与 manifest 不一致(实际 ${actualSha},manifest ${expectedSha})——产物被改过或仓库损坏`;
240
+ }
241
+
242
+ // 本平台产物直接执行校验 —— 这是最强的一档,能真实发现 stub / 版本不符 / 动态库缺失。
243
+ //
244
+ // 跨平台产物不能执行(Linux CI 上核对 macOS 产物会得到 exec format error,那是正常现象,
245
+ // 不是产物坏了)。所以退一步:完整性由 manifest 的 sha256 保证(上面已核对),
246
+ // 再用容器格式确认「这一槽位里放的是不是那一类二进制」——文本 stub 与放错平台的文件都能拦下。
247
+ if (triple !== hostTriple) {
248
+ const container = readBinaryContainerFormat(sourcePath);
249
+ const expectedContainer = TRIPLE_CONTAINER_FORMAT[triple.split("-")[0]];
250
+ if (expectedContainer && container !== expectedContainer) {
251
+ return `${triple}: 产物容器格式不对(期望 ${expectedContainer},实际 ${container})——放错平台的文件或是 stub`;
252
+ }
253
+ log(`${triple}: 非本平台产物,跳过 --version 执行校验(已核对 sha256 与容器格式 ${container})`);
254
+ return null;
255
+ }
256
+
257
+ const probe = withExecutableCopy(sourcePath, probeRenderBinary);
258
+ if (!probe.ran) {
259
+ return `${triple}: 无法执行 ${sourcePath} --version(架构不符 / 动态库缺失 / 文件损坏):${probe.output || "无输出"}`;
260
+ }
261
+ if (probe.isStub) return `${triple}: 产物是 stub(--version 输出 ${JSON.stringify(probe.output)}),拒绝进入分发目录`;
262
+ if (!probe.version) return `${triple}: --version 输出里读不出语义化版本号:${JSON.stringify(probe.output)}`;
263
+ if (probe.version !== expectedVersion) {
264
+ return `${triple}: 产物版本 ${probe.version} 与 manifest latest ${expectedVersion} 不一致`;
265
+ }
266
+ if (protocolVersion !== null && !probe.output.includes(`(protocol ${protocolVersion})`)) {
267
+ return `${triple}: structured 产物协议版本与 manifest 不一致`;
268
+ }
269
+ return null;
270
+ }
271
+
272
+ function writeTextAtomically(filePath, content) {
273
+ const tempPath = `${filePath}.tmp-${process.pid}-${randomBytes(4).toString("hex")}`;
274
+ try {
275
+ writeFileSync(tempPath, content, { mode: 0o644 });
276
+ renameSync(tempPath, filePath);
277
+ } catch (error) {
278
+ try {
279
+ rmSync(tempPath, { force: true });
280
+ } catch {
281
+ // 清理失败不该掩盖真正的错误
282
+ }
283
+ throw error;
284
+ }
285
+ }
286
+
287
+ /**
288
+ * 把单个 triple 的产物 stage 到 dist。写路径用「临时文件 → chmod → rename」:
289
+ * 绝不留下半截的 `wand-render`,也绝不就地改写一个可能正在被执行的二进制。
290
+ * @returns {{ error: string | null, targetPath: string, sha256: string, version: string }}
291
+ */
292
+ function stageTriple({ triple, artifact, sourcePath, expectedVersion, distDir, write, hostTriple,
293
+ binaryName = BINARY_NAME, protocolVersion = null }) {
294
+ const targetPath = path.join(distDir, "native", triple, binaryName);
295
+ const sha256 = sha256File(sourcePath);
296
+ if (!write) return { error: null, targetPath, sha256, version: expectedVersion };
297
+
298
+ mkdirSync(path.dirname(targetPath), { recursive: true, mode: 0o755 });
299
+ const tempPath = `${targetPath}.tmp-${process.pid}-${randomBytes(4).toString("hex")}`;
300
+ try {
301
+ copyFileSync(sourcePath, tempPath);
302
+ // 可执行位必须在 rename 之前设好:先就位再 chmod 会留下一个短暂的不可执行文件。
303
+ chmodSync(tempPath, 0o755);
304
+ // 自检「复制有没有损坏、有没有丢可执行位、是不是同一个文件」。
305
+ // 本平台可以执行二进制(最强),跨平台只能比对内容哈希 + 容器格式 ——
306
+ // 在 Linux runner 上执行 macOS 产物必然失败,那是预期现象,不是损坏。
307
+ const copiedSha = sha256File(tempPath);
308
+ if (copiedSha !== sha256) {
309
+ throw new Error(`就位后的副本 sha256 与源不一致(${copiedSha} != ${sha256})`);
310
+ }
311
+ if (triple === hostTriple) {
312
+ const probe = probeRenderBinary(tempPath);
313
+ if (!probe.ran || probe.isStub || probe.version !== expectedVersion
314
+ || (protocolVersion !== null && !probe.output.includes(`(protocol ${protocolVersion})`))) {
315
+ throw new Error(
316
+ `就位后的副本自检失败(ran=${probe.ran} stub=${probe.isStub} version=${probe.version},期望 ${expectedVersion}):${probe.output || "无输出"}`,
317
+ );
318
+ }
319
+ } else {
320
+ const container = readBinaryContainerFormat(tempPath);
321
+ const expectedContainer = TRIPLE_CONTAINER_FORMAT[triple.split("-")[0]];
322
+ if (expectedContainer && container !== expectedContainer) {
323
+ throw new Error(`就位后的副本容器格式不对(期望 ${expectedContainer},实际 ${container})`);
324
+ }
325
+ }
326
+ renameSync(tempPath, targetPath);
327
+ } catch (error) {
328
+ try {
329
+ rmSync(tempPath, { force: true });
330
+ } catch {
331
+ // 同上
332
+ }
333
+ return { error: `${triple}: ${error instanceof Error ? error.message : String(error)}`, targetPath, sha256, version: expectedVersion };
334
+ }
335
+ writeTextAtomically(path.join(path.dirname(targetPath), `${binaryName}.version`), `${expectedVersion}\n`);
336
+ writeTextAtomically(path.join(path.dirname(targetPath), `${binaryName}.sha256`), `${sha256} ${binaryName}\n`);
337
+ return { error: null, targetPath, sha256, version: expectedVersion };
338
+ }
339
+
340
+ /**
341
+ * 核心实现(除了 dist/native 之外不写任何磁盘;不联网、不读 config、不杀进程)。
342
+ *
343
+ * @param {{
344
+ * check?: boolean, dryRun?: boolean, all?: boolean, strict?: boolean,
345
+ * triples?: string[], packageRoot?: string, renderBinDir?: string, distDir?: string,
346
+ * platform?: string, arch?: string, unameMachine?: string | null, rosettaTranslated?: boolean,
347
+ * logger?: (message: string) => void,
348
+ * }} [options]
349
+ * @returns {{
350
+ * exitCode: number,
351
+ * action: "staged" | "would-stage" | "checked" | "skipped" | "failed",
352
+ * version: string | null,
353
+ * triples: string[],
354
+ * staged: { triple: string, sourcePath: string, targetPath: string, sha256: string, version: string }[],
355
+ * warnings: string[],
356
+ * messages: string[],
357
+ * }}
358
+ */
359
+ export function stageRenderBinaries(options = {}) {
360
+ const messages = [];
361
+ const log = (message) => {
362
+ messages.push(message);
363
+ (options.logger ?? (() => {}))(message);
364
+ };
365
+ const warnings = [];
366
+ const warn = (message) => {
367
+ warnings.push(message);
368
+ log(`warning: ${message}`);
369
+ };
370
+
371
+ const check = options.check === true;
372
+ const dryRun = options.dryRun === true;
373
+ const strict = options.strict === true;
374
+ const all = options.all === true;
375
+ const requested = (options.triples ?? []).filter((triple) => typeof triple === "string" && triple);
376
+ const packageRoot = path.resolve(options.packageRoot ?? PACKAGE_ROOT);
377
+ const renderBinDir = path.resolve(options.renderBinDir ?? path.join(packageRoot, RENDER_BIN_DIR_NAME));
378
+ const distDir = path.resolve(options.distDir ?? path.join(packageRoot, "dist"));
379
+ const manifestPath = path.join(renderBinDir, MANIFEST_FILE_NAME);
380
+ const platform = options.platform ?? process.platform;
381
+ const arch = options.arch ?? process.arch;
382
+ const unameMachine = options.unameMachine !== undefined ? options.unameMachine : runUnameMachine();
383
+ const rosettaTranslated = options.rosettaTranslated ?? isRosettaTranslated();
384
+ const base = { version: null, triples: [], staged: [], warnings, messages };
385
+
386
+ if (all && requested.length > 0) {
387
+ log("error: --all 与 --triple 不能同时使用");
388
+ return { ...base, exitCode: EXIT_FAILED, action: "failed" };
389
+ }
390
+
391
+ const manifest = readManifest(manifestPath);
392
+ if (!manifest.ok) {
393
+ if (manifest.missing && !strict) {
394
+ // 没拉子模块的开发机不该因为缺少发布产物而构建失败。
395
+ warn(`${manifest.error};跳过内嵌平台二进制(本机构建仍可用,只是包里没有 wand-render)`);
396
+ log(`需要内嵌产物时:git submodule update --init ${RENDER_BIN_DIR_NAME}`);
397
+ return { ...base, exitCode: EXIT_OK, action: "skipped" };
398
+ }
399
+ log(`error: ${manifest.error}${manifest.missing ? "(--strict 下视为失败)" : ""}`);
400
+ return { ...base, exitCode: EXIT_FAILED, action: "failed" };
401
+ }
402
+
403
+ const { latest, triples: versionTriples } = manifest.data;
404
+ const available = Object.keys(versionTriples);
405
+ log(`${RENDER_BIN_DIR_NAME} v${latest}(协议 ${JSON.stringify(manifest.data.entry.protocolVersion ?? "?")}),可用平台:${available.join(", ") || "无"}`);
406
+ log(`目标目录:${path.join(distDir, "native")}`);
407
+
408
+ // 宿主平台三元组:只有它才允许执行校验(跨平台 exec 必然失败)。
409
+ const hostTriple = resolvePlatformTriple(platform, arch, unameMachine, rosettaTranslated);
410
+
411
+ let selected;
412
+ if (all) {
413
+ selected = available.slice().sort();
414
+ } else if (requested.length > 0) {
415
+ const unknown = requested.filter((triple) => !available.includes(triple));
416
+ if (unknown.length > 0) {
417
+ log(`error: manifest v${latest} 里没有这些平台:${unknown.join(", ")}`);
418
+ return { ...base, version: latest, exitCode: EXIT_FAILED, action: "failed" };
419
+ }
420
+ selected = requested;
421
+ } else {
422
+ const triple = resolvePlatformTriple(platform, arch, unameMachine, rosettaTranslated);
423
+ if (!triple) {
424
+ warn(`不支持的平台:${platform}-${normalizeMachineArch(unameMachine) ?? arch}(支持 ${SUPPORTED_TRIPLES.join(" / ")})`);
425
+ return { ...base, version: latest, exitCode: strict ? EXIT_FAILED : EXIT_OK, action: strict ? "failed" : "skipped" };
426
+ }
427
+ if (!available.includes(triple)) {
428
+ warn(`manifest v${latest} 里没有 ${triple} 的产物(本平台可能未分发)`);
429
+ return { ...base, version: latest, exitCode: strict ? EXIT_FAILED : EXIT_OK, action: strict ? "failed" : "skipped" };
430
+ }
431
+ selected = [triple];
432
+ }
433
+
434
+ const staged = [];
435
+ const failures = [];
436
+ for (const triple of selected) {
437
+ const artifact = versionTriples[triple] ?? {};
438
+ const sourcePath = path.join(renderBinDir, typeof artifact.path === "string" && artifact.path ? artifact.path : path.join(`v${latest}`, triple, BINARY_NAME));
439
+ const problem = verifyArtifact({ triple, artifact, sourcePath, expectedVersion: latest, hostTriple, log });
440
+ if (problem) {
441
+ failures.push(problem);
442
+ log(`error: ${problem}`);
443
+ continue;
444
+ }
445
+ const sizeLabel = typeof artifact.size === "number" ? `${artifact.size} bytes` : "size 未知";
446
+ log(`${triple}: 校验通过(sha256 ${sha256File(sourcePath).slice(0, 12)}…,${sizeLabel},v${latest})`);
447
+
448
+ const result = stageTriple({ triple, artifact, sourcePath, expectedVersion: latest, distDir, write: !check && !dryRun, hostTriple });
449
+ if (result.error) {
450
+ failures.push(result.error);
451
+ log(`error: ${result.error}`);
452
+ continue;
453
+ }
454
+ staged.push({ triple, sourcePath, targetPath: result.targetPath, sha256: result.sha256, version: result.version });
455
+ let structuredStage = null;
456
+ if (artifact.structured !== undefined) {
457
+ const structured = artifact.structured;
458
+ if (structured?.protocolVersion !== 2 || typeof structured.sha256 !== "string") {
459
+ const message = `${triple}: v2 structured artifact metadata is invalid`;
460
+ failures.push(message);
461
+ log(`error: ${message}`);
462
+ continue;
463
+ }
464
+ const structuredSource = path.join(renderBinDir, typeof structured.path === "string" && structured.path
465
+ ? structured.path : path.join(`v${latest}`, triple, STRUCTURED_BINARY_NAME));
466
+ const structuredProblem = verifyArtifact({ triple, artifact: structured, sourcePath: structuredSource,
467
+ expectedVersion: latest, hostTriple, log, protocolVersion: 2 });
468
+ if (structuredProblem) {
469
+ failures.push(structuredProblem);
470
+ log(`error: ${structuredProblem}`);
471
+ continue;
472
+ }
473
+ const stagedV2 = stageTriple({ triple, artifact: structured, sourcePath: structuredSource,
474
+ expectedVersion: latest, distDir, write: !check && !dryRun, hostTriple,
475
+ binaryName: STRUCTURED_BINARY_NAME, protocolVersion: 2 });
476
+ if (stagedV2.error) {
477
+ failures.push(stagedV2.error);
478
+ log(`error: ${stagedV2.error}`);
479
+ continue;
480
+ }
481
+ structuredStage = { sourcePath: structuredSource, ...stagedV2 };
482
+ staged.push({ triple, sourcePath: structuredSource, targetPath: stagedV2.targetPath,
483
+ sha256: stagedV2.sha256, version: stagedV2.version });
484
+ }
485
+
486
+ if (check) {
487
+ if (!isFile(result.targetPath)) {
488
+ warn(`${result.targetPath} 尚未 stage(构建时由本脚本写入)`);
489
+ if (strict) {
490
+ const message = `${triple}: --check --strict 要求 dist 里已经有产物,但 ${result.targetPath} 不存在`;
491
+ failures.push(message);
492
+ log(`error: ${message}`);
493
+ }
494
+ } else {
495
+ const stagedSha = sha256File(result.targetPath);
496
+ if (stagedSha !== result.sha256) {
497
+ failures.push(`${triple}: 已 stage 的 ${result.targetPath} sha256 与产物不一致(实际 ${stagedSha})`);
498
+ log(`error: ${failures[failures.length - 1]}`);
499
+ } else {
500
+ log(`${triple}: 已 stage 的副本一致(${result.targetPath})`);
501
+ }
502
+ }
503
+ if (structuredStage) {
504
+ if (!isFile(structuredStage.targetPath)) {
505
+ warn(`${structuredStage.targetPath} 尚未 stage`);
506
+ if (strict) failures.push(`${triple}: --check --strict requires the v2 binary`);
507
+ } else if (sha256File(structuredStage.targetPath) !== structuredStage.sha256) {
508
+ failures.push(`${triple}: staged v2 sha256 mismatch`);
509
+ }
510
+ }
511
+ continue;
512
+ }
513
+ if (dryRun) {
514
+ log(`${triple}: 将写入 ${result.targetPath}(mode 0755)与 ${VERSION_FILE_NAME} / ${SHA256_FILE_NAME} sidecar`);
515
+ if (structuredStage) log(`${triple}: 将写入 ${structuredStage.targetPath}(v2 protocol 2)`);
516
+ continue;
517
+ }
518
+ log(`${triple}: 已 stage ${result.targetPath}(mode 0755,v${latest})`);
519
+ if (structuredStage) log(`${triple}: 已 stage ${structuredStage.targetPath}(protocol 2)`);
520
+ }
521
+
522
+ if (failures.length > 0) {
523
+ log(`error: ${failures.length} 个平台校验/就位失败,未写入的产物不会被内嵌`);
524
+ return { ...base, version: latest, triples: selected, staged, exitCode: EXIT_FAILED, action: "failed" };
525
+ }
526
+ const action = check ? "checked" : dryRun ? "would-stage" : "staged";
527
+ return { ...base, version: latest, triples: selected, staged, exitCode: EXIT_OK, action };
528
+ }
529
+
530
+ /**
531
+ * @param {string[]} argv
532
+ * @returns {{
533
+ * check: boolean, dryRun: boolean, all: boolean, strict: boolean, help: boolean,
534
+ * triples: string[], renderBinDir?: string, distDir?: string, error?: string,
535
+ * }}
536
+ */
537
+ export function parseArgs(argv) {
538
+ const options = { check: false, dryRun: false, all: false, strict: false, help: false, triples: [] };
539
+ for (let index = 0; index < argv.length; index += 1) {
540
+ const arg = argv[index];
541
+ const [name, inlineValue] = arg.startsWith("--") && arg.includes("=")
542
+ ? [arg.slice(0, arg.indexOf("=")), arg.slice(arg.indexOf("=") + 1)]
543
+ : [arg, null];
544
+ const takeValue = () => {
545
+ if (inlineValue !== null) return inlineValue;
546
+ index += 1;
547
+ return argv[index];
548
+ };
549
+ if (name === "--check") options.check = true;
550
+ else if (name === "--dry-run") options.dryRun = true;
551
+ else if (name === "--all") options.all = true;
552
+ else if (name === "--strict") options.strict = true;
553
+ else if (name === "--help" || name === "-h") options.help = true;
554
+ else if (name === "--triple") {
555
+ const value = takeValue();
556
+ if (!value) return { ...options, error: "--triple 需要一个三元组名(如 darwin-arm64)" };
557
+ options.triples.push(value);
558
+ } else if (name === "--render-bin") {
559
+ const value = takeValue();
560
+ if (!value) return { ...options, error: "--render-bin 需要一个目录" };
561
+ options.renderBinDir = value;
562
+ } else if (name === "--dist") {
563
+ const value = takeValue();
564
+ if (!value) return { ...options, error: "--dist 需要一个目录" };
565
+ options.distDir = value;
566
+ } else {
567
+ return { ...options, error: `未知参数 ${arg}` };
568
+ }
569
+ }
570
+ return options;
571
+ }
572
+
573
+ const USAGE = `用法:node scripts/stage-render-binaries.js [选项]
574
+
575
+ 把 ${RENDER_BIN_DIR_NAME}/manifest.json 里 pin 住的 wand-render 产物 stage 到 dist/native/<triple>/,
576
+ 供 npm 包内嵌(dist/ 在 package.json 的 files 里)。
577
+
578
+ 选项:
579
+ --triple <name> 只 stage 指定三元组(可重复;默认按当前平台,含 Rosetta / uname 判定)
580
+ --all stage manifest 列出的所有平台
581
+ --check 只校验(sha256 / 可执行 / 版本 / stub;并比对已 stage 的副本),不写磁盘
582
+ --dry-run 只报告将写入什么,不写磁盘(仍做全部校验)
583
+ --strict ${RENDER_BIN_DIR_NAME} 子模块未初始化 / 本平台无产物 / --check 时 dist 里没有已 stage 的产物 → 视为失败(CI 发布链路用)
584
+ --render-bin <dir> 指定 ${RENDER_BIN_DIR_NAME} 目录(默认 <pkg>/${RENDER_BIN_DIR_NAME})
585
+ --dist <dir> 指定 dist 目录(默认 <pkg>/dist)
586
+ -h, --help 显示本帮助
587
+
588
+ 退出码:
589
+ 0 已 stage / 可 stage / --check 校验通过(子模块未初始化时也是 0:警告并跳过)
590
+ 1 校验失败(sha256 不符 / 版本不符 / stub / 无法执行)、参数错误、--strict 下的缺产物
591
+
592
+ 双重校验说明:sha256 是唯一「产物是否被改动」的判据(manifest 是 pin);--version 自检
593
+ 是唯一「这个二进制真能跑、且不是 stub」的判据。两者都不通过就不写进 dist。`;
594
+
595
+ /**
596
+ * @param {string[]} argv
597
+ * @returns {number} 退出码
598
+ */
599
+ export function main(argv) {
600
+ const options = parseArgs(argv);
601
+ if (options.help) {
602
+ console.log(USAGE);
603
+ return EXIT_OK;
604
+ }
605
+ if (options.error) {
606
+ console.error(`error: ${options.error}`);
607
+ console.error(USAGE);
608
+ return EXIT_FAILED;
609
+ }
610
+ const result = stageRenderBinaries({
611
+ check: options.check,
612
+ dryRun: options.dryRun,
613
+ all: options.all,
614
+ strict: options.strict,
615
+ triples: options.triples,
616
+ renderBinDir: options.renderBinDir,
617
+ distDir: options.distDir,
618
+ logger: (message) => console.log(`[wand-render] ${message}`),
619
+ });
620
+ return result.exitCode;
621
+ }
622
+
623
+ // 只有被直接执行时才跑 main;被 import(测试 / Server 自检)时只暴露函数。
624
+ if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url))) {
625
+ process.exitCode = main(process.argv.slice(2));
626
+ }