opensteer 0.6.11 → 0.6.13

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.
@@ -25,284 +25,2655 @@ import {
25
25
  } from "./chunk-3H5RRIMZ.js";
26
26
 
27
27
  // src/browser/persistent-profile.ts
28
- import { createHash } from "crypto";
29
- import { existsSync } from "fs";
28
+ import { createHash, randomUUID as randomUUID3 } from "crypto";
29
+ import { execFile as execFile2 } from "child_process";
30
+ import { createReadStream, existsSync as existsSync3 } from "fs";
30
31
  import {
31
32
  cp,
32
33
  copyFile,
33
- mkdir,
34
+ mkdir as mkdir3,
34
35
  mkdtemp,
35
- readdir,
36
- rename,
37
- rm,
36
+ readdir as readdir2,
37
+ readFile as readFile4,
38
+ rename as rename3,
39
+ rm as rm3,
38
40
  stat,
39
- writeFile
41
+ writeFile as writeFile3
40
42
  } from "fs/promises";
41
- import { homedir } from "os";
42
- import { basename, dirname, join } from "path";
43
- var OPENSTEER_META_FILE = ".opensteer-meta.json";
43
+ import { homedir, tmpdir } from "os";
44
+ import { basename as basename3, dirname as dirname4, join as join4, relative, sep } from "path";
45
+ import { promisify as promisify2 } from "util";
46
+
47
+ // src/browser/persistent-profile-coordination.ts
48
+ import { basename, dirname as dirname2, join as join2 } from "path";
49
+
50
+ // src/browser/dir-lock.ts
51
+ import { randomUUID } from "crypto";
52
+ import { existsSync } from "fs";
53
+ import { mkdir, readFile as readFile2, rename, rm, writeFile } from "fs/promises";
54
+ import { dirname, join } from "path";
55
+
56
+ // src/browser/process-owner.ts
57
+ import { execFile } from "child_process";
58
+ import { readFile } from "fs/promises";
59
+ import { promisify } from "util";
60
+ var execFileAsync = promisify(execFile);
44
61
  var PROCESS_STARTED_AT_MS = Math.floor(Date.now() - process.uptime() * 1e3);
45
62
  var PROCESS_START_TIME_TOLERANCE_MS = 1e3;
46
- var CHROME_SINGLETON_ENTRIES = /* @__PURE__ */ new Set([
47
- "SingletonCookie",
48
- "SingletonLock",
49
- "SingletonSocket",
50
- "DevToolsActivePort",
51
- "lockfile"
52
- ]);
53
- var COPY_SKIP_ENTRIES = /* @__PURE__ */ new Set([
54
- ...CHROME_SINGLETON_ENTRIES,
55
- OPENSTEER_META_FILE
56
- ]);
57
- var SKIPPED_ROOT_DIRECTORIES = /* @__PURE__ */ new Set([
58
- "Crash Reports",
59
- "Crashpad",
60
- "BrowserMetrics",
61
- "GrShaderCache",
62
- "ShaderCache",
63
- "GraphiteDawnCache",
64
- "component_crx_cache",
65
- "Crowd Deny",
66
- "hyphen-data",
67
- "OnDeviceHeadSuggestModel",
68
- "OptimizationGuidePredictionModels",
69
- "Segmentation Platform",
70
- "SmartCardDeviceNames",
71
- "WidevineCdm",
72
- "pnacl"
73
- ]);
74
- async function getOrCreatePersistentProfile(sourceUserDataDir, profileDirectory, profilesRootDir = defaultPersistentProfilesRootDir()) {
75
- const resolvedSourceUserDataDir = expandHome(sourceUserDataDir);
76
- const targetUserDataDir = join(
77
- expandHome(profilesRootDir),
78
- buildPersistentProfileKey(resolvedSourceUserDataDir, profileDirectory)
79
- );
80
- const sourceProfileDir = join(resolvedSourceUserDataDir, profileDirectory);
81
- const metadata = buildPersistentProfileMetadata(
82
- resolvedSourceUserDataDir,
83
- profileDirectory
84
- );
85
- await mkdir(dirname(targetUserDataDir), { recursive: true });
86
- await cleanOrphanedTempDirs(
87
- dirname(targetUserDataDir),
88
- basename(targetUserDataDir)
89
- );
90
- if (!existsSync(sourceProfileDir)) {
91
- throw new Error(
92
- `Chrome profile "${profileDirectory}" was not found in "${resolvedSourceUserDataDir}".`
93
- );
63
+ var PROCESS_LIST_MAX_BUFFER_BYTES = 16 * 1024 * 1024;
64
+ var PS_COMMAND_ENV = { ...process.env, LC_ALL: "C" };
65
+ var LINUX_STAT_START_TIME_FIELD_INDEX = 19;
66
+ var CURRENT_PROCESS_OWNER = {
67
+ pid: process.pid,
68
+ processStartedAtMs: PROCESS_STARTED_AT_MS
69
+ };
70
+ var linuxClockTicksPerSecondPromise = null;
71
+ function parseProcessOwner(value) {
72
+ if (!value || typeof value !== "object") {
73
+ return null;
74
+ }
75
+ const parsed = value;
76
+ const pid = Number(parsed.pid);
77
+ const processStartedAtMs = Number(parsed.processStartedAtMs);
78
+ if (!Number.isInteger(pid) || pid <= 0) {
79
+ return null;
80
+ }
81
+ if (!Number.isInteger(processStartedAtMs) || processStartedAtMs <= 0) {
82
+ return null;
94
83
  }
95
- const created = await createPersistentProfileClone(
96
- resolvedSourceUserDataDir,
97
- sourceProfileDir,
98
- targetUserDataDir,
99
- profileDirectory,
100
- metadata
101
- );
102
- await ensurePersistentProfileMetadata(targetUserDataDir, metadata);
103
84
  return {
104
- created,
105
- userDataDir: targetUserDataDir
85
+ pid,
86
+ processStartedAtMs
106
87
  };
107
88
  }
108
- async function clearPersistentProfileSingletons(userDataDir) {
109
- await Promise.all(
110
- [...CHROME_SINGLETON_ENTRIES].map(
111
- (entry) => rm(join(userDataDir, entry), {
112
- force: true,
113
- recursive: true
114
- }).catch(() => void 0)
115
- )
116
- );
89
+ function processOwnersEqual(left, right) {
90
+ if (!left || !right) {
91
+ return left === right;
92
+ }
93
+ return left.pid === right.pid && left.processStartedAtMs === right.processStartedAtMs;
117
94
  }
118
- function buildPersistentProfileKey(sourceUserDataDir, profileDirectory) {
119
- const hash = createHash("sha256").update(`${sourceUserDataDir}\0${profileDirectory}`).digest("hex").slice(0, 16);
120
- const sourceLabel = sanitizePathSegment(basename(sourceUserDataDir) || "user-data");
121
- const profileLabel = sanitizePathSegment(profileDirectory || "Default");
122
- return `${sourceLabel}-${profileLabel}-${hash}`;
95
+ async function getProcessLiveness(owner) {
96
+ if (owner.pid === process.pid && hasMatchingProcessStartTime(
97
+ owner.processStartedAtMs,
98
+ PROCESS_STARTED_AT_MS
99
+ )) {
100
+ return "live";
101
+ }
102
+ const startedAtMs = await readProcessStartedAtMs(owner.pid);
103
+ if (typeof startedAtMs === "number") {
104
+ return hasMatchingProcessStartTime(
105
+ owner.processStartedAtMs,
106
+ startedAtMs
107
+ ) ? "live" : "dead";
108
+ }
109
+ return isProcessRunning(owner.pid) ? "unknown" : "dead";
123
110
  }
124
- function defaultPersistentProfilesRootDir() {
125
- return join(homedir(), ".opensteer", "real-browser-profiles");
111
+ function isProcessRunning(pid) {
112
+ try {
113
+ process.kill(pid, 0);
114
+ return true;
115
+ } catch (error) {
116
+ const code = typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : void 0;
117
+ return code !== "ESRCH";
118
+ }
126
119
  }
127
- function sanitizePathSegment(value) {
128
- const sanitized = value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/-+/g, "-");
129
- return sanitized.replace(/^-|-$/g, "") || "profile";
120
+ async function readProcessOwner(pid) {
121
+ const processStartedAtMs = await readProcessStartedAtMs(pid);
122
+ if (processStartedAtMs === null) {
123
+ return null;
124
+ }
125
+ return {
126
+ pid,
127
+ processStartedAtMs
128
+ };
130
129
  }
131
- function isProfileDirectory(userDataDir, entry) {
132
- return existsSync(join(userDataDir, entry, "Preferences"));
130
+ function hasMatchingProcessStartTime(expectedStartedAtMs, actualStartedAtMs) {
131
+ return Math.abs(expectedStartedAtMs - actualStartedAtMs) <= PROCESS_START_TIME_TOLERANCE_MS;
133
132
  }
134
- async function copyRootLevelEntries(sourceUserDataDir, targetUserDataDir, targetProfileDirectory) {
135
- let entries;
133
+ async function readProcessStartedAtMs(pid) {
134
+ if (pid <= 0) {
135
+ return null;
136
+ }
137
+ if (process.platform === "linux") {
138
+ return await readLinuxProcessStartedAtMs(pid);
139
+ }
140
+ if (process.platform === "win32") {
141
+ return await readWindowsProcessStartedAtMs(pid);
142
+ }
143
+ return await readPsProcessStartedAtMs(pid);
144
+ }
145
+ async function readLinuxProcessStartedAtMs(pid) {
146
+ let statRaw;
136
147
  try {
137
- entries = await readdir(sourceUserDataDir);
148
+ statRaw = await readFile(`/proc/${pid}/stat`, "utf8");
138
149
  } catch {
139
- return;
150
+ return null;
140
151
  }
141
- const copyTasks = [];
142
- for (const entry of entries) {
143
- if (COPY_SKIP_ENTRIES.has(entry)) continue;
144
- if (entry === targetProfileDirectory) continue;
145
- const sourcePath = join(sourceUserDataDir, entry);
146
- const targetPath = join(targetUserDataDir, entry);
147
- if (existsSync(targetPath)) continue;
148
- let entryStat;
149
- try {
150
- entryStat = await stat(sourcePath);
151
- } catch {
152
- continue;
153
- }
154
- if (entryStat.isFile()) {
155
- copyTasks.push(copyFile(sourcePath, targetPath).catch(() => void 0));
156
- } else if (entryStat.isDirectory()) {
157
- if (isProfileDirectory(sourceUserDataDir, entry)) continue;
158
- if (SKIPPED_ROOT_DIRECTORIES.has(entry)) continue;
159
- copyTasks.push(
160
- cp(sourcePath, targetPath, { recursive: true }).catch(
161
- () => void 0
162
- )
163
- );
164
- }
152
+ const startTicks = parseLinuxProcessStartTicks(statRaw);
153
+ if (startTicks === null) {
154
+ return null;
165
155
  }
166
- await Promise.all(copyTasks);
167
- }
168
- async function writePersistentProfileMetadata(userDataDir, metadata) {
169
- await writeFile(
170
- join(userDataDir, OPENSTEER_META_FILE),
171
- JSON.stringify(metadata, null, 2)
156
+ const [bootTimeMs, clockTicksPerSecond] = await Promise.all([
157
+ readLinuxBootTimeMs(),
158
+ readLinuxClockTicksPerSecond()
159
+ ]);
160
+ if (bootTimeMs === null || clockTicksPerSecond === null) {
161
+ return null;
162
+ }
163
+ return Math.floor(
164
+ bootTimeMs + startTicks * 1e3 / clockTicksPerSecond
172
165
  );
173
166
  }
174
- function buildPersistentProfileMetadata(sourceUserDataDir, profileDirectory) {
175
- return {
176
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
177
- profileDirectory,
178
- source: sourceUserDataDir
179
- };
167
+ function parseLinuxProcessStartTicks(statRaw) {
168
+ const closingParenIndex = statRaw.lastIndexOf(")");
169
+ if (closingParenIndex === -1) {
170
+ return null;
171
+ }
172
+ const fields = statRaw.slice(closingParenIndex + 2).trim().split(/\s+/);
173
+ const startTicks = Number(fields[LINUX_STAT_START_TIME_FIELD_INDEX]);
174
+ return Number.isFinite(startTicks) && startTicks >= 0 ? startTicks : null;
180
175
  }
181
- async function createPersistentProfileClone(sourceUserDataDir, sourceProfileDir, targetUserDataDir, profileDirectory, metadata) {
182
- if (existsSync(targetUserDataDir)) {
183
- return false;
176
+ async function readLinuxBootTimeMs() {
177
+ try {
178
+ const statRaw = await readFile("/proc/stat", "utf8");
179
+ const bootTimeLine = statRaw.split("\n").find((line) => line.startsWith("btime "));
180
+ if (!bootTimeLine) {
181
+ return null;
182
+ }
183
+ const bootTimeSeconds = Number.parseInt(
184
+ bootTimeLine.slice("btime ".length),
185
+ 10
186
+ );
187
+ return Number.isFinite(bootTimeSeconds) ? bootTimeSeconds * 1e3 : null;
188
+ } catch {
189
+ return null;
184
190
  }
185
- const tempUserDataDir = await mkdtemp(
186
- buildPersistentProfileTempDirPrefix(targetUserDataDir)
187
- );
188
- let published = false;
191
+ }
192
+ async function readLinuxClockTicksPerSecond() {
193
+ if (!linuxClockTicksPerSecondPromise) {
194
+ linuxClockTicksPerSecondPromise = execFileAsync("getconf", ["CLK_TCK"], {
195
+ encoding: "utf8",
196
+ maxBuffer: PROCESS_LIST_MAX_BUFFER_BYTES
197
+ }).then(({ stdout }) => {
198
+ const value = Number.parseInt(stdout.trim(), 10);
199
+ return Number.isFinite(value) && value > 0 ? value : null;
200
+ }).catch(() => null);
201
+ }
202
+ return await linuxClockTicksPerSecondPromise;
203
+ }
204
+ async function readWindowsProcessStartedAtMs(pid) {
189
205
  try {
190
- await cp(sourceProfileDir, join(tempUserDataDir, profileDirectory), {
191
- recursive: true
192
- });
193
- await copyRootLevelEntries(
194
- sourceUserDataDir,
195
- tempUserDataDir,
196
- profileDirectory
206
+ const { stdout } = await execFileAsync(
207
+ "powershell.exe",
208
+ [
209
+ "-NoProfile",
210
+ "-Command",
211
+ `(Get-Process -Id ${pid}).StartTime.ToUniversalTime().ToString("o")`
212
+ ],
213
+ {
214
+ encoding: "utf8",
215
+ maxBuffer: PROCESS_LIST_MAX_BUFFER_BYTES
216
+ }
197
217
  );
198
- await writePersistentProfileMetadata(tempUserDataDir, metadata);
199
- try {
200
- await rename(tempUserDataDir, targetUserDataDir);
201
- } catch (error) {
202
- if (wasProfilePublishedByAnotherProcess(error, targetUserDataDir)) {
203
- return false;
218
+ const isoTimestamp = stdout.trim();
219
+ if (!isoTimestamp) {
220
+ return null;
221
+ }
222
+ const startedAtMs = Date.parse(isoTimestamp);
223
+ return Number.isFinite(startedAtMs) ? startedAtMs : null;
224
+ } catch {
225
+ return null;
226
+ }
227
+ }
228
+ async function readPsProcessStartedAtMs(pid) {
229
+ try {
230
+ const { stdout } = await execFileAsync(
231
+ "ps",
232
+ ["-o", "lstart=", "-p", String(pid)],
233
+ {
234
+ encoding: "utf8",
235
+ env: PS_COMMAND_ENV,
236
+ maxBuffer: PROCESS_LIST_MAX_BUFFER_BYTES
204
237
  }
205
- throw error;
238
+ );
239
+ const startedAt = stdout.trim();
240
+ if (!startedAt) {
241
+ return null;
206
242
  }
207
- published = true;
208
- return true;
243
+ const startedAtMs = Date.parse(startedAt.replace(/\s+/g, " "));
244
+ return Number.isFinite(startedAtMs) ? startedAtMs : null;
245
+ } catch {
246
+ return null;
247
+ }
248
+ }
249
+
250
+ // src/browser/dir-lock.ts
251
+ var LOCK_OWNER_FILE = "owner.json";
252
+ var LOCK_RECLAIMER_DIR = "reclaimer";
253
+ var LOCK_RETRY_DELAY_MS = 50;
254
+ async function withDirLock(lockDirPath, action) {
255
+ const releaseLock = await acquireDirLock(lockDirPath);
256
+ try {
257
+ return await action();
209
258
  } finally {
210
- if (!published) {
211
- await rm(tempUserDataDir, {
259
+ await releaseLock();
260
+ }
261
+ }
262
+ async function acquireDirLock(lockDirPath) {
263
+ while (true) {
264
+ const releaseLock = await tryAcquireDirLock(lockDirPath);
265
+ if (releaseLock) {
266
+ return releaseLock;
267
+ }
268
+ await sleep(LOCK_RETRY_DELAY_MS);
269
+ }
270
+ }
271
+ async function tryAcquireDirLock(lockDirPath) {
272
+ await mkdir(dirname(lockDirPath), { recursive: true });
273
+ while (true) {
274
+ const tempLockDirPath = `${lockDirPath}-${process.pid}-${CURRENT_PROCESS_OWNER.processStartedAtMs}-${randomUUID()}`;
275
+ try {
276
+ await mkdir(tempLockDirPath);
277
+ await writeLockOwner(tempLockDirPath, CURRENT_PROCESS_OWNER);
278
+ try {
279
+ await rename(tempLockDirPath, lockDirPath);
280
+ break;
281
+ } catch (error) {
282
+ if (!wasDirPublishedByAnotherProcess(error, lockDirPath)) {
283
+ throw error;
284
+ }
285
+ }
286
+ } finally {
287
+ await rm(tempLockDirPath, {
212
288
  recursive: true,
213
289
  force: true
214
290
  }).catch(() => void 0);
215
291
  }
292
+ const owner = await readLockOwner(lockDirPath);
293
+ if ((!owner || await getProcessLiveness(owner) === "dead") && await tryReclaimStaleLock(lockDirPath, owner)) {
294
+ continue;
295
+ }
296
+ return null;
216
297
  }
298
+ return async () => {
299
+ await rm(lockDirPath, {
300
+ recursive: true,
301
+ force: true
302
+ }).catch(() => void 0);
303
+ };
217
304
  }
218
- async function ensurePersistentProfileMetadata(userDataDir, metadata) {
219
- if (existsSync(join(userDataDir, OPENSTEER_META_FILE))) {
220
- return;
305
+ async function isDirLockHeld(lockDirPath) {
306
+ if (!existsSync(lockDirPath)) {
307
+ return false;
221
308
  }
222
- await writePersistentProfileMetadata(userDataDir, metadata);
309
+ const owner = await readLockOwner(lockDirPath);
310
+ if ((!owner || await getProcessLiveness(owner) === "dead") && await tryReclaimStaleLock(lockDirPath, owner)) {
311
+ return false;
312
+ }
313
+ return existsSync(lockDirPath);
223
314
  }
224
- function wasProfilePublishedByAnotherProcess(error, targetUserDataDir) {
225
- const code = typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : void 0;
226
- return existsSync(targetUserDataDir) && (code === "EEXIST" || code === "ENOTEMPTY" || code === "EPERM");
315
+ function getErrorCode(error) {
316
+ return typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : void 0;
227
317
  }
228
- function buildPersistentProfileTempDirPrefix(targetUserDataDir) {
229
- return join(
230
- dirname(targetUserDataDir),
231
- `${basename(targetUserDataDir)}-tmp-${process.pid}-${PROCESS_STARTED_AT_MS}-`
318
+ function wasDirPublishedByAnotherProcess(error, targetDirPath) {
319
+ const code = getErrorCode(error);
320
+ return existsSync(targetDirPath) && (code === "EEXIST" || code === "ENOTEMPTY" || code === "EPERM");
321
+ }
322
+ async function writeLockOwner(lockDirPath, owner) {
323
+ await writeFile(join(lockDirPath, LOCK_OWNER_FILE), JSON.stringify(owner));
324
+ }
325
+ async function readLockOwner(lockDirPath) {
326
+ return await readLockParticipant(join(lockDirPath, LOCK_OWNER_FILE));
327
+ }
328
+ async function readLockParticipant(filePath) {
329
+ return (await readLockParticipantRecord(filePath)).owner;
330
+ }
331
+ async function readLockParticipantRecord(filePath) {
332
+ try {
333
+ const raw = await readFile2(filePath, "utf8");
334
+ const owner = parseProcessOwner(JSON.parse(raw));
335
+ return {
336
+ exists: true,
337
+ owner
338
+ };
339
+ } catch (error) {
340
+ return {
341
+ exists: getErrorCode(error) !== "ENOENT",
342
+ owner: null
343
+ };
344
+ }
345
+ }
346
+ async function readLockReclaimerRecord(lockDirPath) {
347
+ return await readLockParticipantRecord(
348
+ join(buildLockReclaimerDirPath(lockDirPath), LOCK_OWNER_FILE)
232
349
  );
233
350
  }
234
- async function cleanOrphanedTempDirs(profilesDir, targetBaseName) {
235
- let entries;
351
+ async function tryReclaimStaleLock(lockDirPath, expectedOwner) {
352
+ if (!await tryAcquireLockReclaimer(lockDirPath)) {
353
+ return false;
354
+ }
355
+ let reclaimed = false;
236
356
  try {
237
- entries = await readdir(profilesDir, {
238
- encoding: "utf8",
239
- withFileTypes: true
240
- });
241
- } catch {
242
- return;
357
+ const owner = await readLockOwner(lockDirPath);
358
+ if (!processOwnersEqual(owner, expectedOwner)) {
359
+ return false;
360
+ }
361
+ if (owner && await getProcessLiveness(owner) !== "dead") {
362
+ return false;
363
+ }
364
+ await rm(lockDirPath, {
365
+ recursive: true,
366
+ force: true
367
+ }).catch(() => void 0);
368
+ reclaimed = !existsSync(lockDirPath);
369
+ return reclaimed;
370
+ } finally {
371
+ if (!reclaimed) {
372
+ await rm(buildLockReclaimerDirPath(lockDirPath), {
373
+ recursive: true,
374
+ force: true
375
+ }).catch(() => void 0);
376
+ }
243
377
  }
244
- const tempDirPrefix = `${targetBaseName}-tmp-`;
245
- await Promise.all(
246
- entries.map(async (entry) => {
247
- if (!entry.isDirectory() || !entry.name.startsWith(tempDirPrefix)) {
248
- return;
378
+ }
379
+ async function tryAcquireLockReclaimer(lockDirPath) {
380
+ const reclaimerDirPath = buildLockReclaimerDirPath(lockDirPath);
381
+ while (true) {
382
+ const tempReclaimerDirPath = `${reclaimerDirPath}-${process.pid}-${CURRENT_PROCESS_OWNER.processStartedAtMs}-${randomUUID()}`;
383
+ try {
384
+ await mkdir(tempReclaimerDirPath);
385
+ await writeLockOwner(tempReclaimerDirPath, CURRENT_PROCESS_OWNER);
386
+ try {
387
+ await rename(tempReclaimerDirPath, reclaimerDirPath);
388
+ return true;
389
+ } catch (error) {
390
+ if (getErrorCode(error) === "ENOENT") {
391
+ return false;
392
+ }
393
+ if (!wasDirPublishedByAnotherProcess(error, reclaimerDirPath)) {
394
+ throw error;
395
+ }
249
396
  }
250
- if (isTempDirOwnedByLiveProcess(entry.name, tempDirPrefix)) {
251
- return;
397
+ } catch (error) {
398
+ const code = getErrorCode(error);
399
+ if (code === "ENOENT") {
400
+ return false;
401
+ }
402
+ throw error;
403
+ } finally {
404
+ await rm(tempReclaimerDirPath, {
405
+ recursive: true,
406
+ force: true
407
+ }).catch(() => void 0);
408
+ }
409
+ const reclaimerRecord = await readLockReclaimerRecord(lockDirPath);
410
+ if (!reclaimerRecord.exists || !reclaimerRecord.owner) {
411
+ return false;
412
+ }
413
+ if (await getProcessLiveness(reclaimerRecord.owner) !== "dead") {
414
+ return false;
415
+ }
416
+ await rm(reclaimerDirPath, {
417
+ recursive: true,
418
+ force: true
419
+ }).catch(() => void 0);
420
+ }
421
+ }
422
+ function buildLockReclaimerDirPath(lockDirPath) {
423
+ return join(lockDirPath, LOCK_RECLAIMER_DIR);
424
+ }
425
+ async function sleep(ms) {
426
+ await new Promise((resolve) => setTimeout(resolve, ms));
427
+ }
428
+
429
+ // src/browser/persistent-profile-coordination.ts
430
+ var PERSISTENT_PROFILE_LOCK_RETRY_DELAY_MS = 50;
431
+ async function withPersistentProfileControlLock(targetUserDataDir, action) {
432
+ return await withDirLock(
433
+ buildPersistentProfileControlLockDirPath(targetUserDataDir),
434
+ action
435
+ );
436
+ }
437
+ async function acquirePersistentProfileWriteLock(targetUserDataDir) {
438
+ const controlLockDirPath = buildPersistentProfileControlLockDirPath(targetUserDataDir);
439
+ const writeLockDirPath = buildPersistentProfileWriteLockDirPath(
440
+ targetUserDataDir
441
+ );
442
+ while (true) {
443
+ let releaseWriteLock = null;
444
+ const releaseControlLock = await acquireDirLock(controlLockDirPath);
445
+ try {
446
+ releaseWriteLock = await tryAcquireDirLock(writeLockDirPath);
447
+ } finally {
448
+ await releaseControlLock();
449
+ }
450
+ if (releaseWriteLock) {
451
+ return releaseWriteLock;
452
+ }
453
+ await sleep2(PERSISTENT_PROFILE_LOCK_RETRY_DELAY_MS);
454
+ }
455
+ }
456
+ async function isPersistentProfileWriteLocked(targetUserDataDir) {
457
+ return await isDirLockHeld(
458
+ buildPersistentProfileWriteLockDirPath(targetUserDataDir)
459
+ );
460
+ }
461
+ function buildPersistentProfileWriteLockDirPath(targetUserDataDir) {
462
+ return join2(dirname2(targetUserDataDir), `${basename(targetUserDataDir)}.lock`);
463
+ }
464
+ function buildPersistentProfileControlLockDirPath(targetUserDataDir) {
465
+ return join2(
466
+ dirname2(targetUserDataDir),
467
+ `${basename(targetUserDataDir)}.control.lock`
468
+ );
469
+ }
470
+ async function sleep2(ms) {
471
+ await new Promise((resolve) => setTimeout(resolve, ms));
472
+ }
473
+
474
+ // src/browser/shared-real-browser-session-state.ts
475
+ import { randomUUID as randomUUID2 } from "crypto";
476
+ import { existsSync as existsSync2 } from "fs";
477
+ import { mkdir as mkdir2, readFile as readFile3, readdir, rename as rename2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
478
+ import { basename as basename2, dirname as dirname3, join as join3 } from "path";
479
+ var SHARED_SESSION_METADATA_FILE = "session.json";
480
+ var SHARED_SESSION_CLIENTS_DIR = "clients";
481
+ var SHARED_SESSION_RETRY_DELAY_MS = 50;
482
+ var SHARED_SESSION_METADATA_TEMP_FILE_PREFIX = `${SHARED_SESSION_METADATA_FILE}.`;
483
+ var SHARED_SESSION_METADATA_TEMP_FILE_SUFFIX = ".tmp";
484
+ function buildSharedSessionDirPath(persistentUserDataDir) {
485
+ return join3(
486
+ dirname3(persistentUserDataDir),
487
+ `${basename2(persistentUserDataDir)}.session`
488
+ );
489
+ }
490
+ function buildSharedSessionLockPath(persistentUserDataDir) {
491
+ return `${buildSharedSessionDirPath(persistentUserDataDir)}.lock`;
492
+ }
493
+ function buildSharedSessionClientsDirPath(persistentUserDataDir) {
494
+ return join3(
495
+ buildSharedSessionDirPath(persistentUserDataDir),
496
+ SHARED_SESSION_CLIENTS_DIR
497
+ );
498
+ }
499
+ function buildSharedSessionClientPath(persistentUserDataDir, clientId) {
500
+ return join3(
501
+ buildSharedSessionClientsDirPath(persistentUserDataDir),
502
+ `${clientId}.json`
503
+ );
504
+ }
505
+ async function readSharedSessionMetadata(persistentUserDataDir) {
506
+ return (await readSharedSessionMetadataRecord(persistentUserDataDir)).metadata;
507
+ }
508
+ async function writeSharedSessionMetadata(persistentUserDataDir, metadata) {
509
+ const sessionDirPath = buildSharedSessionDirPath(persistentUserDataDir);
510
+ const metadataPath = buildSharedSessionMetadataPath(persistentUserDataDir);
511
+ const tempPath = buildSharedSessionMetadataTempPath(sessionDirPath);
512
+ await mkdir2(sessionDirPath, { recursive: true });
513
+ try {
514
+ await writeFile2(tempPath, JSON.stringify(metadata, null, 2));
515
+ await rename2(tempPath, metadataPath);
516
+ } finally {
517
+ await rm2(tempPath, { force: true }).catch(() => void 0);
518
+ }
519
+ }
520
+ async function hasLiveSharedRealBrowserSession(persistentUserDataDir) {
521
+ const sessionDirPath = buildSharedSessionDirPath(persistentUserDataDir);
522
+ const metadataRecord = await readSharedSessionMetadataRecord(
523
+ persistentUserDataDir
524
+ );
525
+ if (!metadataRecord.exists) {
526
+ return await hasLiveSharedSessionPublisherOrClients(sessionDirPath);
527
+ }
528
+ if (!metadataRecord.metadata) {
529
+ return true;
530
+ }
531
+ if (await getProcessLiveness(metadataRecord.metadata.browserOwner) === "dead") {
532
+ await rm2(sessionDirPath, {
533
+ force: true,
534
+ recursive: true
535
+ }).catch(() => void 0);
536
+ return false;
537
+ }
538
+ return true;
539
+ }
540
+ async function waitForSharedRealBrowserSessionToDrain(persistentUserDataDir) {
541
+ while (true) {
542
+ if (!await hasLiveSharedRealBrowserSession(persistentUserDataDir)) {
543
+ return;
544
+ }
545
+ await sleep3(SHARED_SESSION_RETRY_DELAY_MS);
546
+ }
547
+ }
548
+ async function readSharedSessionMetadataRecord(persistentUserDataDir) {
549
+ try {
550
+ const raw = await readFile3(
551
+ buildSharedSessionMetadataPath(persistentUserDataDir),
552
+ "utf8"
553
+ );
554
+ return {
555
+ exists: true,
556
+ metadata: parseSharedSessionMetadata(JSON.parse(raw))
557
+ };
558
+ } catch (error) {
559
+ return {
560
+ exists: getErrorCode2(error) !== "ENOENT",
561
+ metadata: null
562
+ };
563
+ }
564
+ }
565
+ async function hasLiveSharedSessionPublisherOrClients(sessionDirPath) {
566
+ if (!existsSync2(sessionDirPath)) {
567
+ return false;
568
+ }
569
+ let entries;
570
+ try {
571
+ entries = await readDirNames(sessionDirPath);
572
+ } catch (error) {
573
+ return getErrorCode2(error) !== "ENOENT";
574
+ }
575
+ let hasUnknownEntries = false;
576
+ for (const entry of entries) {
577
+ if (entry === SHARED_SESSION_METADATA_FILE) {
578
+ return true;
579
+ }
580
+ if (entry === SHARED_SESSION_CLIENTS_DIR) {
581
+ if (await hasDirectoryEntries(join3(sessionDirPath, entry))) {
582
+ return true;
583
+ }
584
+ continue;
585
+ }
586
+ const owner = parseSharedSessionMetadataTempOwner(entry);
587
+ if (!owner) {
588
+ if (isSharedSessionMetadataTempFile(entry)) {
589
+ continue;
590
+ }
591
+ hasUnknownEntries = true;
592
+ continue;
593
+ }
594
+ if (await getProcessLiveness(owner) !== "dead") {
595
+ return true;
596
+ }
597
+ }
598
+ if (hasUnknownEntries) {
599
+ return true;
600
+ }
601
+ await rm2(sessionDirPath, {
602
+ force: true,
603
+ recursive: true
604
+ }).catch(() => void 0);
605
+ return false;
606
+ }
607
+ function buildSharedSessionMetadataPath(persistentUserDataDir) {
608
+ return join3(
609
+ buildSharedSessionDirPath(persistentUserDataDir),
610
+ SHARED_SESSION_METADATA_FILE
611
+ );
612
+ }
613
+ function buildSharedSessionMetadataTempPath(sessionDirPath) {
614
+ return join3(
615
+ sessionDirPath,
616
+ [
617
+ SHARED_SESSION_METADATA_FILE,
618
+ CURRENT_PROCESS_OWNER.pid,
619
+ CURRENT_PROCESS_OWNER.processStartedAtMs,
620
+ randomUUID2(),
621
+ "tmp"
622
+ ].join(".")
623
+ );
624
+ }
625
+ function parseSharedSessionMetadata(value) {
626
+ if (!value || typeof value !== "object") {
627
+ return null;
628
+ }
629
+ const parsed = value;
630
+ const browserOwner = parseProcessOwner(parsed.browserOwner);
631
+ const stateOwner = parseProcessOwner(parsed.stateOwner);
632
+ const state = parsed.state === "launching" || parsed.state === "ready" || parsed.state === "closing" ? parsed.state : null;
633
+ if (!browserOwner || !stateOwner || typeof parsed.createdAt !== "string" || typeof parsed.debugPort !== "number" || typeof parsed.executablePath !== "string" || typeof parsed.headless !== "boolean" || typeof parsed.persistentUserDataDir !== "string" || typeof parsed.profileDirectory !== "string" || typeof parsed.sessionId !== "string" || !state) {
634
+ return null;
635
+ }
636
+ return {
637
+ browserOwner,
638
+ createdAt: parsed.createdAt,
639
+ debugPort: parsed.debugPort,
640
+ executablePath: parsed.executablePath,
641
+ headless: parsed.headless,
642
+ persistentUserDataDir: parsed.persistentUserDataDir,
643
+ profileDirectory: parsed.profileDirectory,
644
+ sessionId: parsed.sessionId,
645
+ state,
646
+ stateOwner
647
+ };
648
+ }
649
+ function parseSharedSessionMetadataTempOwner(entryName) {
650
+ if (!isSharedSessionMetadataTempFile(entryName)) {
651
+ return null;
652
+ }
653
+ const segments = entryName.split(".");
654
+ if (segments.length < 5) {
655
+ return null;
656
+ }
657
+ return parseProcessOwner({
658
+ pid: Number.parseInt(segments[2] ?? "", 10),
659
+ processStartedAtMs: Number.parseInt(segments[3] ?? "", 10)
660
+ });
661
+ }
662
+ function isSharedSessionMetadataTempFile(entryName) {
663
+ return entryName.startsWith(SHARED_SESSION_METADATA_TEMP_FILE_PREFIX) && entryName.endsWith(SHARED_SESSION_METADATA_TEMP_FILE_SUFFIX);
664
+ }
665
+ function getErrorCode2(error) {
666
+ return typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : void 0;
667
+ }
668
+ async function hasDirectoryEntries(dirPath) {
669
+ try {
670
+ return (await readDirNames(dirPath)).length > 0;
671
+ } catch (error) {
672
+ return getErrorCode2(error) !== "ENOENT";
673
+ }
674
+ }
675
+ async function readDirNames(dirPath) {
676
+ return await readdir(dirPath, { encoding: "utf8" });
677
+ }
678
+ async function sleep3(ms) {
679
+ await new Promise((resolve) => setTimeout(resolve, ms));
680
+ }
681
+
682
+ // src/browser/persistent-profile.ts
683
+ var execFileAsync2 = promisify2(execFile2);
684
+ var OPENSTEER_META_FILE = ".opensteer-meta.json";
685
+ var OPENSTEER_RUNTIME_META_FILE = ".opensteer-runtime.json";
686
+ var OPENSTEER_RUNTIME_CREATING_FILE = ".opensteer-runtime-creating.json";
687
+ var PROCESS_LIST_MAX_BUFFER_BYTES2 = 16 * 1024 * 1024;
688
+ var PS_COMMAND_ENV2 = { ...process.env, LC_ALL: "C" };
689
+ var CHROME_SINGLETON_ENTRIES = /* @__PURE__ */ new Set([
690
+ "SingletonCookie",
691
+ "SingletonLock",
692
+ "SingletonSocket",
693
+ "DevToolsActivePort",
694
+ "lockfile"
695
+ ]);
696
+ var COPY_SKIP_ENTRIES = /* @__PURE__ */ new Set([
697
+ ...CHROME_SINGLETON_ENTRIES,
698
+ OPENSTEER_META_FILE,
699
+ OPENSTEER_RUNTIME_META_FILE,
700
+ OPENSTEER_RUNTIME_CREATING_FILE
701
+ ]);
702
+ var SKIPPED_ROOT_DIRECTORIES = /* @__PURE__ */ new Set([
703
+ "Crash Reports",
704
+ "Crashpad",
705
+ "BrowserMetrics",
706
+ "GrShaderCache",
707
+ "ShaderCache",
708
+ "GraphiteDawnCache",
709
+ "component_crx_cache",
710
+ "Crowd Deny",
711
+ "hyphen-data",
712
+ "OnDeviceHeadSuggestModel",
713
+ "OptimizationGuidePredictionModels",
714
+ "Segmentation Platform",
715
+ "SmartCardDeviceNames",
716
+ "WidevineCdm",
717
+ "pnacl"
718
+ ]);
719
+ async function getOrCreatePersistentProfile(sourceUserDataDir, profileDirectory, profilesRootDir = defaultPersistentProfilesRootDir()) {
720
+ const resolvedSourceUserDataDir = expandHome(sourceUserDataDir);
721
+ const targetUserDataDir = join4(
722
+ expandHome(profilesRootDir),
723
+ buildPersistentProfileKey(resolvedSourceUserDataDir, profileDirectory)
724
+ );
725
+ const sourceProfileDir = join4(resolvedSourceUserDataDir, profileDirectory);
726
+ const metadata = buildPersistentProfileMetadata(
727
+ resolvedSourceUserDataDir,
728
+ profileDirectory
729
+ );
730
+ await mkdir3(dirname4(targetUserDataDir), { recursive: true });
731
+ if (await isHealthyPersistentProfile(
732
+ targetUserDataDir,
733
+ resolvedSourceUserDataDir,
734
+ profileDirectory
735
+ ) && !await isPersistentProfileWriteLocked(targetUserDataDir)) {
736
+ return {
737
+ created: false,
738
+ userDataDir: targetUserDataDir
739
+ };
740
+ }
741
+ return await withPersistentProfileWriteAccess(targetUserDataDir, async () => {
742
+ await recoverPersistentProfileBackup(targetUserDataDir);
743
+ await cleanOrphanedOwnedDirs(
744
+ dirname4(targetUserDataDir),
745
+ buildPersistentProfileTempDirNamePrefix(targetUserDataDir)
746
+ );
747
+ if (!existsSync3(sourceProfileDir)) {
748
+ throw new Error(
749
+ `Chrome profile "${profileDirectory}" was not found in "${resolvedSourceUserDataDir}".`
750
+ );
751
+ }
752
+ const created = await createPersistentProfileClone(
753
+ resolvedSourceUserDataDir,
754
+ sourceProfileDir,
755
+ targetUserDataDir,
756
+ profileDirectory,
757
+ metadata
758
+ );
759
+ await ensurePersistentProfileMetadata(targetUserDataDir, metadata);
760
+ return {
761
+ created,
762
+ userDataDir: targetUserDataDir
763
+ };
764
+ });
765
+ }
766
+ async function clearPersistentProfileSingletons(userDataDir) {
767
+ await Promise.all(
768
+ [...CHROME_SINGLETON_ENTRIES].map(
769
+ (entry) => rm3(join4(userDataDir, entry), {
770
+ force: true,
771
+ recursive: true
772
+ }).catch(() => void 0)
773
+ )
774
+ );
775
+ }
776
+ async function createIsolatedRuntimeProfile(sourceUserDataDir, runtimesRootDir = defaultRuntimeProfilesRootDir()) {
777
+ const resolvedSourceUserDataDir = expandHome(sourceUserDataDir);
778
+ const runtimeRootDir = expandHome(runtimesRootDir);
779
+ await mkdir3(runtimeRootDir, { recursive: true });
780
+ const sourceMetadata = await requirePersistentProfileMetadata(
781
+ resolvedSourceUserDataDir
782
+ );
783
+ const runtimeProfile = await reserveRuntimeProfileCreation(
784
+ resolvedSourceUserDataDir,
785
+ runtimeRootDir,
786
+ sourceMetadata.profileDirectory
787
+ );
788
+ try {
789
+ await cleanOrphanedRuntimeProfileDirs(
790
+ runtimeRootDir,
791
+ buildRuntimeProfileDirNamePrefix(resolvedSourceUserDataDir)
792
+ );
793
+ await copyUserDataDirSnapshot(
794
+ resolvedSourceUserDataDir,
795
+ runtimeProfile.userDataDir
796
+ );
797
+ const currentSourceMetadata = await readPersistentProfileMetadata(
798
+ resolvedSourceUserDataDir
799
+ );
800
+ await writeRuntimeProfileMetadata(
801
+ runtimeProfile.userDataDir,
802
+ await buildRuntimeProfileMetadata(
803
+ runtimeProfile.userDataDir,
804
+ resolvedSourceUserDataDir,
805
+ currentSourceMetadata?.profileDirectory ?? sourceMetadata.profileDirectory
806
+ )
807
+ );
808
+ await clearRuntimeProfileCreationState(
809
+ runtimeProfile.userDataDir,
810
+ resolvedSourceUserDataDir
811
+ );
812
+ return {
813
+ persistentUserDataDir: resolvedSourceUserDataDir,
814
+ userDataDir: runtimeProfile.userDataDir
815
+ };
816
+ } catch (error) {
817
+ await clearRuntimeProfileCreationState(
818
+ runtimeProfile.userDataDir,
819
+ resolvedSourceUserDataDir
820
+ );
821
+ await rm3(runtimeProfile.userDataDir, {
822
+ recursive: true,
823
+ force: true
824
+ }).catch(() => void 0);
825
+ throw error;
826
+ }
827
+ }
828
+ async function persistIsolatedRuntimeProfile(runtimeUserDataDir, persistentUserDataDir) {
829
+ const resolvedRuntimeUserDataDir = expandHome(runtimeUserDataDir);
830
+ const resolvedPersistentUserDataDir = expandHome(persistentUserDataDir);
831
+ let claimedRuntimeUserDataDir = null;
832
+ try {
833
+ await withPersistentProfileWriteAccess(
834
+ resolvedPersistentUserDataDir,
835
+ async () => {
836
+ await mkdir3(dirname4(resolvedPersistentUserDataDir), {
837
+ recursive: true
838
+ });
839
+ await recoverPersistentProfileBackup(resolvedPersistentUserDataDir);
840
+ await cleanOrphanedOwnedDirs(
841
+ dirname4(resolvedPersistentUserDataDir),
842
+ buildPersistentProfileTempDirNamePrefix(
843
+ resolvedPersistentUserDataDir
844
+ )
845
+ );
846
+ const metadata = await requirePersistentProfileMetadata(
847
+ resolvedPersistentUserDataDir
848
+ );
849
+ claimedRuntimeUserDataDir = await claimRuntimeProfileForPersist(
850
+ resolvedRuntimeUserDataDir
851
+ );
852
+ const runtimeMetadata = await requireRuntimeProfileMetadata(
853
+ claimedRuntimeUserDataDir,
854
+ resolvedPersistentUserDataDir,
855
+ metadata.profileDirectory,
856
+ resolvedRuntimeUserDataDir
857
+ );
858
+ await mergePersistentProfileSnapshot(
859
+ claimedRuntimeUserDataDir,
860
+ resolvedPersistentUserDataDir,
861
+ metadata,
862
+ runtimeMetadata
863
+ );
864
+ }
865
+ );
866
+ } catch (error) {
867
+ if (claimedRuntimeUserDataDir) {
868
+ try {
869
+ await restoreClaimedRuntimeProfile(
870
+ claimedRuntimeUserDataDir,
871
+ resolvedRuntimeUserDataDir
872
+ );
873
+ } catch (restoreError) {
874
+ throw new AggregateError(
875
+ [error, restoreError],
876
+ `Failed to restore runtime profile "${resolvedRuntimeUserDataDir}" after persistence failed.`
877
+ );
878
+ }
879
+ }
880
+ throw error;
881
+ }
882
+ if (claimedRuntimeUserDataDir) {
883
+ await rm3(claimedRuntimeUserDataDir, {
884
+ recursive: true,
885
+ force: true
886
+ }).catch(() => void 0);
887
+ }
888
+ }
889
+ function buildPersistentProfileKey(sourceUserDataDir, profileDirectory) {
890
+ const hash = createHash("sha256").update(`${sourceUserDataDir}\0${profileDirectory}`).digest("hex").slice(0, 16);
891
+ const sourceLabel = sanitizePathSegment(basename3(sourceUserDataDir) || "user-data");
892
+ const profileLabel = sanitizePathSegment(profileDirectory || "Default");
893
+ return `${sourceLabel}-${profileLabel}-${hash}`;
894
+ }
895
+ function defaultPersistentProfilesRootDir() {
896
+ return join4(homedir(), ".opensteer", "real-browser-profiles");
897
+ }
898
+ function defaultRuntimeProfilesRootDir() {
899
+ return join4(tmpdir(), "opensteer-real-browser-runtimes");
900
+ }
901
+ function sanitizePathSegment(value) {
902
+ const sanitized = value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/-+/g, "-");
903
+ return sanitized.replace(/^-|-$/g, "") || "profile";
904
+ }
905
+ function isProfileDirectory(userDataDir, entry) {
906
+ return existsSync3(join4(userDataDir, entry, "Preferences"));
907
+ }
908
+ async function copyUserDataDirSnapshot(sourceUserDataDir, targetUserDataDir) {
909
+ await cp(sourceUserDataDir, targetUserDataDir, {
910
+ recursive: true,
911
+ filter: (candidatePath) => shouldCopyRuntimeSnapshotEntry(sourceUserDataDir, candidatePath)
912
+ });
913
+ }
914
+ function shouldCopyRuntimeSnapshotEntry(userDataDir, candidatePath) {
915
+ const candidateRelativePath = relative(userDataDir, candidatePath);
916
+ if (!candidateRelativePath) {
917
+ return true;
918
+ }
919
+ const segments = candidateRelativePath.split(sep).filter(Boolean);
920
+ if (segments.length !== 1) {
921
+ return true;
922
+ }
923
+ return !COPY_SKIP_ENTRIES.has(segments[0]);
924
+ }
925
+ async function copyRootLevelEntries(sourceUserDataDir, targetUserDataDir, targetProfileDirectory) {
926
+ let entries;
927
+ try {
928
+ entries = await readdir2(sourceUserDataDir);
929
+ } catch {
930
+ return;
931
+ }
932
+ const copyTasks = [];
933
+ for (const entry of entries) {
934
+ if (COPY_SKIP_ENTRIES.has(entry)) continue;
935
+ if (entry === targetProfileDirectory) continue;
936
+ const sourcePath = join4(sourceUserDataDir, entry);
937
+ const targetPath = join4(targetUserDataDir, entry);
938
+ if (existsSync3(targetPath)) continue;
939
+ let entryStat;
940
+ try {
941
+ entryStat = await stat(sourcePath);
942
+ } catch {
943
+ continue;
944
+ }
945
+ if (entryStat.isFile()) {
946
+ copyTasks.push(copyFile(sourcePath, targetPath).catch(() => void 0));
947
+ } else if (entryStat.isDirectory()) {
948
+ if (isProfileDirectory(sourceUserDataDir, entry)) continue;
949
+ if (SKIPPED_ROOT_DIRECTORIES.has(entry)) continue;
950
+ copyTasks.push(
951
+ cp(sourcePath, targetPath, { recursive: true }).catch(
952
+ () => void 0
953
+ )
954
+ );
955
+ }
956
+ }
957
+ await Promise.all(copyTasks);
958
+ }
959
+ async function writePersistentProfileMetadata(userDataDir, metadata) {
960
+ await writeFile3(
961
+ join4(userDataDir, OPENSTEER_META_FILE),
962
+ JSON.stringify(metadata, null, 2)
963
+ );
964
+ }
965
+ function buildPersistentProfileMetadata(sourceUserDataDir, profileDirectory) {
966
+ return {
967
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
968
+ profileDirectory,
969
+ source: sourceUserDataDir
970
+ };
971
+ }
972
+ async function createPersistentProfileClone(sourceUserDataDir, sourceProfileDir, targetUserDataDir, profileDirectory, metadata) {
973
+ if (existsSync3(targetUserDataDir)) {
974
+ return false;
975
+ }
976
+ const tempUserDataDir = await mkdtemp(
977
+ buildPersistentProfileTempDirPrefix(targetUserDataDir)
978
+ );
979
+ let published = false;
980
+ try {
981
+ await materializePersistentProfileSnapshot(
982
+ sourceUserDataDir,
983
+ sourceProfileDir,
984
+ tempUserDataDir,
985
+ profileDirectory,
986
+ metadata
987
+ );
988
+ try {
989
+ await rename3(tempUserDataDir, targetUserDataDir);
990
+ } catch (error) {
991
+ if (wasDirPublishedByAnotherProcess2(error, targetUserDataDir)) {
992
+ return false;
993
+ }
994
+ throw error;
995
+ }
996
+ published = true;
997
+ return true;
998
+ } finally {
999
+ if (!published) {
1000
+ await rm3(tempUserDataDir, {
1001
+ recursive: true,
1002
+ force: true
1003
+ }).catch(() => void 0);
1004
+ }
1005
+ }
1006
+ }
1007
+ async function materializePersistentProfileSnapshot(sourceUserDataDir, sourceProfileDir, targetUserDataDir, profileDirectory, metadata) {
1008
+ if (!existsSync3(sourceProfileDir)) {
1009
+ throw new Error(
1010
+ `Chrome profile "${profileDirectory}" was not found in "${sourceUserDataDir}".`
1011
+ );
1012
+ }
1013
+ await cp(sourceProfileDir, join4(targetUserDataDir, profileDirectory), {
1014
+ recursive: true
1015
+ });
1016
+ await copyRootLevelEntries(sourceUserDataDir, targetUserDataDir, profileDirectory);
1017
+ await writePersistentProfileMetadata(targetUserDataDir, metadata);
1018
+ }
1019
+ async function mergePersistentProfileSnapshot(runtimeUserDataDir, persistentUserDataDir, metadata, runtimeMetadata) {
1020
+ const tempUserDataDir = await mkdtemp(
1021
+ buildPersistentProfileTempDirPrefix(persistentUserDataDir)
1022
+ );
1023
+ let published = false;
1024
+ try {
1025
+ const baseEntries = deserializeSnapshotManifestEntries(
1026
+ runtimeMetadata.baseEntries
1027
+ );
1028
+ const currentEntries = await collectPersistentSnapshotEntries(
1029
+ persistentUserDataDir,
1030
+ metadata.profileDirectory
1031
+ );
1032
+ const runtimeEntries = await collectPersistentSnapshotEntries(
1033
+ runtimeUserDataDir,
1034
+ metadata.profileDirectory
1035
+ );
1036
+ const mergedEntries = resolveMergedSnapshotEntries(
1037
+ baseEntries,
1038
+ currentEntries,
1039
+ runtimeEntries
1040
+ );
1041
+ await materializeMergedPersistentProfileSnapshot(
1042
+ tempUserDataDir,
1043
+ currentEntries,
1044
+ runtimeEntries,
1045
+ mergedEntries
1046
+ );
1047
+ await writePersistentProfileMetadata(tempUserDataDir, metadata);
1048
+ await replaceProfileDirectory(persistentUserDataDir, tempUserDataDir);
1049
+ published = true;
1050
+ } finally {
1051
+ if (!published) {
1052
+ await rm3(tempUserDataDir, {
1053
+ recursive: true,
1054
+ force: true
1055
+ }).catch(() => void 0);
1056
+ }
1057
+ }
1058
+ }
1059
+ async function buildRuntimeProfileMetadata(runtimeUserDataDir, persistentUserDataDir, profileDirectory) {
1060
+ const baseEntries = profileDirectory ? serializeSnapshotManifestEntries(
1061
+ await collectPersistentSnapshotEntries(
1062
+ runtimeUserDataDir,
1063
+ profileDirectory
1064
+ )
1065
+ ) : {};
1066
+ return {
1067
+ baseEntries,
1068
+ creator: CURRENT_PROCESS_OWNER,
1069
+ persistentUserDataDir,
1070
+ profileDirectory
1071
+ };
1072
+ }
1073
+ async function writeRuntimeProfileMetadata(userDataDir, metadata) {
1074
+ await writeFile3(
1075
+ join4(userDataDir, OPENSTEER_RUNTIME_META_FILE),
1076
+ JSON.stringify(metadata, null, 2)
1077
+ );
1078
+ }
1079
+ async function writeRuntimeProfileCreationMarker(userDataDir, marker) {
1080
+ await writeFile3(
1081
+ join4(userDataDir, OPENSTEER_RUNTIME_CREATING_FILE),
1082
+ JSON.stringify(marker, null, 2)
1083
+ );
1084
+ }
1085
+ async function readRuntimeProfileMetadata(userDataDir) {
1086
+ try {
1087
+ const raw = await readFile4(
1088
+ join4(userDataDir, OPENSTEER_RUNTIME_META_FILE),
1089
+ "utf8"
1090
+ );
1091
+ const parsed = JSON.parse(raw);
1092
+ const creator = parseProcessOwner(parsed.creator);
1093
+ const persistentUserDataDir = typeof parsed.persistentUserDataDir === "string" ? parsed.persistentUserDataDir : void 0;
1094
+ const profileDirectory = parsed.profileDirectory === null ? null : typeof parsed.profileDirectory === "string" ? parsed.profileDirectory : void 0;
1095
+ if (!creator || persistentUserDataDir === void 0 || profileDirectory === void 0 || typeof parsed.baseEntries !== "object" || parsed.baseEntries === null || Array.isArray(parsed.baseEntries)) {
1096
+ return null;
1097
+ }
1098
+ const baseEntries = deserializeSnapshotManifestEntries(
1099
+ parsed.baseEntries
1100
+ );
1101
+ return {
1102
+ baseEntries: Object.fromEntries(baseEntries),
1103
+ creator,
1104
+ persistentUserDataDir,
1105
+ profileDirectory
1106
+ };
1107
+ } catch {
1108
+ return null;
1109
+ }
1110
+ }
1111
+ async function readRuntimeProfileCreationMarker(userDataDir) {
1112
+ try {
1113
+ const raw = await readFile4(
1114
+ join4(userDataDir, OPENSTEER_RUNTIME_CREATING_FILE),
1115
+ "utf8"
1116
+ );
1117
+ return parseRuntimeProfileCreationMarker(JSON.parse(raw));
1118
+ } catch {
1119
+ return null;
1120
+ }
1121
+ }
1122
+ async function requireRuntimeProfileMetadata(userDataDir, expectedPersistentUserDataDir, expectedProfileDirectory, displayUserDataDir = userDataDir) {
1123
+ const metadata = await readRuntimeProfileMetadata(userDataDir);
1124
+ if (!metadata) {
1125
+ throw new Error(
1126
+ `Runtime profile metadata was not found for "${displayUserDataDir}".`
1127
+ );
1128
+ }
1129
+ if (metadata.profileDirectory !== expectedProfileDirectory) {
1130
+ throw new Error(
1131
+ `Runtime profile "${displayUserDataDir}" was created for profile "${metadata.profileDirectory ?? "unknown"}", expected "${expectedProfileDirectory}".`
1132
+ );
1133
+ }
1134
+ if (metadata.persistentUserDataDir !== expectedPersistentUserDataDir) {
1135
+ throw new Error(
1136
+ `Runtime profile "${displayUserDataDir}" does not belong to persistent profile "${expectedPersistentUserDataDir}".`
1137
+ );
1138
+ }
1139
+ return metadata;
1140
+ }
1141
+ async function collectPersistentSnapshotEntries(userDataDir, profileDirectory) {
1142
+ let rootEntries;
1143
+ try {
1144
+ rootEntries = await readdir2(userDataDir, {
1145
+ encoding: "utf8",
1146
+ withFileTypes: true
1147
+ });
1148
+ } catch {
1149
+ return /* @__PURE__ */ new Map();
1150
+ }
1151
+ rootEntries.sort((left, right) => left.name.localeCompare(right.name));
1152
+ const collected = /* @__PURE__ */ new Map();
1153
+ for (const entry of rootEntries) {
1154
+ if (!shouldIncludePersistentRootEntry(
1155
+ userDataDir,
1156
+ profileDirectory,
1157
+ entry.name
1158
+ )) {
1159
+ continue;
1160
+ }
1161
+ await collectSnapshotEntry(
1162
+ join4(userDataDir, entry.name),
1163
+ entry.name,
1164
+ collected
1165
+ );
1166
+ }
1167
+ return collected;
1168
+ }
1169
+ function shouldIncludePersistentRootEntry(userDataDir, profileDirectory, entry) {
1170
+ if (entry === profileDirectory) {
1171
+ return true;
1172
+ }
1173
+ if (COPY_SKIP_ENTRIES.has(entry)) {
1174
+ return false;
1175
+ }
1176
+ if (SKIPPED_ROOT_DIRECTORIES.has(entry)) {
1177
+ return false;
1178
+ }
1179
+ return !isProfileDirectory(userDataDir, entry);
1180
+ }
1181
+ async function collectSnapshotEntry(sourcePath, relativePath, collected) {
1182
+ let entryStat;
1183
+ try {
1184
+ entryStat = await stat(sourcePath);
1185
+ } catch {
1186
+ return;
1187
+ }
1188
+ if (entryStat.isDirectory()) {
1189
+ collected.set(relativePath, {
1190
+ kind: "directory",
1191
+ hash: null,
1192
+ sourcePath
1193
+ });
1194
+ let children;
1195
+ try {
1196
+ children = await readdir2(sourcePath, {
1197
+ encoding: "utf8",
1198
+ withFileTypes: true
1199
+ });
1200
+ } catch {
1201
+ return;
1202
+ }
1203
+ children.sort((left, right) => left.name.localeCompare(right.name));
1204
+ for (const child of children) {
1205
+ await collectSnapshotEntry(
1206
+ join4(sourcePath, child.name),
1207
+ join4(relativePath, child.name),
1208
+ collected
1209
+ );
1210
+ }
1211
+ return;
1212
+ }
1213
+ if (entryStat.isFile()) {
1214
+ collected.set(relativePath, {
1215
+ kind: "file",
1216
+ hash: await hashSnapshotFile(sourcePath, relativePath),
1217
+ sourcePath
1218
+ });
1219
+ }
1220
+ }
1221
+ function serializeSnapshotManifestEntries(entries) {
1222
+ return Object.fromEntries(
1223
+ [...entries.entries()].sort(([leftPath], [rightPath]) => leftPath.localeCompare(rightPath)).map(([relativePath, entry]) => [
1224
+ relativePath,
1225
+ {
1226
+ kind: entry.kind,
1227
+ hash: entry.hash
1228
+ }
1229
+ ])
1230
+ );
1231
+ }
1232
+ function deserializeSnapshotManifestEntries(entries) {
1233
+ const manifestEntries = /* @__PURE__ */ new Map();
1234
+ for (const [relativePath, entry] of Object.entries(entries)) {
1235
+ if (!entry || entry.kind !== "directory" && entry.kind !== "file" || !(entry.hash === null || typeof entry.hash === "string")) {
1236
+ throw new Error(
1237
+ `Runtime profile metadata for "${relativePath}" is invalid.`
1238
+ );
1239
+ }
1240
+ manifestEntries.set(relativePath, {
1241
+ kind: entry.kind,
1242
+ hash: entry.hash
1243
+ });
1244
+ }
1245
+ return manifestEntries;
1246
+ }
1247
+ function resolveMergedSnapshotEntries(baseEntries, currentEntries, runtimeEntries) {
1248
+ const mergedEntries = /* @__PURE__ */ new Map();
1249
+ const relativePaths = /* @__PURE__ */ new Set([
1250
+ ...baseEntries.keys(),
1251
+ ...currentEntries.keys(),
1252
+ ...runtimeEntries.keys()
1253
+ ]);
1254
+ for (const relativePath of [...relativePaths].sort(compareSnapshotPaths)) {
1255
+ mergedEntries.set(
1256
+ relativePath,
1257
+ resolveMergedSnapshotEntrySelection(
1258
+ relativePath,
1259
+ baseEntries.get(relativePath) ?? null,
1260
+ currentEntries.get(relativePath) ?? null,
1261
+ runtimeEntries.get(relativePath) ?? null
1262
+ )
1263
+ );
1264
+ }
1265
+ return mergedEntries;
1266
+ }
1267
+ function resolveMergedSnapshotEntrySelection(relativePath, baseEntry, currentEntry, runtimeEntry) {
1268
+ if (snapshotEntriesEqual(runtimeEntry, baseEntry)) {
1269
+ return currentEntry ? "current" : null;
1270
+ }
1271
+ if (snapshotEntriesEqual(currentEntry, baseEntry)) {
1272
+ return runtimeEntry ? "runtime" : null;
1273
+ }
1274
+ if (!baseEntry) {
1275
+ if (!currentEntry) {
1276
+ return runtimeEntry ? "runtime" : null;
1277
+ }
1278
+ if (!runtimeEntry) {
1279
+ return "current";
1280
+ }
1281
+ if (snapshotEntriesEqual(currentEntry, runtimeEntry)) {
1282
+ return "current";
1283
+ }
1284
+ throw new Error(
1285
+ `Concurrent runtime updates changed "${relativePath}" differently; refusing to overwrite the persistent profile.`
1286
+ );
1287
+ }
1288
+ if (!currentEntry && !runtimeEntry) {
1289
+ return null;
1290
+ }
1291
+ if (snapshotEntriesEqual(currentEntry, runtimeEntry)) {
1292
+ return currentEntry ? "current" : null;
1293
+ }
1294
+ throw new Error(
1295
+ `Concurrent runtime updates changed "${relativePath}" differently; refusing to overwrite the persistent profile.`
1296
+ );
1297
+ }
1298
+ function snapshotEntriesEqual(left, right) {
1299
+ if (!left || !right) {
1300
+ return left === right;
1301
+ }
1302
+ return left.kind === right.kind && left.hash === right.hash;
1303
+ }
1304
+ async function materializeMergedPersistentProfileSnapshot(targetUserDataDir, currentEntries, runtimeEntries, mergedEntries) {
1305
+ const selectedEntries = [...mergedEntries.entries()].filter(([, selection]) => selection !== null).sort(
1306
+ ([leftPath], [rightPath]) => compareSnapshotPaths(leftPath, rightPath)
1307
+ );
1308
+ for (const [relativePath, selection] of selectedEntries) {
1309
+ const entry = (selection === "current" ? currentEntries.get(relativePath) : runtimeEntries.get(relativePath)) ?? null;
1310
+ if (!entry) {
1311
+ continue;
1312
+ }
1313
+ const targetPath = join4(targetUserDataDir, relativePath);
1314
+ if (entry.kind === "directory") {
1315
+ await mkdir3(targetPath, { recursive: true });
1316
+ continue;
1317
+ }
1318
+ await mkdir3(dirname4(targetPath), { recursive: true });
1319
+ await copyFile(entry.sourcePath, targetPath);
1320
+ }
1321
+ }
1322
+ function compareSnapshotPaths(left, right) {
1323
+ const leftDepth = left.split(sep).length;
1324
+ const rightDepth = right.split(sep).length;
1325
+ if (leftDepth !== rightDepth) {
1326
+ return leftDepth - rightDepth;
1327
+ }
1328
+ return left.localeCompare(right);
1329
+ }
1330
+ async function hashSnapshotFile(filePath, relativePath) {
1331
+ const normalizedJson = await readNormalizedSnapshotJson(filePath, relativePath);
1332
+ if (normalizedJson !== null) {
1333
+ return createHash("sha256").update(JSON.stringify(normalizedJson)).digest("hex");
1334
+ }
1335
+ return await hashFile(filePath);
1336
+ }
1337
+ async function readNormalizedSnapshotJson(filePath, relativePath) {
1338
+ const normalizer = SNAPSHOT_JSON_NORMALIZERS.get(relativePath);
1339
+ if (!normalizer) {
1340
+ return null;
1341
+ }
1342
+ try {
1343
+ const parsed = JSON.parse(await readFile4(filePath, "utf8"));
1344
+ return normalizer(parsed);
1345
+ } catch {
1346
+ return null;
1347
+ }
1348
+ }
1349
+ var SNAPSHOT_JSON_NORMALIZERS = /* @__PURE__ */ new Map([["Local State", normalizeLocalStateSnapshotJson]]);
1350
+ function normalizeLocalStateSnapshotJson(value) {
1351
+ if (!isJsonRecord(value)) {
1352
+ return value;
1353
+ }
1354
+ const { user_experience_metrics: _ignored, ...rest } = value;
1355
+ return rest;
1356
+ }
1357
+ function isJsonRecord(value) {
1358
+ return !!value && typeof value === "object" && !Array.isArray(value);
1359
+ }
1360
+ async function hashFile(filePath) {
1361
+ return new Promise((resolve, reject) => {
1362
+ const hash = createHash("sha256");
1363
+ const stream = createReadStream(filePath);
1364
+ stream.on("data", (chunk) => {
1365
+ hash.update(chunk);
1366
+ });
1367
+ stream.on("error", reject);
1368
+ stream.on("end", () => {
1369
+ resolve(hash.digest("hex"));
1370
+ });
1371
+ });
1372
+ }
1373
+ async function ensurePersistentProfileMetadata(userDataDir, metadata) {
1374
+ if (existsSync3(join4(userDataDir, OPENSTEER_META_FILE))) {
1375
+ return;
1376
+ }
1377
+ await writePersistentProfileMetadata(userDataDir, metadata);
1378
+ }
1379
+ async function recoverPersistentProfileBackup(targetUserDataDir) {
1380
+ const backupDirPaths = await listPersistentProfileBackupDirs(targetUserDataDir);
1381
+ if (backupDirPaths.length === 0) {
1382
+ return;
1383
+ }
1384
+ if (!existsSync3(targetUserDataDir)) {
1385
+ const [latestBackupDirPath, ...staleBackupDirPaths] = backupDirPaths;
1386
+ await rename3(latestBackupDirPath, targetUserDataDir);
1387
+ await Promise.all(
1388
+ staleBackupDirPaths.map(
1389
+ (backupDirPath) => rm3(backupDirPath, {
1390
+ recursive: true,
1391
+ force: true
1392
+ }).catch(() => void 0)
1393
+ )
1394
+ );
1395
+ return;
1396
+ }
1397
+ await Promise.all(
1398
+ backupDirPaths.map(
1399
+ (backupDirPath) => rm3(backupDirPath, {
1400
+ recursive: true,
1401
+ force: true
1402
+ }).catch(() => void 0)
1403
+ )
1404
+ );
1405
+ }
1406
+ async function listPersistentProfileBackupDirs(targetUserDataDir) {
1407
+ const profilesDir = dirname4(targetUserDataDir);
1408
+ let entries;
1409
+ try {
1410
+ entries = await readdir2(profilesDir, {
1411
+ encoding: "utf8",
1412
+ withFileTypes: true
1413
+ });
1414
+ } catch {
1415
+ return [];
1416
+ }
1417
+ const backupDirNamePrefix = buildPersistentProfileBackupDirNamePrefix(targetUserDataDir);
1418
+ return entries.filter(
1419
+ (entry) => entry.isDirectory() && entry.name.startsWith(backupDirNamePrefix)
1420
+ ).map((entry) => join4(profilesDir, entry.name)).sort((leftPath, rightPath) => rightPath.localeCompare(leftPath));
1421
+ }
1422
+ async function readPersistentProfileMetadata(userDataDir) {
1423
+ try {
1424
+ const raw = await readFile4(join4(userDataDir, OPENSTEER_META_FILE), "utf8");
1425
+ const parsed = JSON.parse(raw);
1426
+ if (typeof parsed.createdAt !== "string" || typeof parsed.profileDirectory !== "string" || typeof parsed.source !== "string") {
1427
+ return null;
1428
+ }
1429
+ return {
1430
+ createdAt: parsed.createdAt,
1431
+ profileDirectory: parsed.profileDirectory,
1432
+ source: parsed.source
1433
+ };
1434
+ } catch {
1435
+ return null;
1436
+ }
1437
+ }
1438
+ async function requirePersistentProfileMetadata(userDataDir) {
1439
+ const metadata = await readPersistentProfileMetadata(userDataDir);
1440
+ if (!metadata) {
1441
+ throw new Error(
1442
+ `Persistent profile metadata was not found for "${userDataDir}".`
1443
+ );
1444
+ }
1445
+ return metadata;
1446
+ }
1447
+ async function isHealthyPersistentProfile(userDataDir, expectedSourceUserDataDir, expectedProfileDirectory) {
1448
+ if (!existsSync3(userDataDir) || !existsSync3(join4(userDataDir, expectedProfileDirectory))) {
1449
+ return false;
1450
+ }
1451
+ const metadata = await readPersistentProfileMetadata(userDataDir);
1452
+ return metadata?.source === expectedSourceUserDataDir && metadata.profileDirectory === expectedProfileDirectory;
1453
+ }
1454
+ function wasDirPublishedByAnotherProcess2(error, targetDirPath) {
1455
+ const code = typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : void 0;
1456
+ return existsSync3(targetDirPath) && (code === "EEXIST" || code === "ENOTEMPTY" || code === "EPERM");
1457
+ }
1458
+ async function replaceProfileDirectory(targetUserDataDir, replacementUserDataDir) {
1459
+ if (!existsSync3(targetUserDataDir)) {
1460
+ await rename3(replacementUserDataDir, targetUserDataDir);
1461
+ return;
1462
+ }
1463
+ const backupUserDataDir = buildPersistentProfileBackupDirPath(targetUserDataDir);
1464
+ let targetMovedToBackup = false;
1465
+ let replacementPublished = false;
1466
+ try {
1467
+ await rename3(targetUserDataDir, backupUserDataDir);
1468
+ targetMovedToBackup = true;
1469
+ await rename3(replacementUserDataDir, targetUserDataDir);
1470
+ replacementPublished = true;
1471
+ } catch (error) {
1472
+ if (targetMovedToBackup && !existsSync3(targetUserDataDir)) {
1473
+ await rename3(backupUserDataDir, targetUserDataDir).catch(() => void 0);
1474
+ }
1475
+ throw error;
1476
+ } finally {
1477
+ if (replacementPublished && targetMovedToBackup && existsSync3(backupUserDataDir)) {
1478
+ await rm3(backupUserDataDir, {
1479
+ recursive: true,
1480
+ force: true
1481
+ }).catch(() => void 0);
1482
+ }
1483
+ }
1484
+ }
1485
+ async function withPersistentProfileWriteAccess(targetUserDataDir, action) {
1486
+ const releaseWriteLock = await acquirePersistentProfileWriteLock(
1487
+ targetUserDataDir
1488
+ );
1489
+ try {
1490
+ await waitForRuntimeProfileCreationsToDrain(targetUserDataDir);
1491
+ await waitForSharedRealBrowserSessionToDrain(targetUserDataDir);
1492
+ return await action();
1493
+ } finally {
1494
+ await releaseWriteLock();
1495
+ }
1496
+ }
1497
+ function buildPersistentProfileTempDirPrefix(targetUserDataDir) {
1498
+ return join4(
1499
+ dirname4(targetUserDataDir),
1500
+ `${buildPersistentProfileTempDirNamePrefix(targetUserDataDir)}${process.pid}-${CURRENT_PROCESS_OWNER.processStartedAtMs}-`
1501
+ );
1502
+ }
1503
+ function buildPersistentProfileTempDirNamePrefix(targetUserDataDir) {
1504
+ return `${basename3(targetUserDataDir)}-tmp-`;
1505
+ }
1506
+ function buildPersistentProfileBackupDirPath(targetUserDataDir) {
1507
+ return join4(
1508
+ dirname4(targetUserDataDir),
1509
+ `${buildPersistentProfileBackupDirNamePrefix(targetUserDataDir)}${Date.now()}-${process.pid}-${CURRENT_PROCESS_OWNER.processStartedAtMs}-${randomUUID3()}`
1510
+ );
1511
+ }
1512
+ function buildPersistentProfileBackupDirNamePrefix(targetUserDataDir) {
1513
+ return `${basename3(targetUserDataDir)}-backup-`;
1514
+ }
1515
+ function buildRuntimeProfileCreationRegistryDirPath(persistentUserDataDir) {
1516
+ return join4(
1517
+ dirname4(persistentUserDataDir),
1518
+ `${basename3(persistentUserDataDir)}.creating`
1519
+ );
1520
+ }
1521
+ function buildRuntimeProfileCreationRegistrationPath(persistentUserDataDir, runtimeUserDataDir) {
1522
+ const key = createHash("sha256").update(runtimeUserDataDir).digest("hex").slice(0, 16);
1523
+ return join4(
1524
+ buildRuntimeProfileCreationRegistryDirPath(persistentUserDataDir),
1525
+ `${key}.json`
1526
+ );
1527
+ }
1528
+ function buildRuntimeProfileKey(sourceUserDataDir) {
1529
+ const hash = createHash("sha256").update(sourceUserDataDir).digest("hex").slice(0, 16);
1530
+ return `${sanitizePathSegment(basename3(sourceUserDataDir) || "profile")}-${hash}`;
1531
+ }
1532
+ function buildRuntimeProfileDirNamePrefix(sourceUserDataDir) {
1533
+ return `${buildRuntimeProfileKey(sourceUserDataDir)}-runtime-`;
1534
+ }
1535
+ function buildRuntimeProfileDirPrefix(runtimesRootDir, sourceUserDataDir) {
1536
+ return join4(
1537
+ runtimesRootDir,
1538
+ buildRuntimeProfileDirNamePrefix(sourceUserDataDir)
1539
+ );
1540
+ }
1541
+ async function reserveRuntimeProfileCreation(persistentUserDataDir, runtimeRootDir, profileDirectory) {
1542
+ while (true) {
1543
+ let runtimeUserDataDir = null;
1544
+ await withPersistentProfileControlLock(
1545
+ persistentUserDataDir,
1546
+ async () => {
1547
+ if (await isPersistentProfileWriteLocked(persistentUserDataDir)) {
1548
+ return;
1549
+ }
1550
+ if (await hasLiveSharedRealBrowserSession(persistentUserDataDir)) {
1551
+ return;
1552
+ }
1553
+ const createdRuntimeUserDataDir = await mkdtemp(
1554
+ buildRuntimeProfileDirPrefix(
1555
+ runtimeRootDir,
1556
+ persistentUserDataDir
1557
+ )
1558
+ );
1559
+ runtimeUserDataDir = createdRuntimeUserDataDir;
1560
+ const marker = {
1561
+ creator: CURRENT_PROCESS_OWNER,
1562
+ persistentUserDataDir,
1563
+ profileDirectory,
1564
+ runtimeUserDataDir: createdRuntimeUserDataDir
1565
+ };
1566
+ await writeRuntimeProfileCreationMarker(
1567
+ createdRuntimeUserDataDir,
1568
+ marker
1569
+ );
1570
+ await writeRuntimeProfileCreationRegistration(marker);
1571
+ }
1572
+ );
1573
+ if (runtimeUserDataDir) {
1574
+ return {
1575
+ persistentUserDataDir,
1576
+ userDataDir: runtimeUserDataDir
1577
+ };
1578
+ }
1579
+ await sleep4(PERSISTENT_PROFILE_LOCK_RETRY_DELAY_MS);
1580
+ }
1581
+ }
1582
+ async function clearRuntimeProfileCreationState(runtimeUserDataDir, persistentUserDataDir) {
1583
+ await Promise.all([
1584
+ rm3(join4(runtimeUserDataDir, OPENSTEER_RUNTIME_CREATING_FILE), {
1585
+ force: true
1586
+ }).catch(() => void 0),
1587
+ rm3(
1588
+ buildRuntimeProfileCreationRegistrationPath(
1589
+ persistentUserDataDir,
1590
+ runtimeUserDataDir
1591
+ ),
1592
+ {
1593
+ force: true
1594
+ }
1595
+ ).catch(() => void 0)
1596
+ ]);
1597
+ }
1598
+ async function writeRuntimeProfileCreationRegistration(marker) {
1599
+ const registryDirPath = buildRuntimeProfileCreationRegistryDirPath(
1600
+ marker.persistentUserDataDir
1601
+ );
1602
+ await mkdir3(registryDirPath, { recursive: true });
1603
+ await writeFile3(
1604
+ buildRuntimeProfileCreationRegistrationPath(
1605
+ marker.persistentUserDataDir,
1606
+ marker.runtimeUserDataDir
1607
+ ),
1608
+ JSON.stringify(marker, null, 2)
1609
+ );
1610
+ }
1611
+ async function listRuntimeProfileCreationRegistrations(persistentUserDataDir) {
1612
+ const registryDirPath = buildRuntimeProfileCreationRegistryDirPath(
1613
+ persistentUserDataDir
1614
+ );
1615
+ let entries;
1616
+ try {
1617
+ entries = await readdir2(registryDirPath, {
1618
+ encoding: "utf8",
1619
+ withFileTypes: true
1620
+ });
1621
+ } catch {
1622
+ return [];
1623
+ }
1624
+ return await Promise.all(
1625
+ entries.filter((entry) => entry.isFile()).map(async (entry) => {
1626
+ const filePath = join4(registryDirPath, entry.name);
1627
+ return {
1628
+ filePath,
1629
+ marker: await readRuntimeProfileCreationRegistration(filePath)
1630
+ };
1631
+ })
1632
+ );
1633
+ }
1634
+ async function readRuntimeProfileCreationRegistration(filePath) {
1635
+ try {
1636
+ const raw = await readFile4(filePath, "utf8");
1637
+ return parseRuntimeProfileCreationMarker(JSON.parse(raw));
1638
+ } catch {
1639
+ return null;
1640
+ }
1641
+ }
1642
+ async function cleanOrphanedRuntimeProfileDirs(rootDir, runtimeDirNamePrefix) {
1643
+ let entries;
1644
+ try {
1645
+ entries = await readdir2(rootDir, {
1646
+ encoding: "utf8",
1647
+ withFileTypes: true
1648
+ });
1649
+ } catch {
1650
+ return;
1651
+ }
1652
+ const liveProcessCommandLines = await listProcessCommandLines();
1653
+ await Promise.all(
1654
+ entries.map(async (entry) => {
1655
+ if (!entry.isDirectory() || !entry.name.startsWith(runtimeDirNamePrefix)) {
1656
+ return;
1657
+ }
1658
+ const runtimeDirPath = join4(rootDir, entry.name);
1659
+ const creationMarker = await readRuntimeProfileCreationMarker(
1660
+ runtimeDirPath
1661
+ );
1662
+ if (await isRuntimeProfileDirInUse(
1663
+ runtimeDirPath,
1664
+ liveProcessCommandLines
1665
+ )) {
1666
+ return;
1667
+ }
1668
+ await rm3(runtimeDirPath, {
1669
+ recursive: true,
1670
+ force: true
1671
+ }).catch(() => void 0);
1672
+ if (creationMarker) {
1673
+ await clearRuntimeProfileCreationState(
1674
+ runtimeDirPath,
1675
+ creationMarker.persistentUserDataDir
1676
+ );
1677
+ }
1678
+ })
1679
+ );
1680
+ }
1681
+ async function cleanOrphanedOwnedDirs(rootDir, ownedDirNamePrefix) {
1682
+ let entries;
1683
+ try {
1684
+ entries = await readdir2(rootDir, {
1685
+ encoding: "utf8",
1686
+ withFileTypes: true
1687
+ });
1688
+ } catch {
1689
+ return;
1690
+ }
1691
+ await Promise.all(
1692
+ entries.map(async (entry) => {
1693
+ if (!entry.isDirectory() || !entry.name.startsWith(ownedDirNamePrefix)) {
1694
+ return;
1695
+ }
1696
+ if (await isOwnedDirByLiveProcess(entry.name, ownedDirNamePrefix)) {
1697
+ return;
1698
+ }
1699
+ await rm3(join4(rootDir, entry.name), {
1700
+ recursive: true,
1701
+ force: true
1702
+ }).catch(() => void 0);
1703
+ })
1704
+ );
1705
+ }
1706
+ async function isOwnedDirByLiveProcess(ownedDirName, ownedDirPrefix) {
1707
+ const owner = parseOwnedDirOwner(ownedDirName, ownedDirPrefix);
1708
+ return owner ? await getProcessLiveness(owner) !== "dead" : false;
1709
+ }
1710
+ async function isRuntimeProfileDirInUse(runtimeDirPath, liveProcessCommandLines) {
1711
+ const creationMarker = await readRuntimeProfileCreationMarker(runtimeDirPath);
1712
+ if (creationMarker && await getProcessLiveness(creationMarker.creator) !== "dead") {
1713
+ return true;
1714
+ }
1715
+ const metadata = await readRuntimeProfileMetadata(runtimeDirPath);
1716
+ if (metadata && await getProcessLiveness(metadata.creator) !== "dead") {
1717
+ return true;
1718
+ }
1719
+ return liveProcessCommandLines.some(
1720
+ (commandLine) => commandLineIncludesUserDataDir(commandLine, runtimeDirPath)
1721
+ );
1722
+ }
1723
+ async function claimRuntimeProfileForPersist(runtimeUserDataDir) {
1724
+ while (true) {
1725
+ await waitForRuntimeProfileProcessesToDrain(runtimeUserDataDir);
1726
+ const claimedRuntimeUserDataDir = buildClaimedRuntimeProfileDirPath(runtimeUserDataDir);
1727
+ try {
1728
+ await rename3(runtimeUserDataDir, claimedRuntimeUserDataDir);
1729
+ } catch (error) {
1730
+ const code = getErrorCode3(error);
1731
+ if (code === "ENOENT") {
1732
+ throw new Error(
1733
+ `Runtime profile "${runtimeUserDataDir}" was not found.`
1734
+ );
1735
+ }
1736
+ if (code === "EACCES" || code === "EBUSY" || code === "EPERM") {
1737
+ await sleep4(PERSISTENT_PROFILE_LOCK_RETRY_DELAY_MS);
1738
+ continue;
1739
+ }
1740
+ throw error;
1741
+ }
1742
+ if (!await hasLiveProcessUsingUserDataDir(runtimeUserDataDir)) {
1743
+ return claimedRuntimeUserDataDir;
1744
+ }
1745
+ await rename3(
1746
+ claimedRuntimeUserDataDir,
1747
+ runtimeUserDataDir
1748
+ ).catch(() => void 0);
1749
+ await sleep4(PERSISTENT_PROFILE_LOCK_RETRY_DELAY_MS);
1750
+ }
1751
+ }
1752
+ async function restoreClaimedRuntimeProfile(claimedRuntimeUserDataDir, runtimeUserDataDir) {
1753
+ if (!existsSync3(claimedRuntimeUserDataDir)) {
1754
+ return;
1755
+ }
1756
+ if (existsSync3(runtimeUserDataDir)) {
1757
+ throw new Error(
1758
+ `Runtime profile "${runtimeUserDataDir}" was recreated before the failed persist could restore it from "${claimedRuntimeUserDataDir}".`
1759
+ );
1760
+ }
1761
+ await rename3(claimedRuntimeUserDataDir, runtimeUserDataDir);
1762
+ }
1763
+ function buildClaimedRuntimeProfileDirPath(runtimeUserDataDir) {
1764
+ return join4(
1765
+ dirname4(runtimeUserDataDir),
1766
+ `${basename3(runtimeUserDataDir)}-persisting-${process.pid}-${CURRENT_PROCESS_OWNER.processStartedAtMs}-${randomUUID3()}`
1767
+ );
1768
+ }
1769
+ async function waitForRuntimeProfileProcessesToDrain(runtimeUserDataDir) {
1770
+ while (await hasLiveProcessUsingUserDataDir(runtimeUserDataDir)) {
1771
+ await sleep4(PERSISTENT_PROFILE_LOCK_RETRY_DELAY_MS);
1772
+ }
1773
+ }
1774
+ async function hasLiveProcessUsingUserDataDir(userDataDir) {
1775
+ const liveProcessCommandLines = await listProcessCommandLines();
1776
+ return liveProcessCommandLines.some(
1777
+ (commandLine) => commandLineIncludesUserDataDir(commandLine, userDataDir)
1778
+ );
1779
+ }
1780
+ async function hasActiveRuntimeProfileCreations(persistentUserDataDir) {
1781
+ const registrations = await listRuntimeProfileCreationRegistrations(
1782
+ persistentUserDataDir
1783
+ );
1784
+ let hasLiveCreation = false;
1785
+ for (const registration of registrations) {
1786
+ const marker = registration.marker;
1787
+ if (!marker || marker.persistentUserDataDir !== persistentUserDataDir) {
1788
+ await rm3(registration.filePath, {
1789
+ force: true
1790
+ }).catch(() => void 0);
1791
+ continue;
1792
+ }
1793
+ const runtimeMarker = await readRuntimeProfileCreationMarker(
1794
+ marker.runtimeUserDataDir
1795
+ );
1796
+ if (!runtimeMarker || runtimeMarker.persistentUserDataDir !== persistentUserDataDir || runtimeMarker.runtimeUserDataDir !== marker.runtimeUserDataDir) {
1797
+ await clearRuntimeProfileCreationState(
1798
+ marker.runtimeUserDataDir,
1799
+ persistentUserDataDir
1800
+ );
1801
+ continue;
1802
+ }
1803
+ if (await getProcessLiveness(runtimeMarker.creator) === "dead") {
1804
+ await clearRuntimeProfileCreationState(
1805
+ marker.runtimeUserDataDir,
1806
+ persistentUserDataDir
1807
+ );
1808
+ await rm3(marker.runtimeUserDataDir, {
1809
+ recursive: true,
1810
+ force: true
1811
+ }).catch(() => void 0);
1812
+ continue;
1813
+ }
1814
+ hasLiveCreation = true;
1815
+ }
1816
+ return hasLiveCreation;
1817
+ }
1818
+ async function waitForRuntimeProfileCreationsToDrain(persistentUserDataDir) {
1819
+ while (true) {
1820
+ if (!await hasActiveRuntimeProfileCreations(persistentUserDataDir)) {
1821
+ return;
1822
+ }
1823
+ await sleep4(PERSISTENT_PROFILE_LOCK_RETRY_DELAY_MS);
1824
+ }
1825
+ }
1826
+ function parseRuntimeProfileCreationMarker(value) {
1827
+ if (!value || typeof value !== "object") {
1828
+ return null;
1829
+ }
1830
+ const parsed = value;
1831
+ const creator = parseProcessOwner(parsed.creator);
1832
+ const persistentUserDataDir = typeof parsed.persistentUserDataDir === "string" ? parsed.persistentUserDataDir : void 0;
1833
+ const profileDirectory = parsed.profileDirectory === null ? null : typeof parsed.profileDirectory === "string" ? parsed.profileDirectory : void 0;
1834
+ const runtimeUserDataDir = typeof parsed.runtimeUserDataDir === "string" ? parsed.runtimeUserDataDir : void 0;
1835
+ if (!creator || persistentUserDataDir === void 0 || profileDirectory === void 0 || runtimeUserDataDir === void 0) {
1836
+ return null;
1837
+ }
1838
+ return {
1839
+ creator,
1840
+ persistentUserDataDir,
1841
+ profileDirectory,
1842
+ runtimeUserDataDir
1843
+ };
1844
+ }
1845
+ function parseOwnedDirOwner(ownedDirName, ownedDirPrefix) {
1846
+ const remainder = ownedDirName.slice(ownedDirPrefix.length);
1847
+ const firstDashIndex = remainder.indexOf("-");
1848
+ const secondDashIndex = firstDashIndex === -1 ? -1 : remainder.indexOf("-", firstDashIndex + 1);
1849
+ if (firstDashIndex === -1 || secondDashIndex === -1) {
1850
+ return null;
1851
+ }
1852
+ const pid = Number.parseInt(remainder.slice(0, firstDashIndex), 10);
1853
+ const processStartedAtMs = Number.parseInt(
1854
+ remainder.slice(firstDashIndex + 1, secondDashIndex),
1855
+ 10
1856
+ );
1857
+ if (!Number.isInteger(pid) || pid <= 0) {
1858
+ return null;
1859
+ }
1860
+ if (!Number.isInteger(processStartedAtMs) || processStartedAtMs <= 0) {
1861
+ return null;
1862
+ }
1863
+ return { pid, processStartedAtMs };
1864
+ }
1865
+ async function listProcessCommandLines() {
1866
+ if (process.platform === "win32") {
1867
+ return await listWindowsProcessCommandLines();
1868
+ }
1869
+ return await listPsProcessCommandLines();
1870
+ }
1871
+ async function listPsProcessCommandLines() {
1872
+ try {
1873
+ const { stdout } = await execFileAsync2(
1874
+ "ps",
1875
+ ["-axww", "-o", "command="],
1876
+ {
1877
+ env: PS_COMMAND_ENV2,
1878
+ maxBuffer: PROCESS_LIST_MAX_BUFFER_BYTES2
1879
+ }
1880
+ );
1881
+ return stdout.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
1882
+ } catch {
1883
+ return [];
1884
+ }
1885
+ }
1886
+ async function listWindowsProcessCommandLines() {
1887
+ const script = [
1888
+ "$processes = Get-CimInstance Win32_Process | Select-Object CommandLine",
1889
+ "$processes | ConvertTo-Json -Compress"
1890
+ ].join("; ");
1891
+ try {
1892
+ const { stdout } = await execFileAsync2(
1893
+ "powershell.exe",
1894
+ ["-NoLogo", "-NoProfile", "-Command", script],
1895
+ {
1896
+ maxBuffer: PROCESS_LIST_MAX_BUFFER_BYTES2
1897
+ }
1898
+ );
1899
+ const parsed = JSON.parse(stdout);
1900
+ const records = Array.isArray(parsed) ? parsed : [parsed];
1901
+ return records.map((record) => record?.CommandLine?.trim() ?? "").filter((commandLine) => commandLine.length > 0);
1902
+ } catch {
1903
+ return [];
1904
+ }
1905
+ }
1906
+ function commandLineIncludesUserDataDir(commandLine, userDataDir) {
1907
+ const unquoted = `--user-data-dir=${userDataDir}`;
1908
+ const unquotedIndex = commandLine.indexOf(unquoted);
1909
+ if (unquotedIndex !== -1) {
1910
+ const after = commandLine[unquotedIndex + unquoted.length];
1911
+ if (after === void 0 || after === " " || after === " ") {
1912
+ return true;
1913
+ }
1914
+ }
1915
+ return [
1916
+ `--user-data-dir="${userDataDir}"`,
1917
+ `--user-data-dir='${userDataDir}'`
1918
+ ].some((candidate) => commandLine.includes(candidate));
1919
+ }
1920
+ function getErrorCode3(error) {
1921
+ return typeof error === "object" && error !== null && "code" in error && (typeof error.code === "string" || typeof error.code === "number") ? error.code : void 0;
1922
+ }
1923
+ async function sleep4(ms) {
1924
+ await new Promise((resolve) => setTimeout(resolve, ms));
1925
+ }
1926
+
1927
+ // src/browser/shared-real-browser-session.ts
1928
+ import { randomUUID as randomUUID4 } from "crypto";
1929
+ import { spawn } from "child_process";
1930
+ import {
1931
+ mkdir as mkdir4,
1932
+ readFile as readFile5,
1933
+ readdir as readdir3,
1934
+ rm as rm4,
1935
+ writeFile as writeFile4
1936
+ } from "fs/promises";
1937
+ import { createServer } from "net";
1938
+ import { join as join5 } from "path";
1939
+ import {
1940
+ chromium
1941
+ } from "playwright";
1942
+ var SHARED_SESSION_RETRY_DELAY_MS2 = 50;
1943
+ async function acquireSharedRealBrowserSession(options) {
1944
+ const reservation = await reserveSharedSessionClient(options);
1945
+ const sessionContext = await attachToSharedSession(reservation, options);
1946
+ let closed = false;
1947
+ return {
1948
+ browser: sessionContext.browser,
1949
+ context: sessionContext.context,
1950
+ page: sessionContext.page,
1951
+ close: async () => {
1952
+ if (closed) {
1953
+ return;
1954
+ }
1955
+ closed = true;
1956
+ await releaseSharedSessionClient(sessionContext);
1957
+ }
1958
+ };
1959
+ }
1960
+ function getOwnedRealBrowserProcessPolicy(platformName = process.platform) {
1961
+ if (platformName === "win32") {
1962
+ return {
1963
+ detached: false,
1964
+ killStrategy: "taskkill",
1965
+ shouldUnref: true
1966
+ };
1967
+ }
1968
+ if (platformName === "darwin") {
1969
+ return {
1970
+ detached: false,
1971
+ killStrategy: "process",
1972
+ shouldUnref: true
1973
+ };
1974
+ }
1975
+ return {
1976
+ detached: true,
1977
+ killStrategy: "process-group",
1978
+ shouldUnref: true
1979
+ };
1980
+ }
1981
+ async function reserveSharedSessionClient(options) {
1982
+ while (true) {
1983
+ const outcome = await withPersistentProfileControlLock(
1984
+ options.persistentProfile.userDataDir,
1985
+ async () => {
1986
+ if (await isPersistentProfileWriteLocked(
1987
+ options.persistentProfile.userDataDir
1988
+ )) {
1989
+ return { kind: "wait" };
1990
+ }
1991
+ if (await hasActiveRuntimeProfileCreations(
1992
+ options.persistentProfile.userDataDir
1993
+ )) {
1994
+ return { kind: "wait" };
1995
+ }
1996
+ return await withSharedSessionLock(
1997
+ options.persistentProfile.userDataDir,
1998
+ async () => {
1999
+ const state = await inspectSharedSessionState(options);
2000
+ if (state.kind === "wait") {
2001
+ return { kind: "wait" };
2002
+ }
2003
+ if (state.kind === "ready") {
2004
+ return {
2005
+ kind: "ready",
2006
+ reservation: await registerSharedSessionClient(
2007
+ options.persistentProfile.userDataDir,
2008
+ state.metadata
2009
+ )
2010
+ };
2011
+ }
2012
+ return {
2013
+ kind: "launch",
2014
+ reservation: await launchSharedSession(options)
2015
+ };
2016
+ }
2017
+ );
2018
+ }
2019
+ );
2020
+ if (outcome.kind === "wait") {
2021
+ await sleep5(SHARED_SESSION_RETRY_DELAY_MS2);
2022
+ continue;
2023
+ }
2024
+ if (outcome.kind === "ready") {
2025
+ return outcome.reservation;
2026
+ }
2027
+ try {
2028
+ await waitForSharedSessionReady(
2029
+ outcome.reservation.metadata,
2030
+ options.timeoutMs
2031
+ );
2032
+ } catch (error) {
2033
+ await cleanupFailedSharedSessionLaunch(outcome.reservation);
2034
+ throw error;
2035
+ }
2036
+ try {
2037
+ return await withSharedSessionLock(
2038
+ options.persistentProfile.userDataDir,
2039
+ async () => {
2040
+ const metadata = await readSharedSessionMetadata(
2041
+ options.persistentProfile.userDataDir
2042
+ );
2043
+ if (!metadata || metadata.sessionId !== outcome.reservation.metadata.sessionId || !processOwnersEqual(
2044
+ metadata.browserOwner,
2045
+ outcome.reservation.launchedBrowserOwner
2046
+ )) {
2047
+ throw new Error(
2048
+ "The shared real-browser session changed before launch finalized."
2049
+ );
2050
+ }
2051
+ const readyMetadata = {
2052
+ ...metadata,
2053
+ state: "ready"
2054
+ };
2055
+ await writeSharedSessionMetadata(
2056
+ options.persistentProfile.userDataDir,
2057
+ readyMetadata
2058
+ );
2059
+ return await registerSharedSessionClient(
2060
+ options.persistentProfile.userDataDir,
2061
+ readyMetadata
2062
+ );
2063
+ }
2064
+ );
2065
+ } catch (error) {
2066
+ await cleanupFailedSharedSessionLaunch(outcome.reservation);
2067
+ throw error;
2068
+ }
2069
+ }
2070
+ }
2071
+ async function attachToSharedSession(reservation, options) {
2072
+ let browser = null;
2073
+ let page = null;
2074
+ try {
2075
+ const browserWsUrl = await resolveCdpWebSocketUrl(
2076
+ buildSharedSessionDiscoveryUrl(reservation.metadata.debugPort),
2077
+ options.timeoutMs
2078
+ );
2079
+ browser = await chromium.connectOverCDP(browserWsUrl, {
2080
+ timeout: options.timeoutMs
2081
+ });
2082
+ const context = getPrimaryBrowserContext(browser);
2083
+ page = await getSharedSessionPage(context, reservation.reuseExistingPage);
2084
+ if (options.initialUrl) {
2085
+ await page.goto(options.initialUrl, {
2086
+ timeout: options.timeoutMs,
2087
+ waitUntil: "domcontentloaded"
2088
+ });
2089
+ }
2090
+ return {
2091
+ browser,
2092
+ clientId: reservation.client.clientId,
2093
+ context,
2094
+ page,
2095
+ persistentUserDataDir: reservation.metadata.persistentUserDataDir,
2096
+ sessionId: reservation.metadata.sessionId
2097
+ };
2098
+ } catch (error) {
2099
+ if (page) {
2100
+ await page.close().catch(() => void 0);
2101
+ }
2102
+ if (browser) {
2103
+ await browser.close().catch(() => void 0);
2104
+ }
2105
+ await cleanupFailedSharedSessionAttach({
2106
+ clientId: reservation.client.clientId,
2107
+ persistentUserDataDir: reservation.metadata.persistentUserDataDir,
2108
+ sessionId: reservation.metadata.sessionId
2109
+ });
2110
+ throw error;
2111
+ }
2112
+ }
2113
+ async function releaseSharedSessionClient(context) {
2114
+ const releasePlan = await prepareSharedSessionCloseIfIdle(
2115
+ context.persistentUserDataDir,
2116
+ context.clientId,
2117
+ context.sessionId
2118
+ );
2119
+ if (releasePlan.closeBrowser) {
2120
+ await closeSharedSessionBrowser(
2121
+ context.persistentUserDataDir,
2122
+ releasePlan,
2123
+ context.browser
2124
+ );
2125
+ return;
2126
+ }
2127
+ await context.page.close().catch(() => void 0);
2128
+ await context.browser.close().catch(() => void 0);
2129
+ }
2130
+ async function inspectSharedSessionState(options) {
2131
+ const persistentUserDataDir = options.persistentProfile.userDataDir;
2132
+ const liveClients = await listLiveSharedSessionClients(persistentUserDataDir);
2133
+ const metadata = await readSharedSessionMetadata(persistentUserDataDir);
2134
+ if (!metadata) {
2135
+ if (liveClients.length > 0) {
2136
+ throw new Error(
2137
+ `Shared real-browser session metadata for "${persistentUserDataDir}" is missing while clients are still attached.`
2138
+ );
2139
+ }
2140
+ await rm4(buildSharedSessionDirPath(persistentUserDataDir), {
2141
+ force: true,
2142
+ recursive: true
2143
+ }).catch(() => void 0);
2144
+ return { kind: "missing" };
2145
+ }
2146
+ assertSharedSessionCompatibility(metadata, options);
2147
+ const browserState = await getProcessLiveness(metadata.browserOwner);
2148
+ if (browserState === "dead") {
2149
+ await rm4(buildSharedSessionDirPath(persistentUserDataDir), {
2150
+ force: true,
2151
+ recursive: true
2152
+ }).catch(() => void 0);
2153
+ return { kind: "missing" };
2154
+ }
2155
+ if (metadata.state === "ready") {
2156
+ return {
2157
+ kind: "ready",
2158
+ metadata
2159
+ };
2160
+ }
2161
+ const stateOwnerState = await getProcessLiveness(metadata.stateOwner);
2162
+ if (stateOwnerState === "dead") {
2163
+ const recoveredMetadata = {
2164
+ ...metadata,
2165
+ state: "ready"
2166
+ };
2167
+ await writeSharedSessionMetadata(persistentUserDataDir, recoveredMetadata);
2168
+ return {
2169
+ kind: "ready",
2170
+ metadata: recoveredMetadata
2171
+ };
2172
+ }
2173
+ return { kind: "wait" };
2174
+ }
2175
+ async function launchSharedSession(options) {
2176
+ const persistentUserDataDir = options.persistentProfile.userDataDir;
2177
+ await clearPersistentProfileSingletons(persistentUserDataDir);
2178
+ const debugPort = await reserveDebugPort();
2179
+ const launchArgs = buildRealBrowserLaunchArgs({
2180
+ debugPort,
2181
+ headless: options.headless,
2182
+ profileDirectory: options.profileDirectory,
2183
+ userDataDir: persistentUserDataDir
2184
+ });
2185
+ const processPolicy = getOwnedRealBrowserProcessPolicy();
2186
+ const processHandle = spawn(options.executablePath, launchArgs, {
2187
+ detached: processPolicy.detached,
2188
+ stdio: "ignore"
2189
+ });
2190
+ if (processPolicy.shouldUnref) {
2191
+ processHandle.unref();
2192
+ }
2193
+ try {
2194
+ const browserOwner = await waitForSpawnedProcessOwner(
2195
+ processHandle.pid,
2196
+ options.timeoutMs
2197
+ );
2198
+ const metadata = {
2199
+ browserOwner,
2200
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2201
+ debugPort,
2202
+ executablePath: options.executablePath,
2203
+ headless: options.headless,
2204
+ persistentUserDataDir,
2205
+ profileDirectory: options.profileDirectory,
2206
+ sessionId: randomUUID4(),
2207
+ state: "launching",
2208
+ stateOwner: CURRENT_PROCESS_OWNER
2209
+ };
2210
+ await writeSharedSessionMetadata(persistentUserDataDir, metadata);
2211
+ return {
2212
+ launchedBrowserOwner: browserOwner,
2213
+ metadata
2214
+ };
2215
+ } catch (error) {
2216
+ await killSpawnedBrowserProcess(processHandle);
2217
+ await rm4(buildSharedSessionDirPath(persistentUserDataDir), {
2218
+ force: true,
2219
+ recursive: true
2220
+ }).catch(() => void 0);
2221
+ throw error;
2222
+ }
2223
+ }
2224
+ async function cleanupFailedSharedSessionLaunch(reservation) {
2225
+ const shouldPreserveLiveBrowser = await withSharedSessionLock(
2226
+ reservation.metadata.persistentUserDataDir,
2227
+ async () => {
2228
+ const metadata = await readSharedSessionMetadata(
2229
+ reservation.metadata.persistentUserDataDir
2230
+ );
2231
+ if (metadata && metadata.sessionId === reservation.metadata.sessionId && processOwnersEqual(
2232
+ metadata.browserOwner,
2233
+ reservation.launchedBrowserOwner
2234
+ )) {
2235
+ if (await getProcessLiveness(metadata.browserOwner) !== "dead") {
2236
+ const readyMetadata = {
2237
+ ...metadata,
2238
+ state: "ready"
2239
+ };
2240
+ await writeSharedSessionMetadata(
2241
+ reservation.metadata.persistentUserDataDir,
2242
+ readyMetadata
2243
+ );
2244
+ return true;
2245
+ }
2246
+ await rm4(
2247
+ buildSharedSessionDirPath(
2248
+ reservation.metadata.persistentUserDataDir
2249
+ ),
2250
+ {
2251
+ force: true,
2252
+ recursive: true
2253
+ }
2254
+ ).catch(() => void 0);
2255
+ }
2256
+ return false;
2257
+ }
2258
+ );
2259
+ if (shouldPreserveLiveBrowser) {
2260
+ return;
2261
+ }
2262
+ await killOwnedBrowserProcess(reservation.launchedBrowserOwner);
2263
+ await waitForProcessToExit(reservation.launchedBrowserOwner, 2e3);
2264
+ }
2265
+ async function cleanupFailedSharedSessionAttach(options) {
2266
+ const closePlan = await prepareSharedSessionCloseIfIdle(
2267
+ options.persistentUserDataDir,
2268
+ options.clientId,
2269
+ options.sessionId
2270
+ );
2271
+ if (!closePlan.closeBrowser) {
2272
+ return;
2273
+ }
2274
+ await closeSharedSessionBrowser(options.persistentUserDataDir, closePlan);
2275
+ }
2276
+ async function waitForSharedSessionReady(metadata, timeoutMs) {
2277
+ await resolveCdpWebSocketUrl(
2278
+ buildSharedSessionDiscoveryUrl(metadata.debugPort),
2279
+ timeoutMs
2280
+ );
2281
+ }
2282
+ function buildRealBrowserLaunchArgs(options) {
2283
+ const args = [
2284
+ `--user-data-dir=${options.userDataDir}`,
2285
+ `--profile-directory=${options.profileDirectory}`,
2286
+ `--remote-debugging-port=${options.debugPort}`,
2287
+ "--disable-blink-features=AutomationControlled"
2288
+ ];
2289
+ if (options.headless) {
2290
+ args.push("--headless=new");
2291
+ }
2292
+ return args;
2293
+ }
2294
+ async function requestBrowserShutdown(browser) {
2295
+ let session = null;
2296
+ try {
2297
+ session = await browser.newBrowserCDPSession();
2298
+ await session.send("Browser.close");
2299
+ } catch {
2300
+ } finally {
2301
+ await session?.detach().catch(() => void 0);
2302
+ }
2303
+ }
2304
+ async function killOwnedBrowserProcess(owner) {
2305
+ if (await getProcessLiveness(owner) === "dead") {
2306
+ return;
2307
+ }
2308
+ await killOwnedBrowserProcessByPid(owner.pid);
2309
+ }
2310
+ async function killSpawnedBrowserProcess(processHandle) {
2311
+ const pid = processHandle.pid;
2312
+ if (!pid || processHandle.exitCode !== null) {
2313
+ return;
2314
+ }
2315
+ await killOwnedBrowserProcessByPid(pid);
2316
+ await waitForPidToExit(pid, 2e3);
2317
+ }
2318
+ async function killOwnedBrowserProcessByPid(pid) {
2319
+ const processPolicy = getOwnedRealBrowserProcessPolicy();
2320
+ if (processPolicy.killStrategy === "taskkill") {
2321
+ await new Promise((resolve) => {
2322
+ const killer = spawn(
2323
+ "taskkill",
2324
+ ["/pid", String(pid), "/t", "/f"],
2325
+ {
2326
+ stdio: "ignore"
2327
+ }
2328
+ );
2329
+ killer.on("error", () => resolve());
2330
+ killer.on("exit", () => resolve());
2331
+ });
2332
+ return;
2333
+ }
2334
+ if (processPolicy.killStrategy === "process-group") {
2335
+ try {
2336
+ process.kill(-pid, "SIGKILL");
2337
+ return;
2338
+ } catch {
2339
+ }
2340
+ }
2341
+ try {
2342
+ process.kill(pid, "SIGKILL");
2343
+ } catch {
2344
+ }
2345
+ }
2346
+ async function waitForProcessToExit(owner, timeoutMs) {
2347
+ const deadline = Date.now() + timeoutMs;
2348
+ while (Date.now() < deadline) {
2349
+ if (await getProcessLiveness(owner) === "dead") {
2350
+ return;
2351
+ }
2352
+ await sleep5(50);
2353
+ }
2354
+ }
2355
+ async function waitForPidToExit(pid, timeoutMs) {
2356
+ const deadline = Date.now() + timeoutMs;
2357
+ while (Date.now() < deadline) {
2358
+ if (!isProcessRunning(pid)) {
2359
+ return;
2360
+ }
2361
+ await sleep5(50);
2362
+ }
2363
+ }
2364
+ async function waitForSpawnedProcessOwner(pid, timeoutMs) {
2365
+ if (!pid || pid <= 0) {
2366
+ throw new Error("Chrome did not expose a child process id.");
2367
+ }
2368
+ const deadline = Date.now() + timeoutMs;
2369
+ while (Date.now() < deadline) {
2370
+ const owner = await readProcessOwner(pid);
2371
+ if (owner) {
2372
+ return owner;
2373
+ }
2374
+ await sleep5(50);
2375
+ }
2376
+ throw new Error(
2377
+ `Chrome process ${pid} did not report a stable process start time.`
2378
+ );
2379
+ }
2380
+ async function withSharedSessionLock(persistentUserDataDir, action) {
2381
+ return await withDirLock(
2382
+ buildSharedSessionLockPath(persistentUserDataDir),
2383
+ action
2384
+ );
2385
+ }
2386
+ async function registerSharedSessionClient(persistentUserDataDir, metadata) {
2387
+ const liveClients = await listLiveSharedSessionClients(persistentUserDataDir);
2388
+ const client = buildSharedSessionClientRegistration();
2389
+ await mkdir4(buildSharedSessionClientsDirPath(persistentUserDataDir), {
2390
+ recursive: true
2391
+ });
2392
+ await writeFile4(
2393
+ buildSharedSessionClientPath(persistentUserDataDir, client.clientId),
2394
+ JSON.stringify(client, null, 2),
2395
+ {
2396
+ flag: "wx"
2397
+ }
2398
+ );
2399
+ return {
2400
+ client,
2401
+ metadata,
2402
+ reuseExistingPage: liveClients.length === 0
2403
+ };
2404
+ }
2405
+ async function removeSharedSessionClientRegistration(persistentUserDataDir, clientId) {
2406
+ await rm4(buildSharedSessionClientPath(persistentUserDataDir, clientId), {
2407
+ force: true
2408
+ }).catch(() => void 0);
2409
+ }
2410
+ async function listLiveSharedSessionClients(persistentUserDataDir) {
2411
+ const clientsDirPath = buildSharedSessionClientsDirPath(persistentUserDataDir);
2412
+ let entries;
2413
+ try {
2414
+ entries = await readdir3(clientsDirPath, {
2415
+ encoding: "utf8",
2416
+ withFileTypes: true
2417
+ });
2418
+ } catch {
2419
+ return [];
2420
+ }
2421
+ const liveClients = [];
2422
+ for (const entry of entries) {
2423
+ if (!entry.isFile()) {
2424
+ continue;
2425
+ }
2426
+ const filePath = join5(clientsDirPath, entry.name);
2427
+ const registration = await readSharedSessionClientRegistration(filePath);
2428
+ if (!registration) {
2429
+ await rm4(filePath, { force: true }).catch(() => void 0);
2430
+ continue;
2431
+ }
2432
+ if (await getProcessLiveness(registration.owner) === "dead") {
2433
+ await rm4(filePath, { force: true }).catch(() => void 0);
2434
+ continue;
2435
+ }
2436
+ liveClients.push(registration);
2437
+ }
2438
+ return liveClients;
2439
+ }
2440
+ async function readSharedSessionClientRegistration(filePath) {
2441
+ try {
2442
+ const raw = await readFile5(filePath, "utf8");
2443
+ return parseSharedSessionClientRegistration(JSON.parse(raw));
2444
+ } catch {
2445
+ return null;
2446
+ }
2447
+ }
2448
+ function buildSharedSessionClientRegistration() {
2449
+ return {
2450
+ clientId: randomUUID4(),
2451
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2452
+ owner: CURRENT_PROCESS_OWNER
2453
+ };
2454
+ }
2455
+ function parseSharedSessionClientRegistration(value) {
2456
+ if (!value || typeof value !== "object") {
2457
+ return null;
2458
+ }
2459
+ const parsed = value;
2460
+ const owner = parseProcessOwner(parsed.owner);
2461
+ if (!owner || typeof parsed.clientId !== "string" || typeof parsed.createdAt !== "string") {
2462
+ return null;
2463
+ }
2464
+ return {
2465
+ clientId: parsed.clientId,
2466
+ createdAt: parsed.createdAt,
2467
+ owner
2468
+ };
2469
+ }
2470
+ function assertSharedSessionCompatibility(metadata, options) {
2471
+ if (metadata.executablePath !== options.executablePath) {
2472
+ throw new Error(
2473
+ `Chrome profile "${options.profileDirectory}" is already running with executable "${metadata.executablePath}", not "${options.executablePath}".`
2474
+ );
2475
+ }
2476
+ if (metadata.headless !== options.headless) {
2477
+ throw new Error(
2478
+ `Chrome profile "${options.profileDirectory}" is already running with headless=${metadata.headless}, not ${options.headless}.`
2479
+ );
2480
+ }
2481
+ }
2482
+ async function prepareSharedSessionCloseIfIdle(persistentUserDataDir, clientId, sessionId) {
2483
+ return await withSharedSessionLock(persistentUserDataDir, async () => {
2484
+ const metadata = await readSharedSessionMetadata(persistentUserDataDir);
2485
+ await removeSharedSessionClientRegistration(
2486
+ persistentUserDataDir,
2487
+ clientId
2488
+ );
2489
+ if (!metadata || metadata.sessionId !== sessionId) {
2490
+ return {
2491
+ closeBrowser: false,
2492
+ sessionId
2493
+ };
2494
+ }
2495
+ const liveClients = await listLiveSharedSessionClients(
2496
+ persistentUserDataDir
2497
+ );
2498
+ if (liveClients.length > 0) {
2499
+ return {
2500
+ closeBrowser: false,
2501
+ sessionId: metadata.sessionId
2502
+ };
2503
+ }
2504
+ const closingMetadata = {
2505
+ ...metadata,
2506
+ state: "closing",
2507
+ stateOwner: CURRENT_PROCESS_OWNER
2508
+ };
2509
+ await writeSharedSessionMetadata(
2510
+ persistentUserDataDir,
2511
+ closingMetadata
2512
+ );
2513
+ return {
2514
+ browserOwner: closingMetadata.browserOwner,
2515
+ closeBrowser: true,
2516
+ sessionId: closingMetadata.sessionId
2517
+ };
2518
+ });
2519
+ }
2520
+ async function closeSharedSessionBrowser(persistentUserDataDir, closePlan, browser) {
2521
+ if (browser) {
2522
+ await requestBrowserShutdown(browser);
2523
+ await waitForProcessToExit(closePlan.browserOwner, 1e3);
2524
+ }
2525
+ if (await getProcessLiveness(closePlan.browserOwner) !== "dead") {
2526
+ await killOwnedBrowserProcess(closePlan.browserOwner);
2527
+ await waitForProcessToExit(closePlan.browserOwner, 2e3);
2528
+ }
2529
+ await finalizeSharedSessionClose(persistentUserDataDir, closePlan.sessionId);
2530
+ }
2531
+ async function finalizeSharedSessionClose(persistentUserDataDir, sessionId) {
2532
+ await withSharedSessionLock(persistentUserDataDir, async () => {
2533
+ const metadata = await readSharedSessionMetadata(persistentUserDataDir);
2534
+ if (!metadata || metadata.sessionId !== sessionId) {
2535
+ return;
2536
+ }
2537
+ const liveClients = await listLiveSharedSessionClients(
2538
+ persistentUserDataDir
2539
+ );
2540
+ if (liveClients.length > 0) {
2541
+ const readyMetadata = {
2542
+ ...metadata,
2543
+ state: "ready"
2544
+ };
2545
+ await writeSharedSessionMetadata(
2546
+ persistentUserDataDir,
2547
+ readyMetadata
2548
+ );
2549
+ return;
2550
+ }
2551
+ if (await getProcessLiveness(metadata.browserOwner) !== "dead") {
2552
+ const readyMetadata = {
2553
+ ...metadata,
2554
+ state: "ready"
2555
+ };
2556
+ await writeSharedSessionMetadata(
2557
+ persistentUserDataDir,
2558
+ readyMetadata
2559
+ );
2560
+ return;
2561
+ }
2562
+ await rm4(buildSharedSessionDirPath(persistentUserDataDir), {
2563
+ force: true,
2564
+ recursive: true
2565
+ }).catch(() => void 0);
2566
+ });
2567
+ }
2568
+ function getPrimaryBrowserContext(browser) {
2569
+ const contexts = browser.contexts();
2570
+ if (contexts.length === 0) {
2571
+ throw new Error(
2572
+ "Connection succeeded but no browser contexts were exposed."
2573
+ );
2574
+ }
2575
+ return contexts[0];
2576
+ }
2577
+ async function getSharedSessionPage(context, reuseExistingPage) {
2578
+ if (reuseExistingPage) {
2579
+ return await getExistingPageOrCreate(context);
2580
+ }
2581
+ return await context.newPage();
2582
+ }
2583
+ async function getExistingPageOrCreate(context) {
2584
+ const existingPage = context.pages()[0];
2585
+ if (existingPage) {
2586
+ return existingPage;
2587
+ }
2588
+ return await context.newPage();
2589
+ }
2590
+ function buildSharedSessionDiscoveryUrl(debugPort) {
2591
+ return `http://127.0.0.1:${debugPort}`;
2592
+ }
2593
+ async function resolveCdpWebSocketUrl(cdpUrl, timeoutMs) {
2594
+ if (cdpUrl.startsWith("ws://") || cdpUrl.startsWith("wss://")) {
2595
+ return cdpUrl;
2596
+ }
2597
+ const versionUrl = normalizeDiscoveryUrl(cdpUrl);
2598
+ const deadline = Date.now() + timeoutMs;
2599
+ let lastError = "CDP discovery did not respond.";
2600
+ while (Date.now() < deadline) {
2601
+ const remaining = Math.max(deadline - Date.now(), 1e3);
2602
+ try {
2603
+ const response = await fetch(versionUrl, {
2604
+ signal: AbortSignal.timeout(Math.min(remaining, 5e3))
2605
+ });
2606
+ if (!response.ok) {
2607
+ lastError = `${response.status} ${response.statusText}`;
2608
+ } else {
2609
+ const payload = await response.json();
2610
+ const wsUrl = payload && typeof payload === "object" && !Array.isArray(payload) && typeof payload.webSocketDebuggerUrl === "string" ? payload.webSocketDebuggerUrl : null;
2611
+ if (wsUrl && wsUrl.trim()) {
2612
+ return wsUrl;
2613
+ }
2614
+ lastError = "CDP discovery response did not include webSocketDebuggerUrl.";
252
2615
  }
253
- await rm(join(profilesDir, entry.name), {
254
- recursive: true,
255
- force: true
256
- }).catch(() => void 0);
257
- })
258
- );
259
- }
260
- function isTempDirOwnedByLiveProcess(tempDirName, tempDirPrefix) {
261
- const owner = parseTempDirOwner(tempDirName, tempDirPrefix);
262
- if (!owner) {
263
- return false;
264
- }
265
- if (owner.pid === process.pid && Math.abs(owner.processStartedAtMs - PROCESS_STARTED_AT_MS) <= PROCESS_START_TIME_TOLERANCE_MS) {
266
- return true;
2616
+ } catch (error) {
2617
+ lastError = error instanceof Error ? error.message : "Unknown error";
2618
+ }
2619
+ await sleep5(100);
267
2620
  }
268
- return isProcessRunning(owner.pid);
2621
+ throw new Error(
2622
+ `Failed to resolve a CDP websocket URL from ${versionUrl.toString()}: ${lastError}`
2623
+ );
269
2624
  }
270
- function parseTempDirOwner(tempDirName, tempDirPrefix) {
271
- const remainder = tempDirName.slice(tempDirPrefix.length);
272
- const firstDashIndex = remainder.indexOf("-");
273
- const secondDashIndex = firstDashIndex === -1 ? -1 : remainder.indexOf("-", firstDashIndex + 1);
274
- if (firstDashIndex === -1 || secondDashIndex === -1) {
275
- return null;
2625
+ function normalizeDiscoveryUrl(cdpUrl) {
2626
+ let parsed;
2627
+ try {
2628
+ parsed = new URL(cdpUrl);
2629
+ } catch {
2630
+ throw new Error(
2631
+ `Invalid CDP URL "${cdpUrl}". Use an http(s) or ws(s) endpoint.`
2632
+ );
276
2633
  }
277
- const pid = Number.parseInt(remainder.slice(0, firstDashIndex), 10);
278
- const processStartedAtMs = Number.parseInt(
279
- remainder.slice(firstDashIndex + 1, secondDashIndex),
280
- 10
281
- );
282
- if (!Number.isInteger(pid) || pid <= 0) {
283
- return null;
2634
+ if (parsed.protocol === "ws:" || parsed.protocol === "wss:") {
2635
+ return parsed;
284
2636
  }
285
- if (!Number.isInteger(processStartedAtMs) || processStartedAtMs <= 0) {
286
- return null;
2637
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
2638
+ throw new Error(
2639
+ `Unsupported CDP URL protocol "${parsed.protocol}". Use http(s) or ws(s).`
2640
+ );
287
2641
  }
288
- return { pid, processStartedAtMs };
2642
+ const normalized = new URL(parsed.toString());
2643
+ normalized.pathname = "/json/version";
2644
+ normalized.search = "";
2645
+ normalized.hash = "";
2646
+ return normalized;
289
2647
  }
290
- function isProcessRunning(pid) {
291
- try {
292
- process.kill(pid, 0);
293
- return true;
294
- } catch (error) {
295
- const code = typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : void 0;
296
- return code !== "ESRCH";
297
- }
2648
+ async function reserveDebugPort() {
2649
+ return await new Promise((resolve, reject) => {
2650
+ const server = createServer();
2651
+ server.unref();
2652
+ server.on("error", reject);
2653
+ server.listen(0, "127.0.0.1", () => {
2654
+ const address = server.address();
2655
+ if (!address || typeof address === "string") {
2656
+ server.close();
2657
+ reject(new Error("Failed to reserve a local debug port."));
2658
+ return;
2659
+ }
2660
+ server.close((error) => {
2661
+ if (error) {
2662
+ reject(error);
2663
+ return;
2664
+ }
2665
+ resolve(address.port);
2666
+ });
2667
+ });
2668
+ });
2669
+ }
2670
+ async function sleep5(ms) {
2671
+ await new Promise((resolve) => setTimeout(resolve, ms));
298
2672
  }
299
2673
 
300
2674
  // src/browser/pool.ts
301
- import { spawn } from "child_process";
302
- import { rm as rm2 } from "fs/promises";
303
- import { createServer } from "net";
304
2675
  import {
305
- chromium
2676
+ chromium as chromium2
306
2677
  } from "playwright";
307
2678
 
308
2679
  // src/browser/cdp-proxy.ts
@@ -722,16 +3093,14 @@ function errorMessage(error) {
722
3093
  // src/browser/pool.ts
723
3094
  var BrowserPool = class {
724
3095
  browser = null;
725
- cdpProxy = null;
726
- launchedProcess = null;
727
- managedUserDataDir = null;
728
- persistentProfile = false;
3096
+ activeSessionClose = null;
3097
+ closeInFlight = null;
729
3098
  defaults;
730
3099
  constructor(defaults = {}) {
731
3100
  this.defaults = defaults;
732
3101
  }
733
3102
  async launch(options = {}) {
734
- if (this.browser || this.cdpProxy || this.launchedProcess || this.managedUserDataDir) {
3103
+ if (this.browser || this.activeSessionClose) {
735
3104
  await this.close();
736
3105
  }
737
3106
  const mode = options.mode ?? this.defaults.mode ?? "chromium";
@@ -778,30 +3147,26 @@ var BrowserPool = class {
778
3147
  return this.launchSandbox(options);
779
3148
  }
780
3149
  async close() {
781
- const browser = this.browser;
782
- const cdpProxy = this.cdpProxy;
783
- const launchedProcess = this.launchedProcess;
784
- const managedUserDataDir = this.managedUserDataDir;
785
- const persistentProfile = this.persistentProfile;
786
- this.browser = null;
787
- this.cdpProxy = null;
788
- this.launchedProcess = null;
789
- this.managedUserDataDir = null;
790
- this.persistentProfile = false;
3150
+ if (this.closeInFlight) {
3151
+ await this.closeInFlight;
3152
+ return;
3153
+ }
3154
+ const closeOperation = this.closeCurrent();
3155
+ this.closeInFlight = closeOperation;
791
3156
  try {
792
- if (browser) {
793
- await browser.close().catch(() => void 0);
794
- }
3157
+ await closeOperation;
3158
+ this.browser = null;
3159
+ this.activeSessionClose = null;
795
3160
  } finally {
796
- cdpProxy?.close();
797
- await killProcessTree(launchedProcess);
798
- if (managedUserDataDir && !persistentProfile) {
799
- await rm2(managedUserDataDir, {
800
- recursive: true,
801
- force: true
802
- }).catch(() => void 0);
803
- }
3161
+ this.closeInFlight = null;
3162
+ }
3163
+ }
3164
+ async closeCurrent() {
3165
+ if (this.activeSessionClose) {
3166
+ await this.activeSessionClose();
3167
+ return;
804
3168
  }
3169
+ await this.browser?.close().catch(() => void 0);
805
3170
  }
806
3171
  async connectToRunning(cdpUrl, timeout) {
807
3172
  let browser = null;
@@ -816,11 +3181,14 @@ var BrowserPool = class {
816
3181
  }
817
3182
  cdpProxy = new CDPProxy(browserWsUrl, targetId);
818
3183
  const proxyWsUrl = await cdpProxy.start();
819
- browser = await chromium.connectOverCDP(proxyWsUrl, {
3184
+ browser = await chromium2.connectOverCDP(proxyWsUrl, {
820
3185
  timeout: timeout ?? 3e4
821
3186
  });
822
3187
  this.browser = browser;
823
- this.cdpProxy = cdpProxy;
3188
+ this.activeSessionClose = async () => {
3189
+ await browser?.close().catch(() => void 0);
3190
+ cdpProxy?.close();
3191
+ };
824
3192
  const { context, page } = await pickBrowserContextAndPage(browser);
825
3193
  return { browser, context, page, isExternal: true };
826
3194
  } catch (error) {
@@ -829,7 +3197,7 @@ var BrowserPool = class {
829
3197
  }
830
3198
  cdpProxy?.close();
831
3199
  this.browser = null;
832
- this.cdpProxy = null;
3200
+ this.activeSessionClose = null;
833
3201
  throw error;
834
3202
  }
835
3203
  }
@@ -849,55 +3217,29 @@ var BrowserPool = class {
849
3217
  sourceUserDataDir,
850
3218
  profileDirectory
851
3219
  );
852
- await clearPersistentProfileSingletons(persistentProfile.userDataDir);
853
- const debugPort = await reserveDebugPort();
854
- const headless = resolveLaunchHeadless(
855
- "real",
856
- options.headless,
857
- this.defaults.headless
858
- );
859
- const launchArgs = buildRealBrowserLaunchArgs({
860
- userDataDir: persistentProfile.userDataDir,
3220
+ const sharedSession = await acquireSharedRealBrowserSession({
3221
+ executablePath,
3222
+ headless: resolveLaunchHeadless(
3223
+ "real",
3224
+ options.headless,
3225
+ this.defaults.headless
3226
+ ),
3227
+ initialUrl: options.initialUrl,
3228
+ persistentProfile,
861
3229
  profileDirectory,
862
- debugPort,
863
- headless
864
- });
865
- const processHandle = spawn(executablePath, launchArgs, {
866
- detached: process.platform !== "win32",
867
- stdio: "ignore"
3230
+ timeoutMs: options.timeout ?? 3e4
868
3231
  });
869
- processHandle.unref();
870
- let browser = null;
871
- try {
872
- const wsUrl = await resolveCdpWebSocketUrl(
873
- `http://127.0.0.1:${debugPort}`,
874
- options.timeout ?? 3e4
875
- );
876
- browser = await chromium.connectOverCDP(wsUrl, {
877
- timeout: options.timeout ?? 3e4
878
- });
879
- const { context, page } = await createOwnedBrowserContextAndPage(
880
- browser
881
- );
882
- if (options.initialUrl) {
883
- await page.goto(options.initialUrl, {
884
- waitUntil: "domcontentloaded",
885
- timeout: options.timeout ?? 3e4
886
- });
887
- }
888
- this.browser = browser;
889
- this.launchedProcess = processHandle;
890
- this.managedUserDataDir = persistentProfile.userDataDir;
891
- this.persistentProfile = true;
892
- return { browser, context, page, isExternal: false };
893
- } catch (error) {
894
- await browser?.close().catch(() => void 0);
895
- await killProcessTree(processHandle);
896
- throw error;
897
- }
3232
+ this.browser = sharedSession.browser;
3233
+ this.activeSessionClose = sharedSession.close;
3234
+ return {
3235
+ browser: sharedSession.browser,
3236
+ context: sharedSession.context,
3237
+ page: sharedSession.page,
3238
+ isExternal: false
3239
+ };
898
3240
  }
899
3241
  async launchSandbox(options) {
900
- const browser = await chromium.launch({
3242
+ const browser = await chromium2.launch({
901
3243
  headless: resolveLaunchHeadless(
902
3244
  "chromium",
903
3245
  options.headless,
@@ -910,11 +3252,14 @@ var BrowserPool = class {
910
3252
  const context = await browser.newContext(options.context || {});
911
3253
  const page = await context.newPage();
912
3254
  this.browser = browser;
3255
+ this.activeSessionClose = async () => {
3256
+ await browser.close().catch(() => void 0);
3257
+ };
913
3258
  return { browser, context, page, isExternal: false };
914
3259
  }
915
3260
  };
916
3261
  async function pickBrowserContextAndPage(browser) {
917
- const context = getPrimaryBrowserContext(browser);
3262
+ const context = getPrimaryBrowserContext2(browser);
918
3263
  const page = await getAttachedPageOrCreate(context);
919
3264
  return { context, page };
920
3265
  }
@@ -927,11 +3272,6 @@ function resolveLaunchHeadless(mode, requestedHeadless, defaultHeadless) {
927
3272
  }
928
3273
  return mode === "real";
929
3274
  }
930
- async function createOwnedBrowserContextAndPage(browser) {
931
- const context = getPrimaryBrowserContext(browser);
932
- const page = await getExistingPageOrCreate(context);
933
- return { context, page };
934
- }
935
3275
  async function getAttachedPageOrCreate(context) {
936
3276
  const pages = context.pages();
937
3277
  const inspectablePage = pages.find(
@@ -946,14 +3286,7 @@ async function getAttachedPageOrCreate(context) {
946
3286
  }
947
3287
  return await context.newPage();
948
3288
  }
949
- async function getExistingPageOrCreate(context) {
950
- const existingPage = context.pages()[0];
951
- if (existingPage) {
952
- return existingPage;
953
- }
954
- return await context.newPage();
955
- }
956
- function getPrimaryBrowserContext(browser) {
3289
+ function getPrimaryBrowserContext2(browser) {
957
3290
  const contexts = browser.contexts();
958
3291
  if (contexts.length === 0) {
959
3292
  throw new Error(
@@ -972,125 +3305,6 @@ function safePageUrl(page) {
972
3305
  return "";
973
3306
  }
974
3307
  }
975
- function normalizeDiscoveryUrl(cdpUrl) {
976
- let parsed;
977
- try {
978
- parsed = new URL(cdpUrl);
979
- } catch {
980
- throw new Error(
981
- `Invalid CDP URL "${cdpUrl}". Use an http(s) or ws(s) endpoint.`
982
- );
983
- }
984
- if (parsed.protocol === "ws:" || parsed.protocol === "wss:") {
985
- return parsed;
986
- }
987
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
988
- throw new Error(
989
- `Unsupported CDP URL protocol "${parsed.protocol}". Use http(s) or ws(s).`
990
- );
991
- }
992
- const normalized = new URL(parsed.toString());
993
- normalized.pathname = "/json/version";
994
- normalized.search = "";
995
- normalized.hash = "";
996
- return normalized;
997
- }
998
- async function resolveCdpWebSocketUrl(cdpUrl, timeoutMs) {
999
- if (cdpUrl.startsWith("ws://") || cdpUrl.startsWith("wss://")) {
1000
- return cdpUrl;
1001
- }
1002
- const versionUrl = normalizeDiscoveryUrl(cdpUrl);
1003
- const deadline = Date.now() + timeoutMs;
1004
- let lastError = "CDP discovery did not respond.";
1005
- while (Date.now() < deadline) {
1006
- const remaining = Math.max(deadline - Date.now(), 1e3);
1007
- try {
1008
- const response = await fetch(versionUrl, {
1009
- signal: AbortSignal.timeout(Math.min(remaining, 5e3))
1010
- });
1011
- if (!response.ok) {
1012
- lastError = `${response.status} ${response.statusText}`;
1013
- } else {
1014
- const payload = await response.json();
1015
- const wsUrl = payload && typeof payload === "object" && !Array.isArray(payload) && typeof payload.webSocketDebuggerUrl === "string" ? payload.webSocketDebuggerUrl : null;
1016
- if (wsUrl && wsUrl.trim()) {
1017
- return wsUrl;
1018
- }
1019
- lastError = "CDP discovery response did not include webSocketDebuggerUrl.";
1020
- }
1021
- } catch (error) {
1022
- lastError = error instanceof Error ? error.message : "Unknown error";
1023
- }
1024
- await sleep(100);
1025
- }
1026
- throw new Error(
1027
- `Failed to resolve a CDP websocket URL from ${versionUrl.toString()}: ${lastError}`
1028
- );
1029
- }
1030
- async function reserveDebugPort() {
1031
- return await new Promise((resolve, reject) => {
1032
- const server = createServer();
1033
- server.unref();
1034
- server.on("error", reject);
1035
- server.listen(0, "127.0.0.1", () => {
1036
- const address = server.address();
1037
- if (!address || typeof address === "string") {
1038
- server.close();
1039
- reject(new Error("Failed to reserve a local debug port."));
1040
- return;
1041
- }
1042
- server.close((error) => {
1043
- if (error) {
1044
- reject(error);
1045
- return;
1046
- }
1047
- resolve(address.port);
1048
- });
1049
- });
1050
- });
1051
- }
1052
- function buildRealBrowserLaunchArgs(options) {
1053
- const args = [
1054
- `--user-data-dir=${options.userDataDir}`,
1055
- `--profile-directory=${options.profileDirectory}`,
1056
- `--remote-debugging-port=${options.debugPort}`,
1057
- "--disable-blink-features=AutomationControlled"
1058
- ];
1059
- if (options.headless) {
1060
- args.push("--headless=new");
1061
- }
1062
- return args;
1063
- }
1064
- async function killProcessTree(processHandle) {
1065
- if (!processHandle || processHandle.pid == null || processHandle.exitCode !== null) {
1066
- return;
1067
- }
1068
- if (process.platform === "win32") {
1069
- await new Promise((resolve) => {
1070
- const killer = spawn(
1071
- "taskkill",
1072
- ["/pid", String(processHandle.pid), "/t", "/f"],
1073
- {
1074
- stdio: "ignore"
1075
- }
1076
- );
1077
- killer.on("error", () => resolve());
1078
- killer.on("exit", () => resolve());
1079
- });
1080
- return;
1081
- }
1082
- try {
1083
- process.kill(-processHandle.pid, "SIGKILL");
1084
- } catch {
1085
- try {
1086
- processHandle.kill("SIGKILL");
1087
- } catch {
1088
- }
1089
- }
1090
- }
1091
- async function sleep(ms) {
1092
- await new Promise((resolve) => setTimeout(resolve, ms));
1093
- }
1094
3308
 
1095
3309
  // src/navigation.ts
1096
3310
  var DEFAULT_TIMEOUT = 3e4;
@@ -1470,7 +3684,7 @@ var StealthCdpRuntime = class _StealthCdpRuntime {
1470
3684
  TRANSIENT_CONTEXT_RETRY_DELAY_MS,
1471
3685
  Math.max(0, deadline - Date.now())
1472
3686
  );
1473
- await sleep2(retryDelay);
3687
+ await sleep6(retryDelay);
1474
3688
  }
1475
3689
  }
1476
3690
  }
@@ -1503,7 +3717,7 @@ var StealthCdpRuntime = class _StealthCdpRuntime {
1503
3717
  () => ({ kind: "resolved" }),
1504
3718
  (error) => ({ kind: "rejected", error })
1505
3719
  );
1506
- const timeoutPromise = sleep2(
3720
+ const timeoutPromise = sleep6(
1507
3721
  timeout + FRAME_EVALUATE_GRACE_MS
1508
3722
  ).then(() => ({ kind: "timeout" }));
1509
3723
  const result = await Promise.race([
@@ -1645,7 +3859,7 @@ function isIgnorableFrameError(error) {
1645
3859
  const message = error.message;
1646
3860
  return message.includes("Frame was detached") || message.includes("Target page, context or browser has been closed") || isTransientExecutionContextError(error) || message.includes("No frame for given id found");
1647
3861
  }
1648
- function sleep2(ms) {
3862
+ function sleep6(ms) {
1649
3863
  return new Promise((resolve) => {
1650
3864
  setTimeout(resolve, ms);
1651
3865
  });
@@ -5897,7 +8111,7 @@ async function closeTab(context, activePage, index) {
5897
8111
  }
5898
8112
 
5899
8113
  // src/actions/cookies.ts
5900
- import { readFile, writeFile as writeFile2 } from "fs/promises";
8114
+ import { readFile as readFile6, writeFile as writeFile5 } from "fs/promises";
5901
8115
  async function getCookies(context, url) {
5902
8116
  return context.cookies(url ? [url] : void 0);
5903
8117
  }
@@ -5909,10 +8123,10 @@ async function clearCookies(context) {
5909
8123
  }
5910
8124
  async function exportCookies(context, filePath, url) {
5911
8125
  const cookies = await context.cookies(url ? [url] : void 0);
5912
- await writeFile2(filePath, JSON.stringify(cookies, null, 2), "utf-8");
8126
+ await writeFile5(filePath, JSON.stringify(cookies, null, 2), "utf-8");
5913
8127
  }
5914
8128
  async function importCookies(context, filePath) {
5915
- const raw = await readFile(filePath, "utf-8");
8129
+ const raw = await readFile6(filePath, "utf-8");
5916
8130
  const cookies = JSON.parse(raw);
5917
8131
  await context.addCookies(cookies);
5918
8132
  }
@@ -7740,7 +9954,7 @@ async function executeAgentAction(page, action) {
7740
9954
  }
7741
9955
  case "wait": {
7742
9956
  const ms = numberOr(action.timeMs, action.time_ms, 1e3);
7743
- await sleep3(ms);
9957
+ await sleep7(ms);
7744
9958
  return;
7745
9959
  }
7746
9960
  case "goto": {
@@ -7905,7 +10119,7 @@ async function pressKeyCombo(page, combo) {
7905
10119
  }
7906
10120
  }
7907
10121
  }
7908
- function sleep3(ms) {
10122
+ function sleep7(ms) {
7909
10123
  return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
7910
10124
  }
7911
10125
 
@@ -7936,7 +10150,7 @@ var OpensteerCuaAgentHandler = class {
7936
10150
  if (isMutatingAgentAction(action)) {
7937
10151
  this.onMutatingAction?.(action);
7938
10152
  }
7939
- await sleep4(this.config.waitBetweenActionsMs);
10153
+ await sleep8(this.config.waitBetweenActionsMs);
7940
10154
  });
7941
10155
  try {
7942
10156
  const result = await this.client.execute({
@@ -7998,7 +10212,7 @@ var OpensteerCuaAgentHandler = class {
7998
10212
  await this.cursorController.preview({ x, y }, "agent");
7999
10213
  }
8000
10214
  };
8001
- function sleep4(ms) {
10215
+ function sleep8(ms) {
8002
10216
  return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
8003
10217
  }
8004
10218
 
@@ -8434,7 +10648,7 @@ var CursorController = class {
8434
10648
  for (const step of motion.points) {
8435
10649
  await this.renderer.move(step, this.style);
8436
10650
  if (motion.stepDelayMs > 0) {
8437
- await sleep5(motion.stepDelayMs);
10651
+ await sleep9(motion.stepDelayMs);
8438
10652
  }
8439
10653
  }
8440
10654
  if (shouldPulse(intent)) {
@@ -8592,12 +10806,12 @@ function clamp2(value, min, max) {
8592
10806
  function shouldPulse(intent) {
8593
10807
  return intent === "click" || intent === "dblclick" || intent === "rightclick" || intent === "agent";
8594
10808
  }
8595
- function sleep5(ms) {
10809
+ function sleep9(ms) {
8596
10810
  return new Promise((resolve) => setTimeout(resolve, ms));
8597
10811
  }
8598
10812
 
8599
10813
  // src/opensteer.ts
8600
- import { createHash as createHash2, randomUUID } from "crypto";
10814
+ import { createHash as createHash2, randomUUID as randomUUID5 } from "crypto";
8601
10815
 
8602
10816
  // src/action-wait.ts
8603
10817
  var ROBUST_PROFILE = {
@@ -8792,7 +11006,7 @@ var AdaptiveNetworkTracker = class {
8792
11006
  this.idleSince = 0;
8793
11007
  }
8794
11008
  const remaining = Math.max(1, options.deadline - now);
8795
- await sleep6(Math.min(NETWORK_POLL_MS, remaining));
11009
+ await sleep10(Math.min(NETWORK_POLL_MS, remaining));
8796
11010
  }
8797
11011
  }
8798
11012
  handleRequestStarted = (request) => {
@@ -8837,7 +11051,7 @@ var AdaptiveNetworkTracker = class {
8837
11051
  return false;
8838
11052
  }
8839
11053
  };
8840
- async function sleep6(ms) {
11054
+ async function sleep10(ms) {
8841
11055
  await new Promise((resolve) => {
8842
11056
  setTimeout(resolve, ms);
8843
11057
  });
@@ -10617,15 +12831,22 @@ var Opensteer = class _Opensteer {
10617
12831
  }
10618
12832
  return;
10619
12833
  }
10620
- if (this.ownsBrowser) {
10621
- await this.pool.close();
10622
- }
10623
- this.browser = null;
10624
- this.pageRef = null;
10625
- this.contextRef = null;
10626
- this.ownsBrowser = false;
10627
- if (this.cursorController) {
10628
- await this.cursorController.dispose().catch(() => void 0);
12834
+ let closedOwnedBrowser = false;
12835
+ try {
12836
+ if (this.ownsBrowser) {
12837
+ await this.pool.close();
12838
+ closedOwnedBrowser = true;
12839
+ }
12840
+ } finally {
12841
+ this.browser = null;
12842
+ this.pageRef = null;
12843
+ this.contextRef = null;
12844
+ if (!this.ownsBrowser || closedOwnedBrowser) {
12845
+ this.ownsBrowser = false;
12846
+ }
12847
+ if (this.cursorController) {
12848
+ await this.cursorController.dispose().catch(() => void 0);
12849
+ }
10629
12850
  }
10630
12851
  }
10631
12852
  async syncLocalSelectorCacheToCloud() {
@@ -12949,12 +15170,16 @@ function normalizeCloudBrowserProfilePreference(value, source) {
12949
15170
  }
12950
15171
  function buildLocalRunId(namespace) {
12951
15172
  const normalized = namespace.trim() || "default";
12952
- return `${normalized}-${Date.now().toString(36)}-${randomUUID().slice(0, 8)}`;
15173
+ return `${normalized}-${Date.now().toString(36)}-${randomUUID5().slice(0, 8)}`;
12953
15174
  }
12954
15175
 
12955
15176
  export {
12956
15177
  getOrCreatePersistentProfile,
12957
15178
  clearPersistentProfileSingletons,
15179
+ createIsolatedRuntimeProfile,
15180
+ persistIsolatedRuntimeProfile,
15181
+ hasActiveRuntimeProfileCreations,
15182
+ getOwnedRealBrowserProcessPolicy,
12958
15183
  BrowserPool,
12959
15184
  waitForVisualStability,
12960
15185
  createEmptyRegistry,