dsh-wechat-remote 1.7.4 → 1.7.6

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.
@@ -1,689 +1,740 @@
1
- /* Generated from the shared plugin installation sources. */
2
-
3
- // src/update-worker.ts
4
- import fs3 from "node:fs";
5
- import path3 from "node:path";
6
- import http from "node:http";
7
- import { spawn as spawn3 } from "node:child_process";
8
- import { createHash as createHash2, createPublicKey } from "node:crypto";
9
- import { fileURLToPath } from "node:url";
10
-
11
- // src/secure-file.ts
12
- import { execFileSync } from "node:child_process";
13
- import {
14
- chmodSync,
15
- closeSync,
16
- existsSync,
17
- fsyncSync,
18
- mkdirSync,
19
- openSync,
20
- readFileSync,
21
- renameSync,
22
- rmSync,
23
- writeFileSync
24
- } from "node:fs";
25
- import { dirname } from "node:path";
26
- import { userInfo } from "node:os";
27
- import { randomBytes } from "node:crypto";
28
- function tightenPrivateFile(file) {
29
- try {
30
- chmodSync(file, 384);
31
- } catch {
32
- }
33
- if (process.platform !== "win32") return;
34
- try {
35
- execFileSync("icacls", [file, "/inheritance:r", "/grant:r", `${userInfo().username}:F`], {
36
- timeout: 5e3,
37
- windowsHide: true,
38
- stdio: "ignore"
39
- });
40
- } catch {
41
- }
42
- }
43
- function writePrivateJsonAtomic(file, value) {
44
- const parent = dirname(file);
45
- mkdirSync(parent, { recursive: true, mode: 448 });
46
- const temporary = `${file}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
47
- try {
48
- writeFileSync(temporary, `${JSON.stringify(value, null, 2)}
49
- `, {
50
- encoding: "utf8",
51
- mode: 384,
52
- flag: "wx"
53
- });
54
- const fd = openSync(temporary, "r+");
55
- try {
56
- try {
57
- fsyncSync(fd);
58
- } catch {
59
- }
60
- } finally {
61
- closeSync(fd);
62
- }
63
- tightenPrivateFile(temporary);
64
- renameSync(temporary, file);
65
- tightenPrivateFile(file);
66
- } finally {
67
- if (existsSync(temporary)) {
68
- try {
69
- rmSync(temporary, { force: true });
70
- } catch {
71
- }
72
- }
73
- }
74
- }
75
-
76
- // src/install-profile.ts
77
- import fs from "node:fs";
78
- import path from "node:path";
79
- import { spawn } from "node:child_process";
80
- import { createHash } from "node:crypto";
81
-
82
- // src/install-runtime.ts
83
- var INSTALL_PNPM_VERSION = "11.22.0";
84
-
85
- // src/install-profile.ts
86
- var PLUGIN_PACKAGE = "@harness-remote/dsh-wechat-remote";
87
- function safeProfileName(value) {
88
- return /^[A-Za-z0-9_-]{1,80}$/.test(value);
89
- }
90
- var hash = (file) => createHash("sha256").update(fs.readFileSync(file)).digest("hex");
91
- function assertRelocatableProfile(root) {
92
- const walk = (directory) => {
93
- for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
94
- const file = path.join(directory, entry.name);
95
- if (entry.isSymbolicLink()) {
96
- const relative = path.relative(root, fs.realpathSync(file));
97
- if (path.isAbsolute(fs.readlinkSync(file)) || !relative || relative.startsWith("..") || path.isAbsolute(relative)) {
98
- throw new Error("\u63D2\u4EF6\u4F9D\u8D56\u5305\u542B\u4E0D\u53EF\u8FC1\u79FB\u7684\u94FE\u63A5\uFF0C\u672A\u4FEE\u6539\u5F53\u524D\u5B89\u88C5\u3002");
99
- }
100
- } else if (entry.isDirectory()) walk(file);
101
- }
102
- };
103
- walk(root);
104
- }
105
- function installToolPath(directory, runtime) {
106
- const bin = path.join(directory, "tool-bin");
107
- fs.mkdirSync(bin, { mode: 448 });
108
- const runner = path.join(bin, "pnpm-run.cjs");
109
- fs.writeFileSync(runner, `require('node:child_process').spawnSync(${JSON.stringify(runtime.executable)},[${JSON.stringify(runtime.cli)},...process.argv.slice(2)],{stdio:'inherit',shell:false,windowsHide:true}).status===0?process.exit(0):process.exit(1)
110
- `, { mode: 384 });
111
- if (process.platform === "win32") {
112
- if (/["\r\n]/.test(runtime.executable)) throw new Error("Node \u5B89\u88C5\u8DEF\u5F84\u4E0D\u53D7\u652F\u6301\u3002");
113
- fs.writeFileSync(path.join(bin, "pnpm.cmd"), `@echo off\r
114
- "${runtime.executable.replace(/%/g, "%%")}" "%~dp0pnpm-run.cjs" %*\r
115
- `, { mode: 448 });
116
- } else {
117
- const quote = (s) => "'" + s.replace(/'/g, `'"'"'`) + "'";
118
- fs.writeFileSync(path.join(bin, "pnpm"), `#!/bin/sh
119
- exec ${quote(runtime.executable)} ${quote(runner)} "$@"
120
- `, { mode: 448 });
121
- }
122
- return bin;
123
- }
124
- function runNativePlugin(cli, profile, home, toolPath, runtime, logFile) {
125
- if (!safeProfileName(profile)) throw new Error("\u65E0\u6548\u7684 DSH profile \u540D\u79F0\u3002");
126
- return new Promise((resolve, reject) => {
127
- const log = fs.openSync(logFile, "a", 384);
128
- const child = spawn(runtime.executable, [
129
- cli,
130
- "plugin",
131
- "--profile",
132
- profile,
133
- "add",
134
- "file:harness-remote-update.tgz",
135
- "--ignore-scripts",
136
- "--config.frozen-lockfile=false",
137
- "--prefer-offline",
138
- "--config.manage-package-manager-versions=false",
139
- "--reporter=append-only"
140
- ], {
141
- cwd: home,
142
- shell: false,
143
- windowsHide: true,
144
- stdio: ["ignore", log, log],
145
- env: {
146
- ...process.env,
147
- DSH_HOME: home,
148
- PATH: toolPath + path.delimiter + (process.env.PATH || ""),
149
- CI: "true",
150
- COREPACK_ENABLE_AUTO_PIN: "0",
151
- npm_config_manage_package_manager_versions: "false"
152
- }
153
- });
154
- fs.closeSync(log);
155
- const timer = setTimeout(() => {
156
- child.kill();
157
- reject(new Error("\u4E0B\u8F7D\u6216\u5B89\u88C5\u8D85\u65F6\uFF0C\u539F\u63D2\u4EF6\u672A\u66FF\u6362\u3002"));
158
- }, 24e4);
159
- child.once("error", () => {
160
- clearTimeout(timer);
161
- reject(new Error("\u65E0\u6CD5\u542F\u52A8 DSH \u539F\u751F\u5B89\u88C5\u7A0B\u5E8F\u3002"));
162
- });
163
- child.once("close", (code) => {
164
- clearTimeout(timer);
165
- code === 0 ? resolve() : reject(new Error("\u5B89\u88C5\u672A\u5B8C\u6210\uFF0C\u539F\u63D2\u4EF6\u672A\u66FF\u6362\uFF1B\u8BE6\u60C5\u5DF2\u4FDD\u7559\u5728\u672C\u673A\u5B89\u88C5\u65E5\u5FD7\u3002"));
166
- });
167
- });
168
- }
169
- async function stageProfile(job) {
170
- const scope = path.basename(job.profile);
171
- if (!safeProfileName(scope) || !fs.statSync(job.directory).isDirectory()) throw new Error("\u5B89\u88C5\u76EE\u6807\u4E0D\u660E\u786E\u3002");
172
- const stagingHome = path.join(job.directory, "staging-home"), staged = path.join(stagingHome, "profiles", scope);
173
- fs.mkdirSync(staged, { recursive: true, mode: 448 });
174
- if (fs.existsSync(job.profile)) {
175
- if (fs.lstatSync(job.profile).isSymbolicLink() || fs.realpathSync(job.profile) !== path.resolve(job.profile)) throw new Error("\u4E0D\u652F\u6301\u81EA\u52A8\u66FF\u6362\u94FE\u63A5\u5F62\u5F0F\u7684 profile\u3002");
176
- fs.cpSync(job.profile, staged, {
177
- recursive: true,
178
- dereference: false,
179
- filter: (p) => !["node_modules", ".harness-remote-update.lock"].includes(path.basename(p))
180
- });
181
- }
182
- let before = null;
183
- const filename = path.join(staged, "package.json");
184
- if (fs.existsSync(filename)) {
185
- before = JSON.parse(fs.readFileSync(filename, "utf8"));
186
- for (const [name, spec] of Object.entries(before.dependencies || {})) {
187
- if (name !== PLUGIN_PACKAGE && !/^[~^]?\d+\.\d+\.\d+(?:-[\w.-]+)?$/.test(String(spec))) throw new Error("\u5176\u4ED6\u63D2\u4EF6\u4F7F\u7528\u4E86\u672C\u5730\u94FE\u63A5\u6216\u7279\u6B8A\u6765\u6E90\uFF0C\u672A\u4FEE\u6539\u5F53\u524D\u5B89\u88C5\u3002");
188
- }
189
- for (const e of fs.readdirSync(staged, { withFileTypes: true })) if (e.isSymbolicLink()) throw new Error("profile \u914D\u7F6E\u5305\u542B\u94FE\u63A5\uFF0C\u672A\u4FEE\u6539\u5F53\u524D\u5B89\u88C5\u3002");
190
- if (before.packageManager && !/^pnpm@\d+\.\d+\.\d+(?:\+.*)?$/.test(before.packageManager)) throw new Error("\u5F53\u524D profile \u4F7F\u7528\u4E86\u5176\u4ED6\u5305\u7BA1\u7406\u5668\uFF0C\u672A\u4FEE\u6539\u5B89\u88C5\u3002");
191
- writePrivateJsonAtomic(filename, { ...before, dependencies: {
192
- ...before.dependencies,
193
- [PLUGIN_PACKAGE]: "file:harness-remote-update.tgz"
194
- }, packageManager: `pnpm@${INSTALL_PNPM_VERSION}` });
195
- }
196
- fs.copyFileSync(path.join(job.directory, "release.tgz"), path.join(staged, "harness-remote-update.tgz"));
197
- const tools = installToolPath(job.directory, job.runtime);
198
- await runNativePlugin(job.cli, scope, stagingHome, tools, job.runtime, path.join(job.directory, "install.log"));
199
- const installed = fs.realpathSync(path.join(staged, "node_modules", PLUGIN_PACKAGE));
200
- const relative = path.relative(staged, installed);
201
- if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) throw new Error("\u6682\u5B58\u63D2\u4EF6\u4E0D\u5728\u5B89\u88C5\u76EE\u5F55\u5185\u3002");
202
- if (JSON.parse(fs.readFileSync(path.join(installed, "package.json"), "utf8")).version !== job.targetVersion) throw new Error("\u5B89\u88C5\u540E\u63D2\u4EF6\u7248\u672C\u4E0D\u5339\u914D\u3002");
203
- const after = JSON.parse(fs.readFileSync(filename, "utf8"));
204
- if (!after.dsh?.profile?.bundles?.includes(PLUGIN_PACKAGE)) throw new Error("DSH \u5C1A\u672A\u5C06\u63D2\u4EF6\u6CE8\u518C\u4E3A\u539F\u751F profile \u5C42\u3002");
205
- for (const name of Object.keys(before?.dependencies || {})) if (name !== PLUGIN_PACKAGE) {
206
- if (hash(path.join(job.profile, "node_modules", name, "package.json")) !== hash(path.join(staged, "node_modules", name, "package.json"))) throw new Error("\u5B89\u88C5\u8BD5\u56FE\u6539\u53D8\u5176\u4ED6\u63D2\u4EF6\uFF0C\u539F\u5B89\u88C5\u4FDD\u6301\u4E0D\u53D8\u3002");
207
- }
208
- assertRelocatableProfile(staged);
209
- return staged;
210
- }
211
-
212
- // src/install-lifecycle.ts
213
- import fs2 from "node:fs";
214
- import path2 from "node:path";
215
- import { execFileSync as execFileSync2, spawn as spawn2 } from "node:child_process";
216
- var serviceName = (s) => /^[A-Za-z0-9_.@-]{1,180}$/.test(s);
217
- function run(command, args) {
218
- return execFileSync2(command, args, { encoding: "utf8", windowsHide: true, timeout: 15e3, maxBuffer: 1024 * 1024, stdio: ["ignore", "pipe", "pipe"] }).trim();
219
- }
220
- function validateManager(value) {
221
- if (value.kind === "process") return;
222
- if (value.kind === "systemd" && process.platform === "linux" && serviceName(value.unit) && value.unit.endsWith(".service")) return;
223
- if (value.kind === "launchd" && process.platform === "darwin" && serviceName(value.label) && /^gui\/\d+$/.test(value.domain) && value.domain === `gui/${process.getuid?.()}` && path2.isAbsolute(value.plist) && fs2.statSync(value.plist).isFile()) return;
224
- throw new Error("\u65E0\u6CD5\u786E\u8BA4\u539F\u540E\u53F0\u670D\u52A1\uFF0C\u672A\u505C\u6B62 DSH\u3002");
225
- }
226
- function stopManagedHost(manager) {
227
- validateManager(manager);
228
- if (manager.kind === "systemd") run("systemctl", ["--user", "stop", manager.unit]);
229
- else if (manager.kind === "launchd") run("/bin/launchctl", ["bootout", `${manager.domain}/${manager.label}`]);
230
- else throw new Error("\u666E\u901A DSH \u8FDB\u7A0B\u5FC5\u987B\u7531\u5176\u542F\u52A8\u63E1\u624B\u505C\u6B62\u3002");
231
- }
232
- function startManagedHost(manager) {
233
- validateManager(manager);
234
- if (manager.kind === "systemd") run("systemctl", ["--user", "start", manager.unit]);
235
- else if (manager.kind === "launchd") run("/bin/launchctl", ["bootstrap", manager.domain, manager.plist]);
236
- else throw new Error("\u666E\u901A DSH \u8FDB\u7A0B\u5FC5\u987B\u7531\u539F\u73AF\u5883\u542F\u52A8\u3002");
237
- }
238
- function finishUpdateWorker(manager, directory) {
239
- const id = path2.basename(directory);
240
- if (manager?.kind !== "launchd" || !/^[a-f0-9]{32}$/.test(id)) return;
241
- try {
242
- run("/bin/launchctl", ["remove", `dsh.wechat.update.${id}`]);
243
- } catch {
244
- }
245
- }
246
-
247
- // src/update-worker.ts
248
- function releaseOwnedUpdateLock(lock, id) {
249
- try {
250
- if (fs3.readFileSync(lock, "utf8") === id) fs3.unlinkSync(lock);
251
- } catch {
252
- }
253
- }
254
- var wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
255
- var hashFile = (f) => createHash2("sha256").update(fs3.readFileSync(f)).digest("hex");
256
- function within(parent, child) {
257
- const relative = path3.relative(parent, child);
258
- return Boolean(relative) && !relative.startsWith("..") && !path3.isAbsolute(relative);
259
- }
260
- function safePlainDirectory(p) {
261
- if (!fs3.statSync(p).isDirectory() || fs3.lstatSync(p).isSymbolicLink() || fs3.realpathSync(p) !== path3.resolve(p)) throw new Error("\u5B89\u88C5\u76EE\u5F55\u4E0D\u662F\u53EF\u5B89\u5168\u66FF\u6362\u7684\u72EC\u7ACB\u76EE\u5F55");
262
- }
263
- function validateJob(job) {
264
- if (!/^[a-f0-9]{32}$/.test(job.id) || path3.basename(job.directory) !== job.id || !within(path3.join(job.home, "harness-remote-updates"), job.directory) || !within(path3.join(job.home, "profiles"), job.profile) || path3.dirname(job.profile) !== path3.join(job.home, "profiles") || !within(job.home, job.stateFile) || !Number.isInteger(job.parentPid) || job.parentPid < 1 || ![job.webPort, job.gatePort, job.localPort].every((p) => Number.isInteger(p) && p > 0 && p <= 65535) || !job.argv.includes(job.cli) || !job.argv.includes("web") || !/^[\w.+-]{1,80}$/.test(job.targetVersion)) throw new Error("\u66F4\u65B0\u4EFB\u52A1\u8303\u56F4\u6821\u9A8C\u5931\u8D25");
265
- safePlainDirectory(job.profile);
266
- safePlainDirectory(job.directory);
267
- if (job.identityFile && !within(job.home, job.identityFile)) throw new Error("\u8282\u70B9\u8EAB\u4EFD\u6587\u4EF6\u4E0D\u5C5E\u4E8E\u5F53\u524D DSH");
268
- if (job.controlOrigin) {
269
- const u = new URL(job.controlOrigin);
270
- if (u.protocol !== "http:" || u.hostname !== "127.0.0.1" || !u.port || u.username || u.password || u.pathname !== "/" || u.search || u.hash) throw new Error("\u5B89\u88C5\u63A7\u5236\u5730\u5740\u65E0\u6548");
271
- validateManager(job.manager);
272
- }
273
- for (const f of [job.executable, job.cli, job.pnpm, ...job.previousVersion === "0.0.0" ? [] : [job.stateFile]]) if (!fs3.statSync(f).isFile()) throw new Error("\u5B89\u88C5\u8FD0\u884C\u65F6\u5DF2\u53D8\u5316");
274
- }
275
- async function control(job, operation, input = {}) {
276
- let res;
277
- for (let attempt = 0; attempt < (operation === "describe" ? 3 : 1); attempt++) {
278
- try {
279
- res = await fetch(job.controlOrigin + "/" + operation, { method: "POST", headers: {
280
- authorization: `Bearer ${job.statusToken}`,
281
- "content-type": "application/json",
282
- connection: "close"
283
- }, body: JSON.stringify(input), signal: AbortSignal.timeout(operation === "quiesce" ? 2e4 : 12e3), redirect: "error" });
284
- break;
285
- } catch (error) {
286
- if (operation !== "describe" || attempt === 2) throw new Error(`\u5B89\u88C5\u63A7\u5236 ${operation} \u672A\u5B8C\u6210`, { cause: error });
287
- await wait(150);
288
- }
289
- }
290
- if (!res) throw new Error("\u5B89\u88C5\u63A7\u5236\u901A\u9053\u672A\u54CD\u5E94");
291
- const value = await res.json();
292
- if (!res.ok) throw new Error(value.error || "\u65E0\u6CD5\u786E\u8BA4\u5F53\u524D\u4E3B\u673A\u72B6\u6001");
293
- return value;
294
- }
295
- async function beforeRpc(job, method, payload = {}) {
296
- return job.controlOrigin ? control(job, "read", { method, payload }) : rpc(job, method, payload);
297
- }
298
- async function rpc(job, method, payload = {}, deadline) {
299
- const state = JSON.parse(fs3.readFileSync(job.stateFile, "utf8"));
300
- const res = await fetch(`http://127.0.0.1:${job.gatePort}/api/${method}`, {
301
- method: "POST",
302
- headers: { authorization: `Bearer ${state.token}`, "content-type": "application/json", "x-harness-update-probe": job.statusToken },
303
- body: JSON.stringify({ type: "client-request", rpcId: "plugin-update-check", method, payload }),
304
- signal: deadline ? AbortSignal.any([deadline, AbortSignal.timeout(5e3)]) : AbortSignal.timeout(5e3),
305
- redirect: "error"
306
- });
307
- const body = await res.json();
308
- if (!res.ok || !body.result?.ok) throw new Error("\u4E3B\u673A\u4F1A\u8BDD\u670D\u52A1\u672A\u5C31\u7EEA");
309
- return body.result.value;
310
- }
311
- function durableSnapshot(job) {
312
- const result = {};
313
- const walk = (dir) => {
314
- if (!fs3.existsSync(dir)) return;
315
- for (const e of fs3.readdirSync(dir, { withFileTypes: true })) {
316
- const file = path3.join(dir, e.name);
317
- if (e.isSymbolicLink()) throw new Error("\u53D7\u4FDD\u62A4\u7684\u6570\u636E\u76EE\u5F55\u542B\u94FE\u63A5\uFF0C\u9700\u624B\u5DE5\u66F4\u65B0");
318
- if (e.isDirectory()) walk(file);
319
- else if (e.isFile()) result[path3.relative(job.home, file)] = hashFile(file);
320
- }
321
- };
322
- walk(path3.join(job.home, "sessions"));
323
- walk(path3.join(job.home, "attachments"));
324
- walk(path3.join(job.home, "harness-remote"));
325
- for (const e of fs3.readdirSync(job.home, { withFileTypes: true })) {
326
- if (e.isFile() && /(?:identity|settings|credentials|public|gate-wechat)/i.test(e.name) && !e.name.endsWith(".log")) {
327
- const f = path3.join(job.home, e.name);
328
- if (f === job.stateFile) continue;
329
- result[e.name] = hashFile(f);
330
- }
331
- }
332
- if (fs3.existsSync(job.stateFile)) {
333
- const state = JSON.parse(fs3.readFileSync(job.stateFile, "utf8"));
334
- result["$binding"] = createHash2("sha256").update(JSON.stringify([state.token, state.wechatBindings])).digest("hex");
335
- }
336
- return result;
337
- }
338
- function assertPreserved(before, after) {
339
- for (const [key, hash2] of Object.entries(before)) if (after[key] !== hash2) throw new Error("\u5347\u7EA7\u540E\u6570\u636E\u6821\u9A8C\u4E0D\u4E00\u81F4\uFF1B\u505C\u6B62\u81EA\u52A8\u64CD\u4F5C\u5E76\u4FDD\u7559\u5907\u4EFD");
340
- }
341
- function migrateLegacyGrantOwner(job) {
342
- if (job.previousVersion !== "1.5.5" || !job.identityFile || !fs3.existsSync(job.identityFile)) return;
343
- const state = JSON.parse(fs3.readFileSync(job.stateFile, "utf8"));
344
- if (state.publicIdentityNodeId) return;
345
- const identity = JSON.parse(fs3.readFileSync(job.identityFile, "utf8"));
346
- const publicKey = createPublicKey(identity.privateKeyPem).export({ format: "der", type: "spki" });
347
- const savedKey = createPublicKey(identity.publicKeyPem).export({ format: "der", type: "spki" });
348
- const nodeId = createHash2("sha256").update(publicKey).digest().subarray(0, 18).toString("base64url");
349
- if (!publicKey.equals(savedKey) || identity.nodeId !== nodeId) throw new Error("\u65E7\u8282\u70B9\u8EAB\u4EFD\u6821\u9A8C\u672A\u901A\u8FC7\uFF0C\u672A\u8FC1\u79FB\u914D\u5BF9");
350
- writePrivateJsonAtomic(job.stateFile, { ...state, publicIdentityNodeId: nodeId });
351
- }
352
- async function describe(job, deadline) {
353
- const value = await rpc(job, "wechatHost/describe", { args: { request: {} } }, deadline);
354
- if (!value?.ok || value.value.agentVersion !== job.dshVersion) throw new Error("DSH \u7248\u672C\u6216\u63CF\u8FF0\u670D\u52A1\u4E0D\u5339\u914D");
355
- return value.value;
356
- }
357
- async function healthy(job, version, timeoutMs = 18e4) {
358
- const deadline = AbortSignal.timeout(timeoutMs);
359
- while (!deadline.aborted) {
360
- try {
361
- if ((await describe(job, deadline)).pluginVersion === version) {
362
- const list = await rpc(job, "session.list", {}, deadline);
363
- if (Array.isArray(list.items)) return;
364
- }
365
- } catch {
366
- }
367
- if (!deadline.aborted) await wait(Math.min(500, timeoutMs));
368
- }
369
- throw new Error("\u91CD\u542F\u5065\u5EB7\u68C0\u67E5\u672A\u901A\u8FC7");
370
- }
371
- async function verifyFence(job) {
372
- for (const port of [job.webPort, job.gatePort, job.localPort]) {
373
- const res = await fetch(`http://127.0.0.1:${port}/gate/status`, { signal: AbortSignal.timeout(5e3), redirect: "error" });
374
- await res.arrayBuffer();
375
- if (res.status !== 503) throw new Error("\u65B0\u63D2\u4EF6\u672A\u4FDD\u6301\u91CD\u542F\u9A8C\u8BC1\u4FDD\u62A4\uFF0C\u4E0D\u80FD\u786E\u8BA4\u5B89\u5168\u66F4\u65B0");
376
- }
377
- }
378
- function start(job) {
379
- if (job.manager && job.manager.kind !== "process") {
380
- startManagedHost(job.manager);
381
- return;
382
- }
383
- const log = fs3.openSync(path3.join(job.directory, "restart.log"), "a", 384);
384
- const child = spawn3(job.executable, [...job.execArgv, ...job.argv], {
385
- cwd: job.cwd,
386
- env: { ...process.env, HARNESS_REMOTE_UPDATE_JOB: job.directory },
387
- stdio: ["ignore", log, log],
388
- detached: true,
389
- windowsHide: true
390
- });
391
- fs3.closeSync(log);
392
- child.on("error", () => {
393
- });
394
- child.unref();
395
- if (!child.pid) throw new Error("\u65E0\u6CD5\u542F\u52A8\u539F DSH \u547D\u4EE4");
396
- writePrivateJsonAtomic(path3.join(job.directory, "restarted-process.json"), { pid: child.pid, cli: job.cli, home: job.home, webPort: job.webPort });
397
- return child;
398
- }
399
- async function stopOriginal(job) {
400
- if (!job.controlOrigin) {
401
- if (process.connected !== true || process.ppid !== job.parentPid) throw new Error("\u542F\u52A8\u8EAB\u4EFD\u5DF2\u53D8\u5316\uFF0C\u672A\u505C\u6B62 DSH");
402
- await stopChild(job.parentPid);
403
- return;
404
- }
405
- const host = await control(job, "describe");
406
- if (host.pid !== job.parentPid || host.cli !== job.cli || host.profile !== job.profile) throw new Error("\u5F53\u524D DSH \u8EAB\u4EFD\u5DF2\u53D8\u5316");
407
- if (job.manager && job.manager.kind !== "process") stopManagedHost(job.manager);
408
- else await control(job, "shutdown");
409
- for (let i = 0; i < 150; i++) {
410
- try {
411
- process.kill(job.parentPid, 0);
412
- } catch {
413
- return;
414
- }
415
- await wait(100);
416
- }
417
- throw new Error("\u539F DSH \u5C1A\u672A\u7ED3\u675F\uFF0C\u672A\u66FF\u6362\u63D2\u4EF6");
418
- }
419
- async function stopRestarted(child, timeoutMs = 3e4) {
420
- if (child.exitCode !== null || child.signalCode !== null) return;
421
- child.kill("SIGTERM");
422
- const deadline = Date.now() + timeoutMs;
423
- while (Date.now() < deadline) {
424
- if (child.exitCode !== null || child.signalCode !== null) return;
425
- await wait(Math.min(100, Math.max(1, deadline - Date.now())));
426
- }
427
- if (child.exitCode !== null || child.signalCode !== null) return;
428
- throw new Error("\u66F4\u65B0\u540E\u7684 DSH \u672A\u6309\u65F6\u505C\u6B62");
429
- }
430
- async function stopChild(pid) {
431
- process.kill(pid, "SIGTERM");
432
- for (let i = 0; i < 100; i++) {
433
- try {
434
- process.kill(pid, 0);
435
- } catch {
436
- return;
437
- }
438
- await wait(100);
439
- }
440
- throw new Error("DSH \u672A\u6309\u65F6\u505C\u6B62\uFF0C\u4E0D\u5F3A\u5236\u7EC8\u6B62\u5176\u4ED6\u8FDB\u7A0B");
441
- }
442
- async function executeUpdate(job, progress, quiesce) {
443
- validateJob(job);
444
- let staged = "";
445
- const previous = path3.join(job.directory, "profile-before");
446
- const emit = (phase, n, message) => progress({ phase, progress: n, message, terminal: false });
447
- let stopped = false, disposed = false, swapped = false, newChild;
448
- let before = {}, sessionIds = [], readableIds = [];
449
- try {
450
- emit("staging", 25, "\u6682\u5B58\u66F4\u65B0\u4E0E\u4F9D\u8D56\uFF0C\u5F53\u524D\u8282\u70B9\u4ECD\u53EF\u4F7F\u7528");
451
- staged = await stageProfile({
452
- profile: job.profile,
453
- directory: job.directory,
454
- cli: job.cli,
455
- targetVersion: job.targetVersion,
456
- runtime: { executable: job.executable, cli: job.pnpm, version: INSTALL_PNPM_VERSION }
457
- });
458
- emit("checking", 50, "\u786E\u8BA4\u4F1A\u8BDD\u7A7A\u95F2\u5E76\u4FDD\u5B58\u72B6\u6001");
459
- const old = job.controlOrigin ? await control(job, "describe") : await describe(job);
460
- if (old.pluginVersion !== job.previousVersion) throw new Error("\u5F53\u524D\u63D2\u4EF6\u5728\u68C0\u67E5\u540E\u53D1\u751F\u53D8\u5316");
461
- const list = (await beforeRpc(job, "session.list")).items;
462
- if (!Array.isArray(list) || list.some((s) => s.running !== false)) throw new Error("\u8BF7\u7B49\u5F85\u5168\u90E8\u4F1A\u8BDD\u7ED3\u675F\u540E\u518D\u66F4\u65B0");
463
- sessionIds = list.map((s) => s.sessionId).sort();
464
- for (const sessionId of sessionIds) {
465
- try {
466
- await beforeRpc(job, "session.history", { sessionId, maxMessages: 1 });
467
- readableIds.push(sessionId);
468
- } catch {
469
- }
470
- }
471
- await quiesce();
472
- disposed = Boolean(job.controlOrigin);
473
- before = durableSnapshot(job);
474
- writePrivateJsonAtomic(path3.join(job.directory, "before-hashes.json"), before);
475
- emit("backup", 60, "\u5907\u4EFD\u914D\u7F6E\u4E0E\u6570\u636E");
476
- const backup = path3.join(job.directory, "home-before");
477
- fs3.mkdirSync(backup, { mode: 448 });
478
- for (const entry of fs3.readdirSync(job.home, { withFileTypes: true })) {
479
- if (["harness-remote-updates", "profiles"].includes(entry.name)) continue;
480
- if (entry.isSymbolicLink()) throw new Error("\u6570\u636E\u76EE\u5F55\u5305\u542B\u5916\u90E8\u94FE\u63A5\uFF0C\u8BF7\u624B\u5DE5\u5907\u4EFD\u540E\u66F4\u65B0");
481
- fs3.cpSync(path3.join(job.home, entry.name), path3.join(backup, entry.name), { recursive: true });
482
- }
483
- emit("restarting", 70, "\u6B63\u5728\u91CD\u542F\u5F53\u524D DSH\uFF0C\u8FDE\u63A5\u4F1A\u6682\u65F6\u65AD\u5F00");
484
- await stopOriginal(job);
485
- stopped = true;
486
- assertPreserved(before, durableSnapshot(job));
487
- migrateLegacyGrantOwner(job);
488
- safePlainDirectory(job.profile);
489
- fs3.renameSync(job.profile, previous);
490
- try {
491
- fs3.renameSync(staged, job.profile);
492
- swapped = true;
493
- } catch (error) {
494
- fs3.renameSync(previous, job.profile);
495
- throw error;
496
- }
497
- newChild = start(job);
498
- emit("verifying", 85, "\u68C0\u67E5\u63D2\u4EF6\u7248\u672C\u3001\u8282\u70B9\u8EAB\u4EFD\u548C\u4F1A\u8BDD");
499
- await healthy(job, job.targetVersion);
500
- await verifyFence(job);
501
- assertPreserved(before, durableSnapshot(job));
502
- const after = (await rpc(job, "session.list")).items.map((s) => s.sessionId).sort();
503
- if (JSON.stringify(after) !== JSON.stringify(sessionIds)) throw new Error("\u91CD\u542F\u540E\u4F1A\u8BDD\u5217\u8868\u4E0D\u4E00\u81F4");
504
- for (const id of readableIds) await rpc(job, "session.history", { sessionId: id, maxMessages: 1 });
505
- writePrivateJsonAtomic(path3.join(job.directory, "verification-complete.json"), { id: job.id });
506
- return { phase: "complete", progress: 100, message: "\u63D2\u4EF6\u66F4\u65B0\u5B8C\u6210\uFF0CDSH \u5DF2\u6062\u590D\uFF1B\u539F\u8282\u70B9\u65E0\u9700\u91CD\u65B0\u914D\u5BF9\u3002", terminal: true, ok: true };
507
- } catch (error) {
508
- let rollback = false;
509
- writePrivateJsonAtomic(path3.join(job.directory, "failure.json"), {
510
- message: error instanceof Error ? error.message : "unknown",
511
- stack: error instanceof Error ? error.stack : void 0,
512
- cause: error instanceof Error && error.cause instanceof Error ? error.cause.message : void 0
513
- });
514
- if (job.controlOrigin && !disposed) {
515
- try {
516
- disposed = (await control(job, "describe")).quiesced === true;
517
- } catch {
518
- }
519
- }
520
- if (stopped || disposed) {
521
- emit("rolling-back", 90, "\u66F4\u65B0\u672A\u901A\u8FC7\u9A8C\u8BC1\uFF0C\u6B63\u5728\u6062\u590D\u539F\u63D2\u4EF6");
522
- try {
523
- if (!stopped) {
524
- await stopOriginal(job);
525
- stopped = true;
526
- }
527
- if (newChild) await stopRestarted(newChild);
528
- else if (swapped && job.manager && job.manager.kind !== "process") stopManagedHost(job.manager);
529
- if (swapped) {
530
- fs3.renameSync(job.profile, path3.join(job.directory, "profile-failed"));
531
- fs3.renameSync(previous, job.profile);
532
- }
533
- start(job);
534
- await healthy(job, job.previousVersion);
535
- assertPreserved(before, durableSnapshot(job));
536
- rollback = true;
537
- writePrivateJsonAtomic(path3.join(job.directory, "verification-complete.json"), { id: job.id });
538
- } catch (rollbackError) {
539
- try {
540
- writePrivateJsonAtomic(path3.join(job.directory, "rollback-failure.json"), {
541
- message: rollbackError instanceof Error ? rollbackError.message : "unknown",
542
- stack: rollbackError instanceof Error ? rollbackError.stack : void 0
543
- });
544
- } catch {
545
- }
546
- return { phase: "attention", progress: 100, message: "\u81EA\u52A8\u6062\u590D\u672A\u5B8C\u6210\u3002\u5907\u4EFD\u5DF2\u4FDD\u7559\uFF0C\u8BF7\u6309\u4E3B\u673A\u66F4\u65B0\u8BB0\u5F55\u6062\u590D\uFF1B\u4E0D\u8981\u5220\u9664\u8282\u70B9\u6216\u6570\u636E\u3002", terminal: true, ok: false, rollback: false };
547
- }
548
- }
549
- return { phase: "failed", progress: 100, message: (error instanceof Error ? error.message : "\u66F4\u65B0\u5931\u8D25") + (rollback ? "\uFF1B\u5DF2\u6062\u590D\u539F\u63D2\u4EF6\u3002" : "\uFF1B\u5F53\u524D\u63D2\u4EF6\u672A\u66FF\u6362\u3002"), terminal: true, ok: false, rollback };
550
- }
551
- }
552
- async function workerMain(filename) {
553
- const job = JSON.parse(fs3.readFileSync(filename, "utf8"));
554
- validateJob(job);
555
- if (!job.controlOrigin && (process.ppid !== job.parentPid || !process.connected)) throw new Error("Updater requires its initiating parent");
556
- let status = { phase: "starting", progress: 20, message: "\u6B63\u5728\u51C6\u5907\u66F4\u65B0", terminal: false };
557
- const record = (value) => {
558
- status = value;
559
- try {
560
- writePrivateJsonAtomic(path3.join(job.directory, "result.json"), value);
561
- } catch (error) {
562
- console.error("Update progress journal unavailable:", error.code || "write-failed");
563
- }
564
- };
565
- const server = http.createServer((req, res) => {
566
- const origin = String(req.headers.origin || "");
567
- const allowed = [`http://127.0.0.1:${job.webPort}`, `http://localhost:${job.webPort}`, `http://[::1]:${job.webPort}`].includes(origin);
568
- if (!allowed || req.headers.host !== `127.0.0.1:${server.address().port}`) {
569
- res.writeHead(403);
570
- res.end();
571
- return;
572
- }
573
- res.setHeader("Access-Control-Allow-Origin", origin);
574
- res.setHeader("Vary", "Origin");
575
- res.setHeader("Cache-Control", "no-store");
576
- res.setHeader("Access-Control-Allow-Headers", "Authorization");
577
- if (req.method === "OPTIONS") {
578
- res.writeHead(204);
579
- res.end();
580
- return;
581
- }
582
- if (req.method !== "GET" || req.url !== "/status" || req.headers.authorization !== `Bearer ${job.statusToken}`) {
583
- res.writeHead(403);
584
- res.end();
585
- return;
586
- }
587
- res.setHeader("Content-Type", "application/json");
588
- res.end(JSON.stringify(status));
589
- });
590
- await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
591
- try {
592
- if (job.controlOrigin) {
593
- writePrivateJsonAtomic(path3.join(job.directory, "worker-ready.json"), { id: job.id, origin: `http://127.0.0.1:${server.address().port}` });
594
- let authorized = false;
595
- for (let i = 0; i < 150; i++) {
596
- try {
597
- authorized = JSON.parse(fs3.readFileSync(path3.join(job.directory, "authorized.json"), "utf8")).id === job.id;
598
- } catch {
599
- }
600
- if (authorized) break;
601
- await wait(100);
602
- }
603
- if (!authorized || (await control(job, "describe")).pid !== job.parentPid) throw new Error("Updater start authorization expired");
604
- } else await new Promise((resolve, reject) => {
605
- const cleanup = () => {
606
- clearTimeout(timer2);
607
- process.off("message", message);
608
- process.off("disconnect", disconnected);
609
- };
610
- const message = (m) => {
611
- if (m.type === "start" && m.id === job.id) {
612
- cleanup();
613
- resolve();
614
- }
615
- };
616
- const disconnected = () => {
617
- cleanup();
618
- reject(new Error("Initiating parent disconnected before authorization"));
619
- };
620
- const timer2 = setTimeout(() => {
621
- cleanup();
622
- reject(new Error("Updater start authorization expired"));
623
- }, 15e3);
624
- process.on("message", message);
625
- process.once("disconnect", disconnected);
626
- process.send?.({ type: "ready", origin: `http://127.0.0.1:${server.address().port}` });
627
- });
628
- } catch (error) {
629
- server.close();
630
- throw error;
631
- }
632
- const quiesce = () => job.controlOrigin ? control(job, "quiesce").then(() => {
633
- }) : new Promise((resolve, reject) => {
634
- const timeout = setTimeout(() => {
635
- process.off("message", handler);
636
- reject(new Error("\u65E0\u6CD5\u786E\u8BA4\u4F1A\u8BDD\u72B6\u6001\u5DF2\u4FDD\u5B58"));
637
- }, 15e3);
638
- const handler = (message) => {
639
- if (message.type !== "quiesced") return;
640
- clearTimeout(timeout);
641
- process.off("message", handler);
642
- message.ok ? resolve() : reject(new Error("\u6709\u4F1A\u8BDD\u6B63\u5728\u8FD0\u884C\u6216\u65E0\u6CD5\u786E\u8BA4\u6301\u4E45\u5316\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5"));
643
- };
644
- process.on("message", handler);
645
- process.send?.({ type: "quiesce" });
646
- });
647
- try {
648
- record(await executeUpdate(job, record, quiesce));
649
- } catch (error) {
650
- console.error("Update transaction ended unexpectedly:", error.code || error.name || "unknown");
651
- record({
652
- phase: "attention",
653
- progress: 100,
654
- terminal: true,
655
- ok: false,
656
- message: "\u66F4\u65B0\u88AB\u5F02\u5E38\u4E2D\u65AD\uFF0C\u65E0\u6CD5\u786E\u8BA4\u6062\u590D\u7ED3\u679C\u3002\u8BF7\u4FDD\u7559\u4E3B\u673A\u66F4\u65B0\u76EE\u5F55\u5E76\u68C0\u67E5\u539F\u63D2\u4EF6\u5907\u4EFD\uFF0C\u4E0D\u8981\u5220\u9664\u8282\u70B9\u6216\u91CD\u590D\u5B89\u88C5\u3002"
657
- });
658
- }
659
- const lock = path3.join(job.profile, ".harness-remote-update.lock");
660
- releaseOwnedUpdateLock(lock, job.id);
661
- if (job.controlOrigin) {
662
- try {
663
- await control(job, "close");
664
- } catch {
665
- }
666
- }
667
- if (process.connected) process.send?.({ type: "finished" });
668
- const timer = setTimeout(() => {
669
- server.close();
670
- if (process.connected) process.disconnect();
671
- finishUpdateWorker(job.manager, job.directory);
672
- }, 12e4);
673
- timer.unref();
674
- }
675
- if (process.argv[1] && path3.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
676
- void workerMain(process.argv[2]).catch(() => {
677
- process.exitCode = 1;
678
- if (process.connected) process.disconnect();
679
- });
680
- }
681
- export {
682
- control,
683
- executeUpdate,
684
- healthy,
685
- migrateLegacyGrantOwner,
686
- releaseOwnedUpdateLock,
687
- stopRestarted,
688
- validateJob
689
- };
1
+ /* Generated from the shared plugin installation sources. */
2
+
3
+ // src/update-worker.ts
4
+ import fs3 from "node:fs";
5
+ import path3 from "node:path";
6
+ import http from "node:http";
7
+ import { spawn as spawn3 } from "node:child_process";
8
+ import { createHash as createHash2, createPublicKey } from "node:crypto";
9
+ import { fileURLToPath } from "node:url";
10
+
11
+ // src/secure-file.ts
12
+ import { execFileSync } from "node:child_process";
13
+ import {
14
+ chmodSync,
15
+ closeSync,
16
+ existsSync,
17
+ fsyncSync,
18
+ mkdirSync,
19
+ openSync,
20
+ readFileSync,
21
+ renameSync,
22
+ rmSync,
23
+ writeFileSync
24
+ } from "node:fs";
25
+ import { dirname } from "node:path";
26
+ import { userInfo } from "node:os";
27
+ import { randomBytes } from "node:crypto";
28
+ function tightenPrivateFile(file) {
29
+ try {
30
+ chmodSync(file, 384);
31
+ } catch {
32
+ }
33
+ if (process.platform !== "win32") return;
34
+ try {
35
+ execFileSync("icacls", [file, "/inheritance:r", "/grant:r", `${userInfo().username}:F`], {
36
+ timeout: 5e3,
37
+ windowsHide: true,
38
+ stdio: "ignore"
39
+ });
40
+ } catch {
41
+ }
42
+ }
43
+ function writePrivateJsonAtomic(file, value) {
44
+ const parent = dirname(file);
45
+ mkdirSync(parent, { recursive: true, mode: 448 });
46
+ const temporary = `${file}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
47
+ try {
48
+ writeFileSync(temporary, `${JSON.stringify(value, null, 2)}
49
+ `, {
50
+ encoding: "utf8",
51
+ mode: 384,
52
+ flag: "wx"
53
+ });
54
+ const fd = openSync(temporary, "r+");
55
+ try {
56
+ try {
57
+ fsyncSync(fd);
58
+ } catch {
59
+ }
60
+ } finally {
61
+ closeSync(fd);
62
+ }
63
+ tightenPrivateFile(temporary);
64
+ renameSync(temporary, file);
65
+ tightenPrivateFile(file);
66
+ } finally {
67
+ if (existsSync(temporary)) {
68
+ try {
69
+ rmSync(temporary, { force: true });
70
+ } catch {
71
+ }
72
+ }
73
+ }
74
+ }
75
+
76
+ // src/install-profile.ts
77
+ import fs from "node:fs";
78
+ import path from "node:path";
79
+ import { spawn } from "node:child_process";
80
+ import { createHash } from "node:crypto";
81
+
82
+ // src/install-runtime.ts
83
+ var INSTALL_PNPM_VERSION = "11.22.0";
84
+
85
+ // src/install-profile.ts
86
+ var PLUGIN_PACKAGE = "@harness-remote/dsh-wechat-remote";
87
+ function safeProfileName(value) {
88
+ return /^[A-Za-z0-9_-]{1,80}$/.test(value);
89
+ }
90
+ var hash = (file) => createHash("sha256").update(fs.readFileSync(file)).digest("hex");
91
+ function assertRelocatableProfile(root) {
92
+ const walk = (directory) => {
93
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
94
+ const file = path.join(directory, entry.name);
95
+ if (entry.isSymbolicLink()) {
96
+ const relative = path.relative(root, fs.realpathSync(file));
97
+ if (path.isAbsolute(fs.readlinkSync(file)) || !relative || relative.startsWith("..") || path.isAbsolute(relative)) {
98
+ throw new Error("\u63D2\u4EF6\u4F9D\u8D56\u5305\u542B\u4E0D\u53EF\u8FC1\u79FB\u7684\u94FE\u63A5\uFF0C\u672A\u4FEE\u6539\u5F53\u524D\u5B89\u88C5\u3002");
99
+ }
100
+ } else if (entry.isDirectory()) walk(file);
101
+ }
102
+ };
103
+ walk(root);
104
+ }
105
+ function installToolPath(directory, runtime) {
106
+ const bin = path.join(directory, "tool-bin");
107
+ fs.mkdirSync(bin, { mode: 448 });
108
+ const runner = path.join(bin, "pnpm-run.cjs");
109
+ fs.writeFileSync(runner, `require('node:child_process').spawnSync(${JSON.stringify(runtime.executable)},[${JSON.stringify(runtime.cli)},...process.argv.slice(2)],{stdio:'inherit',shell:false,windowsHide:true}).status===0?process.exit(0):process.exit(1)
110
+ `, { mode: 384 });
111
+ if (process.platform === "win32") {
112
+ if (/["\r\n]/.test(runtime.executable)) throw new Error("Node \u5B89\u88C5\u8DEF\u5F84\u4E0D\u53D7\u652F\u6301\u3002");
113
+ fs.writeFileSync(path.join(bin, "pnpm.cmd"), `@echo off\r
114
+ "${runtime.executable.replace(/%/g, "%%")}" "%~dp0pnpm-run.cjs" %*\r
115
+ `, { mode: 448 });
116
+ } else {
117
+ const quote = (s) => "'" + s.replace(/'/g, `'"'"'`) + "'";
118
+ fs.writeFileSync(path.join(bin, "pnpm"), `#!/bin/sh
119
+ exec ${quote(runtime.executable)} ${quote(runner)} "$@"
120
+ `, { mode: 448 });
121
+ }
122
+ return bin;
123
+ }
124
+ function runNativePlugin(cli, profile, home, toolPath, runtime, logFile) {
125
+ if (!safeProfileName(profile)) throw new Error("\u65E0\u6548\u7684 DSH profile \u540D\u79F0\u3002");
126
+ return new Promise((resolve, reject) => {
127
+ const log = fs.openSync(logFile, "a", 384);
128
+ const child = spawn(runtime.executable, [
129
+ cli,
130
+ "plugin",
131
+ "--profile",
132
+ profile,
133
+ "add",
134
+ "file:harness-remote-update.tgz",
135
+ "--ignore-scripts",
136
+ "--config.frozen-lockfile=false",
137
+ "--prefer-offline",
138
+ "--config.manage-package-manager-versions=false",
139
+ "--reporter=append-only"
140
+ ], {
141
+ cwd: home,
142
+ shell: false,
143
+ windowsHide: true,
144
+ stdio: ["ignore", log, log],
145
+ env: {
146
+ ...process.env,
147
+ DSH_HOME: home,
148
+ PATH: toolPath + path.delimiter + (process.env.PATH || ""),
149
+ CI: "true",
150
+ COREPACK_ENABLE_AUTO_PIN: "0",
151
+ npm_config_manage_package_manager_versions: "false"
152
+ }
153
+ });
154
+ fs.closeSync(log);
155
+ const timer = setTimeout(() => {
156
+ child.kill();
157
+ reject(new Error("\u4E0B\u8F7D\u6216\u5B89\u88C5\u8D85\u65F6\uFF0C\u539F\u63D2\u4EF6\u672A\u66FF\u6362\u3002"));
158
+ }, 24e4);
159
+ child.once("error", () => {
160
+ clearTimeout(timer);
161
+ reject(new Error("\u65E0\u6CD5\u542F\u52A8 DSH \u539F\u751F\u5B89\u88C5\u7A0B\u5E8F\u3002"));
162
+ });
163
+ child.once("close", (code) => {
164
+ clearTimeout(timer);
165
+ code === 0 ? resolve() : reject(new Error("\u5B89\u88C5\u672A\u5B8C\u6210\uFF0C\u539F\u63D2\u4EF6\u672A\u66FF\u6362\uFF1B\u8BE6\u60C5\u5DF2\u4FDD\u7559\u5728\u672C\u673A\u5B89\u88C5\u65E5\u5FD7\u3002"));
166
+ });
167
+ });
168
+ }
169
+ async function stageProfile(job) {
170
+ const scope = path.basename(job.profile);
171
+ if (!safeProfileName(scope) || !fs.statSync(job.directory).isDirectory()) throw new Error("\u5B89\u88C5\u76EE\u6807\u4E0D\u660E\u786E\u3002");
172
+ const stagingHome = path.join(job.directory, "staging-home"), staged = path.join(stagingHome, "profiles", scope);
173
+ fs.mkdirSync(staged, { recursive: true, mode: 448 });
174
+ if (fs.existsSync(job.profile)) {
175
+ if (fs.lstatSync(job.profile).isSymbolicLink() || fs.realpathSync(job.profile) !== path.resolve(job.profile)) throw new Error("\u4E0D\u652F\u6301\u81EA\u52A8\u66FF\u6362\u94FE\u63A5\u5F62\u5F0F\u7684 profile\u3002");
176
+ fs.cpSync(job.profile, staged, {
177
+ recursive: true,
178
+ dereference: false,
179
+ filter: (p) => !["node_modules", ".harness-remote-update.lock"].includes(path.basename(p))
180
+ });
181
+ }
182
+ let before = null;
183
+ const filename = path.join(staged, "package.json");
184
+ if (fs.existsSync(filename)) {
185
+ before = JSON.parse(fs.readFileSync(filename, "utf8"));
186
+ for (const [name, spec] of Object.entries(before.dependencies || {})) {
187
+ if (name !== PLUGIN_PACKAGE && !/^[~^]?\d+\.\d+\.\d+(?:-[\w.-]+)?$/.test(String(spec))) throw new Error("\u5176\u4ED6\u63D2\u4EF6\u4F7F\u7528\u4E86\u672C\u5730\u94FE\u63A5\u6216\u7279\u6B8A\u6765\u6E90\uFF0C\u672A\u4FEE\u6539\u5F53\u524D\u5B89\u88C5\u3002");
188
+ }
189
+ for (const e of fs.readdirSync(staged, { withFileTypes: true })) if (e.isSymbolicLink()) throw new Error("profile \u914D\u7F6E\u5305\u542B\u94FE\u63A5\uFF0C\u672A\u4FEE\u6539\u5F53\u524D\u5B89\u88C5\u3002");
190
+ if (before.packageManager && !/^pnpm@\d+\.\d+\.\d+(?:\+.*)?$/.test(before.packageManager)) throw new Error("\u5F53\u524D profile \u4F7F\u7528\u4E86\u5176\u4ED6\u5305\u7BA1\u7406\u5668\uFF0C\u672A\u4FEE\u6539\u5B89\u88C5\u3002");
191
+ writePrivateJsonAtomic(filename, { ...before, dependencies: {
192
+ ...before.dependencies,
193
+ [PLUGIN_PACKAGE]: "file:harness-remote-update.tgz"
194
+ }, packageManager: `pnpm@${INSTALL_PNPM_VERSION}` });
195
+ }
196
+ fs.copyFileSync(path.join(job.directory, "release.tgz"), path.join(staged, "harness-remote-update.tgz"));
197
+ const tools = installToolPath(job.directory, job.runtime);
198
+ await runNativePlugin(job.cli, scope, stagingHome, tools, job.runtime, path.join(job.directory, "install.log"));
199
+ const installed = fs.realpathSync(path.join(staged, "node_modules", PLUGIN_PACKAGE));
200
+ const relative = path.relative(staged, installed);
201
+ if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) throw new Error("\u6682\u5B58\u63D2\u4EF6\u4E0D\u5728\u5B89\u88C5\u76EE\u5F55\u5185\u3002");
202
+ if (JSON.parse(fs.readFileSync(path.join(installed, "package.json"), "utf8")).version !== job.targetVersion) throw new Error("\u5B89\u88C5\u540E\u63D2\u4EF6\u7248\u672C\u4E0D\u5339\u914D\u3002");
203
+ const after = JSON.parse(fs.readFileSync(filename, "utf8"));
204
+ if (!after.dsh?.profile?.bundles?.includes(PLUGIN_PACKAGE)) throw new Error("DSH \u5C1A\u672A\u5C06\u63D2\u4EF6\u6CE8\u518C\u4E3A\u539F\u751F profile \u5C42\u3002");
205
+ for (const name of Object.keys(before?.dependencies || {})) if (name !== PLUGIN_PACKAGE) {
206
+ if (hash(path.join(job.profile, "node_modules", name, "package.json")) !== hash(path.join(staged, "node_modules", name, "package.json"))) throw new Error("\u5B89\u88C5\u8BD5\u56FE\u6539\u53D8\u5176\u4ED6\u63D2\u4EF6\uFF0C\u539F\u5B89\u88C5\u4FDD\u6301\u4E0D\u53D8\u3002");
207
+ }
208
+ assertRelocatableProfile(staged);
209
+ return staged;
210
+ }
211
+
212
+ // src/install-lifecycle.ts
213
+ import fs2 from "node:fs";
214
+ import path2 from "node:path";
215
+ import { execFileSync as execFileSync2, spawn as spawn2 } from "node:child_process";
216
+ var serviceName = (s) => /^[A-Za-z0-9_.@-]{1,180}$/.test(s);
217
+ function run(command, args) {
218
+ return execFileSync2(command, args, { encoding: "utf8", windowsHide: true, timeout: 15e3, maxBuffer: 1024 * 1024, stdio: ["ignore", "pipe", "pipe"] }).trim();
219
+ }
220
+ function validateManager(value) {
221
+ if (value.kind === "process") return;
222
+ if (value.kind === "systemd" && process.platform === "linux" && serviceName(value.unit) && value.unit.endsWith(".service")) return;
223
+ if (value.kind === "launchd" && process.platform === "darwin" && serviceName(value.label) && /^gui\/\d+$/.test(value.domain) && value.domain === `gui/${process.getuid?.()}` && path2.isAbsolute(value.plist) && fs2.statSync(value.plist).isFile()) return;
224
+ throw new Error("\u65E0\u6CD5\u786E\u8BA4\u539F\u540E\u53F0\u670D\u52A1\uFF0C\u672A\u505C\u6B62 DSH\u3002");
225
+ }
226
+ function stopManagedHost(manager) {
227
+ validateManager(manager);
228
+ if (manager.kind === "systemd") run("systemctl", ["--user", "stop", manager.unit]);
229
+ else if (manager.kind === "launchd") run("/bin/launchctl", ["bootout", `${manager.domain}/${manager.label}`]);
230
+ else throw new Error("\u666E\u901A DSH \u8FDB\u7A0B\u5FC5\u987B\u7531\u5176\u542F\u52A8\u63E1\u624B\u505C\u6B62\u3002");
231
+ }
232
+ function startManagedHost(manager) {
233
+ validateManager(manager);
234
+ if (manager.kind === "systemd") run("systemctl", ["--user", "start", manager.unit]);
235
+ else if (manager.kind === "launchd") run("/bin/launchctl", ["bootstrap", manager.domain, manager.plist]);
236
+ else throw new Error("\u666E\u901A DSH \u8FDB\u7A0B\u5FC5\u987B\u7531\u539F\u73AF\u5883\u542F\u52A8\u3002");
237
+ }
238
+ function finishUpdateWorker(manager, directory) {
239
+ const id = path2.basename(directory);
240
+ if (manager?.kind !== "launchd" || !/^[a-f0-9]{32}$/.test(id)) return;
241
+ try {
242
+ run("/bin/launchctl", ["remove", `dsh.wechat.update.${id}`]);
243
+ } catch {
244
+ }
245
+ }
246
+
247
+ // src/update-worker.ts
248
+ function releaseOwnedUpdateLock(lock, id) {
249
+ try {
250
+ if (fs3.readFileSync(lock, "utf8") === id) fs3.unlinkSync(lock);
251
+ } catch {
252
+ }
253
+ }
254
+ var wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
255
+ var hashFile = (f) => createHash2("sha256").update(fs3.readFileSync(f)).digest("hex");
256
+ function within(parent, child) {
257
+ const relative = path3.relative(parent, child);
258
+ return Boolean(relative) && !relative.startsWith("..") && !path3.isAbsolute(relative);
259
+ }
260
+ function safePlainDirectory(p) {
261
+ if (!fs3.statSync(p).isDirectory() || fs3.lstatSync(p).isSymbolicLink() || fs3.realpathSync(p) !== path3.resolve(p)) throw new Error("\u5B89\u88C5\u76EE\u5F55\u4E0D\u662F\u53EF\u5B89\u5168\u66FF\u6362\u7684\u72EC\u7ACB\u76EE\u5F55");
262
+ }
263
+ function validateJob(job) {
264
+ if (!/^[a-f0-9]{32}$/.test(job.id) || path3.basename(job.directory) !== job.id || !within(path3.join(job.home, "harness-remote-updates"), job.directory) || !within(path3.join(job.home, "profiles"), job.profile) || path3.dirname(job.profile) !== path3.join(job.home, "profiles") || !within(job.home, job.stateFile) || !Number.isInteger(job.parentPid) || job.parentPid < 1 || ![job.webPort, job.gatePort, job.localPort].every((p) => Number.isInteger(p) && p > 0 && p <= 65535) || !job.argv.includes(job.cli) || !job.argv.includes("web") || !/^[\w.+-]{1,80}$/.test(job.targetVersion)) throw new Error("\u66F4\u65B0\u4EFB\u52A1\u8303\u56F4\u6821\u9A8C\u5931\u8D25");
265
+ safePlainDirectory(job.profile);
266
+ safePlainDirectory(job.directory);
267
+ if (job.identityFile && !within(job.home, job.identityFile)) throw new Error("\u8282\u70B9\u8EAB\u4EFD\u6587\u4EF6\u4E0D\u5C5E\u4E8E\u5F53\u524D DSH");
268
+ if (job.controlOrigin) {
269
+ const u = new URL(job.controlOrigin);
270
+ if (u.protocol !== "http:" || u.hostname !== "127.0.0.1" || !u.port || u.username || u.password || u.pathname !== "/" || u.search || u.hash) throw new Error("\u5B89\u88C5\u63A7\u5236\u5730\u5740\u65E0\u6548");
271
+ validateManager(job.manager);
272
+ }
273
+ for (const f of [job.executable, job.cli, job.pnpm, ...job.previousVersion === "0.0.0" ? [] : [job.stateFile]]) if (!fs3.statSync(f).isFile()) throw new Error("\u5B89\u88C5\u8FD0\u884C\u65F6\u5DF2\u53D8\u5316");
274
+ }
275
+ async function control(job, operation, input = {}) {
276
+ let res;
277
+ for (let attempt = 0; attempt < (operation === "describe" ? 3 : 1); attempt++) {
278
+ try {
279
+ res = await fetch(job.controlOrigin + "/" + operation, { method: "POST", headers: {
280
+ authorization: `Bearer ${job.statusToken}`,
281
+ "content-type": "application/json",
282
+ connection: "close"
283
+ }, body: JSON.stringify(input), signal: AbortSignal.timeout(operation === "quiesce" ? 2e4 : 12e3), redirect: "error" });
284
+ break;
285
+ } catch (error) {
286
+ if (operation !== "describe" || attempt === 2) throw new Error(`\u5B89\u88C5\u63A7\u5236 ${operation} \u672A\u5B8C\u6210`, { cause: error });
287
+ await wait(150);
288
+ }
289
+ }
290
+ if (!res) throw new Error("\u5B89\u88C5\u63A7\u5236\u901A\u9053\u672A\u54CD\u5E94");
291
+ const value = await res.json();
292
+ if (!res.ok) throw new Error(value.error || "\u65E0\u6CD5\u786E\u8BA4\u5F53\u524D\u4E3B\u673A\u72B6\u6001");
293
+ return value;
294
+ }
295
+ async function beforeRpc(job, method, payload = {}) {
296
+ return job.controlOrigin ? control(job, "read", { method, payload }) : rpc(job, method, payload);
297
+ }
298
+ async function rpc(job, method, payload = {}, deadline) {
299
+ const state = JSON.parse(fs3.readFileSync(job.stateFile, "utf8"));
300
+ const res = await fetch(`http://127.0.0.1:${job.gatePort}/api/${method}`, {
301
+ method: "POST",
302
+ headers: { authorization: `Bearer ${state.token}`, "content-type": "application/json", "x-harness-update-probe": job.statusToken },
303
+ body: JSON.stringify({ type: "client-request", rpcId: "plugin-update-check", method, payload }),
304
+ signal: deadline ? AbortSignal.any([deadline, AbortSignal.timeout(5e3)]) : AbortSignal.timeout(5e3),
305
+ redirect: "error"
306
+ });
307
+ const body = await res.json();
308
+ if (!res.ok || !body.result?.ok) throw new Error("\u4E3B\u673A\u4F1A\u8BDD\u670D\u52A1\u672A\u5C31\u7EEA");
309
+ return body.result.value;
310
+ }
311
+ function durableSnapshot(job) {
312
+ const result = {};
313
+ const walk = (dir) => {
314
+ if (!fs3.existsSync(dir)) return;
315
+ for (const e of fs3.readdirSync(dir, { withFileTypes: true })) {
316
+ const file = path3.join(dir, e.name);
317
+ if (e.isSymbolicLink()) throw new Error("\u53D7\u4FDD\u62A4\u7684\u6570\u636E\u76EE\u5F55\u542B\u94FE\u63A5\uFF0C\u9700\u624B\u5DE5\u66F4\u65B0");
318
+ if (e.isDirectory()) walk(file);
319
+ else if (e.isFile()) result[path3.relative(job.home, file)] = hashFile(file);
320
+ }
321
+ };
322
+ walk(path3.join(job.home, "sessions"));
323
+ walk(path3.join(job.home, "attachments"));
324
+ walk(path3.join(job.home, "harness-remote"));
325
+ for (const e of fs3.readdirSync(job.home, { withFileTypes: true })) {
326
+ if (e.isFile() && /(?:identity|settings|credentials|public|gate-wechat)/i.test(e.name) && !e.name.endsWith(".log")) {
327
+ const f = path3.join(job.home, e.name);
328
+ if (f === job.stateFile) continue;
329
+ result[e.name] = hashFile(f);
330
+ }
331
+ }
332
+ if (fs3.existsSync(job.stateFile)) {
333
+ const state = JSON.parse(fs3.readFileSync(job.stateFile, "utf8"));
334
+ result["$binding"] = createHash2("sha256").update(JSON.stringify([state.token, state.wechatBindings])).digest("hex");
335
+ }
336
+ return result;
337
+ }
338
+ function assertPreserved(before, after) {
339
+ for (const [key, hash2] of Object.entries(before)) if (after[key] !== hash2) throw new Error("\u5347\u7EA7\u540E\u6570\u636E\u6821\u9A8C\u4E0D\u4E00\u81F4\uFF1B\u505C\u6B62\u81EA\u52A8\u64CD\u4F5C\u5E76\u4FDD\u7559\u5907\u4EFD");
340
+ }
341
+ function migrateLegacyGrantOwner(job) {
342
+ if (job.previousVersion !== "1.5.5" || !job.identityFile || !fs3.existsSync(job.identityFile)) return;
343
+ const state = JSON.parse(fs3.readFileSync(job.stateFile, "utf8"));
344
+ if (state.publicIdentityNodeId) return;
345
+ const identity = JSON.parse(fs3.readFileSync(job.identityFile, "utf8"));
346
+ const publicKey = createPublicKey(identity.privateKeyPem).export({ format: "der", type: "spki" });
347
+ const savedKey = createPublicKey(identity.publicKeyPem).export({ format: "der", type: "spki" });
348
+ const nodeId = createHash2("sha256").update(publicKey).digest().subarray(0, 18).toString("base64url");
349
+ if (!publicKey.equals(savedKey) || identity.nodeId !== nodeId) throw new Error("\u65E7\u8282\u70B9\u8EAB\u4EFD\u6821\u9A8C\u672A\u901A\u8FC7\uFF0C\u672A\u8FC1\u79FB\u914D\u5BF9");
350
+ writePrivateJsonAtomic(job.stateFile, { ...state, publicIdentityNodeId: nodeId });
351
+ }
352
+ async function describe(job, deadline) {
353
+ const value = await rpc(job, "wechatHost/describe", { args: { request: {} } }, deadline);
354
+ if (!value?.ok || value.value.agentVersion !== job.dshVersion) throw new Error("DSH \u7248\u672C\u6216\u63CF\u8FF0\u670D\u52A1\u4E0D\u5339\u914D");
355
+ return value.value;
356
+ }
357
+ async function healthy(job, version, timeoutMs = 18e4) {
358
+ const deadline = AbortSignal.timeout(timeoutMs);
359
+ while (!deadline.aborted) {
360
+ try {
361
+ if ((await describe(job, deadline)).pluginVersion === version) {
362
+ const list = await rpc(job, "session.list", {}, deadline);
363
+ if (Array.isArray(list.items)) return;
364
+ }
365
+ } catch {
366
+ }
367
+ if (!deadline.aborted) await wait(Math.min(500, timeoutMs));
368
+ }
369
+ throw new Error("\u91CD\u542F\u5065\u5EB7\u68C0\u67E5\u672A\u901A\u8FC7");
370
+ }
371
+ async function verifyFence(job) {
372
+ for (const port of [job.webPort, job.gatePort, job.localPort]) {
373
+ const res = await fetch(`http://127.0.0.1:${port}/gate/status`, { signal: AbortSignal.timeout(5e3), redirect: "error" });
374
+ await res.arrayBuffer();
375
+ if (res.status !== 503) throw new Error("\u65B0\u63D2\u4EF6\u672A\u4FDD\u6301\u91CD\u542F\u9A8C\u8BC1\u4FDD\u62A4\uFF0C\u4E0D\u80FD\u786E\u8BA4\u5B89\u5168\u66F4\u65B0");
376
+ }
377
+ }
378
+ function start(job) {
379
+ if (job.manager && job.manager.kind !== "process") {
380
+ startManagedHost(job.manager);
381
+ return;
382
+ }
383
+ const log = fs3.openSync(path3.join(job.directory, "restart.log"), "a", 384);
384
+ const child = spawn3(job.executable, [...job.execArgv, ...job.argv], {
385
+ cwd: job.cwd,
386
+ env: { ...process.env, HARNESS_REMOTE_UPDATE_JOB: job.directory },
387
+ stdio: ["ignore", log, log],
388
+ detached: true,
389
+ windowsHide: true
390
+ });
391
+ fs3.closeSync(log);
392
+ child.on("error", () => {
393
+ });
394
+ child.unref();
395
+ if (!child.pid) throw new Error("\u65E0\u6CD5\u542F\u52A8\u539F DSH \u547D\u4EE4");
396
+ writePrivateJsonAtomic(path3.join(job.directory, "restarted-process.json"), { pid: child.pid, cli: job.cli, home: job.home, webPort: job.webPort });
397
+ return child;
398
+ }
399
+ async function stopOriginal(job) {
400
+ if (!job.controlOrigin) {
401
+ if (process.connected !== true || process.ppid !== job.parentPid) throw new Error("\u542F\u52A8\u8EAB\u4EFD\u5DF2\u53D8\u5316\uFF0C\u672A\u505C\u6B62 DSH");
402
+ await stopChild(job.parentPid);
403
+ return;
404
+ }
405
+ const host = await control(job, "describe");
406
+ if (host.pid !== job.parentPid || host.cli !== job.cli || host.profile !== job.profile) throw new Error("\u5F53\u524D DSH \u8EAB\u4EFD\u5DF2\u53D8\u5316");
407
+ if (job.manager && job.manager.kind !== "process") stopManagedHost(job.manager);
408
+ else await control(job, "shutdown");
409
+ for (let i = 0; i < 150; i++) {
410
+ try {
411
+ process.kill(job.parentPid, 0);
412
+ } catch {
413
+ return;
414
+ }
415
+ await wait(100);
416
+ }
417
+ throw new Error("\u539F DSH \u5C1A\u672A\u7ED3\u675F\uFF0C\u672A\u66FF\u6362\u63D2\u4EF6");
418
+ }
419
+ async function stopRestarted(child, timeoutMs = 3e4, forceTimeoutMs = 5e3) {
420
+ if (child.exitCode !== null || child.signalCode !== null) return;
421
+ child.kill("SIGTERM");
422
+ const deadline = Date.now() + timeoutMs;
423
+ while (Date.now() < deadline) {
424
+ if (child.exitCode !== null || child.signalCode !== null) return;
425
+ await wait(Math.min(100, Math.max(1, deadline - Date.now())));
426
+ }
427
+ if (child.exitCode !== null || child.signalCode !== null) return;
428
+ child.kill("SIGKILL");
429
+ const forcedDeadline = Date.now() + forceTimeoutMs;
430
+ while (Date.now() < forcedDeadline) {
431
+ if (child.exitCode !== null || child.signalCode !== null) return;
432
+ await wait(Math.min(100, Math.max(1, forcedDeadline - Date.now())));
433
+ }
434
+ if (child.exitCode !== null || child.signalCode !== null) return;
435
+ throw new Error("\u66F4\u65B0\u540E\u7684 DSH \u672A\u6309\u65F6\u505C\u6B62");
436
+ }
437
+ function captureCandidateLock(home, child) {
438
+ if (!child.pid || child.exitCode !== null || child.signalCode !== null) return;
439
+ const filename = path3.join(home, ".credentials.yaml.lock");
440
+ try {
441
+ const info = fs3.lstatSync(filename);
442
+ if (!info.isFile() || info.isSymbolicLink() || info.nlink !== 1 || info.size > 32) return;
443
+ const value = fs3.readFileSync(filename, "utf8");
444
+ let owned = value === `${child.pid}
445
+ `;
446
+ if (!owned && value === "" && process.platform === "linux") {
447
+ const directory = `/proc/${child.pid}/fd`;
448
+ owned = fs3.readdirSync(directory).some((fd) => {
449
+ try {
450
+ const file = path3.join(directory, fd);
451
+ if (fs3.readlinkSync(file) !== filename) return false;
452
+ const opened = fs3.statSync(file);
453
+ return opened.dev === info.dev && opened.ino === info.ino;
454
+ } catch {
455
+ return false;
456
+ }
457
+ });
458
+ }
459
+ if (owned) return { filename, dev: info.dev, ino: info.ino, pid: child.pid };
460
+ } catch {
461
+ }
462
+ }
463
+ function retireCandidateLock(lock, child, directory) {
464
+ if (!lock || child.pid !== lock.pid || child.exitCode === null && child.signalCode === null) return;
465
+ try {
466
+ const info = fs3.lstatSync(lock.filename);
467
+ if (!info.isFile() || info.isSymbolicLink() || info.nlink !== 1 || info.dev !== lock.dev || info.ino !== lock.ino || info.size > 32) return;
468
+ const value = fs3.readFileSync(lock.filename, "utf8");
469
+ if (value !== "" && value !== `${lock.pid}
470
+ `) return;
471
+ const saved = path3.join(directory, "candidate-credentials-lock.before-rollback");
472
+ if (!fs3.existsSync(saved)) fs3.renameSync(lock.filename, saved);
473
+ } catch {
474
+ }
475
+ }
476
+ async function stopChild(pid) {
477
+ process.kill(pid, "SIGTERM");
478
+ for (let i = 0; i < 100; i++) {
479
+ try {
480
+ process.kill(pid, 0);
481
+ } catch {
482
+ return;
483
+ }
484
+ await wait(100);
485
+ }
486
+ throw new Error("DSH \u672A\u6309\u65F6\u505C\u6B62\uFF0C\u4E0D\u5F3A\u5236\u7EC8\u6B62\u5176\u4ED6\u8FDB\u7A0B");
487
+ }
488
+ async function executeUpdate(job, progress, quiesce) {
489
+ validateJob(job);
490
+ let staged = "";
491
+ const previous = path3.join(job.directory, "profile-before");
492
+ const emit = (phase, n, message) => progress({ phase, progress: n, message, terminal: false });
493
+ let stopped = false, disposed = false, swapped = false, newChild;
494
+ let before = {}, sessionIds = [], readableIds = [];
495
+ try {
496
+ emit("staging", 25, "\u6682\u5B58\u66F4\u65B0\u4E0E\u4F9D\u8D56\uFF0C\u5F53\u524D\u8282\u70B9\u4ECD\u53EF\u4F7F\u7528");
497
+ staged = await stageProfile({
498
+ profile: job.profile,
499
+ directory: job.directory,
500
+ cli: job.cli,
501
+ targetVersion: job.targetVersion,
502
+ runtime: { executable: job.executable, cli: job.pnpm, version: INSTALL_PNPM_VERSION }
503
+ });
504
+ emit("checking", 50, "\u786E\u8BA4\u4F1A\u8BDD\u7A7A\u95F2\u5E76\u4FDD\u5B58\u72B6\u6001");
505
+ const old = job.controlOrigin ? await control(job, "describe") : await describe(job);
506
+ if (old.pluginVersion !== job.previousVersion) throw new Error("\u5F53\u524D\u63D2\u4EF6\u5728\u68C0\u67E5\u540E\u53D1\u751F\u53D8\u5316");
507
+ const list = (await beforeRpc(job, "session.list")).items;
508
+ if (!Array.isArray(list) || list.some((s) => s.running !== false)) throw new Error("\u8BF7\u7B49\u5F85\u5168\u90E8\u4F1A\u8BDD\u7ED3\u675F\u540E\u518D\u66F4\u65B0");
509
+ sessionIds = list.map((s) => s.sessionId).sort();
510
+ for (const sessionId of sessionIds) {
511
+ try {
512
+ await beforeRpc(job, "session.history", { sessionId, maxMessages: 1 });
513
+ readableIds.push(sessionId);
514
+ } catch {
515
+ }
516
+ }
517
+ await quiesce();
518
+ disposed = Boolean(job.controlOrigin);
519
+ before = durableSnapshot(job);
520
+ writePrivateJsonAtomic(path3.join(job.directory, "before-hashes.json"), before);
521
+ emit("backup", 60, "\u5907\u4EFD\u914D\u7F6E\u4E0E\u6570\u636E");
522
+ const backup = path3.join(job.directory, "home-before");
523
+ fs3.mkdirSync(backup, { mode: 448 });
524
+ for (const entry of fs3.readdirSync(job.home, { withFileTypes: true })) {
525
+ if (["harness-remote-updates", "profiles"].includes(entry.name)) continue;
526
+ if (entry.isSymbolicLink()) throw new Error("\u6570\u636E\u76EE\u5F55\u5305\u542B\u5916\u90E8\u94FE\u63A5\uFF0C\u8BF7\u624B\u5DE5\u5907\u4EFD\u540E\u66F4\u65B0");
527
+ fs3.cpSync(path3.join(job.home, entry.name), path3.join(backup, entry.name), { recursive: true });
528
+ }
529
+ emit("restarting", 70, "\u6B63\u5728\u91CD\u542F\u5F53\u524D DSH\uFF0C\u8FDE\u63A5\u4F1A\u6682\u65F6\u65AD\u5F00");
530
+ await stopOriginal(job);
531
+ stopped = true;
532
+ assertPreserved(before, durableSnapshot(job));
533
+ migrateLegacyGrantOwner(job);
534
+ safePlainDirectory(job.profile);
535
+ fs3.renameSync(job.profile, previous);
536
+ try {
537
+ fs3.renameSync(staged, job.profile);
538
+ swapped = true;
539
+ } catch (error) {
540
+ fs3.renameSync(previous, job.profile);
541
+ throw error;
542
+ }
543
+ newChild = start(job);
544
+ emit("verifying", 85, "\u68C0\u67E5\u63D2\u4EF6\u7248\u672C\u3001\u8282\u70B9\u8EAB\u4EFD\u548C\u4F1A\u8BDD");
545
+ await healthy(job, job.targetVersion);
546
+ await verifyFence(job);
547
+ assertPreserved(before, durableSnapshot(job));
548
+ const after = (await rpc(job, "session.list")).items.map((s) => s.sessionId).sort();
549
+ if (JSON.stringify(after) !== JSON.stringify(sessionIds)) throw new Error("\u91CD\u542F\u540E\u4F1A\u8BDD\u5217\u8868\u4E0D\u4E00\u81F4");
550
+ for (const id of readableIds) await rpc(job, "session.history", { sessionId: id, maxMessages: 1 });
551
+ writePrivateJsonAtomic(path3.join(job.directory, "verification-complete.json"), { id: job.id });
552
+ return { phase: "complete", progress: 100, message: "\u63D2\u4EF6\u66F4\u65B0\u5B8C\u6210\uFF0CDSH \u5DF2\u6062\u590D\uFF1B\u539F\u8282\u70B9\u65E0\u9700\u91CD\u65B0\u914D\u5BF9\u3002", terminal: true, ok: true };
553
+ } catch (error) {
554
+ let rollback = false;
555
+ writePrivateJsonAtomic(path3.join(job.directory, "failure.json"), {
556
+ message: error instanceof Error ? error.message : "unknown",
557
+ stack: error instanceof Error ? error.stack : void 0,
558
+ cause: error instanceof Error && error.cause instanceof Error ? error.cause.message : void 0
559
+ });
560
+ if (job.controlOrigin && !disposed) {
561
+ try {
562
+ disposed = (await control(job, "describe")).quiesced === true;
563
+ } catch {
564
+ }
565
+ }
566
+ if (stopped || disposed) {
567
+ emit("rolling-back", 90, "\u66F4\u65B0\u672A\u901A\u8FC7\u9A8C\u8BC1\uFF0C\u6B63\u5728\u6062\u590D\u539F\u63D2\u4EF6");
568
+ try {
569
+ if (!stopped) {
570
+ await stopOriginal(job);
571
+ stopped = true;
572
+ }
573
+ if (newChild) {
574
+ const ownedLock = captureCandidateLock(job.home, newChild);
575
+ await stopRestarted(newChild);
576
+ retireCandidateLock(ownedLock, newChild, job.directory);
577
+ } else if (swapped && job.manager && job.manager.kind !== "process") stopManagedHost(job.manager);
578
+ if (swapped) {
579
+ fs3.renameSync(job.profile, path3.join(job.directory, "profile-failed"));
580
+ fs3.renameSync(previous, job.profile);
581
+ }
582
+ start(job);
583
+ await healthy(job, job.previousVersion);
584
+ assertPreserved(before, durableSnapshot(job));
585
+ rollback = true;
586
+ writePrivateJsonAtomic(path3.join(job.directory, "verification-complete.json"), { id: job.id });
587
+ } catch (rollbackError) {
588
+ try {
589
+ writePrivateJsonAtomic(path3.join(job.directory, "rollback-failure.json"), {
590
+ message: rollbackError instanceof Error ? rollbackError.message : "unknown",
591
+ stack: rollbackError instanceof Error ? rollbackError.stack : void 0
592
+ });
593
+ } catch {
594
+ }
595
+ return { phase: "attention", progress: 100, message: "\u81EA\u52A8\u6062\u590D\u672A\u5B8C\u6210\u3002\u5907\u4EFD\u5DF2\u4FDD\u7559\uFF0C\u8BF7\u6309\u4E3B\u673A\u66F4\u65B0\u8BB0\u5F55\u6062\u590D\uFF1B\u4E0D\u8981\u5220\u9664\u8282\u70B9\u6216\u6570\u636E\u3002", terminal: true, ok: false, rollback: false };
596
+ }
597
+ }
598
+ return { phase: "failed", progress: 100, message: (error instanceof Error ? error.message : "\u66F4\u65B0\u5931\u8D25") + (rollback ? "\uFF1B\u5DF2\u6062\u590D\u539F\u63D2\u4EF6\u3002" : "\uFF1B\u5F53\u524D\u63D2\u4EF6\u672A\u66FF\u6362\u3002"), terminal: true, ok: false, rollback };
599
+ }
600
+ }
601
+ async function workerMain(filename) {
602
+ const job = JSON.parse(fs3.readFileSync(filename, "utf8"));
603
+ validateJob(job);
604
+ if (!job.controlOrigin && (process.ppid !== job.parentPid || !process.connected)) throw new Error("Updater requires its initiating parent");
605
+ let status = { phase: "starting", progress: 20, message: "\u6B63\u5728\u51C6\u5907\u66F4\u65B0", terminal: false };
606
+ const record = (value) => {
607
+ status = value;
608
+ try {
609
+ writePrivateJsonAtomic(path3.join(job.directory, "result.json"), value);
610
+ } catch (error) {
611
+ console.error("Update progress journal unavailable:", error.code || "write-failed");
612
+ }
613
+ };
614
+ const server = http.createServer((req, res) => {
615
+ const origin = String(req.headers.origin || "");
616
+ const allowed = [`http://127.0.0.1:${job.webPort}`, `http://localhost:${job.webPort}`, `http://[::1]:${job.webPort}`].includes(origin);
617
+ if (!allowed || req.headers.host !== `127.0.0.1:${server.address().port}`) {
618
+ res.writeHead(403);
619
+ res.end();
620
+ return;
621
+ }
622
+ res.setHeader("Access-Control-Allow-Origin", origin);
623
+ res.setHeader("Vary", "Origin");
624
+ res.setHeader("Cache-Control", "no-store");
625
+ res.setHeader("Access-Control-Allow-Headers", "Authorization");
626
+ if (req.method === "OPTIONS") {
627
+ res.writeHead(204);
628
+ res.end();
629
+ return;
630
+ }
631
+ if (req.method !== "GET" || req.url !== "/status" || req.headers.authorization !== `Bearer ${job.statusToken}`) {
632
+ res.writeHead(403);
633
+ res.end();
634
+ return;
635
+ }
636
+ res.setHeader("Content-Type", "application/json");
637
+ res.end(JSON.stringify(status));
638
+ });
639
+ await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
640
+ try {
641
+ if (job.controlOrigin) {
642
+ writePrivateJsonAtomic(path3.join(job.directory, "worker-ready.json"), { id: job.id, origin: `http://127.0.0.1:${server.address().port}` });
643
+ let authorized = false;
644
+ for (let i = 0; i < 150; i++) {
645
+ try {
646
+ authorized = JSON.parse(fs3.readFileSync(path3.join(job.directory, "authorized.json"), "utf8")).id === job.id;
647
+ } catch {
648
+ }
649
+ if (authorized) break;
650
+ await wait(100);
651
+ }
652
+ if (!authorized || (await control(job, "describe")).pid !== job.parentPid) throw new Error("Updater start authorization expired");
653
+ } else await new Promise((resolve, reject) => {
654
+ const cleanup = () => {
655
+ clearTimeout(timer2);
656
+ process.off("message", message);
657
+ process.off("disconnect", disconnected);
658
+ };
659
+ const message = (m) => {
660
+ if (m.type === "start" && m.id === job.id) {
661
+ cleanup();
662
+ resolve();
663
+ }
664
+ };
665
+ const disconnected = () => {
666
+ cleanup();
667
+ reject(new Error("Initiating parent disconnected before authorization"));
668
+ };
669
+ const timer2 = setTimeout(() => {
670
+ cleanup();
671
+ reject(new Error("Updater start authorization expired"));
672
+ }, 15e3);
673
+ process.on("message", message);
674
+ process.once("disconnect", disconnected);
675
+ process.send?.({ type: "ready", origin: `http://127.0.0.1:${server.address().port}` });
676
+ });
677
+ } catch (error) {
678
+ server.close();
679
+ throw error;
680
+ }
681
+ const quiesce = () => job.controlOrigin ? control(job, "quiesce").then(() => {
682
+ }) : new Promise((resolve, reject) => {
683
+ const timeout = setTimeout(() => {
684
+ process.off("message", handler);
685
+ reject(new Error("\u65E0\u6CD5\u786E\u8BA4\u4F1A\u8BDD\u72B6\u6001\u5DF2\u4FDD\u5B58"));
686
+ }, 15e3);
687
+ const handler = (message) => {
688
+ if (message.type !== "quiesced") return;
689
+ clearTimeout(timeout);
690
+ process.off("message", handler);
691
+ message.ok ? resolve() : reject(new Error("\u6709\u4F1A\u8BDD\u6B63\u5728\u8FD0\u884C\u6216\u65E0\u6CD5\u786E\u8BA4\u6301\u4E45\u5316\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5"));
692
+ };
693
+ process.on("message", handler);
694
+ process.send?.({ type: "quiesce" });
695
+ });
696
+ try {
697
+ record(await executeUpdate(job, record, quiesce));
698
+ } catch (error) {
699
+ console.error("Update transaction ended unexpectedly:", error.code || error.name || "unknown");
700
+ record({
701
+ phase: "attention",
702
+ progress: 100,
703
+ terminal: true,
704
+ ok: false,
705
+ message: "\u66F4\u65B0\u88AB\u5F02\u5E38\u4E2D\u65AD\uFF0C\u65E0\u6CD5\u786E\u8BA4\u6062\u590D\u7ED3\u679C\u3002\u8BF7\u4FDD\u7559\u4E3B\u673A\u66F4\u65B0\u76EE\u5F55\u5E76\u68C0\u67E5\u539F\u63D2\u4EF6\u5907\u4EFD\uFF0C\u4E0D\u8981\u5220\u9664\u8282\u70B9\u6216\u91CD\u590D\u5B89\u88C5\u3002"
706
+ });
707
+ }
708
+ const lock = path3.join(job.profile, ".harness-remote-update.lock");
709
+ releaseOwnedUpdateLock(lock, job.id);
710
+ if (job.controlOrigin) {
711
+ try {
712
+ await control(job, "close");
713
+ } catch {
714
+ }
715
+ }
716
+ if (process.connected) process.send?.({ type: "finished" });
717
+ const timer = setTimeout(() => {
718
+ server.close();
719
+ if (process.connected) process.disconnect();
720
+ finishUpdateWorker(job.manager, job.directory);
721
+ }, 12e4);
722
+ timer.unref();
723
+ }
724
+ if (process.argv[1] && path3.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
725
+ void workerMain(process.argv[2]).catch(() => {
726
+ process.exitCode = 1;
727
+ if (process.connected) process.disconnect();
728
+ });
729
+ }
730
+ export {
731
+ captureCandidateLock,
732
+ control,
733
+ executeUpdate,
734
+ healthy,
735
+ migrateLegacyGrantOwner,
736
+ releaseOwnedUpdateLock,
737
+ retireCandidateLock,
738
+ stopRestarted,
739
+ validateJob
740
+ };