weapp-ide-cli 6.2.0 → 6.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,17 +1,17 @@
1
- import { a as assertWechatDevtoolsPort, c as withPowerShellUtf8Output, i as assertWechatDevtoolsHost, o as resolveWechatDevtoolsInstallationRoot, r as resolveWechatDevtoolsTarget, s as resolveWechatInspectionTimeout } from "./sessionStore-zdzy2UFM.js";
1
+ import { E as withPowerShellUtf8Output, T as inspectWechatDevtoolsInstallation, g as resolveWechatInspectionTimeout, h as resolveWechatDevtoolsInstallationRoot, m as assertWechatDevtoolsPort, p as assertWechatDevtoolsHost, u as resolveWechatDevtoolsTarget } from "./automatorProject-DPU-pEF0.js";
2
2
  import path from "node:path";
3
3
  import { TextDecoder, debuglog, isDeepStrictEqual, stripVTControlCharacters } from "node:util";
4
4
  import { assertMachineE2ELeaseRecoveryScope, mutateLease, withMachineE2ELease, withMachineE2ELeaseRecoveryOperation } from "@weapp-vite/devtools-runtime";
5
5
  import { execa } from "execa";
6
- import { createHash, randomUUID } from "node:crypto";
7
6
  import fs from "node:fs/promises";
8
7
  import process from "node:process";
8
+ import { createHash, randomUUID } from "node:crypto";
9
9
  import { Buffer } from "node:buffer";
10
10
  import net from "node:net";
11
11
  import { setTimeout } from "node:timers/promises";
12
12
  import { z } from "zod";
13
- import { performance } from "node:perf_hooks";
14
- //#region src/cli/agentStart.ts
13
+ import { performance as performance$1 } from "node:perf_hooks";
14
+ // src/cli/agentStart.ts
15
15
  /** 提取独立的完整 JSON 对象,普通日志和嵌套对象不能替代命令响应。 */
16
16
  function readResponseObjects(stdout) {
17
17
  const objects = [];
@@ -91,6 +91,9 @@ async function startWechatIdeAgent(options) {
91
91
  String(options.port)
92
92
  ];
93
93
  if (options.trustProject === true) argv.push("--trust-project");
94
+ options.signal?.throwIfAborted();
95
+ await options.onBeforeDispatch?.();
96
+ options.signal?.throwIfAborted();
94
97
  const started = parseAgentStartResponse((await execa(target.cliPath, argv, {
95
98
  cancelSignal: options.signal,
96
99
  timeout,
@@ -113,35 +116,179 @@ async function startWechatIdeAgent(options) {
113
116
  }
114
117
  });
115
118
  }
116
- //#endregion
117
- //#region src/devtoolsProjectOwnership/host.ts
118
- const inspectionOptions$1 = {
119
+ // src/devtoolsProjectOwnership/installationExit/darwinKernelPaths.ts
120
+ const inspectionTimeoutMs$1 = 1e4;
121
+ const kernelPathsScript = `
122
+ ObjC.import('Foundation');
123
+ ObjC.bindFunction('malloc', ['void *', ['unsigned long']]);
124
+ ObjC.bindFunction('free', ['void', ['void *']]);
125
+ ObjC.bindFunction('proc_pidpath', ['int', ['int', 'void *', 'uint32_t']]);
126
+ ObjC.bindFunction('strchr', ['char *', ['void *', 'int']]);
127
+ function run(argv) {
128
+ var pids = JSON.parse(argv[0]);
129
+ var buffer = $.malloc(4096);
130
+ if (!buffer) throw new Error('Cannot allocate process path buffer.');
131
+ try {
132
+ return JSON.stringify(pids.map(function(pid) {
133
+ var count = $.proc_pidpath(pid, buffer, 4096);
134
+ return { pid: pid, executable: count > 0 ? $.strchr(buffer, 47) : null };
135
+ }));
136
+ } finally { $.free(buffer); }
137
+ }
138
+ `;
139
+ /** 在 JSON 边界严格核验内核响应,不让进程身份查询依赖 schema 运行时。 */
140
+ function parseKernelPaths(stdout) {
141
+ const parsed = JSON.parse(stdout);
142
+ if (!Array.isArray(parsed)) throw new TypeError("Invalid kernel process paths.");
143
+ return parsed.map((entry) => {
144
+ if (!entry || typeof entry !== "object" || Array.isArray(entry) || Object.keys(entry).length !== 2 || !("pid" in entry) || typeof entry.pid !== "number" || !Number.isSafeInteger(entry.pid) || entry.pid <= 0 || !("executable" in entry) || entry.executable !== null && typeof entry.executable !== "string") throw new TypeError("Invalid kernel process path entry.");
145
+ return {
146
+ pid: entry.pid,
147
+ executable: entry.executable
148
+ };
149
+ });
150
+ }
151
+ /** 系统 JXA/libproc 不可用时明确拒绝核验,不安装工具或以进程标题降级。 */
152
+ async function readDarwinKernelPaths(pids, timeoutMs = inspectionTimeoutMs$1) {
153
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) throw new Error("Installation-exit recovery cannot inspect kernel executable paths.");
154
+ const result = await execa("/usr/bin/osascript", [
155
+ "-l",
156
+ "JavaScript",
157
+ "-e",
158
+ kernelPathsScript,
159
+ JSON.stringify(pids)
160
+ ], {
161
+ timeout: Math.min(inspectionTimeoutMs$1, timeoutMs),
162
+ reject: false,
163
+ env: { LC_ALL: "C" }
164
+ });
165
+ if (result.exitCode !== 0 || result.timedOut || result.signal || result.stderr?.trim()) throw new Error("Installation-exit recovery cannot inspect kernel executable paths.");
166
+ let parsed;
167
+ try {
168
+ parsed = parseKernelPaths(result.stdout);
169
+ } catch {
170
+ throw new Error("Installation-exit recovery received incomplete kernel executable paths.");
171
+ }
172
+ const entries = new Map(parsed.map((entry) => [entry.pid, entry.executable]));
173
+ if (entries.size !== pids.length || parsed.length !== pids.length || pids.some((pid) => !entries.has(pid))) throw new Error("Installation-exit recovery received incomplete kernel process identities.");
174
+ return entries;
175
+ }
176
+ // src/devtoolsProjectOwnership/darwinProcessIdentity.ts
177
+ const inspectionTimeoutMs = 3e3;
178
+ function invalidIdentity$2(stage, reason, details = {}, cause) {
179
+ return new Error(`Managed DevTools process identity could not be verified; no project was closed. ${JSON.stringify({
180
+ stage,
181
+ reason,
182
+ ...details
183
+ })}`, { cause });
184
+ }
185
+ async function readStarted(pids, remaining, stage) {
186
+ const result = await execa("/bin/ps", [
187
+ "-p",
188
+ pids.join(","),
189
+ "-o",
190
+ "pid=,stat=,lstart="
191
+ ], {
192
+ timeout: remaining(),
193
+ reject: false,
194
+ env: { LC_ALL: "C" }
195
+ });
196
+ remaining();
197
+ if (result.timedOut || result.signal || result.stderr?.trim() || result.exitCode === 0 && !result.stdout.trim() || result.exitCode !== 0 && !(result.exitCode === 1 && !result.stdout.trim())) throw invalidIdentity$2(stage, "birth-query-failed", { requestedCount: pids.length });
198
+ const started = /* @__PURE__ */ new Map();
199
+ for (const line of result.stdout.split(/\r?\n/).filter((line) => line.trim())) {
200
+ const match = /^\s*(\d+)\s+(\S+)\s+(\w{3}\s+\w{3}\s+\d+\s+\d{2}:\d{2}:\d{2}\s+\d{4})\s*$/.exec(line);
201
+ const pid = Number(match?.[1]);
202
+ if (!match || !pids.includes(pid) || started.has(pid)) throw invalidIdentity$2(stage, "invalid-birth-inventory", { requestedCount: pids.length });
203
+ started.set(pid, {
204
+ started: match[3].replace(/\s+/g, " "),
205
+ zombie: match[2].startsWith("Z")
206
+ });
207
+ }
208
+ return started;
209
+ }
210
+ /** 批量读取内核路径,并以前后启动时间绑定 PID;进程标题变化不改变身份。 */
211
+ async function readDarwinManagedProcessIdentities(pids) {
212
+ if (pids.some((pid) => !Number.isSafeInteger(pid) || pid <= 0)) throw invalidIdentity$2("request", "invalid-pid");
213
+ const requested = [...new Set(pids)];
214
+ const identities = new Map(requested.map((pid) => [pid, void 0]));
215
+ if (!requested.length) return identities;
216
+ const deadline = performance.now() + inspectionTimeoutMs;
217
+ let stage = "before";
218
+ const remaining = () => {
219
+ const timeout = Math.floor(deadline - performance.now());
220
+ if (timeout <= 0) throw invalidIdentity$2(stage, "deadline-exceeded", { requestedCount: requested.length });
221
+ return timeout;
222
+ };
223
+ const before = await readStarted(requested, remaining, "before");
224
+ if (!before.size) return identities;
225
+ stage = "kernel";
226
+ const paths = await readDarwinKernelPaths([...before.keys()], remaining()).catch((cause) => {
227
+ throw invalidIdentity$2("kernel", "kernel-query-failed", { requestedCount: before.size }, cause);
228
+ });
229
+ stage = "after";
230
+ const after = await readStarted(requested, remaining, "after");
231
+ for (const pid of requested) {
232
+ const current = after.get(pid);
233
+ if (!current) continue;
234
+ const executable = paths.get(pid);
235
+ const previous = before.get(pid);
236
+ if (previous?.started !== current.started) throw invalidIdentity$2("after", "birth-changed", {
237
+ pid,
238
+ beforePresent: before.has(pid),
239
+ afterPresent: true
240
+ });
241
+ if (previous.zombie && current.zombie) continue;
242
+ if (previous.zombie !== current.zombie) throw invalidIdentity$2("after", "terminal-state-changed", {
243
+ pid,
244
+ beforeZombie: previous.zombie,
245
+ afterZombie: current.zombie
246
+ });
247
+ if (!executable?.startsWith("/")) throw invalidIdentity$2("after", "kernel-path-unavailable", {
248
+ pid,
249
+ beforePresent: true,
250
+ afterPresent: true,
251
+ kernelPath: executable ? "non-absolute" : "empty"
252
+ });
253
+ identities.set(pid, {
254
+ pid,
255
+ started: current.started,
256
+ executable
257
+ });
258
+ }
259
+ stage = "complete";
260
+ remaining();
261
+ return identities;
262
+ }
263
+ // src/devtoolsProjectOwnership/processIdentity.ts
264
+ const inspectionOptions$2 = {
119
265
  timeout: 3e3,
120
266
  reject: false,
121
267
  windowsHide: true
122
268
  };
123
- const windowsInspectionOptions = {
124
- ...inspectionOptions$1,
269
+ const windowsInspectionOptions$1 = {
270
+ ...inspectionOptions$2,
125
271
  timeout: resolveWechatInspectionTimeout("win32")
126
272
  };
127
- function invalidIdentity(result, timeout) {
273
+ function invalidIdentity$1(result, timeout) {
128
274
  const details = result ? ` Inspection: exitCode=${result.exitCode ?? "none"}, signal=${result.signal ?? "none"}, timedOut=${result.timedOut ?? false}, timeoutMs=${timeout}.` : "";
129
275
  return /* @__PURE__ */ new Error(`Managed DevTools process identity could not be verified; no project was closed.${details}`);
130
276
  }
131
277
  /** PID 必须连同启动时间和可执行文件核验;进程号复用不继承所有权。 */
132
278
  async function readManagedProcessIdentity(pid, platform = process.platform) {
133
- if (!Number.isSafeInteger(pid) || pid <= 0) throw invalidIdentity();
279
+ if (!Number.isSafeInteger(pid) || pid <= 0) throw invalidIdentity$1();
280
+ if (platform === "darwin") return (await readDarwinManagedProcessIdentities([pid])).get(pid);
134
281
  if (platform === "win32") {
135
282
  const result = await execa("powershell.exe", [
136
283
  "-NoProfile",
137
284
  "-NonInteractive",
138
285
  "-Command",
139
286
  withPowerShellUtf8Output(`$ErrorActionPreference='Stop'; Get-CimInstance Win32_Process -Filter "ProcessId=${pid}" | Select-Object ProcessId,ExecutablePath,@{Name='Started';Expression={$_.CreationDate.ToUniversalTime().ToString('o')}} | ConvertTo-Json -Compress`)
140
- ], windowsInspectionOptions);
141
- if (result.exitCode !== 0) throw invalidIdentity(result, windowsInspectionOptions.timeout);
287
+ ], windowsInspectionOptions$1);
288
+ if (result.exitCode !== 0) throw invalidIdentity$1(result, windowsInspectionOptions$1.timeout);
142
289
  if (!result.stdout.trim()) return;
143
290
  const value = JSON.parse(result.stdout);
144
- if (!value || typeof value !== "object" || !("ProcessId" in value) || value.ProcessId !== pid || !("ExecutablePath" in value) || typeof value.ExecutablePath !== "string" || !value.ExecutablePath || !("Started" in value) || typeof value.Started !== "string" || !value.Started) throw invalidIdentity();
291
+ if (!value || typeof value !== "object" || !("ProcessId" in value) || value.ProcessId !== pid || !("ExecutablePath" in value) || typeof value.ExecutablePath !== "string" || !value.ExecutablePath || !("Started" in value) || typeof value.Started !== "string" || !value.Started) throw invalidIdentity$1();
145
292
  return {
146
293
  pid,
147
294
  executable: value.ExecutablePath,
@@ -151,17 +298,18 @@ async function readManagedProcessIdentity(pid, platform = process.platform) {
151
298
  if (platform === "linux") try {
152
299
  const [executable, stat] = await Promise.all([fs.readlink(`/proc/${pid}/exe`), fs.readFile(`/proc/${pid}/stat`, "utf8")]);
153
300
  const boot = await fs.readFile("/proc/sys/kernel/random/boot_id", "utf8").catch(() => {
154
- throw invalidIdentity();
301
+ throw invalidIdentity$1();
155
302
  });
156
303
  const started = stat.slice(stat.lastIndexOf(")") + 2).split(/\s+/)[19];
157
- if (!started || !/^\d+$/.test(started) || !boot.trim()) throw invalidIdentity();
304
+ if (!started || !/^\d+$/.test(started) || !boot.trim()) throw invalidIdentity$1();
158
305
  return {
159
306
  pid,
160
307
  executable,
161
308
  started: `${boot.trim()}:${started}`
162
309
  };
163
310
  } catch (error) {
164
- if (error.code === "ENOENT") return;
311
+ const code = error.code;
312
+ if (code === "ENOENT" || code === "ESRCH") return;
165
313
  throw error;
166
314
  }
167
315
  const result = await execa("ps", [
@@ -172,12 +320,12 @@ async function readManagedProcessIdentity(pid, platform = process.platform) {
172
320
  "-o",
173
321
  "comm="
174
322
  ], {
175
- ...inspectionOptions$1,
323
+ ...inspectionOptions$2,
176
324
  env: { LC_ALL: "C" }
177
325
  });
178
326
  if (result.exitCode === 1 && !result.stdout.trim()) return;
179
327
  const match = /^(\w{3}\s+\w{3}\s+\d+\s+\d{2}:\d{2}:\d{2}\s+\d{4})\s+(\S[^\r\n]*)$/.exec(result.stdout.trim());
180
- if (result.exitCode !== 0 || !match) throw invalidIdentity();
328
+ if (result.exitCode !== 0 || !match) throw invalidIdentity$1();
181
329
  return {
182
330
  pid,
183
331
  started: match[1].replace(/\s+/g, " "),
@@ -187,6 +335,20 @@ async function readManagedProcessIdentity(pid, platform = process.platform) {
187
335
  function sameManagedProcess(first, second) {
188
336
  return first.pid === second.pid && first.started === second.started && first.executable === second.executable;
189
337
  }
338
+ // src/devtoolsProjectOwnership/host.ts
339
+ const inspectionOptions$1 = {
340
+ timeout: 3e3,
341
+ reject: false,
342
+ windowsHide: true
343
+ };
344
+ const windowsInspectionOptions = {
345
+ ...inspectionOptions$1,
346
+ timeout: resolveWechatInspectionTimeout("win32")
347
+ };
348
+ function invalidIdentity(result, timeout) {
349
+ const details = result ? ` Inspection: exitCode=${result.exitCode ?? "none"}, signal=${result.signal ?? "none"}, timedOut=${result.timedOut ?? false}, timeoutMs=${timeout}.` : "";
350
+ return /* @__PURE__ */ new Error(`Managed DevTools process identity could not be verified; no project was closed.${details}`);
351
+ }
190
352
  async function listenerPid(port, platform = process.platform) {
191
353
  const result = platform === "win32" ? await execa("powershell.exe", [
192
354
  "-NoProfile",
@@ -214,7 +376,7 @@ async function inspectManagedProjectHost(target, port, platform = process.platfo
214
376
  return identity;
215
377
  }
216
378
  async function assertManagedInstallation(target) {
217
- const current = await resolveWechatDevtoolsTarget({ cliPath: target.cliPath });
379
+ const current = await inspectWechatDevtoolsInstallation(target.cliPath);
218
380
  if (current.installationId !== target.installationId || current.appPath !== target.appPath || current.cliPath !== target.cliPath || current.profileDir !== target.profileDir || current.version !== target.version || current.channel !== target.channel) throw new Error("Managed DevTools installation changed; no project was closed.");
219
381
  }
220
382
  function isManagedPortClosed(port) {
@@ -241,8 +403,7 @@ async function waitForManagedPortClosed(port, timeoutMs = 1e4) {
241
403
  } while (Date.now() < deadline);
242
404
  throw new Error("Managed DevTools project close returned but its automator port remained open.");
243
405
  }
244
- //#endregion
245
- //#region src/devtoolsProjectOwnership/installationExit/schema.ts
406
+ // src/devtoolsProjectOwnership/installationExit/schema.ts
246
407
  const installationExitRecoverySchema = z.object({
247
408
  protocol: z.literal("wechat-devtools-installation-exit-v1"),
248
409
  recoveredAt: z.string().datetime(),
@@ -255,7 +416,8 @@ const installationExitRecoverySchema = z.object({
255
416
  state: z.enum([
256
417
  "starting",
257
418
  "unconfirmed",
258
- "failed"
419
+ "failed",
420
+ "borrowed"
259
421
  ]),
260
422
  error: z.string().optional(),
261
423
  updatedAt: z.string().min(1),
@@ -275,14 +437,7 @@ const installationExitRecoverySchema = z.object({
275
437
  selectedProcessCount: z.literal(0)
276
438
  }).strict()
277
439
  }).strict();
278
- //#endregion
279
- //#region src/devtoolsProjectOwnership/journal/constants.ts
280
- /** 受管项目日志的环境变量名称;基础设施预检可在构建包之前读取。 */
281
- const MANAGED_PROJECT_JOURNAL_ENV = "WEAPP_IDE_MANAGED_PROJECT_JOURNAL";
282
- /** 仅供显式受管任务声明双项目窗口验收;不允许无限扩大并行窗口。 */
283
- const MANAGED_PROJECT_MAX_WINDOWS_ENV = "WEAPP_IDE_MANAGED_PROJECT_MAX_WINDOWS";
284
- //#endregion
285
- //#region src/devtoolsProjectOwnership/journal/scope.ts
440
+ // src/devtoolsProjectOwnership/journal/scope.ts
286
441
  const markerName = ".ownership-scope";
287
442
  const scopeSchema = z.object({
288
443
  schemaVersion: z.literal(1),
@@ -335,8 +490,7 @@ async function resolveManagedJournalScope(directory) {
335
490
  if (path.resolve(rootPath, relative) !== currentPath) throw new Error("Managed DevTools journal path changed from its registered task children.");
336
491
  return scope;
337
492
  }
338
- //#endregion
339
- //#region src/devtoolsProjectOwnership/journal/windowsSelfIdentity/wire.ts
493
+ // src/devtoolsProjectOwnership/journal/windowsSelfIdentity/wire.ts
340
494
  const SELF_IDENTITY_WIRE = "WEAPP_JOURNAL_SELF_V1";
341
495
  const epochTicks = 621355968000000000n;
342
496
  const maximumTicks = 3155378975999999999n;
@@ -370,8 +524,7 @@ function parseWindowsSelfIdentity(stdout, expectedPid) {
370
524
  started
371
525
  };
372
526
  }
373
- //#endregion
374
- //#region src/devtoolsProjectOwnership/journal/windowsSelfIdentity/command.ts
527
+ // src/devtoolsProjectOwnership/journal/windowsSelfIdentity/command.ts
375
528
  /** 当前 writer 的 PID 由 Node 进程提供;脚本只读查询,不执行任何进程或 IDE 清理。 */
376
529
  function windowsSelfIdentityCommand(pid) {
377
530
  if (!Number.isSafeInteger(pid) || pid <= 0) throw new TypeError("A positive writer PID is required.");
@@ -411,8 +564,7 @@ finally {
411
564
  exit $writerExitCode
412
565
  `;
413
566
  }
414
- //#endregion
415
- //#region src/devtoolsProjectOwnership/journal/windowsSelfIdentity/trace.ts
567
+ // src/devtoolsProjectOwnership/journal/windowsSelfIdentity/trace.ts
416
568
  const marker = `${SELF_IDENTITY_WIRE}:phase:`;
417
569
  const statements = /* @__PURE__ */ new Map([
418
570
  ["$writerFirst=[System.Diagnostics.Process]", "lookup-before"],
@@ -459,15 +611,14 @@ function observeWindowsSelfIdentityPhases(report) {
459
611
  function stripWindowsSelfIdentityPhases(stderr) {
460
612
  return stderr.replace(phaseLines, "");
461
613
  }
462
- //#endregion
463
- //#region src/devtoolsProjectOwnership/journal/windowsSelfIdentity/index.ts
614
+ // src/devtoolsProjectOwnership/journal/windowsSelfIdentity/index.ts
464
615
  const debug = debuglog("weapp-ide-journal-writer");
465
616
  /** 仅核验当前 Node 写入进程;不能用于任意宿主、旧锁存活判断或进程清理。 */
466
617
  async function readWindowsJournalWriterIdentity() {
467
618
  const timeout = resolveWechatInspectionTimeout("win32");
468
619
  const trace = debug.enabled;
469
620
  const command = windowsSelfIdentityCommand(process.pid);
470
- const started = performance.now();
621
+ const started = performance$1.now();
471
622
  const query = execa("powershell.exe", [
472
623
  "-NoProfile",
473
624
  "-NonInteractive",
@@ -481,22 +632,21 @@ async function readWindowsJournalWriterIdentity() {
481
632
  stripFinalNewline: !trace
482
633
  });
483
634
  if (trace) {
484
- const report = (phase) => debug("phase=%s elapsedMs=%s", phase, (performance.now() - started).toFixed(1));
635
+ const report = (phase) => debug("phase=%s elapsedMs=%s", phase, (performance$1.now() - started).toFixed(1));
485
636
  report("launch");
486
637
  query.nodeChildProcess.once("spawn", () => report("spawn"));
487
638
  query.stderr?.on("data", observeWindowsSelfIdentityPhases(report));
488
639
  }
489
640
  const result = await query;
490
641
  const stderr = trace ? stripWindowsSelfIdentityPhases(result.stderr) : result.stderr;
491
- if (trace) debug("phase=settled elapsedMs=%s exitCode=%s signal=%s timedOut=%s", (performance.now() - started).toFixed(1), result.exitCode ?? "none", result.signal ?? "none", result.timedOut ?? false);
642
+ if (trace) debug("phase=settled elapsedMs=%s exitCode=%s signal=%s timedOut=%s", (performance$1.now() - started).toFixed(1), result.exitCode ?? "none", result.signal ?? "none", result.timedOut ?? false);
492
643
  if (result.exitCode !== 0 || result.failed || result.timedOut || result.signal || stderr.trim()) {
493
644
  const reason = /^WEAPP_JOURNAL_SELF_V1:error:([\w.]+):-?\d+\r?\n?$/.exec(stderr)?.[1] ?? "unconfirmed";
494
645
  throw new Error(`Managed journal writer identity could not be verified; the journal remains unchanged. Inspection: exitCode=${result.exitCode ?? "none"}, signal=${result.signal ?? "none"}, timedOut=${result.timedOut ?? false}, timeoutMs=${timeout}, reason=${reason}.`);
495
646
  }
496
647
  return parseWindowsSelfIdentity(result.stdout, process.pid);
497
648
  }
498
- //#endregion
499
- //#region src/devtoolsProjectOwnership/journal/writerIdentity.ts
649
+ // src/devtoolsProjectOwnership/journal/writerIdentity.ts
500
650
  let currentIdentity$1;
501
651
  /** 共享在途与成功身份;失败仍拒绝当前调用,仅允许下一次显式操作重新核验。 */
502
652
  function readManagedJournalWriterIdentity() {
@@ -512,8 +662,26 @@ function readManagedJournalWriterIdentity() {
512
662
  }
513
663
  return currentIdentity$1;
514
664
  }
515
- //#endregion
516
- //#region src/devtoolsProjectOwnership/windowClose/schema.ts
665
+ // src/devtoolsProjectOwnership/snapshot/schema.ts
666
+ const managedSnapshotSchema = z.object({
667
+ protocol: z.literal("wechat-devtools-project-snapshot-v1"),
668
+ sourceProjectPath: z.string().min(1),
669
+ token: z.string().uuid(),
670
+ state: z.enum([
671
+ "reserved",
672
+ "ready",
673
+ "removed"
674
+ ]),
675
+ directoryIdentity: z.object({
676
+ dev: z.string().min(1),
677
+ ino: z.string().min(1)
678
+ }).strict().optional(),
679
+ dispatchedAt: z.string().datetime().optional(),
680
+ cleanupStartedAt: z.string().datetime().optional(),
681
+ removedAt: z.string().datetime().optional(),
682
+ cleanupError: z.string().optional()
683
+ }).strict();
684
+ // src/devtoolsProjectOwnership/windowClose/schema.ts
517
685
  const text = z.string().min(1);
518
686
  const cursorSchema = z.object({
519
687
  name: z.string().regex(/^[^/\\]+\.log$/),
@@ -554,8 +722,7 @@ const managedWindowCloseSchema = z.object({
554
722
  cursors: z.array(cursorSchema).min(1)
555
723
  }).optional()
556
724
  });
557
- //#endregion
558
- //#region src/devtoolsProjectOwnership/journal.ts
725
+ // src/devtoolsProjectOwnership/journal.ts
559
726
  const managedProcessToken = randomUUID();
560
727
  const idSchema = z.string().uuid();
561
728
  const hostSchema = z.object({
@@ -603,8 +770,10 @@ const recordSchema = z.object({
603
770
  releasedReason: z.enum([
604
771
  "borrowed",
605
772
  "project-closed",
606
- "installation-exited"
773
+ "installation-exited",
774
+ "not-started"
607
775
  ]).optional(),
776
+ snapshot: managedSnapshotSchema.optional(),
608
777
  installationExitRecovery: installationExitRecoverySchema.optional(),
609
778
  error: z.string().optional()
610
779
  }).refine((record) => record.releasedReason === "installation-exited" ? record.state === "released" && record.installationExitRecovery !== void 0 : record.installationExitRecovery === void 0, { message: "Installation exit recovery requires its distinct release reason and forensic evidence." });
@@ -619,7 +788,7 @@ async function readManagedRecord(journalPath, id) {
619
788
  const file = managedRecordPath(journalPath, id);
620
789
  if (!(await fs.lstat(file)).isFile()) throw new Error("Managed DevTools journal record is not a regular file.");
621
790
  const record = recordSchema.parse(JSON.parse(await fs.readFile(file, "utf8")));
622
- if (record.id !== id || record.journalPath !== journalPath || !path.isAbsolute(record.projectPath)) throw new Error("Managed DevTools journal identity does not match its task directory.");
791
+ if (record.id !== id || record.journalPath !== journalPath || !path.isAbsolute(record.projectPath) || record.snapshot && (record.projectPath !== path.join(journalPath, "snapshots", id) || !path.isAbsolute(record.snapshot.sourceProjectPath) || record.snapshot.sourceProjectPath === record.projectPath)) throw new Error("Managed DevTools journal identity does not match its task directory.");
623
792
  return record;
624
793
  }
625
794
  async function writeManagedRecord(record) {
@@ -637,16 +806,15 @@ async function writeManagedRecord(record) {
637
806
  await fs.rm(temporary, { force: true });
638
807
  }
639
808
  }
640
- /** 仅递归当前任务明确传递的 children;不扫描用户缓存,不跟随符号链接。 */
641
- async function readManagedWechatProjectRecords(journalPath) {
642
- const directory = resolveManagedJournal(journalPath);
643
- if (!directory) return [];
809
+ async function readManagedJournalDirectory(directory, parentScope) {
810
+ if (parentScope && !(await fs.lstat(directory)).isDirectory()) throw new Error("Managed DevTools journal scope must use regular directories.");
644
811
  const entries = await fs.readdir(directory, { withFileTypes: true }).catch((error) => {
645
- if (error.code === "ENOENT") return;
812
+ if (error.code === "ENOENT" && !parentScope) return;
646
813
  throw error;
647
814
  });
648
815
  if (!entries) return [];
649
816
  const scope = await resolveManagedJournalScope(directory);
817
+ if (parentScope && (parentScope.scopeId !== void 0 || scope.scopeId !== void 0) && (parentScope.scopeId !== scope.scopeId || parentScope.rootPath !== scope.rootPath)) throw new Error("Managed DevTools journal scope changed inside its registered children; refusing another task tree.");
650
818
  const records = [];
651
819
  for (const entry of entries) if (entry.name.endsWith(".json")) records.push(await readManagedRecord(directory, entry.name.slice(0, -5)));
652
820
  else if (entry.name === "children") {
@@ -655,13 +823,17 @@ async function readManagedWechatProjectRecords(journalPath) {
655
823
  for (const child of children) {
656
824
  if (!child.isDirectory()) throw new Error("Managed DevTools child journal is not a regular directory.");
657
825
  const childPath = path.join(directory, "children", child.name);
658
- const childScope = await resolveManagedJournalScope(childPath);
659
- if ((scope.scopeId !== void 0 || childScope.scopeId !== void 0) && (scope.scopeId !== childScope.scopeId || scope.rootPath !== childScope.rootPath)) throw new Error("Managed DevTools journal scope changed inside its registered children; refusing another task tree.");
660
- records.push(...await readManagedWechatProjectRecords(childPath));
826
+ records.push(...await readManagedJournalDirectory(childPath, scope));
661
827
  }
662
828
  }
663
829
  return records;
664
830
  }
831
+ /** 仅递归当前任务明确传递的 children;不扫描用户缓存,不跟随符号链接。 */
832
+ async function readManagedWechatProjectRecords(journalPath) {
833
+ const directory = resolveManagedJournal(journalPath);
834
+ if (!directory) return [];
835
+ return await readManagedJournalDirectory(directory);
836
+ }
665
837
  const lockOwnerSchema = z.object({
666
838
  token: idSchema,
667
839
  identity: hostSchema
@@ -737,8 +909,110 @@ async function withManagedJournalLock(directory, run) {
737
909
  return await run(scope.rootPath);
738
910
  });
739
911
  }
740
- //#endregion
741
- //#region src/devtoolsProjectOwnership/windowBudget.ts
912
+ // src/devtoolsProjectOwnership/snapshot/index.ts
913
+ const MARKER = ".weapp-ide-snapshot-owner";
914
+ function managedSnapshotPath(journalPath, id) {
915
+ return path.join(journalPath, "snapshots", id);
916
+ }
917
+ /** 窗口已经释放不代表本任务快照也已回收。 */
918
+ function isManagedWechatProjectFullyReleased(record) {
919
+ return record.state === "released" && (!record.snapshot || record.snapshot.state === "removed");
920
+ }
921
+ function markerValue(record) {
922
+ return JSON.stringify({
923
+ protocol: record.snapshot.protocol,
924
+ journalPath: record.journalPath,
925
+ id: record.id,
926
+ generation: record.generation,
927
+ ownerToken: record.ownerToken,
928
+ token: record.snapshot.token,
929
+ sourceProjectPath: record.snapshot.sourceProjectPath,
930
+ projectPath: record.projectPath,
931
+ target: record.target
932
+ });
933
+ }
934
+ async function directoryStat(directory) {
935
+ const stat = await fs.lstat(directory, { bigint: true });
936
+ if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error("Managed DevTools snapshot directory identity changed; refusing filesystem cleanup.");
937
+ return {
938
+ dev: String(stat.dev),
939
+ ino: String(stat.ino)
940
+ };
941
+ }
942
+ function assertPath(record) {
943
+ if (!record.snapshot || record.projectPath !== managedSnapshotPath(record.journalPath, record.id) || !path.isAbsolute(record.snapshot.sourceProjectPath) || record.snapshot.sourceProjectPath === record.projectPath) throw new Error("Managed DevTools snapshot is not bound to its exact task journal.");
944
+ }
945
+ async function verifySnapshot(record, allowIncompleteCreation = false) {
946
+ assertPath(record);
947
+ const snapshot = record.snapshot;
948
+ await directoryStat(record.journalPath);
949
+ await directoryStat(path.dirname(record.projectPath));
950
+ const actual = await directoryStat(record.projectPath);
951
+ if (await fs.realpath(record.projectPath) !== path.join(await fs.realpath(record.journalPath), "snapshots", record.id)) throw new Error("Managed DevTools snapshot escaped its canonical task journal.");
952
+ if (!snapshot.directoryIdentity || actual.dev !== snapshot.directoryIdentity.dev || actual.ino !== snapshot.directoryIdentity.ino) throw new Error("Managed DevTools snapshot directory was replaced; refusing cleanup.");
953
+ const marker = path.join(record.projectPath, MARKER);
954
+ try {
955
+ const stat = await fs.lstat(marker);
956
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1 || await fs.readFile(marker, "utf8") !== markerValue(record)) throw new Error("Managed DevTools snapshot creation marker changed; refusing cleanup.");
957
+ } catch (error) {
958
+ if (error.code !== "ENOENT" || !snapshot.cleanupStartedAt && !(allowIncompleteCreation && snapshot.state === "reserved" && !snapshot.dispatchedAt)) throw error;
959
+ }
960
+ }
961
+ /** 调用方持有 journal 根锁;先登记预算,再独占创建本次 generation 的快照。 */
962
+ async function prepareManagedProjectSnapshot(record, populate) {
963
+ assertPath(record);
964
+ const snapshot = record.snapshot;
965
+ if (record.state !== "starting" || snapshot.state !== "reserved" || snapshot.directoryIdentity || snapshot.dispatchedAt) throw new Error("Managed DevTools snapshot preparation requires its unused project reservation.");
966
+ await directoryStat(record.journalPath);
967
+ const parent = path.dirname(record.projectPath);
968
+ await fs.mkdir(parent, { mode: 448 }).catch((error) => {
969
+ if (error.code !== "EEXIST") throw error;
970
+ });
971
+ await directoryStat(parent);
972
+ await fs.mkdir(record.projectPath, { mode: 448 });
973
+ snapshot.directoryIdentity = await directoryStat(record.projectPath);
974
+ await writeManagedRecord(record);
975
+ await fs.writeFile(path.join(record.projectPath, MARKER), markerValue(record), {
976
+ encoding: "utf8",
977
+ mode: 384,
978
+ flag: "wx"
979
+ });
980
+ await populate(record.projectPath);
981
+ await verifySnapshot(record);
982
+ snapshot.state = "ready";
983
+ await writeManagedRecord(record);
984
+ }
985
+ /** 窗口与端口证据由原有 owner 协议提供;这里仅回收匹配 provenance 的快照。 */
986
+ async function finalizeManagedProjectSnapshot(record) {
987
+ if (!record.snapshot || record.snapshot.state === "removed") return;
988
+ const snapshot = record.snapshot;
989
+ try {
990
+ assertPath(record);
991
+ if (record.state !== "released" || ![
992
+ "project-closed",
993
+ "installation-exited",
994
+ "not-started"
995
+ ].includes(record.releasedReason ?? "") || record.releasedReason === "not-started" && (snapshot.dispatchedAt || record.openedProjectWindow !== void 0)) throw new Error("Managed DevTools snapshot is still used by an unresolved project; preserving its files.");
996
+ if (await fs.lstat(record.projectPath).then(() => true).catch((error) => {
997
+ if (error.code === "ENOENT") return false;
998
+ throw error;
999
+ })) {
1000
+ await verifySnapshot(record, true);
1001
+ snapshot.cleanupStartedAt ??= (/* @__PURE__ */ new Date()).toISOString();
1002
+ await writeManagedRecord(record);
1003
+ await fs.rm(record.projectPath, { recursive: true });
1004
+ }
1005
+ snapshot.state = "removed";
1006
+ snapshot.removedAt = (/* @__PURE__ */ new Date()).toISOString();
1007
+ delete snapshot.cleanupError;
1008
+ await writeManagedRecord(record);
1009
+ } catch (error) {
1010
+ snapshot.cleanupError = error instanceof Error ? error.message : String(error);
1011
+ await writeManagedRecord(record);
1012
+ throw error;
1013
+ }
1014
+ }
1015
+ // src/devtoolsProjectOwnership/windowBudget.ts
742
1016
  function maximumOwnedWindows(options) {
743
1017
  const selected = options.maxOwnedWindows ?? process.env["WEAPP_IDE_MANAGED_PROJECT_MAX_WINDOWS"] ?? 1;
744
1018
  if (selected !== 1 && selected !== 2 && selected !== "1" && selected !== "2") throw new Error("Managed DevTools maximum owned windows must be 1 or 2.");
@@ -751,12 +1025,11 @@ function sameTarget(first, second) {
751
1025
  function checkManagedWindowBudget(records, options) {
752
1026
  const maximum = maximumOwnedWindows(options);
753
1027
  const projectPath = path.resolve(options.projectPath);
754
- if (records.some((record) => record.state !== "released" && record.projectPath === projectPath && sameTarget(record.target, options.target))) throw new Error("Managed DevTools project already has an active session; use its existing endpoint in connect-only mode instead of starting it again.");
1028
+ if (records.some((record) => !isManagedWechatProjectFullyReleased(record) && (record.projectPath === projectPath || record.snapshot?.sourceProjectPath === projectPath) && sameTarget(record.target, options.target))) throw new Error("Managed DevTools project already has an active session; use its existing endpoint in connect-only mode instead of starting it again.");
755
1029
  const owned = records.filter((record) => record.state === "owned").length;
756
1030
  if (owned >= maximum) throw new Error(`Managed DevTools window budget exhausted (${owned}/${maximum}); close the previous owned project before another start.`);
757
1031
  }
758
- //#endregion
759
- //#region src/devtoolsProjectOwnership/windowClose/logCursor.ts
1032
+ // src/devtoolsProjectOwnership/windowClose/logCursor.ts
760
1033
  const readLimit = 2097152;
761
1034
  const scanLimit = 33554432;
762
1035
  const scanChunkSize = 65536;
@@ -960,8 +1233,7 @@ async function consumeFreshLogLines(directory, cursors, consume) {
960
1233
  await Promise.all(streams.map(({ file }) => file.close()));
961
1234
  }
962
1235
  }
963
- //#endregion
964
- //#region src/devtoolsProjectOwnership/windowClose/activeLog.ts
1236
+ // src/devtoolsProjectOwnership/windowClose/activeLog.ts
965
1237
  const inspectionOptions = {
966
1238
  timeout: 3e3,
967
1239
  reject: false,
@@ -1120,8 +1392,7 @@ async function captureActiveMainLogCursors(record, directory, platform = process
1120
1392
  host: main
1121
1393
  };
1122
1394
  }
1123
- //#endregion
1124
- //#region src/devtoolsProjectOwnership/windowClose/protocol.ts
1395
+ // src/devtoolsProjectOwnership/windowClose/protocol.ts
1125
1396
  function decodeString(value) {
1126
1397
  const quote = value[0];
1127
1398
  if (!quote || ![
@@ -1262,8 +1533,7 @@ function consumeWindowCloseTrace(evidence, projectPath, input) {
1262
1533
  }
1263
1534
  }
1264
1535
  }
1265
- //#endregion
1266
- //#region src/devtoolsProjectOwnership/windowClose.ts
1536
+ // src/devtoolsProjectOwnership/windowClose.ts
1267
1537
  /** 只捕获本次显式安装的日志,必须先于关闭命令持久化。 */
1268
1538
  async function captureManagedWindowClose(record) {
1269
1539
  if (!record.target.version) throw new Error("Managed DevTools window-close evidence requires the selected product version.");
@@ -1324,41 +1594,44 @@ async function waitForManagedWindowClosed(record, persist, options = {}) {
1324
1594
  } while (true);
1325
1595
  throw new Error("Managed DevTools project close has no complete native-window destruction evidence; stop the acceptance lane.");
1326
1596
  }
1327
- //#endregion
1328
- //#region src/devtoolsProjectOwnership/installationExit/darwin.ts
1597
+ // src/devtoolsProjectOwnership/installationExit/darwin.ts
1329
1598
  const options = {
1330
1599
  timeout: 1e4,
1331
1600
  reject: false,
1332
1601
  env: { LC_ALL: "C" }
1333
1602
  };
1334
- const pathSchema = z.array(z.object({
1335
- pid: z.number().int().positive(),
1336
- executable: z.string().nullable()
1337
- }).strict());
1338
- const kernelPathsScript = `
1339
- ObjC.import('Foundation');
1340
- ObjC.bindFunction('malloc', ['void *', ['unsigned long']]);
1341
- ObjC.bindFunction('free', ['void', ['void *']]);
1342
- ObjC.bindFunction('proc_pidpath', ['int', ['int', 'void *', 'uint32_t']]);
1343
- ObjC.bindFunction('strchr', ['char *', ['void *', 'int']]);
1344
- function run(argv) {
1345
- var pids = JSON.parse(argv[0]);
1346
- var buffer = $.malloc(4096);
1347
- if (!buffer) throw new Error('Cannot allocate process path buffer.');
1348
- try {
1349
- return JSON.stringify(pids.map(function(pid) {
1350
- var count = $.proc_pidpath(pid, buffer, 4096);
1351
- return { pid: pid, executable: count > 0 ? $.strchr(buffer, 47) : null };
1352
- }));
1353
- } finally { $.free(buffer); }
1603
+ const textImageDecoder = new TextDecoder("utf-8", { fatal: true });
1604
+ /** 按本机 LC_ALL=C 的 lsof 表示还原字节;有歧义的控制表示不能用于负归属判断。 */
1605
+ function decodeTextImagePath(wire) {
1606
+ if (!wire.startsWith("/") || /[^\u0020-\u007E]|\^[A-GKN-Z[\]\\^_]/.test(wire)) throw new Error("Installation-exit recovery received an unverifiable executable image path.");
1607
+ if (!wire.includes("\\")) return wire;
1608
+ const bytes = Buffer.allocUnsafe(wire.length);
1609
+ let length = 0;
1610
+ for (let index = 0; index < wire.length; index++) {
1611
+ let byte = wire.charCodeAt(index);
1612
+ if (wire[index] === "\\") {
1613
+ const escape = wire[++index];
1614
+ if (escape === "\\") byte = 92;
1615
+ else if (escape === "x" && /^[\da-f]{2}$/.test(wire.slice(index + 1, index + 3))) {
1616
+ byte = Number.parseInt(wire.slice(index + 1, index + 3), 16);
1617
+ index += 2;
1618
+ if (byte < 128) throw new Error("Installation-exit recovery received an unverifiable executable image path.");
1619
+ } else throw new Error("Installation-exit recovery received an unverifiable executable image path.");
1620
+ }
1621
+ bytes[length++] = byte;
1622
+ }
1623
+ return textImageDecoder.decode(bytes.subarray(0, length));
1354
1624
  }
1355
- `;
1356
1625
  /** ps 仅用于 PID、启动时间和僵尸状态,不能用可修改的 comm 代替实际可执行文件。 */
1357
- async function readDarwinProcessStates() {
1358
- const inspection = execa("/bin/ps", ["-axo", "pid=,stat=,lstart="], options);
1626
+ async function readDarwinProcessStates(timeoutMs = options.timeout) {
1627
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) throw new Error("Installation-exit recovery cannot inspect the process inventory without a positive timeout.");
1628
+ const inspection = execa("/bin/ps", ["-axo", "pid=,stat=,lstart="], {
1629
+ ...options,
1630
+ timeout: Math.min(options.timeout, timeoutMs)
1631
+ });
1359
1632
  const inspectorPid = inspection.pid;
1360
1633
  const result = await inspection;
1361
- if (result.exitCode !== 0 || result.stderr?.trim() || !result.stdout.trim()) throw new Error("Installation-exit recovery could not read the complete process inventory.");
1634
+ if (result.exitCode !== 0 || result.timedOut || result.signal || result.stderr?.trim() || !result.stdout.trim()) throw new Error("Installation-exit recovery could not read the complete process inventory.");
1362
1635
  const records = result.stdout.trim().split(/\r?\n/).map((line) => {
1363
1636
  const match = /^\s*(\d+)\s+(\S+)\s+(\w{3}\s+\w{3}\s+\d+\s+\d{2}:\d{2}:\d{2}\s+\d{4})\s*$/.exec(line);
1364
1637
  const pid = Number(match?.[1]);
@@ -1372,28 +1645,9 @@ async function readDarwinProcessStates() {
1372
1645
  if (new Set(records.map((record) => record.pid)).size !== records.length) throw new Error("Installation-exit recovery found duplicate process identities.");
1373
1646
  return records.filter((record) => record.pid !== inspectorPid);
1374
1647
  }
1375
- /** 系统 JXA/libproc 不可用时明确拒绝恢复,不安装工具或以进程标题降级。 */
1376
- async function readDarwinKernelPaths(pids) {
1377
- const result = await execa("/usr/bin/osascript", [
1378
- "-l",
1379
- "JavaScript",
1380
- "-e",
1381
- kernelPathsScript,
1382
- JSON.stringify(pids)
1383
- ], options);
1384
- if (result.exitCode !== 0 || result.stderr?.trim()) throw new Error("Installation-exit recovery cannot inspect kernel executable paths.");
1385
- let parsed;
1386
- try {
1387
- parsed = pathSchema.parse(JSON.parse(result.stdout));
1388
- } catch {
1389
- throw new Error("Installation-exit recovery received incomplete kernel executable paths.");
1390
- }
1391
- const entries = new Map(parsed.map((entry) => [entry.pid, entry.executable]));
1392
- if (entries.size !== pids.length || parsed.length !== pids.length || pids.some((pid) => !entries.has(pid))) throw new Error("Installation-exit recovery received incomplete kernel process identities.");
1393
- return entries;
1394
- }
1395
1648
  /** 已删除可执行文件无法经 proc_pidpath 读取时,核验全部 program-text 映像而非猜测首项。 */
1396
- async function readDarwinTextImages(pid) {
1649
+ async function readDarwinTextImages(pid, timeoutMs = options.timeout) {
1650
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) throw new Error("Installation-exit recovery cannot inspect executable text images without a positive timeout.");
1397
1651
  const result = await execa("/usr/sbin/lsof", [
1398
1652
  "-nP",
1399
1653
  "-a",
@@ -1402,45 +1656,54 @@ async function readDarwinTextImages(pid) {
1402
1656
  "-d",
1403
1657
  "txt",
1404
1658
  "-F0pftn"
1405
- ], options);
1406
- if (result.exitCode !== 0 || result.stderr?.trim()) throw new Error("Installation-exit recovery cannot inspect all executable text images.");
1407
- const fields = result.stdout.split("\0").map((field) => field.replace(/^\r?\n/, "")).filter(Boolean);
1408
- if (fields.shift() !== `p${pid}` || fields.length === 0 || fields.length % 3 !== 0) throw new Error("Installation-exit recovery received incomplete executable text images.");
1659
+ ], {
1660
+ ...options,
1661
+ timeout: Math.min(options.timeout, timeoutMs)
1662
+ });
1663
+ if (result.exitCode !== 0 || result.timedOut || result.signal || result.stderr?.trim()) throw new Error(`Installation-exit recovery cannot inspect all executable text images for PID ${pid}.`, { cause: {
1664
+ exitCode: result.exitCode,
1665
+ stderr: result.stderr?.trim()
1666
+ } });
1667
+ const fields = result.stdout.split("\0").map((field) => field.replace(/^\r?\n/, ""));
1668
+ if (fields.pop() !== "" || fields.shift() !== `p${pid}` || fields.length === 0 || fields.length % 3 !== 0) throw new Error("Installation-exit recovery received incomplete executable text images.");
1409
1669
  const paths = [];
1410
1670
  for (let index = 0; index < fields.length; index += 3) {
1411
1671
  if (fields[index] !== "ftxt" || fields[index + 1] !== "tREG" || !fields[index + 2]?.startsWith("n")) throw new Error("Installation-exit recovery received unverifiable executable text images.");
1412
- paths.push(fields[index + 2].slice(1));
1672
+ paths.push(decodeTextImagePath(fields[index + 2].slice(1)));
1413
1673
  }
1414
1674
  return paths;
1415
1675
  }
1416
- //#endregion
1417
- //#region src/devtoolsProjectOwnership/installationExit/processes.ts
1418
- /** 已删除映像仍有内核 vnode 路径;从现存祖先解析符号链接,不把缺失文件当成已退出进程。 */
1419
- async function canonicalImagePath(file) {
1420
- if (!path.isAbsolute(file) || /[\r\n\0]/.test(file)) throw new Error("Installation-exit recovery received an unverifiable executable image path.");
1421
- try {
1422
- return await fs.realpath(file);
1423
- } catch (error) {
1424
- if (error.code !== "ENOENT" || path.dirname(file) === file) throw new Error("Installation-exit recovery cannot resolve an executable image path.", { cause: error });
1425
- return path.join(await canonicalImagePath(path.dirname(file)), path.basename(file));
1426
- }
1427
- }
1676
+ // src/devtoolsProjectOwnership/installationExit/processes.ts
1428
1677
  /** 仅支持已验证的 macOS 全安装清单;未知平台、权限错误或不完整映像均拒绝推断退出。 */
1429
- async function inspectExitedWechatInstallation(target, platform = process.platform) {
1678
+ async function inspectExitedWechatInstallation(target, platform = process.platform, options = {}) {
1430
1679
  if (platform !== "darwin") throw new Error("Installation-exit recovery requires the verified macOS process inventory.");
1680
+ const timeoutMs = options.timeoutMs ?? 3e4;
1681
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) throw new Error("Installation-exit recovery requires a positive process inventory timeout.");
1682
+ const deadline = performance.now() + Math.min(timeoutMs, 3e4);
1683
+ const remaining = () => {
1684
+ const timeout = Math.floor(deadline - performance.now());
1685
+ if (timeout <= 0) throw new Error("Installation-exit recovery process inventory deadline expired; continuing new process activity or incomplete inspection prevents proving exit.");
1686
+ return timeout;
1687
+ };
1688
+ const readStates = async () => {
1689
+ const states = await readDarwinProcessStates(remaining());
1690
+ remaining();
1691
+ return states;
1692
+ };
1431
1693
  const selectedRoot = resolveWechatDevtoolsInstallationRoot(target, platform);
1432
1694
  if (!selectedRoot || !path.isAbsolute(selectedRoot) || !selectedRoot.endsWith(".app") || !target.appPath.includes("/Contents/")) throw new Error("Installation-exit recovery cannot resolve the selected application bundle.");
1433
1695
  const installationRoot = await fs.realpath(selectedRoot);
1434
- let pending = await readDarwinProcessStates();
1696
+ let pending = await readStates();
1435
1697
  const verified = /* @__PURE__ */ new Map();
1436
1698
  let kernelPathProcessCount = 0;
1437
1699
  let textImageProcessCount = 0;
1438
1700
  let exitedProcessCount = 0;
1439
1701
  let zombieProcessCount = 0;
1440
- for (let round = 0; round < 3; round++) {
1441
- const kernelPaths = await readDarwinKernelPaths(pending.map((entry) => entry.pid));
1442
- const after = new Map((await readDarwinProcessStates()).map((entry) => [entry.pid, entry]));
1702
+ while (true) {
1703
+ const kernelPaths = await readDarwinKernelPaths(pending.map((entry) => entry.pid), remaining());
1704
+ const after = new Map((await readStates()).map((entry) => [entry.pid, entry]));
1443
1705
  for (const entry of pending) {
1706
+ remaining();
1444
1707
  const current = after.get(entry.pid);
1445
1708
  if (!current) {
1446
1709
  exitedProcessCount++;
@@ -1453,11 +1716,25 @@ async function inspectExitedWechatInstallation(target, platform = process.platfo
1453
1716
  continue;
1454
1717
  }
1455
1718
  const executable = kernelPaths.get(entry.pid);
1456
- const images = executable ? [executable] : await readDarwinTextImages(entry.pid);
1719
+ let images;
1720
+ try {
1721
+ images = executable ? [executable] : await readDarwinTextImages(entry.pid, remaining());
1722
+ } catch (error) {
1723
+ const latest = await readStates().then((states) => states.find((process) => process.pid === entry.pid), (confirmationError) => {
1724
+ throw new AggregateError([error, confirmationError], "Installation-exit recovery cannot independently confirm process exit after failed image inspection.");
1725
+ });
1726
+ if (!latest) {
1727
+ exitedProcessCount++;
1728
+ continue;
1729
+ }
1730
+ if (latest.started !== entry.started) throw new Error("Installation-exit recovery process identity changed during inspection.", { cause: error });
1731
+ throw error;
1732
+ }
1733
+ remaining();
1457
1734
  if (!images.length) throw new Error("Installation-exit recovery received no verifiable executable text images.");
1458
1735
  for (const image of images) {
1459
- const resolved = await canonicalImagePath(image);
1460
- const relative = path.relative(installationRoot, resolved);
1736
+ if (!path.isAbsolute(image) || /[\r\n\0]/.test(image)) throw new Error("Installation-exit recovery received an unverifiable executable image path.");
1737
+ const relative = path.relative(installationRoot, image);
1461
1738
  if (!relative || !path.isAbsolute(relative) && relative !== ".." && !relative.startsWith(`..${path.sep}`)) throw new Error("The selected WeChat DevTools installation still has a live process; no ownership was recovered.");
1462
1739
  }
1463
1740
  if (executable) kernelPathProcessCount++;
@@ -1467,7 +1744,7 @@ async function inspectExitedWechatInstallation(target, platform = process.platfo
1467
1744
  zombie: false
1468
1745
  });
1469
1746
  }
1470
- const final = await readDarwinProcessStates();
1747
+ const final = await readStates();
1471
1748
  for (const entry of final) {
1472
1749
  const previous = verified.get(entry.pid);
1473
1750
  if (previous && (previous.started !== entry.started || previous.zombie && !entry.zombie)) throw new Error("Installation-exit recovery process identity changed during inspection.");
@@ -1475,7 +1752,7 @@ async function inspectExitedWechatInstallation(target, platform = process.platfo
1475
1752
  pending = final.filter((entry) => !verified.has(entry.pid));
1476
1753
  if (!pending.length) break;
1477
1754
  }
1478
- if (pending.length) throw new Error("Installation-exit recovery found continuing new process activity; retry after it becomes idle.");
1755
+ remaining();
1479
1756
  return {
1480
1757
  platform,
1481
1758
  installationRoot,
@@ -1488,8 +1765,7 @@ async function inspectExitedWechatInstallation(target, platform = process.platfo
1488
1765
  selectedProcessCount: 0
1489
1766
  };
1490
1767
  }
1491
- //#endregion
1492
- //#region src/devtoolsProjectOwnership/installationExit/index.ts
1768
+ // src/devtoolsProjectOwnership/installationExit/index.ts
1493
1769
  async function fingerprint(record) {
1494
1770
  const file = managedRecordPath(record.journalPath, record.id);
1495
1771
  const stat = await fs.lstat(file);
@@ -1497,7 +1773,7 @@ async function fingerprint(record) {
1497
1773
  return createHash("sha256").update(await fs.readFile(file)).digest("hex");
1498
1774
  }
1499
1775
  function canRecoverAfterExit(record) {
1500
- return record.state === "starting" || record.state === "unconfirmed" || record.state === "failed" && record.openedProjectWindow === true && record.host !== void 0 && record.port !== void 0 && record.error !== void 0;
1776
+ return record.state === "starting" || record.state === "unconfirmed" || record.state === "borrowed" && record.snapshot !== void 0 && record.openedProjectWindow === false || record.state === "failed" && record.openedProjectWindow === true && record.host !== void 0 && record.port !== void 0 && record.error !== void 0;
1501
1777
  }
1502
1778
  async function recoverAfterExit(options) {
1503
1779
  const { target, recoveryScope } = options;
@@ -1512,9 +1788,12 @@ async function recoverAfterExit(options) {
1512
1788
  if (new Set(records.map((record) => record.id)).size !== records.length) throw new Error("Installation-exit recovery found duplicate record identities.");
1513
1789
  if (records.some((record) => !isDeepStrictEqual(record.target, target))) throw new Error("Installation-exit recovery cannot include another installation or profile.");
1514
1790
  if (records.some((record) => record.state !== "released" && !canRecoverAfterExit(record))) throw new Error("Installation-exit recovery refuses confirmed or unresolved project-close ownership.");
1515
- const candidates = (await readManagedWechatProjectRecords(journalPath)).filter(canRecoverAfterExit);
1791
+ const scopeRecords = await readManagedWechatProjectRecords(journalPath);
1792
+ const candidates = scopeRecords.filter(canRecoverAfterExit);
1793
+ const pendingSnapshots = scopeRecords.filter((record) => record.state === "released" && !isManagedWechatProjectFullyReleased(record));
1516
1794
  if (!candidates.length) {
1517
1795
  await assertMachineE2ELeaseRecoveryScope(recoveryScope);
1796
+ for (const record of pendingSnapshots) await finalizeManagedProjectSnapshot(record);
1518
1797
  return { recoveredRecordIds: [] };
1519
1798
  }
1520
1799
  const before = new Map(await Promise.all(records.map(async (record) => [record.id, await fingerprint(record)])));
@@ -1544,7 +1823,7 @@ async function recoverAfterExit(options) {
1544
1823
  await assertMachineE2ELeaseRecoveryScope(recoveryScope);
1545
1824
  if (await fingerprint(record) !== before.get(record.id)) throw new Error("Installation-exit recovery journal bytes changed before recording recovery.");
1546
1825
  const previousState = record.state;
1547
- if (previousState !== "starting" && previousState !== "unconfirmed" && previousState !== "failed") throw new Error("Installation-exit recovery cannot replace a confirmed ownership state.");
1826
+ if (previousState !== "starting" && previousState !== "unconfirmed" && previousState !== "failed" && previousState !== "borrowed") throw new Error("Installation-exit recovery cannot replace a confirmed ownership state.");
1548
1827
  record.installationExitRecovery = {
1549
1828
  protocol: "wechat-devtools-installation-exit-v1",
1550
1829
  recoveredAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -1566,8 +1845,10 @@ async function recoverAfterExit(options) {
1566
1845
  record.state = "released";
1567
1846
  record.releasedReason = "installation-exited";
1568
1847
  await writeManagedRecord(record);
1848
+ await finalizeManagedProjectSnapshot(record);
1569
1849
  recoveredRecordIds.push(record.id);
1570
1850
  }
1851
+ for (const record of pendingSnapshots) await finalizeManagedProjectSnapshot(record);
1571
1852
  return { recoveredRecordIds };
1572
1853
  });
1573
1854
  }
@@ -1575,8 +1856,7 @@ async function recoverAfterExit(options) {
1575
1856
  function recoverManagedWechatProjectsAfterInstallationExit(options) {
1576
1857
  return withMachineE2ELeaseRecoveryOperation(options.recoveryScope, () => recoverAfterExit(options));
1577
1858
  }
1578
- //#endregion
1579
- //#region src/devtoolsProjectOwnership/index.ts
1859
+ // src/devtoolsProjectOwnership/index.ts
1580
1860
  const pendingClosures = /* @__PURE__ */ new Map();
1581
1861
  function describe(error) {
1582
1862
  return error instanceof Error ? error.message : String(error);
@@ -1587,7 +1867,7 @@ async function release(record, reason) {
1587
1867
  delete record.error;
1588
1868
  await writeManagedRecord(record);
1589
1869
  }
1590
- async function closeRecord(record) {
1870
+ async function closeWindowRecord(record) {
1591
1871
  if (record.state === "released") return;
1592
1872
  if (record.state === "borrowed" && record.openedProjectWindow === false) {
1593
1873
  await release(record, "borrowed");
@@ -1631,6 +1911,17 @@ async function closeRecord(record) {
1631
1911
  throw error;
1632
1912
  }
1633
1913
  }
1914
+ async function closeRecord(record) {
1915
+ if (isManagedWechatProjectFullyReleased(record)) return;
1916
+ if (record.state !== "released") {
1917
+ if (record.snapshot && !record.snapshot.dispatchedAt && record.openedProjectWindow === void 0) await release(record, "not-started");
1918
+ else {
1919
+ if (record.snapshot && record.openedProjectWindow === false) throw new Error("Managed DevTools snapshot received a borrowed window receipt; preserving files until installation exit is proven.");
1920
+ await closeWindowRecord(record);
1921
+ }
1922
+ }
1923
+ await finalizeManagedProjectSnapshot(record);
1924
+ }
1634
1925
  /** 窗口销毁与连接断开分别验证;失败保留登记供后续继续观察。 */
1635
1926
  function closeManagedWechatProject(options) {
1636
1927
  const journalPath = resolveManagedJournal(options.journalPath);
@@ -1648,6 +1939,7 @@ async function beginManagedWechatProject(options) {
1648
1939
  const journalPath = resolveManagedJournal(options.journalPath);
1649
1940
  if (!journalPath) return;
1650
1941
  const id = randomUUID();
1942
+ const projectPath = options.snapshotProject ? managedSnapshotPath(journalPath, id) : path.resolve(options.projectPath);
1651
1943
  await withManagedJournalLock(journalPath, async (scopeRoot) => {
1652
1944
  const previous = await readManagedWechatProjectRecords(scopeRoot);
1653
1945
  if (previous.some((record) => [
@@ -1655,7 +1947,7 @@ async function beginManagedWechatProject(options) {
1655
1947
  "unconfirmed",
1656
1948
  "closing",
1657
1949
  "failed"
1658
- ].includes(record.state))) throw new Error("Managed DevTools journal contains unresolved ownership or cleanup; refusing another project start.");
1950
+ ].includes(record.state) || record.state === "released" && !isManagedWechatProjectFullyReleased(record) || record.state === "borrowed" && record.snapshot?.state !== void 0 && record.snapshot.state !== "removed")) throw new Error("Managed DevTools journal contains unresolved ownership or cleanup; refusing another project start.");
1659
1951
  checkManagedWindowBudget(previous, options);
1660
1952
  const now = (/* @__PURE__ */ new Date()).toISOString();
1661
1953
  await writeManagedRecord({
@@ -1666,7 +1958,13 @@ async function beginManagedWechatProject(options) {
1666
1958
  ownerToken: managedProcessToken,
1667
1959
  ownerPid: process.pid,
1668
1960
  target: { ...options.target },
1669
- projectPath: path.resolve(options.projectPath),
1961
+ projectPath,
1962
+ ...options.snapshotProject ? { snapshot: {
1963
+ protocol: "wechat-devtools-project-snapshot-v1",
1964
+ sourceProjectPath: path.resolve(options.projectPath),
1965
+ token: randomUUID(),
1966
+ state: "reserved"
1967
+ } } : {},
1670
1968
  state: "starting",
1671
1969
  port: options.port,
1672
1970
  createdAt: now,
@@ -1676,10 +1974,36 @@ async function beginManagedWechatProject(options) {
1676
1974
  return {
1677
1975
  id,
1678
1976
  journalPath,
1977
+ projectPath,
1978
+ prepareSnapshot: async (populate) => {
1979
+ await withManagedJournalLock(journalPath, async () => {
1980
+ const record = await readManagedRecord(journalPath, id);
1981
+ try {
1982
+ await prepareManagedProjectSnapshot(record, populate);
1983
+ } catch (error) {
1984
+ if (record.snapshot) {
1985
+ record.snapshot.cleanupError = describe(error);
1986
+ await writeManagedRecord(record);
1987
+ }
1988
+ throw error;
1989
+ }
1990
+ });
1991
+ },
1992
+ markLaunchDispatched: async () => {
1993
+ await withManagedJournalLock(journalPath, async () => {
1994
+ const record = await readManagedRecord(journalPath, id);
1995
+ if (record.snapshot) {
1996
+ if (record.state !== "starting" || record.snapshot.state !== "ready" || record.snapshot.dispatchedAt) throw new Error("Managed DevTools snapshot is not ready for its first start dispatch.");
1997
+ record.snapshot.dispatchedAt = (/* @__PURE__ */ new Date()).toISOString();
1998
+ await writeManagedRecord(record);
1999
+ }
2000
+ });
2001
+ },
1679
2002
  confirm: async (result) => {
1680
2003
  await withManagedJournalLock(journalPath, async () => {
1681
2004
  const record = await readManagedRecord(journalPath, id);
1682
2005
  if (record.state !== "starting" && record.state !== "unconfirmed") throw new Error("Managed DevTools start receipt was already recorded.");
2006
+ if (record.snapshot && (!record.snapshot.dispatchedAt || record.snapshot.state !== "ready")) throw new Error("Managed DevTools snapshot start receipt has no prepared dispatch intent.");
1683
2007
  if (typeof result.openedProjectWindow !== "boolean" || !Number.isInteger(result.port) || result.port <= 0 || result.port > 65535 || record.port !== void 0 && record.port !== result.port) throw new Error("Managed DevTools start receipt does not match its requested port.");
1684
2008
  record.openedProjectWindow = result.openedProjectWindow;
1685
2009
  record.port = result.port;
@@ -1722,5 +2046,4 @@ async function cleanupManagedWechatProjects(options = {}) {
1722
2046
  }
1723
2047
  if (errors.length) throw new AggregateError(errors, "Managed DevTools project cleanup remains incomplete; stop the acceptance lane.");
1724
2048
  }
1725
- //#endregion
1726
- export { readManagedRecord as a, withManagedJournalLock as c, MANAGED_PROJECT_MAX_WINDOWS_ENV as d, assertManagedInstallation as f, startWechatIdeAgent as h, recoverManagedWechatProjectsAfterInstallationExit as i, writeManagedRecord as l, sameManagedProcess as m, cleanupManagedWechatProjects as n, readManagedWechatProjectRecords as o, inspectManagedProjectHost as p, closeManagedWechatProject as r, resolveManagedJournal as s, beginManagedWechatProject as t, MANAGED_PROJECT_JOURNAL_ENV as u };
2049
+ export { isManagedWechatProjectFullyReleased as a, resolveManagedJournal as c, assertManagedInstallation as d, inspectManagedProjectHost as f, recoverManagedWechatProjectsAfterInstallationExit as i, withManagedJournalLock as l, startWechatIdeAgent as m, cleanupManagedWechatProjects as n, readManagedRecord as o, sameManagedProcess as p, closeManagedWechatProject as r, readManagedWechatProjectRecords as s, beginManagedWechatProject as t, writeManagedRecord as u };