dsh-wechat-remote 1.7.5 → 1.7.8

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,12 +1,12 @@
1
1
  /* Generated from the shared plugin installation sources. */
2
2
 
3
3
  // src/update-worker.ts
4
- import fs3 from "node:fs";
5
- import path3 from "node:path";
4
+ import fs4 from "node:fs";
5
+ import path4 from "node:path";
6
6
  import http from "node:http";
7
- import { spawn as spawn3 } from "node:child_process";
7
+ import { spawn as spawn4 } from "node:child_process";
8
8
  import { createHash as createHash2, createPublicKey } from "node:crypto";
9
- import { fileURLToPath } from "node:url";
9
+ import { fileURLToPath, pathToFileURL } from "node:url";
10
10
 
11
11
  // src/secure-file.ts
12
12
  import { execFileSync } from "node:child_process";
@@ -76,62 +76,70 @@ function writePrivateJsonAtomic(file, value) {
76
76
  // src/install-profile.ts
77
77
  import fs from "node:fs";
78
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
79
+ import { createHash, randomBytes as randomBytes2 } from "node:crypto";
80
+ import { spawn, execFile } from "node:child_process";
86
81
  var PLUGIN_PACKAGE = "@harness-remote/dsh-wechat-remote";
87
82
  function safeProfileName(value) {
88
83
  return /^[A-Za-z0-9_-]{1,80}$/.test(value);
89
84
  }
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);
85
+ function backupProfile(profile, backup) {
86
+ if (fs.existsSync(backup)) throw new Error("\u672C\u6B21\u5B89\u88C5\u5907\u4EFD\u5DF2\u5B58\u5728\uFF0C\u672A\u8986\u76D6\u3002");
87
+ fs.cpSync(profile, backup, {
88
+ recursive: true,
89
+ dereference: false,
90
+ verbatimSymlinks: true,
91
+ mode: fs.constants.COPYFILE_FICLONE,
92
+ filter(source, target) {
93
+ if (!fs.lstatSync(source).isSymbolicLink()) return true;
94
+ const type = process.platform === "win32" ? fs.statSync(source, { throwIfNoEntry: false })?.isDirectory() ? "junction" : "file" : void 0;
95
+ fs.symlinkSync(fs.readlinkSync(source), target, type);
96
+ return false;
101
97
  }
102
- };
103
- walk(root);
98
+ });
104
99
  }
105
100
  function installToolPath(directory, runtime) {
106
101
  const bin = path.join(directory, "tool-bin");
107
102
  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
103
  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 });
104
+ if (/["\r\n]/.test(runtime.executable + runtime.cli)) throw new Error("\u5B89\u88C5\u5DE5\u5177\u8DEF\u5F84\u5305\u542B\u65E0\u6548\u5B57\u7B26\u3002");
105
+ fs.writeFileSync(path.join(bin, "pnpm.cmd"), '@echo off\r\nsetlocal DisableDelayedExpansion\r\n"%HARNESS_INSTALL_NODE%" "%HARNESS_INSTALL_PNPM%" %*\r\n', { mode: 448 });
116
106
  } 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 });
107
+ fs.writeFileSync(path.join(bin, "pnpm"), '#!/bin/sh\nexec "$HARNESS_INSTALL_NODE" "$HARNESS_INSTALL_PNPM" "$@"\n', { mode: 448 });
121
108
  }
122
109
  return bin;
123
110
  }
124
- function runNativePlugin(cli, profile, home, toolPath, runtime, logFile) {
125
- if (!safeProfileName(profile)) throw new Error("\u65E0\u6548\u7684 DSH profile \u540D\u79F0\u3002");
111
+ var NativeInstallError = class extends Error {
112
+ constructor(message, mayStillBeRunning = false) {
113
+ super(message);
114
+ this.mayStillBeRunning = mayStillBeRunning;
115
+ this.name = "NativeInstallError";
116
+ }
117
+ };
118
+ function runNativePlugin(cli, profile, home, toolPath, runtime, logFile, archiveName, timeoutMs = 6e5, operation = "add") {
119
+ if (!safeProfileName(profile) || !/^harness-remote-[\w.+-]+\.tgz$/.test(archiveName)) throw new Error("\u65E0\u6548\u7684\u5B89\u88C5\u76EE\u6807\u3002");
126
120
  return new Promise((resolve, reject) => {
127
121
  const log = fs.openSync(logFile, "a", 384);
122
+ const env = { ...process.env };
123
+ const pathKey = Object.keys(env).find((key) => key.toLowerCase() === "path");
124
+ const inheritedPath = pathKey ? env[pathKey] : "";
125
+ if (process.platform === "win32") {
126
+ for (const key of Object.keys(env)) if (key.toLowerCase() === "path") delete env[key];
127
+ }
128
+ Object.assign(env, {
129
+ DSH_HOME: home,
130
+ PATH: toolPath + path.delimiter + (inheritedPath || ""),
131
+ HARNESS_INSTALL_NODE: runtime.executable,
132
+ HARNESS_INSTALL_PNPM: runtime.cli,
133
+ CI: "true",
134
+ COREPACK_ENABLE_AUTO_PIN: "0",
135
+ npm_config_manage_package_manager_versions: "false"
136
+ });
128
137
  const child = spawn(runtime.executable, [
129
138
  cli,
130
139
  "plugin",
131
140
  "--profile",
132
141
  profile,
133
- "add",
134
- "file:harness-remote-update.tgz",
142
+ ...operation === "install" ? ["install"] : ["add", `file:${archiveName}`],
135
143
  "--ignore-scripts",
136
144
  "--config.frozen-lockfile=false",
137
145
  "--prefer-offline",
@@ -141,78 +149,142 @@ function runNativePlugin(cli, profile, home, toolPath, runtime, logFile) {
141
149
  cwd: home,
142
150
  shell: false,
143
151
  windowsHide: true,
152
+ detached: process.platform !== "win32",
144
153
  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
- }
154
+ env
153
155
  });
154
156
  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", () => {
157
+ let finished = false, terminating = false, closed = false, terminationTimer;
158
+ const fail = (message, uncertain = false) => finish(new NativeInstallError(`${message} \u5B89\u88C5\u65E5\u5FD7\uFF1A${logFile}`, uncertain));
159
+ const finish = (error) => {
160
+ if (finished) return;
161
+ finished = true;
160
162
  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"));
163
+ clearTimeout(terminationTimer);
164
+ error ? reject(error) : resolve();
165
+ };
166
+ const timer = setTimeout(() => {
167
+ if (closed) return;
168
+ terminating = true;
169
+ terminationTimer = setTimeout(() => fail("\u5B89\u88C5\u8D85\u65F6\uFF0C\u5C1A\u4E0D\u80FD\u786E\u8BA4\u5B89\u88C5\u8FDB\u7A0B\u5DF2\u9000\u51FA\uFF1B\u672A\u81EA\u52A8\u56DE\u9000\uFF0C\u8BF7\u52FF\u91CD\u590D\u5B89\u88C5\u3002", true), 15e3);
170
+ if (process.platform === "win32") {
171
+ if (!child.pid) return fail("\u5B89\u88C5\u7A0B\u5E8F\u672A\u542F\u52A8\u3002");
172
+ execFile(
173
+ path.join(process.env.SystemRoot || "C:\\Windows", "System32", "taskkill.exe"),
174
+ ["/PID", String(child.pid), "/T", "/F"],
175
+ { windowsHide: true, timeout: 1e4 },
176
+ (error) => {
177
+ if (error) fail("\u5B89\u88C5\u8D85\u65F6\uFF0C\u65E0\u6CD5\u786E\u8BA4\u5B89\u88C5\u8FDB\u7A0B\u6811\u5DF2\u9000\u51FA\uFF1B\u672A\u81EA\u52A8\u56DE\u9000\uFF0C\u8BF7\u52FF\u91CD\u590D\u5B89\u88C5\u3002", true);
178
+ else if (closed) fail("\u4E0B\u8F7D\u6216\u5B89\u88C5\u8D85\u65F6\uFF0C\u5B89\u88C5\u8FDB\u7A0B\u5DF2\u505C\u6B62\u3002");
179
+ else child.once("close", () => fail("\u4E0B\u8F7D\u6216\u5B89\u88C5\u8D85\u65F6\uFF0C\u5B89\u88C5\u8FDB\u7A0B\u5DF2\u505C\u6B62\u3002"));
180
+ }
181
+ );
182
+ } else {
183
+ try {
184
+ if (child.pid) process.kill(-child.pid, "SIGKILL");
185
+ } catch (error) {
186
+ if (error.code !== "ESRCH") return fail("\u65E0\u6CD5\u505C\u6B62\u8D85\u65F6\u7684\u5B89\u88C5\u8FDB\u7A0B\uFF1B\u672A\u81EA\u52A8\u56DE\u9000\u3002", true);
187
+ }
188
+ if (closed) fail("\u4E0B\u8F7D\u6216\u5B89\u88C5\u8D85\u65F6\uFF0C\u5B89\u88C5\u8FDB\u7A0B\u5DF2\u505C\u6B62\u3002");
189
+ else child.once("close", () => fail("\u4E0B\u8F7D\u6216\u5B89\u88C5\u8D85\u65F6\uFF0C\u5B89\u88C5\u8FDB\u7A0B\u5DF2\u505C\u6B62\u3002"));
190
+ }
191
+ }, timeoutMs);
192
+ child.once("error", (error) => fail(`\u65E0\u6CD5\u542F\u52A8 DSH \u539F\u751F\u5B89\u88C5\u7A0B\u5E8F\uFF08${error.code || error.name}\uFF09\u3002`));
193
+ child.once("close", (code, signal) => {
194
+ closed = true;
195
+ if (terminating) return;
196
+ if (code === 0) finish();
197
+ else fail(`DSH \u539F\u751F\u5B89\u88C5\u672A\u5B8C\u6210\uFF08${signal ? `\u4FE1\u53F7 ${signal}` : `\u9000\u51FA\u7801 ${code}`}\uFF09\u3002`);
166
198
  });
167
199
  });
168
200
  }
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"));
201
+ async function installProfile(job) {
202
+ const scope = path.basename(job.profile), home = path.dirname(path.dirname(job.profile));
203
+ if (!safeProfileName(scope) || path.dirname(job.profile) !== path.join(home, "profiles") || !/^[\w.+-]{1,80}$/.test(job.targetVersion)) throw new Error("\u5B89\u88C5\u76EE\u6807\u4E0D\u660E\u786E\u3002");
204
+ fs.mkdirSync(job.profile, { recursive: true, mode: 448 });
205
+ const archive = path.join(job.directory, "release.tgz");
206
+ const digest = createHash("sha256").update(fs.readFileSync(archive)).digest("hex");
207
+ const archiveBase = `harness-remote-${job.targetVersion}-${digest}`;
208
+ const suffix = fs.existsSync(path.join(job.profile, `${archiveBase}.tgz`)) ? `-${randomBytes2(8).toString("hex")}` : "";
209
+ const archiveName = `${archiveBase}${suffix}.tgz`;
210
+ fs.copyFileSync(archive, path.join(job.profile, archiveName), fs.constants.COPYFILE_EXCL);
197
211
  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");
212
+ const logFile = path.join(job.directory, "install.log");
213
+ const deadline = Date.now() + 6e5;
214
+ const run2 = (operation = "add") => runNativePlugin(job.cli, scope, home, tools, job.runtime, logFile, archiveName, Math.max(1, deadline - Date.now()), operation);
215
+ try {
216
+ await run2();
217
+ } catch (error) {
218
+ const layoutError = /^\s*(?:\[ERR_PNPM_|ERR_PNPM_)(?:UNEXPECTED_VIRTUAL_STORE|UNEXPECTED_STORE|MODULES_BREAKING_CHANGE|VIRTUAL_STORE_DIR_MAX_LENGTH_DIFF|PUBLIC_HOIST_PATTERN_DIFF|HOIST_PATTERN_DIFF)(?:\]|\s)/m;
219
+ if (!(error instanceof NativeInstallError) || error.mayStillBeRunning || !layoutError.test(fs.readFileSync(logFile, "utf8"))) throw error;
220
+ fs.appendFileSync(logFile, "\nRestoring native pnpm layout with dsh plugin install.\n");
221
+ await run2("install");
222
+ await run2();
223
+ }
224
+ const installed = path.join(job.profile, "node_modules", PLUGIN_PACKAGE);
202
225
  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"));
226
+ const after = JSON.parse(fs.readFileSync(path.join(job.profile, "package.json"), "utf8"));
204
227
  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
228
  }
211
229
 
212
- // src/install-lifecycle.ts
230
+ // src/install-runtime.ts
213
231
  import fs2 from "node:fs";
214
232
  import path2 from "node:path";
215
- import { execFileSync as execFileSync2, spawn as spawn2 } from "node:child_process";
233
+ import { spawn as spawn2 } from "node:child_process";
234
+ var INSTALL_PNPM_VERSION = "11.22.0";
235
+ async function pinInstallRuntime(runtime, directory) {
236
+ let root = path2.dirname(runtime.cli);
237
+ while (!fs2.existsSync(path2.join(root, "package.json"))) {
238
+ const parent = path2.dirname(root);
239
+ if (parent === root) throw new Error("\u65E0\u6CD5\u5B9A\u4F4D\u5B89\u88C5\u5DE5\u5177\u5305\u3002");
240
+ root = parent;
241
+ }
242
+ const manifest = JSON.parse(fs2.readFileSync(path2.join(root, "package.json"), "utf8"));
243
+ if (manifest.name !== "pnpm" || manifest.version !== runtime.version) throw new Error("\u5B89\u88C5\u5DE5\u5177\u7248\u672C\u4E0D\u4E00\u81F4\u3002");
244
+ const target = path2.join(directory, "install-runtime");
245
+ if (fs2.existsSync(target)) throw new Error("\u672C\u6B21\u5B89\u88C5\u5DE5\u5177\u76EE\u5F55\u5DF2\u5B58\u5728\u3002");
246
+ fs2.cpSync(root, target, { recursive: true, dereference: false, verbatimSymlinks: true, mode: fs2.constants.COPYFILE_FICLONE });
247
+ const pinned = { ...runtime, cli: path2.join(target, path2.relative(root, runtime.cli)) };
248
+ await verifyInstallRuntime(pinned);
249
+ return pinned;
250
+ }
251
+ function verifyInstallRuntime(runtime) {
252
+ return new Promise((resolve, reject) => {
253
+ const child = spawn2(runtime.executable, [runtime.cli, "--version"], {
254
+ windowsHide: true,
255
+ shell: false,
256
+ stdio: ["ignore", "pipe", "pipe"],
257
+ env: { ...process.env, COREPACK_ENABLE_AUTO_PIN: "0", npm_config_manage_package_manager_versions: "false" }
258
+ });
259
+ let stdout = "", size = 0, settled = false;
260
+ const finish = (error) => {
261
+ if (settled) return;
262
+ settled = true;
263
+ clearTimeout(timer);
264
+ error ? reject(error) : resolve();
265
+ };
266
+ const timer = setTimeout(() => {
267
+ child.kill();
268
+ finish(new Error("\u5B89\u88C5\u5DE5\u5177\u542F\u52A8\u8D85\u65F6\uFF0C\u672A\u4FEE\u6539\u63D2\u4EF6\u3002"));
269
+ }, 15e3);
270
+ child.stdout.on("data", (b) => {
271
+ size += b.length;
272
+ if (size > 16384) {
273
+ child.kill();
274
+ finish(new Error("\u5B89\u88C5\u5DE5\u5177\u54CD\u5E94\u5F02\u5E38\u3002"));
275
+ } else stdout += b.toString();
276
+ });
277
+ child.stderr.on("data", () => {
278
+ });
279
+ child.once("error", () => finish(new Error("\u65E0\u6CD5\u542F\u52A8\u5B89\u88C5\u5DE5\u5177\uFF0C\u672A\u4FEE\u6539\u63D2\u4EF6\u3002")));
280
+ child.once("close", (code) => finish(code === 0 && stdout.trim() === runtime.version ? void 0 : new Error("\u5B89\u88C5\u5DE5\u5177\u8FD0\u884C\u9A8C\u8BC1\u5931\u8D25\uFF0C\u672A\u4FEE\u6539\u63D2\u4EF6\u3002")));
281
+ });
282
+ }
283
+
284
+ // src/install-lifecycle.ts
285
+ import fs3 from "node:fs";
286
+ import path3 from "node:path";
287
+ import { execFileSync as execFileSync2, spawn as spawn3 } from "node:child_process";
216
288
  var serviceName = (s) => /^[A-Za-z0-9_.@-]{1,180}$/.test(s);
217
289
  function run(command, args) {
218
290
  return execFileSync2(command, args, { encoding: "utf8", windowsHide: true, timeout: 15e3, maxBuffer: 1024 * 1024, stdio: ["ignore", "pipe", "pipe"] }).trim();
@@ -220,7 +292,7 @@ function run(command, args) {
220
292
  function validateManager(value) {
221
293
  if (value.kind === "process") return;
222
294
  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;
295
+ if (value.kind === "launchd" && process.platform === "darwin" && serviceName(value.label) && /^gui\/\d+$/.test(value.domain) && value.domain === `gui/${process.getuid?.()}` && path3.isAbsolute(value.plist) && fs3.statSync(value.plist).isFile()) return;
224
296
  throw new Error("\u65E0\u6CD5\u786E\u8BA4\u539F\u540E\u53F0\u670D\u52A1\uFF0C\u672A\u505C\u6B62 DSH\u3002");
225
297
  }
226
298
  function stopManagedHost(manager) {
@@ -236,7 +308,7 @@ function startManagedHost(manager) {
236
308
  else throw new Error("\u666E\u901A DSH \u8FDB\u7A0B\u5FC5\u987B\u7531\u539F\u73AF\u5883\u542F\u52A8\u3002");
237
309
  }
238
310
  function finishUpdateWorker(manager, directory) {
239
- const id = path2.basename(directory);
311
+ const id = path3.basename(directory);
240
312
  if (manager?.kind !== "launchd" || !/^[a-f0-9]{32}$/.test(id)) return;
241
313
  try {
242
314
  run("/bin/launchctl", ["remove", `dsh.wechat.update.${id}`]);
@@ -247,21 +319,21 @@ function finishUpdateWorker(manager, directory) {
247
319
  // src/update-worker.ts
248
320
  function releaseOwnedUpdateLock(lock, id) {
249
321
  try {
250
- if (fs3.readFileSync(lock, "utf8") === id) fs3.unlinkSync(lock);
322
+ if (fs4.readFileSync(lock, "utf8") === id) fs4.unlinkSync(lock);
251
323
  } catch {
252
324
  }
253
325
  }
254
326
  var wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
255
- var hashFile = (f) => createHash2("sha256").update(fs3.readFileSync(f)).digest("hex");
327
+ var hashFile = (f) => createHash2("sha256").update(fs4.readFileSync(f)).digest("hex");
256
328
  function within(parent, child) {
257
- const relative = path3.relative(parent, child);
258
- return Boolean(relative) && !relative.startsWith("..") && !path3.isAbsolute(relative);
329
+ const relative = path4.relative(parent, child);
330
+ return Boolean(relative) && !relative.startsWith("..") && !path4.isAbsolute(relative);
259
331
  }
260
332
  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");
333
+ if (!fs4.statSync(p).isDirectory() || fs4.lstatSync(p).isSymbolicLink() || fs4.realpathSync(p) !== path4.resolve(p)) throw new Error("\u5B89\u88C5\u76EE\u5F55\u4E0D\u662F\u53EF\u5B89\u5168\u66FF\u6362\u7684\u72EC\u7ACB\u76EE\u5F55");
262
334
  }
263
335
  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");
336
+ if (!/^[a-f0-9]{32}$/.test(job.id) || path4.basename(job.directory) !== job.id || !within(path4.join(job.home, "harness-remote-updates"), job.directory) || !within(path4.join(job.home, "profiles"), job.profile) || path4.dirname(job.profile) !== path4.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[0] !== job.cli || !/^[\w.+-]{1,80}$/.test(job.targetVersion)) throw new Error("\u66F4\u65B0\u4EFB\u52A1\u8303\u56F4\u6821\u9A8C\u5931\u8D25");
265
337
  safePlainDirectory(job.profile);
266
338
  safePlainDirectory(job.directory);
267
339
  if (job.identityFile && !within(job.home, job.identityFile)) throw new Error("\u8282\u70B9\u8EAB\u4EFD\u6587\u4EF6\u4E0D\u5C5E\u4E8E\u5F53\u524D DSH");
@@ -270,7 +342,7 @@ function validateJob(job) {
270
342
  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
343
  validateManager(job.manager);
272
344
  }
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");
345
+ for (const f of [job.executable, job.cli, job.pnpm, ...job.previousVersion === "0.0.0" ? [] : [job.stateFile]]) if (!fs4.statSync(f).isFile()) throw new Error("\u5B89\u88C5\u8FD0\u884C\u65F6\u5DF2\u53D8\u5316");
274
346
  }
275
347
  async function control(job, operation, input = {}) {
276
348
  let res;
@@ -296,7 +368,7 @@ async function beforeRpc(job, method, payload = {}) {
296
368
  return job.controlOrigin ? control(job, "read", { method, payload }) : rpc(job, method, payload);
297
369
  }
298
370
  async function rpc(job, method, payload = {}, deadline) {
299
- const state = JSON.parse(fs3.readFileSync(job.stateFile, "utf8"));
371
+ const state = JSON.parse(fs4.readFileSync(job.stateFile, "utf8"));
300
372
  const res = await fetch(`http://127.0.0.1:${job.gatePort}/api/${method}`, {
301
373
  method: "POST",
302
374
  headers: { authorization: `Bearer ${state.token}`, "content-type": "application/json", "x-harness-update-probe": job.statusToken },
@@ -311,38 +383,38 @@ async function rpc(job, method, payload = {}, deadline) {
311
383
  function durableSnapshot(job) {
312
384
  const result = {};
313
385
  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);
386
+ if (!fs4.existsSync(dir)) return;
387
+ for (const e of fs4.readdirSync(dir, { withFileTypes: true })) {
388
+ const file = path4.join(dir, e.name);
389
+ if (e.isSymbolicLink()) result[path4.relative(job.home, file)] = createHash2("sha256").update(fs4.readlinkSync(file)).digest("hex");
390
+ else if (e.isDirectory()) walk(file);
391
+ else if (e.isFile()) result[path4.relative(job.home, file)] = hashFile(file);
320
392
  }
321
393
  };
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 })) {
394
+ walk(path4.join(job.home, "sessions"));
395
+ walk(path4.join(job.home, "attachments"));
396
+ walk(path4.join(job.home, "harness-remote"));
397
+ for (const e of fs4.readdirSync(job.home, { withFileTypes: true })) {
326
398
  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);
399
+ const f = path4.join(job.home, e.name);
328
400
  if (f === job.stateFile) continue;
329
401
  result[e.name] = hashFile(f);
330
402
  }
331
403
  }
332
- if (fs3.existsSync(job.stateFile)) {
333
- const state = JSON.parse(fs3.readFileSync(job.stateFile, "utf8"));
404
+ if (fs4.existsSync(job.stateFile)) {
405
+ const state = JSON.parse(fs4.readFileSync(job.stateFile, "utf8"));
334
406
  result["$binding"] = createHash2("sha256").update(JSON.stringify([state.token, state.wechatBindings])).digest("hex");
335
407
  }
336
408
  return result;
337
409
  }
338
410
  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");
411
+ for (const [key, hash] of Object.entries(before)) if (after[key] !== hash) 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
412
  }
341
413
  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"));
414
+ if (job.previousVersion !== "1.5.5" || !job.identityFile || !fs4.existsSync(job.identityFile)) return;
415
+ const state = JSON.parse(fs4.readFileSync(job.stateFile, "utf8"));
344
416
  if (state.publicIdentityNodeId) return;
345
- const identity = JSON.parse(fs3.readFileSync(job.identityFile, "utf8"));
417
+ const identity = JSON.parse(fs4.readFileSync(job.identityFile, "utf8"));
346
418
  const publicKey = createPublicKey(identity.privateKeyPem).export({ format: "der", type: "spki" });
347
419
  const savedKey = createPublicKey(identity.publicKeyPem).export({ format: "der", type: "spki" });
348
420
  const nodeId = createHash2("sha256").update(publicKey).digest().subarray(0, 18).toString("base64url");
@@ -380,20 +452,20 @@ function start(job) {
380
452
  startManagedHost(job.manager);
381
453
  return;
382
454
  }
383
- const log = fs3.openSync(path3.join(job.directory, "restart.log"), "a", 384);
384
- const child = spawn3(job.executable, [...job.execArgv, ...job.argv], {
455
+ const log = fs4.openSync(path4.join(job.directory, "restart.log"), "a", 384);
456
+ const child = spawn4(job.executable, [...job.execArgv, ...job.argv], {
385
457
  cwd: job.cwd,
386
458
  env: { ...process.env, HARNESS_REMOTE_UPDATE_JOB: job.directory },
387
459
  stdio: ["ignore", log, log],
388
460
  detached: true,
389
461
  windowsHide: true
390
462
  });
391
- fs3.closeSync(log);
463
+ fs4.closeSync(log);
392
464
  child.on("error", () => {
393
465
  });
394
466
  child.unref();
395
467
  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 });
468
+ writePrivateJsonAtomic(path4.join(job.directory, "restarted-process.json"), { pid: child.pid, cli: job.cli, home: job.home, webPort: job.webPort });
397
469
  return child;
398
470
  }
399
471
  async function stopOriginal(job) {
@@ -416,7 +488,7 @@ async function stopOriginal(job) {
416
488
  }
417
489
  throw new Error("\u539F DSH \u5C1A\u672A\u7ED3\u675F\uFF0C\u672A\u66FF\u6362\u63D2\u4EF6");
418
490
  }
419
- async function stopRestarted(child, timeoutMs = 3e4) {
491
+ async function stopRestarted(child, timeoutMs = 3e4, forceTimeoutMs = 5e3) {
420
492
  if (child.exitCode !== null || child.signalCode !== null) return;
421
493
  child.kill("SIGTERM");
422
494
  const deadline = Date.now() + timeoutMs;
@@ -425,8 +497,54 @@ async function stopRestarted(child, timeoutMs = 3e4) {
425
497
  await wait(Math.min(100, Math.max(1, deadline - Date.now())));
426
498
  }
427
499
  if (child.exitCode !== null || child.signalCode !== null) return;
500
+ child.kill("SIGKILL");
501
+ const forcedDeadline = Date.now() + forceTimeoutMs;
502
+ while (Date.now() < forcedDeadline) {
503
+ if (child.exitCode !== null || child.signalCode !== null) return;
504
+ await wait(Math.min(100, Math.max(1, forcedDeadline - Date.now())));
505
+ }
506
+ if (child.exitCode !== null || child.signalCode !== null) return;
428
507
  throw new Error("\u66F4\u65B0\u540E\u7684 DSH \u672A\u6309\u65F6\u505C\u6B62");
429
508
  }
509
+ function captureCandidateLock(home, child) {
510
+ if (!child.pid || child.exitCode !== null || child.signalCode !== null) return;
511
+ const filename = path4.join(home, ".credentials.yaml.lock");
512
+ try {
513
+ const info = fs4.lstatSync(filename);
514
+ if (!info.isFile() || info.isSymbolicLink() || info.nlink !== 1 || info.size > 32) return;
515
+ const value = fs4.readFileSync(filename, "utf8");
516
+ let owned = value === `${child.pid}
517
+ `;
518
+ if (!owned && value === "" && process.platform === "linux") {
519
+ const directory = `/proc/${child.pid}/fd`;
520
+ owned = fs4.readdirSync(directory).some((fd) => {
521
+ try {
522
+ const file = path4.join(directory, fd);
523
+ if (fs4.readlinkSync(file) !== filename) return false;
524
+ const opened = fs4.statSync(file);
525
+ return opened.dev === info.dev && opened.ino === info.ino;
526
+ } catch {
527
+ return false;
528
+ }
529
+ });
530
+ }
531
+ if (owned) return { filename, dev: info.dev, ino: info.ino, pid: child.pid };
532
+ } catch {
533
+ }
534
+ }
535
+ function retireCandidateLock(lock, child, directory) {
536
+ if (!lock || child.pid !== lock.pid || child.exitCode === null && child.signalCode === null) return;
537
+ try {
538
+ const info = fs4.lstatSync(lock.filename);
539
+ if (!info.isFile() || info.isSymbolicLink() || info.nlink !== 1 || info.dev !== lock.dev || info.ino !== lock.ino || info.size > 32) return;
540
+ const value = fs4.readFileSync(lock.filename, "utf8");
541
+ if (value !== "" && value !== `${lock.pid}
542
+ `) return;
543
+ const saved = path4.join(directory, "candidate-credentials-lock.before-rollback");
544
+ if (!fs4.existsSync(saved)) fs4.renameSync(lock.filename, saved);
545
+ } catch {
546
+ }
547
+ }
430
548
  async function stopChild(pid) {
431
549
  process.kill(pid, "SIGTERM");
432
550
  for (let i = 0; i < 100; i++) {
@@ -441,20 +559,15 @@ async function stopChild(pid) {
441
559
  }
442
560
  async function executeUpdate(job, progress, quiesce) {
443
561
  validateJob(job);
444
- let staged = "";
445
- const previous = path3.join(job.directory, "profile-before");
562
+ const previous = path4.join(job.directory, "profile-before");
446
563
  const emit = (phase, n, message) => progress({ phase, progress: n, message, terminal: false });
447
- let stopped = false, disposed = false, swapped = false, newChild;
564
+ let stopped = false, disposed = false, modified = false, newChild;
448
565
  let before = {}, sessionIds = [], readableIds = [];
449
566
  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
- });
567
+ emit("preparing", 25, "\u51C6\u5907\u5B89\u88C5\u5DE5\u5177\uFF0C\u5F53\u524D\u8282\u70B9\u4ECD\u53EF\u4F7F\u7528");
568
+ const runtime = await pinInstallRuntime({ executable: job.executable, cli: job.pnpm, version: INSTALL_PNPM_VERSION }, job.directory);
569
+ job.pnpm = runtime.cli;
570
+ writePrivateJsonAtomic(path4.join(job.directory, "job.json"), job);
458
571
  emit("checking", 50, "\u786E\u8BA4\u4F1A\u8BDD\u7A7A\u95F2\u5E76\u4FDD\u5B58\u72B6\u6001");
459
572
  const old = job.controlOrigin ? await control(job, "describe") : await describe(job);
460
573
  if (old.pluginVersion !== job.previousVersion) throw new Error("\u5F53\u524D\u63D2\u4EF6\u5728\u68C0\u67E5\u540E\u53D1\u751F\u53D8\u5316");
@@ -471,29 +584,17 @@ async function executeUpdate(job, progress, quiesce) {
471
584
  await quiesce();
472
585
  disposed = Boolean(job.controlOrigin);
473
586
  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");
587
+ writePrivateJsonAtomic(path4.join(job.directory, "before-hashes.json"), before);
588
+ emit("installing", 60, "\u6B63\u5728\u5B89\u88C5\u63D2\u4EF6\uFF0C\u5F53\u524D DSH \u8FDE\u63A5\u4F1A\u6682\u65F6\u65AD\u5F00");
484
589
  await stopOriginal(job);
485
590
  stopped = true;
486
591
  assertPreserved(before, durableSnapshot(job));
592
+ backupProfile(job.profile, previous);
487
593
  migrateLegacyGrantOwner(job);
488
594
  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
- }
595
+ modified = true;
596
+ await installProfile({ profile: job.profile, directory: job.directory, cli: job.cli, targetVersion: job.targetVersion, runtime });
597
+ emit("restarting", 75, "\u5B89\u88C5\u5B8C\u6210\uFF0C\u6B63\u5728\u6062\u590D\u5F53\u524D DSH");
497
598
  newChild = start(job);
498
599
  emit("verifying", 85, "\u68C0\u67E5\u63D2\u4EF6\u7248\u672C\u3001\u8282\u70B9\u8EAB\u4EFD\u548C\u4F1A\u8BDD");
499
600
  await healthy(job, job.targetVersion);
@@ -502,15 +603,23 @@ async function executeUpdate(job, progress, quiesce) {
502
603
  const after = (await rpc(job, "session.list")).items.map((s) => s.sessionId).sort();
503
604
  if (JSON.stringify(after) !== JSON.stringify(sessionIds)) throw new Error("\u91CD\u542F\u540E\u4F1A\u8BDD\u5217\u8868\u4E0D\u4E00\u81F4");
504
605
  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 });
606
+ writePrivateJsonAtomic(path4.join(job.directory, "verification-complete.json"), { id: job.id });
506
607
  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
608
  } catch (error) {
508
609
  let rollback = false;
509
- writePrivateJsonAtomic(path3.join(job.directory, "failure.json"), {
610
+ writePrivateJsonAtomic(path4.join(job.directory, "failure.json"), {
510
611
  message: error instanceof Error ? error.message : "unknown",
511
612
  stack: error instanceof Error ? error.stack : void 0,
512
613
  cause: error instanceof Error && error.cause instanceof Error ? error.cause.message : void 0
513
614
  });
615
+ if (error instanceof NativeInstallError && error.mayStillBeRunning) return {
616
+ phase: "attention",
617
+ progress: 100,
618
+ message: error.message,
619
+ terminal: true,
620
+ ok: false,
621
+ rollback: false
622
+ };
514
623
  if (job.controlOrigin && !disposed) {
515
624
  try {
516
625
  disposed = (await control(job, "describe")).quiesced === true;
@@ -524,20 +633,28 @@ async function executeUpdate(job, progress, quiesce) {
524
633
  await stopOriginal(job);
525
634
  stopped = true;
526
635
  }
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);
636
+ if (newChild) {
637
+ const ownedLock = captureCandidateLock(job.home, newChild);
638
+ await stopRestarted(newChild);
639
+ retireCandidateLock(ownedLock, newChild, job.directory);
640
+ } else if (modified && job.manager && job.manager.kind !== "process") stopManagedHost(job.manager);
641
+ if (modified) {
642
+ fs4.renameSync(job.profile, path4.join(job.directory, "profile-failed"));
643
+ fs4.renameSync(previous, job.profile);
644
+ }
645
+ if (job.previousVersion === "0.0.0") {
646
+ const recovery = await import(pathToFileURL(path4.join(job.directory, "native-recovery.js")).href);
647
+ await recovery.verifyNativeRestore(job, () => start(job), sessionIds, readableIds);
648
+ } else {
649
+ start(job);
650
+ await healthy(job, job.previousVersion);
532
651
  }
533
- start(job);
534
- await healthy(job, job.previousVersion);
535
652
  assertPreserved(before, durableSnapshot(job));
536
653
  rollback = true;
537
- writePrivateJsonAtomic(path3.join(job.directory, "verification-complete.json"), { id: job.id });
654
+ writePrivateJsonAtomic(path4.join(job.directory, "verification-complete.json"), { id: job.id });
538
655
  } catch (rollbackError) {
539
656
  try {
540
- writePrivateJsonAtomic(path3.join(job.directory, "rollback-failure.json"), {
657
+ writePrivateJsonAtomic(path4.join(job.directory, "rollback-failure.json"), {
541
658
  message: rollbackError instanceof Error ? rollbackError.message : "unknown",
542
659
  stack: rollbackError instanceof Error ? rollbackError.stack : void 0
543
660
  });
@@ -550,14 +667,14 @@ async function executeUpdate(job, progress, quiesce) {
550
667
  }
551
668
  }
552
669
  async function workerMain(filename) {
553
- const job = JSON.parse(fs3.readFileSync(filename, "utf8"));
670
+ const job = JSON.parse(fs4.readFileSync(filename, "utf8"));
554
671
  validateJob(job);
555
672
  if (!job.controlOrigin && (process.ppid !== job.parentPid || !process.connected)) throw new Error("Updater requires its initiating parent");
556
673
  let status = { phase: "starting", progress: 20, message: "\u6B63\u5728\u51C6\u5907\u66F4\u65B0", terminal: false };
557
674
  const record = (value) => {
558
675
  status = value;
559
676
  try {
560
- writePrivateJsonAtomic(path3.join(job.directory, "result.json"), value);
677
+ writePrivateJsonAtomic(path4.join(job.directory, "result.json"), value);
561
678
  } catch (error) {
562
679
  console.error("Update progress journal unavailable:", error.code || "write-failed");
563
680
  }
@@ -590,11 +707,11 @@ async function workerMain(filename) {
590
707
  await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
591
708
  try {
592
709
  if (job.controlOrigin) {
593
- writePrivateJsonAtomic(path3.join(job.directory, "worker-ready.json"), { id: job.id, origin: `http://127.0.0.1:${server.address().port}` });
710
+ writePrivateJsonAtomic(path4.join(job.directory, "worker-ready.json"), { id: job.id, origin: `http://127.0.0.1:${server.address().port}` });
594
711
  let authorized = false;
595
712
  for (let i = 0; i < 150; i++) {
596
713
  try {
597
- authorized = JSON.parse(fs3.readFileSync(path3.join(job.directory, "authorized.json"), "utf8")).id === job.id;
714
+ authorized = JSON.parse(fs4.readFileSync(path4.join(job.directory, "authorized.json"), "utf8")).id === job.id;
598
715
  } catch {
599
716
  }
600
717
  if (authorized) break;
@@ -656,8 +773,8 @@ async function workerMain(filename) {
656
773
  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
774
  });
658
775
  }
659
- const lock = path3.join(job.profile, ".harness-remote-update.lock");
660
- releaseOwnedUpdateLock(lock, job.id);
776
+ const lock = path4.join(job.profile, ".harness-remote-update.lock");
777
+ if (status.phase !== "attention") releaseOwnedUpdateLock(lock, job.id);
661
778
  if (job.controlOrigin) {
662
779
  try {
663
780
  await control(job, "close");
@@ -672,18 +789,20 @@ async function workerMain(filename) {
672
789
  }, 12e4);
673
790
  timer.unref();
674
791
  }
675
- if (process.argv[1] && path3.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
792
+ if (process.argv[1] && path4.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
676
793
  void workerMain(process.argv[2]).catch(() => {
677
794
  process.exitCode = 1;
678
795
  if (process.connected) process.disconnect();
679
796
  });
680
797
  }
681
798
  export {
799
+ captureCandidateLock,
682
800
  control,
683
801
  executeUpdate,
684
802
  healthy,
685
803
  migrateLegacyGrantOwner,
686
804
  releaseOwnedUpdateLock,
805
+ retireCandidateLock,
687
806
  stopRestarted,
688
807
  validateJob
689
808
  };