jorgex-stack 1.9.62 → 1.9.64

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.
@@ -0,0 +1,955 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/lib/browser-command.ts
4
+ import { spawnSync as spawnSync2 } from "child_process";
5
+ import path9 from "path";
6
+
7
+ // src/lib/paths.ts
8
+ import os from "os";
9
+ import path from "path";
10
+ import { existsSync } from "fs";
11
+ import { fileURLToPath } from "url";
12
+ var HOME = os.homedir();
13
+ function dataDir() {
14
+ return path.join(HOME, ".jorgex-stack");
15
+ }
16
+
17
+ // src/lib/browser-managed.ts
18
+ import { createHash as createHash3, randomBytes as randomBytes2 } from "crypto";
19
+ import fs3 from "fs";
20
+ import path5 from "path";
21
+
22
+ // src/lib/browser-stage.ts
23
+ import { spawnSync } from "child_process";
24
+ import fs2 from "fs";
25
+ import path4 from "path";
26
+ import { createHash as createHash2, timingSafeEqual as timingSafeEqual2 } from "crypto";
27
+
28
+ // src/lib/npm-provider.ts
29
+ import fs from "fs";
30
+ import path2 from "path";
31
+ import { createHash, randomBytes, timingSafeEqual } from "crypto";
32
+ var MAX_METADATA_BYTES = 4 * 1024 * 1024;
33
+ var STABLE_SEMVER = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
34
+ function isStableSemverVersion(value) {
35
+ return typeof value === "string" && STABLE_SEMVER.test(value);
36
+ }
37
+ function isCanonicalSha512Integrity(integrity) {
38
+ if (typeof integrity !== "string" || !integrity.startsWith("sha512-")) return false;
39
+ const b64 = integrity.slice("sha512-".length);
40
+ if (!/^[A-Za-z0-9+/]+={0,2}$/.test(b64)) return false;
41
+ let bytes;
42
+ try {
43
+ bytes = Buffer.from(b64, "base64");
44
+ } catch {
45
+ return false;
46
+ }
47
+ return bytes.length === 64 && bytes.toString("base64") === b64;
48
+ }
49
+ function isValidObservedVersion(value) {
50
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
51
+ const observed = value;
52
+ return isStableSemverVersion(observed.version) && isCanonicalSha512Integrity(observed.integrity);
53
+ }
54
+ var MAX_TARBALL_BYTES = 128 * 1024 * 1024;
55
+
56
+ // src/lib/detect.ts
57
+ import path3 from "path";
58
+ import { existsSync as existsSync2, statSync } from "fs";
59
+ import { execFileSync } from "child_process";
60
+
61
+ // src/lib/browser-stage.ts
62
+ var MAX_ARTIFACT_BYTES = 128 * 1024 * 1024;
63
+ var MAX_JSON_BYTES = 4 * 1024 * 1024;
64
+ var MAX_TREE_BYTES = 512 * 1024 * 1024;
65
+ var MAX_TREE_ENTRIES = 1e5;
66
+ var MAX_TREE_METADATA_BYTES = 32 * 1024 * 1024;
67
+ var MAX_TREE_PATH_BYTES = 16 * 1024;
68
+ var MAX_TREE_SYMLINK_BYTES = 16 * 1024;
69
+ var TREE_HASH_CHUNK_BYTES = 1024 * 1024;
70
+ var CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/;
71
+ function fail(message) {
72
+ throw new Error(`browser-stage: ${message}`);
73
+ }
74
+ function lstatOrFail(file, label) {
75
+ let stat;
76
+ try {
77
+ stat = fs2.lstatSync(file);
78
+ } catch {
79
+ fail(`missing ${label}: ${file}`);
80
+ }
81
+ return stat;
82
+ }
83
+ function assertContained(realRoot, candidate, label) {
84
+ let resolved;
85
+ try {
86
+ resolved = fs2.realpathSync(candidate);
87
+ } catch {
88
+ fail(`cannot resolve ${label}: ${candidate}`);
89
+ }
90
+ if (resolved !== realRoot && !resolved.startsWith(realRoot + path4.sep)) {
91
+ fail(`${label} escapes the stage`);
92
+ }
93
+ return resolved;
94
+ }
95
+ function isContainedTreePath(root, candidate, allowEqual) {
96
+ const relative = path4.relative(path4.resolve(root), path4.resolve(candidate));
97
+ if (relative === "") return allowEqual;
98
+ return relative !== ".." && !relative.startsWith(`..${path4.sep}`) && !path4.isAbsolute(relative);
99
+ }
100
+ function readContainedTreeSymlinkTarget(linkPath, root, metadata) {
101
+ let raw;
102
+ try {
103
+ raw = fs2.readlinkSync(linkPath, "buffer");
104
+ } catch {
105
+ fail(`cannot read staged symlink: ${linkPath}`);
106
+ }
107
+ const target = raw.toString("utf8");
108
+ if (!Buffer.from(target, "utf8").equals(raw) || target === "" || CONTROL_CHARACTERS.test(target) || raw.byteLength > MAX_TREE_SYMLINK_BYTES || path4.isAbsolute(target) || process.platform === "win32" && /^[a-zA-Z]:/.test(target)) {
109
+ fail(`staged symlink must be an internal relative UTF-8 target: ${linkPath}`);
110
+ }
111
+ const separator = process.platform === "win32" ? /[\\/]+/ : /\/+/;
112
+ const parts = target.split(separator).filter((part) => part !== "" && part !== ".");
113
+ if (parts.length === 0) fail(`staged symlink target is empty: ${linkPath}`);
114
+ let current = path4.dirname(path4.resolve(linkPath));
115
+ for (let index = 0; index < parts.length; index += 1) {
116
+ const part = parts[index];
117
+ const last = index === parts.length - 1;
118
+ current = part === ".." ? path4.dirname(current) : path4.join(current, part);
119
+ if (!isContainedTreePath(root, current, true)) fail(`staged symlink escapes its tree: ${linkPath}`);
120
+ let stat;
121
+ try {
122
+ stat = fs2.lstatSync(current);
123
+ } catch {
124
+ fail(`staged symlink target is missing: ${linkPath}`);
125
+ }
126
+ if (stat.isSymbolicLink()) fail(`staged symlink chain is not allowed: ${linkPath}`);
127
+ if (last) {
128
+ if (!stat.isFile() && !stat.isDirectory()) fail(`staged symlink target is not a file or directory: ${linkPath}`);
129
+ } else if (!stat.isDirectory()) {
130
+ fail(`staged symlink target traverses a non-directory: ${linkPath}`);
131
+ }
132
+ }
133
+ if (metadata.bytes + raw.byteLength > MAX_TREE_METADATA_BYTES) {
134
+ fail("staged tree exceeds its metadata bound");
135
+ }
136
+ metadata.bytes += raw.byteLength;
137
+ return target;
138
+ }
139
+ function readTreeDirectoryEntries(directoryPath, root, remainingEntries, metadata) {
140
+ let directory;
141
+ try {
142
+ directory = fs2.opendirSync(directoryPath);
143
+ } catch {
144
+ fail(`cannot open staged tree directory: ${directoryPath}`);
145
+ }
146
+ const entries = [];
147
+ let primary = null;
148
+ try {
149
+ for (; ; ) {
150
+ const entry = directory.readSync();
151
+ if (entry === null) break;
152
+ if (entries.length >= remainingEntries) fail("staged tree exceeds its entry bound");
153
+ const fullPath = path4.join(directoryPath, entry.name);
154
+ const relativePath = path4.relative(path4.resolve(root), fullPath).split(path4.sep).join("/");
155
+ const pathBytes = Buffer.byteLength(relativePath, "utf8");
156
+ if (relativePath === "" || CONTROL_CHARACTERS.test(relativePath) || pathBytes > MAX_TREE_PATH_BYTES || metadata.bytes + pathBytes > MAX_TREE_METADATA_BYTES) {
157
+ fail(`staged tree contains an invalid or oversized path: ${fullPath}`);
158
+ }
159
+ metadata.bytes += pathBytes;
160
+ entries.push({ fullPath, relativePath });
161
+ }
162
+ } catch (error) {
163
+ primary = error;
164
+ if (error instanceof Error && error.message.startsWith("browser-stage: ")) throw error;
165
+ fail(`cannot read staged tree directory: ${directoryPath}`);
166
+ } finally {
167
+ try {
168
+ directory.closeSync();
169
+ } catch (error) {
170
+ if (primary === null) fail(`cannot close staged tree directory: ${directoryPath}`);
171
+ }
172
+ }
173
+ return entries;
174
+ }
175
+ function openTrustedTreeFile(filePath) {
176
+ const flags = fs2.constants.O_RDONLY | (fs2.constants.O_NONBLOCK ?? 0) | (fs2.constants.O_NOFOLLOW ?? 0);
177
+ let fd;
178
+ try {
179
+ fd = fs2.openSync(filePath, flags);
180
+ } catch {
181
+ fail(`cannot open staged tree file: ${filePath}`);
182
+ }
183
+ let stat;
184
+ try {
185
+ stat = fs2.fstatSync(fd);
186
+ } catch {
187
+ try {
188
+ fs2.closeSync(fd);
189
+ } catch {
190
+ }
191
+ fail(`cannot stat staged tree file: ${filePath}`);
192
+ }
193
+ if (!stat.isFile() || !Number.isSafeInteger(stat.size) || stat.size < 0) {
194
+ try {
195
+ fs2.closeSync(fd);
196
+ } catch {
197
+ }
198
+ fail(`staged tree entry is not a regular file: ${filePath}`);
199
+ }
200
+ return { fd, size: stat.size };
201
+ }
202
+ function hashTrustedTreeFile(hash, filePath, total) {
203
+ const opened = openTrustedTreeFile(filePath);
204
+ let primary = null;
205
+ try {
206
+ if (opened.size > MAX_TREE_BYTES || total.bytes + opened.size > MAX_TREE_BYTES) {
207
+ fail("staged tree exceeds its byte bound");
208
+ }
209
+ const length = Buffer.alloc(8);
210
+ length.writeBigUInt64BE(BigInt(opened.size));
211
+ hash.update(length);
212
+ const buffer = Buffer.allocUnsafe(TREE_HASH_CHUNK_BYTES);
213
+ let readTotal = 0;
214
+ for (; ; ) {
215
+ let read;
216
+ try {
217
+ read = fs2.readSync(opened.fd, buffer, 0, buffer.length, null);
218
+ } catch {
219
+ fail(`cannot read staged tree file: ${filePath}`);
220
+ }
221
+ if (read === 0) break;
222
+ readTotal += read;
223
+ if (readTotal > opened.size || total.bytes + readTotal > MAX_TREE_BYTES) {
224
+ fail(`staged tree file changed or exceeds its byte bound: ${filePath}`);
225
+ }
226
+ hash.update(buffer.subarray(0, read));
227
+ }
228
+ const finalStat = fs2.fstatSync(opened.fd);
229
+ if (!finalStat.isFile() || finalStat.size !== readTotal || readTotal !== opened.size) {
230
+ fail(`staged tree file changed while being verified: ${filePath}`);
231
+ }
232
+ total.bytes += readTotal;
233
+ } catch (error) {
234
+ primary = error;
235
+ throw error;
236
+ } finally {
237
+ try {
238
+ fs2.closeSync(opened.fd);
239
+ } catch {
240
+ if (primary === null) fail(`cannot close staged tree file: ${filePath}`);
241
+ }
242
+ }
243
+ }
244
+ function browserTreeSha256(nodeModulesPath, realStage) {
245
+ const nodeModulesStat = lstatOrFail(nodeModulesPath, "node_modules");
246
+ if (!nodeModulesStat.isDirectory() || nodeModulesStat.isSymbolicLink()) {
247
+ fail("node_modules must be a real directory");
248
+ }
249
+ assertContained(realStage, nodeModulesPath, "node_modules");
250
+ const root = path4.resolve(nodeModulesPath);
251
+ const entries = [];
252
+ const pending = [root];
253
+ const metadata = { bytes: 0 };
254
+ while (pending.length > 0) {
255
+ const directory = pending.pop();
256
+ for (const { fullPath, relativePath } of readTreeDirectoryEntries(
257
+ directory,
258
+ root,
259
+ MAX_TREE_ENTRIES - entries.length,
260
+ metadata
261
+ )) {
262
+ if (entries.length >= MAX_TREE_ENTRIES) fail("staged tree exceeds its entry bound");
263
+ const stat = lstatOrFail(fullPath, "staged tree entry");
264
+ if (stat.isSymbolicLink()) {
265
+ entries.push({
266
+ kind: "symlink",
267
+ relativePath,
268
+ target: readContainedTreeSymlinkTarget(fullPath, root, metadata)
269
+ });
270
+ } else if (stat.isDirectory()) {
271
+ entries.push({ kind: "dir", relativePath });
272
+ pending.push(fullPath);
273
+ } else if (stat.isFile()) {
274
+ entries.push({ kind: "file", relativePath, filePath: fullPath });
275
+ } else {
276
+ fail(`staged tree contains an unsupported entry: ${fullPath}`);
277
+ }
278
+ }
279
+ }
280
+ entries.sort(
281
+ (left, right) => left.relativePath < right.relativePath ? -1 : left.relativePath > right.relativePath ? 1 : 0
282
+ );
283
+ const hash = createHash2("sha256");
284
+ hash.update("browser-v2\0", "utf8");
285
+ const total = { bytes: 0 };
286
+ for (const entry of entries) {
287
+ hash.update(`${entry.kind}\0${entry.relativePath}\0`, "utf8");
288
+ if (entry.kind === "symlink") {
289
+ const payload = Buffer.from(entry.target, "utf8");
290
+ const length = Buffer.alloc(8);
291
+ length.writeBigUInt64BE(BigInt(payload.length));
292
+ hash.update(length);
293
+ hash.update(payload);
294
+ total.bytes += payload.length;
295
+ if (total.bytes > MAX_TREE_BYTES) fail("staged tree exceeds its byte bound");
296
+ } else if (entry.kind === "dir") {
297
+ hash.update(Buffer.alloc(8));
298
+ } else {
299
+ hashTrustedTreeFile(hash, entry.filePath, total);
300
+ }
301
+ }
302
+ return hash.digest("hex");
303
+ }
304
+
305
+ // src/lib/browser-managed.ts
306
+ var MANAGED_ROOT = ".browser-managed";
307
+ var LOCK_FILE = ".activation.lock";
308
+ var ACTIVE_POINTER_FILE = "active.v1.json";
309
+ var RECEIPT_FILE = "receipt.json";
310
+ var HASH = /^[a-f0-9]{64}$/;
311
+ var PACKAGE_NAME = /^(?:@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
312
+ var NPM_VERSION = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
313
+ var MAX_TREE_BYTES2 = 512 * 1024 * 1024;
314
+ var MAX_TREE_ENTRIES2 = 1e5;
315
+ var MAX_TREE_METADATA_BYTES2 = 32 * 1024 * 1024;
316
+ var MAX_TREE_PATH_BYTES2 = 16 * 1024;
317
+ var MAX_TREE_SYMLINK_BYTES2 = 16 * 1024;
318
+ var TREE_HASH_CHUNK_BYTES2 = 1024 * 1024;
319
+ var CONTROL_CHARACTERS2 = /[\u0000-\u001f\u007f]/;
320
+ function fail2(message) {
321
+ throw new Error(`browser-managed: ${message}`);
322
+ }
323
+ function isRecord(value) {
324
+ return value !== null && typeof value === "object" && !Array.isArray(value);
325
+ }
326
+ function absolutePath(value, label) {
327
+ if (typeof value !== "string" || value === "" || !path5.isAbsolute(value)) {
328
+ fail2(`${label} must be a non-empty absolute path`);
329
+ }
330
+ return path5.resolve(value);
331
+ }
332
+ function lstatOrFail2(file, label) {
333
+ try {
334
+ return fs3.lstatSync(file);
335
+ } catch {
336
+ fail2(`missing ${label}: ${file}`);
337
+ }
338
+ }
339
+ function fileIdentity(stat) {
340
+ return { dev: stat.dev, ino: stat.ino };
341
+ }
342
+ function realpathOrFail(file, label) {
343
+ try {
344
+ return fs3.realpathSync(file);
345
+ } catch {
346
+ fail2(`cannot resolve ${label}: ${file}`);
347
+ }
348
+ }
349
+ function assertRealDirectory(directory, label) {
350
+ const stat = lstatOrFail2(directory, label);
351
+ if (!stat.isDirectory() || stat.isSymbolicLink()) fail2(`${label} must be a real directory: ${directory}`);
352
+ return realpathOrFail(directory, label);
353
+ }
354
+ function existingRealDirectory(directory, label) {
355
+ let stat;
356
+ try {
357
+ stat = fs3.lstatSync(directory);
358
+ } catch (error) {
359
+ if (error.code === "ENOENT") return null;
360
+ fail2(`cannot inspect ${label}: ${directory}`);
361
+ }
362
+ if (!stat.isDirectory() || stat.isSymbolicLink()) fail2(`${label} must be a real directory`);
363
+ return realpathOrFail(directory, label);
364
+ }
365
+ function isContained(root, candidate, allowEqual) {
366
+ const relative = path5.relative(path5.resolve(root), path5.resolve(candidate));
367
+ if (relative === "") return allowEqual;
368
+ return relative !== ".." && !relative.startsWith(`..${path5.sep}`) && !path5.isAbsolute(relative);
369
+ }
370
+ function assertContained2(root, candidate, label) {
371
+ const resolved = realpathOrFail(candidate, label);
372
+ if (!isContained(root, resolved, false)) fail2(`${label} escapes its root`);
373
+ return resolved;
374
+ }
375
+ function packageDirectoryName(packageName) {
376
+ return packageName === "@playwright/cli" ? "playwright-cli" : "chrome-devtools-mcp";
377
+ }
378
+ function validateClosureIdentity(value, packageName, version, integrity) {
379
+ if (!Array.isArray(value) || value.length === 0) fail2("staged closure must be a non-empty array");
380
+ const seen = /* @__PURE__ */ new Set();
381
+ const closure = [];
382
+ for (const item of value) {
383
+ if (!isRecord(item)) fail2("staged closure entry must be an object");
384
+ if (typeof item.name !== "string" || !PACKAGE_NAME.test(item.name) || typeof item.version !== "string" || !NPM_VERSION.test(item.version) || !isCanonicalSha512Integrity(item.integrity)) {
385
+ fail2("staged closure entry is invalid");
386
+ }
387
+ const key = `${item.name}@${item.version}`;
388
+ if (seen.has(key)) fail2(`staged closure contains a duplicate: ${key}`);
389
+ seen.add(key);
390
+ closure.push({ name: item.name, version: item.version, integrity: item.integrity });
391
+ }
392
+ const root = closure.find((item) => item.name === packageName);
393
+ if (root === void 0 || root.version !== version || root.integrity !== integrity) {
394
+ fail2("staged closure root differs from release");
395
+ }
396
+ return closure;
397
+ }
398
+ function readBoundedRegularFile(file, label, maxBytes) {
399
+ const stat = lstatOrFail2(file, label);
400
+ if (!stat.isFile() || stat.isSymbolicLink()) fail2(`${label} must be a regular file`);
401
+ if (stat.size > maxBytes) fail2(`${label} exceeds its size bound`);
402
+ try {
403
+ return fs3.readFileSync(file);
404
+ } catch {
405
+ fail2(`cannot read ${label}: ${file}`);
406
+ }
407
+ }
408
+ function readOptionalRegularFile(file, label, maxBytes) {
409
+ let stat;
410
+ try {
411
+ stat = fs3.lstatSync(file);
412
+ } catch (error) {
413
+ if (error.code === "ENOENT") return null;
414
+ fail2(`cannot inspect ${label}: ${file}`);
415
+ }
416
+ if (!stat.isFile() || stat.isSymbolicLink()) fail2(`${label} must be a regular file`);
417
+ if (stat.size > maxBytes) fail2(`${label} exceeds its size bound`);
418
+ try {
419
+ return fs3.readFileSync(file);
420
+ } catch {
421
+ fail2(`cannot read ${label}: ${file}`);
422
+ }
423
+ }
424
+ function assertStrictKeys(value, expected, label) {
425
+ const actual = Object.keys(value).sort();
426
+ const required = [...expected].sort();
427
+ if (actual.length !== required.length || actual.some((key, index) => key !== required[index])) {
428
+ fail2(`${label} has unexpected fields`);
429
+ }
430
+ }
431
+ function parseJsonObject(raw, label) {
432
+ let parsed;
433
+ try {
434
+ parsed = JSON.parse(raw.toString("utf8"));
435
+ } catch {
436
+ fail2(`${label} is malformed`);
437
+ }
438
+ if (!isRecord(parsed)) fail2(`${label} must be an object`);
439
+ return parsed;
440
+ }
441
+ function validateReceiptObject(value, packageDir, expectedPackageName, expectedRootPath) {
442
+ if (!isRecord(value)) fail2("managed receipt must be an object");
443
+ assertStrictKeys(
444
+ value,
445
+ [
446
+ "closure",
447
+ "entryPath",
448
+ "integrity",
449
+ "launcherPath",
450
+ "launcherSha256",
451
+ "packageName",
452
+ "rootPath",
453
+ "schemaVersion",
454
+ "treePath",
455
+ "treeSha256",
456
+ "version"
457
+ ],
458
+ "managed receipt"
459
+ );
460
+ if (value.schemaVersion !== 1 || value.packageName !== expectedPackageName) fail2("managed receipt identity is invalid");
461
+ if (!isStableSemverVersion(value.version) || !isCanonicalSha512Integrity(value.integrity)) {
462
+ fail2("managed receipt release identity is invalid");
463
+ }
464
+ const rootPath = absolutePath(value.rootPath, "managed receipt rootPath");
465
+ if (rootPath !== expectedRootPath || path5.dirname(rootPath) !== packageDir || !path5.basename(rootPath).startsWith("release-")) {
466
+ fail2("managed receipt rootPath is not an owned release");
467
+ }
468
+ const rootStat = lstatOrFail2(rootPath, "managed release root");
469
+ if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) fail2("managed release root is not a real directory");
470
+ const treePath = absolutePath(value.treePath, "managed receipt treePath");
471
+ if (treePath !== path5.join(rootPath, "node_modules")) fail2("managed receipt treePath is invalid");
472
+ assertRealDirectory(treePath, "managed receipt tree");
473
+ const entryPath = absolutePath(value.entryPath, "managed receipt entryPath");
474
+ const launcherPath = absolutePath(value.launcherPath, "managed receipt launcherPath");
475
+ if (!isContained(treePath, entryPath, false)) fail2("managed receipt entryPath escapes the tree");
476
+ if (!isContained(rootPath, launcherPath, false)) fail2("managed receipt launcherPath escapes the release");
477
+ assertContained2(treePath, entryPath, "managed receipt entryPath");
478
+ assertContained2(rootPath, launcherPath, "managed receipt launcherPath");
479
+ const entryStat = lstatOrFail2(entryPath, "managed receipt entry");
480
+ if (!entryStat.isFile() || entryStat.isSymbolicLink()) fail2("managed receipt entry is not a regular file");
481
+ const launcherStat = lstatOrFail2(launcherPath, "managed receipt launcher");
482
+ if (!launcherStat.isFile() || launcherStat.isSymbolicLink()) fail2("managed receipt launcher is not a regular file");
483
+ if (typeof value.treeSha256 !== "string" || !HASH.test(value.treeSha256)) fail2("managed receipt treeSha256 is invalid");
484
+ if (typeof value.launcherSha256 !== "string" || !HASH.test(value.launcherSha256)) fail2("managed receipt launcherSha256 is invalid");
485
+ const closure = validateClosureIdentity(value.closure, expectedPackageName, value.version, value.integrity);
486
+ const observedTreeSha256 = browserTreeSha256(treePath, rootPath);
487
+ if (observedTreeSha256 !== value.treeSha256) fail2("managed receipt tree digest drifted");
488
+ if (hashRegularFile(launcherPath) !== value.launcherSha256) fail2("managed receipt launcher digest drifted");
489
+ return {
490
+ schemaVersion: 1,
491
+ packageName: expectedPackageName,
492
+ version: value.version,
493
+ integrity: value.integrity,
494
+ rootPath,
495
+ treePath,
496
+ entryPath,
497
+ launcherPath,
498
+ treeSha256: value.treeSha256,
499
+ launcherSha256: value.launcherSha256,
500
+ closure
501
+ };
502
+ }
503
+ function readValidatedActiveBrowser(packageDir, packageName) {
504
+ const pointerPath = path5.join(packageDir, ACTIVE_POINTER_FILE);
505
+ const pointerRaw = readOptionalRegularFile(pointerPath, "active browser pointer", 64 * 1024);
506
+ if (pointerRaw === null) return null;
507
+ const pointerIdentity = fileIdentity(lstatOrFail2(pointerPath, "active browser pointer"));
508
+ const pointer = parseJsonObject(pointerRaw, "active browser pointer");
509
+ assertStrictKeys(pointer, ["packageName", "receiptSha256", "rootPath", "schemaVersion"], "active browser pointer");
510
+ if (pointer.schemaVersion !== 1 || pointer.packageName !== packageName) fail2("active browser pointer identity is invalid");
511
+ if (typeof pointer.receiptSha256 !== "string" || !HASH.test(pointer.receiptSha256)) {
512
+ fail2("active browser pointer receiptSha256 is invalid");
513
+ }
514
+ const rootPath = absolutePath(pointer.rootPath, "active browser pointer rootPath");
515
+ if (path5.dirname(rootPath) !== packageDir || !path5.basename(rootPath).startsWith("release-")) {
516
+ fail2("active browser pointer rootPath is not an owned release");
517
+ }
518
+ const receiptPath = path5.join(rootPath, RECEIPT_FILE);
519
+ const receiptRaw = readBoundedRegularFile(receiptPath, "managed receipt", 4 * 1024 * 1024);
520
+ const expectedReceiptSha256 = createHash3("sha256").update(receiptRaw).digest("hex");
521
+ if (expectedReceiptSha256 !== pointer.receiptSha256) fail2("active browser pointer receipt hash differs");
522
+ const receipt = validateReceiptObject(parseJsonObject(receiptRaw, "managed receipt"), packageDir, packageName, rootPath);
523
+ return { pointerPath, pointerIdentity, pointerRaw, receiptPath, receiptRaw, receipt };
524
+ }
525
+ function assertNoOrphanedManagedBrowserState(packageDir, allowActiveLock) {
526
+ let entries;
527
+ try {
528
+ entries = fs3.readdirSync(packageDir, { withFileTypes: true });
529
+ } catch {
530
+ fail2(`cannot inspect managed browser package root: ${packageDir}`);
531
+ }
532
+ const managedArtifacts = entries.filter((entry) => {
533
+ if (allowActiveLock && entry.name === LOCK_FILE) return false;
534
+ return entry.name === LOCK_FILE || entry.name === RECEIPT_FILE || entry.name.startsWith("release-") || entry.name.startsWith(".pending-") || entry.name.startsWith(".active-");
535
+ });
536
+ if (managedArtifacts.length > 0) {
537
+ fail2(
538
+ `orphaned managed browser state without an active pointer: ${managedArtifacts.map((entry) => entry.name).join(", ")}`
539
+ );
540
+ }
541
+ }
542
+ function loadVerifiedManagedBrowserReceipt(stateDir, packageName) {
543
+ if (packageName !== "@playwright/cli" && packageName !== "chrome-devtools-mcp") {
544
+ fail2("unsupported managed browser package");
545
+ }
546
+ const statePath = absolutePath(stateDir, "stateDir");
547
+ const realStateDir = existingRealDirectory(statePath, "stateDir");
548
+ if (realStateDir === null) return null;
549
+ const managedRoot = existingRealDirectory(path5.join(realStateDir, MANAGED_ROOT), "managed browser root");
550
+ if (managedRoot === null) return null;
551
+ const packageDirectory = existingRealDirectory(
552
+ path5.join(managedRoot, packageDirectoryName(packageName)),
553
+ "managed browser package root"
554
+ );
555
+ if (packageDirectory === null) return null;
556
+ const active = readValidatedActiveBrowser(packageDirectory, packageName);
557
+ if (active !== null) return active.receipt;
558
+ assertNoOrphanedManagedBrowserState(packageDirectory, false);
559
+ return null;
560
+ }
561
+ function hashRegularFile(file) {
562
+ const stat = lstatOrFail2(file, "launcher");
563
+ if (!stat.isFile() || stat.isSymbolicLink()) fail2("launcher must be a regular file");
564
+ let fd;
565
+ try {
566
+ fd = fs3.openSync(file, fs3.constants.O_RDONLY | (fs3.constants.O_NOFOLLOW ?? 0));
567
+ } catch {
568
+ fail2(`cannot open launcher: ${file}`);
569
+ }
570
+ const hash = createHash3("sha256");
571
+ const buffer = Buffer.allocUnsafe(TREE_HASH_CHUNK_BYTES2);
572
+ try {
573
+ for (; ; ) {
574
+ const read = fs3.readSync(fd, buffer, 0, buffer.length, null);
575
+ if (read === 0) break;
576
+ hash.update(buffer.subarray(0, read));
577
+ }
578
+ const finalStat = fs3.fstatSync(fd);
579
+ if (!finalStat.isFile() || finalStat.size !== stat.size) fail2("launcher changed while being hashed");
580
+ return hash.digest("hex");
581
+ } finally {
582
+ try {
583
+ fs3.closeSync(fd);
584
+ } catch {
585
+ }
586
+ }
587
+ }
588
+ function launcherSource(nodeModulesPath, entryPath, expectedTreeSha256) {
589
+ return `const fs = (await import("node:fs")).default;
590
+ const path = (await import("node:path")).default;
591
+ const { createHash } = await import("node:crypto");
592
+ const { pathToFileURL } = await import("node:url");
593
+
594
+ const root = ${JSON.stringify(nodeModulesPath)};
595
+ const entry = ${JSON.stringify(entryPath)};
596
+ const expected = ${JSON.stringify(expectedTreeSha256)};
597
+ const MAX_BYTES = ${MAX_TREE_BYTES2};
598
+ const MAX_ENTRIES = ${MAX_TREE_ENTRIES2};
599
+ const MAX_METADATA_BYTES = ${MAX_TREE_METADATA_BYTES2};
600
+ const MAX_PATH_BYTES = ${MAX_TREE_PATH_BYTES2};
601
+ const MAX_SYMLINK_BYTES = ${MAX_TREE_SYMLINK_BYTES2};
602
+ const CHUNK_BYTES = ${TREE_HASH_CHUNK_BYTES2};
603
+ const CONTROL = /[\\u0000-\\u001f\\u007f]/;
604
+
605
+ function contained(base, candidate, allowEqual) {
606
+ const relative = path.relative(path.resolve(base), path.resolve(candidate));
607
+ if (relative === "") return allowEqual;
608
+ return relative !== ".." && !relative.startsWith(".." + path.sep) && !path.isAbsolute(relative);
609
+ }
610
+
611
+ function targetFor(linkPath, tree, metadata) {
612
+ const raw = fs.readlinkSync(linkPath, "buffer");
613
+ const target = raw.toString("utf8");
614
+ if (Buffer.from(target, "utf8").compare(raw) !== 0 || target === "" || CONTROL.test(target) || raw.byteLength > MAX_SYMLINK_BYTES || path.isAbsolute(target) || (process.platform === "win32" && /^[a-zA-Z]:/.test(target))) {
615
+ throw new Error("managed browser symlink is not an internal relative target");
616
+ }
617
+ const parts = target.split(process.platform === "win32" ? /[\\\\/]+/ : /\\/+/).filter((part) => part !== "" && part !== ".");
618
+ if (parts.length === 0) throw new Error("managed browser symlink target is empty");
619
+ let current = path.dirname(path.resolve(linkPath));
620
+ for (let index = 0; index < parts.length; index += 1) {
621
+ const part = parts[index];
622
+ const last = index === parts.length - 1;
623
+ current = part === ".." ? path.dirname(current) : path.join(current, part);
624
+ if (!contained(tree, current, true)) throw new Error("managed browser symlink escapes tree");
625
+ const stat = fs.lstatSync(current);
626
+ if (stat.isSymbolicLink()) throw new Error("managed browser symlink chain is not allowed");
627
+ if (last ? (!stat.isFile() && !stat.isDirectory()) : !stat.isDirectory()) throw new Error("managed browser symlink target is invalid");
628
+ }
629
+ if (metadata.bytes + raw.byteLength > MAX_METADATA_BYTES) throw new Error("managed browser metadata is too large");
630
+ metadata.bytes += raw.byteLength;
631
+ return target;
632
+ }
633
+
634
+ function readEntries(directory, tree, remaining, metadata) {
635
+ const handle = fs.opendirSync(directory);
636
+ const entries = [];
637
+ let primary = null;
638
+ try {
639
+ for (;;) {
640
+ const dirent = handle.readSync();
641
+ if (dirent === null) break;
642
+ if (entries.length >= remaining) throw new Error("managed browser tree has too many entries");
643
+ const full = path.join(directory, dirent.name);
644
+ const relative = path.relative(path.resolve(tree), full).split(path.sep).join("/");
645
+ const pathBytes = Buffer.byteLength(relative, "utf8");
646
+ if (relative === "" || CONTROL.test(relative) || pathBytes > MAX_PATH_BYTES || metadata.bytes + pathBytes > MAX_METADATA_BYTES) throw new Error("managed browser tree contains an invalid path");
647
+ metadata.bytes += pathBytes;
648
+ entries.push({ full, relative });
649
+ }
650
+ } catch (error) {
651
+ primary = error;
652
+ throw error;
653
+ } finally {
654
+ try { handle.closeSync(); } catch (error) { if (primary === null) throw error; }
655
+ }
656
+ return entries;
657
+ }
658
+
659
+ function openFile(file) {
660
+ const flags = fs.constants.O_RDONLY | (fs.constants.O_NONBLOCK || 0) | (fs.constants.O_NOFOLLOW || 0);
661
+ const fd = fs.openSync(file, flags);
662
+ const stat = fs.fstatSync(fd);
663
+ if (!stat.isFile() || !Number.isSafeInteger(stat.size) || stat.size < 0) { fs.closeSync(fd); throw new Error("managed browser tree entry is not a regular file"); }
664
+ return { fd, size: stat.size };
665
+ }
666
+
667
+ function hashFile(hash, file, total) {
668
+ const opened = openFile(file);
669
+ let primary = null;
670
+ try {
671
+ if (opened.size > MAX_BYTES || total.bytes + opened.size > MAX_BYTES) throw new Error("managed browser tree is too large");
672
+ const length = Buffer.alloc(8);
673
+ length.writeBigUInt64BE(BigInt(opened.size));
674
+ hash.update(length);
675
+ const buffer = Buffer.allocUnsafe(CHUNK_BYTES);
676
+ let readTotal = 0;
677
+ for (;;) {
678
+ const read = fs.readSync(opened.fd, buffer, 0, buffer.length, null);
679
+ if (read === 0) break;
680
+ readTotal += read;
681
+ if (readTotal > opened.size || total.bytes + readTotal > MAX_BYTES) throw new Error("managed browser tree changed");
682
+ hash.update(buffer.subarray(0, read));
683
+ }
684
+ const finalStat = fs.fstatSync(opened.fd);
685
+ if (!finalStat.isFile() || finalStat.size !== readTotal || readTotal !== opened.size) throw new Error("managed browser tree changed");
686
+ total.bytes += readTotal;
687
+ } catch (error) {
688
+ primary = error;
689
+ throw error;
690
+ } finally {
691
+ try { fs.closeSync(opened.fd); } catch (error) { if (primary === null) throw error; }
692
+ }
693
+ }
694
+
695
+ function treeHash(tree) {
696
+ const rootStat = fs.lstatSync(tree);
697
+ if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) throw new Error("managed browser node_modules is invalid");
698
+ const entries = [];
699
+ const pending = [path.resolve(tree)];
700
+ const metadata = { bytes: 0 };
701
+ while (pending.length > 0) {
702
+ const directory = pending.pop();
703
+ for (const { full, relative } of readEntries(directory, tree, MAX_ENTRIES - entries.length, metadata)) {
704
+ if (entries.length >= MAX_ENTRIES) throw new Error("managed browser tree has too many entries");
705
+ const stat = fs.lstatSync(full);
706
+ if (stat.isSymbolicLink()) entries.push({ kind: "symlink", relative, target: targetFor(full, tree, metadata) });
707
+ else if (stat.isDirectory()) { entries.push({ kind: "dir", relative }); pending.push(full); }
708
+ else if (stat.isFile()) entries.push({ kind: "file", relative, filePath: full });
709
+ else throw new Error("managed browser tree contains an unsupported entry");
710
+ }
711
+ }
712
+ entries.sort((left, right) => left.relative < right.relative ? -1 : left.relative > right.relative ? 1 : 0);
713
+ const hash = createHash("sha256");
714
+ hash.update("browser-v2\\0", "utf8");
715
+ const total = { bytes: 0 };
716
+ for (const item of entries) {
717
+ hash.update(item.kind + "\\0" + item.relative + "\\0", "utf8");
718
+ if (item.kind === "symlink") {
719
+ const payload = Buffer.from(item.target, "utf8");
720
+ const length = Buffer.alloc(8);
721
+ length.writeBigUInt64BE(BigInt(payload.length));
722
+ hash.update(length); hash.update(payload); total.bytes += payload.length;
723
+ } else if (item.kind === "dir") hash.update(Buffer.alloc(8));
724
+ else hashFile(hash, item.filePath, total);
725
+ }
726
+ return hash.digest("hex");
727
+ }
728
+
729
+ if (treeHash(root) !== expected) throw new Error("managed browser tree digest drifted before launch");
730
+ process.argv[1] = entry;
731
+ await import(pathToFileURL(entry).href);
732
+ `;
733
+ }
734
+ function managedTreeVerifierSource() {
735
+ const source = launcherSource("/jorgex-browser-verifier/node_modules", "/jorgex-browser-verifier/entry.mjs", "0".repeat(64));
736
+ const start = source.indexOf("function contained(base, candidate, allowEqual)");
737
+ const end = source.indexOf("if (treeHash(root) !== expected)");
738
+ if (start < 0 || end < 0 || end <= start) fail2("managed browser tree verifier source is unavailable");
739
+ return source.slice(start, end);
740
+ }
741
+ var DEVTOOLS_PRIVACY_FLAGS = [
742
+ "--isolated",
743
+ "--redact-network-headers",
744
+ "--no-performance-crux",
745
+ "--no-usage-statistics"
746
+ ];
747
+ function invocationGuardSource(receipt, runtimeArgs) {
748
+ const treeVerifier = managedTreeVerifierSource();
749
+ return `const fs = (await import("node:fs")).default;
750
+ const { createHash } = await import("node:crypto");
751
+ const path = (await import("node:path")).default;
752
+
753
+ const expectedLauncherPath = ${JSON.stringify(receipt.launcherPath)};
754
+ const expectedLauncherSha256 = ${JSON.stringify(receipt.launcherSha256)};
755
+ const root = ${JSON.stringify(receipt.treePath)};
756
+ const expected = ${JSON.stringify(receipt.treeSha256)};
757
+ const expectedRuntimeArgs = ${JSON.stringify([...runtimeArgs])};
758
+ const MAX_LAUNCHER_BYTES = 4 * 1024 * 1024;
759
+ const MAX_BYTES = ${MAX_TREE_BYTES2};
760
+ const MAX_ENTRIES = ${MAX_TREE_ENTRIES2};
761
+ const MAX_METADATA_BYTES = ${MAX_TREE_METADATA_BYTES2};
762
+ const MAX_PATH_BYTES = ${MAX_TREE_PATH_BYTES2};
763
+ const MAX_SYMLINK_BYTES = ${MAX_TREE_SYMLINK_BYTES2};
764
+ const CHUNK_BYTES = ${TREE_HASH_CHUNK_BYTES2};
765
+ const CONTROL = /[\\u0000-\\u001f\\u007f]/;
766
+
767
+ ${treeVerifier}
768
+
769
+ function readVerifiedLauncher(file) {
770
+ let initial;
771
+ try { initial = fs.lstatSync(file); } catch { throw new Error("managed browser launcher is unavailable"); }
772
+ if (!initial.isFile() || initial.isSymbolicLink()) throw new Error("managed browser launcher is not a regular file");
773
+ if (initial.size > MAX_LAUNCHER_BYTES) throw new Error("managed browser launcher exceeds 4 MiB");
774
+ const flags = fs.constants.O_RDONLY | (fs.constants.O_NONBLOCK || 0) | (fs.constants.O_NOFOLLOW || 0);
775
+ const fd = fs.openSync(file, flags);
776
+ const chunks = [];
777
+ let total = 0;
778
+ let primary = null;
779
+ try {
780
+ const opened = fs.fstatSync(fd);
781
+ if (!opened.isFile() || opened.size > MAX_LAUNCHER_BYTES) throw new Error("managed browser launcher is invalid");
782
+ const buffer = Buffer.allocUnsafe(CHUNK_BYTES);
783
+ for (;;) {
784
+ const read = fs.readSync(fd, buffer, 0, buffer.length, null);
785
+ if (read === 0) break;
786
+ total += read;
787
+ if (total > MAX_LAUNCHER_BYTES) throw new Error("managed browser launcher exceeds 4 MiB");
788
+ chunks.push(Buffer.from(buffer.subarray(0, read)));
789
+ }
790
+ const finalStat = fs.fstatSync(fd);
791
+ if (!finalStat.isFile() || finalStat.size !== total || total !== opened.size) throw new Error("managed browser launcher changed while being read");
792
+ } catch (error) {
793
+ primary = error;
794
+ throw error;
795
+ } finally {
796
+ try { fs.closeSync(fd); } catch (error) { if (primary === null) throw error; }
797
+ }
798
+ const bytes = Buffer.concat(chunks);
799
+ const text = bytes.toString("utf8");
800
+ if (!Buffer.from(text, "utf8").equals(bytes)) throw new Error("managed browser launcher is not strict UTF-8");
801
+ if (createHash("sha256").update(bytes).digest("hex") !== expectedLauncherSha256) throw new Error("managed browser launcher digest drifted");
802
+ return text;
803
+ }
804
+
805
+ if (process.argv[1] !== expectedLauncherPath) throw new Error("managed browser launcher path changed");
806
+ if (JSON.stringify(process.argv.slice(2)) !== JSON.stringify(expectedRuntimeArgs)) throw new Error("managed browser runtime args changed");
807
+ const launcherSource = readVerifiedLauncher(expectedLauncherPath);
808
+ if (treeHash(root) !== expected) throw new Error("managed browser tree digest drifted before launch");
809
+ process.argv[1] = expectedLauncherPath;
810
+ const evaluateVerifiedLauncher = (verifiedSource) => new Function("return (async()=>{\\n" + verifiedSource + "\\n})()")();
811
+ await evaluateVerifiedLauncher(launcherSource);
812
+ `;
813
+ }
814
+ function planManagedBrowserInvocation(stateDir, packageName, runtimeArgs) {
815
+ if (packageName !== "@playwright/cli" && packageName !== "chrome-devtools-mcp") {
816
+ fail2("unsupported managed browser package");
817
+ }
818
+ if (!Array.isArray(runtimeArgs)) fail2("runtimeArgs must be an array");
819
+ if (runtimeArgs.some((arg) => typeof arg !== "string" || CONTROL_CHARACTERS2.test(arg))) {
820
+ fail2("runtimeArgs contain an invalid control character");
821
+ }
822
+ if (packageName === "chrome-devtools-mcp" && (runtimeArgs.length !== DEVTOOLS_PRIVACY_FLAGS.length || runtimeArgs.some((arg, index) => arg !== DEVTOOLS_PRIVACY_FLAGS[index]))) {
823
+ fail2("chrome-devtools-mcp requires the fixed privacy flags in order");
824
+ }
825
+ const receipt = loadVerifiedManagedBrowserReceipt(stateDir, packageName);
826
+ if (receipt === null) fail2("verified managed browser receipt not found");
827
+ return {
828
+ command: process.execPath,
829
+ args: [
830
+ "--input-type=module",
831
+ "--eval",
832
+ invocationGuardSource(receipt, runtimeArgs),
833
+ receipt.launcherPath,
834
+ ...runtimeArgs
835
+ ]
836
+ };
837
+ }
838
+
839
+ // src/lib/tool-preferences.ts
840
+ import fs5 from "fs";
841
+ import path7 from "path";
842
+
843
+ // src/lib/fsx.ts
844
+ import fs4 from "fs";
845
+ import path6 from "path";
846
+
847
+ // src/lib/tool-preferences.ts
848
+ var PLAYWRIGHT_RUNTIMES = ["opencode", "claude-code", "codex", "pi"];
849
+ function normalizeObservedVersion(value) {
850
+ return { version: value.version, integrity: value.integrity };
851
+ }
852
+ function parseObservedField(value) {
853
+ if (value.observed === void 0) return void 0;
854
+ if (!isValidObservedVersion(value.observed)) return null;
855
+ const observed = value.observed;
856
+ return normalizeObservedVersion(observed);
857
+ }
858
+ function readPreference(file) {
859
+ try {
860
+ return { raw: fs5.readFileSync(file, "utf8"), errorCode: null };
861
+ } catch (error) {
862
+ const code = error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : "UNKNOWN";
863
+ return code === "ENOENT" ? { raw: null, errorCode: null } : { raw: null, errorCode: code };
864
+ }
865
+ }
866
+ function playwrightCliPreferenceFile(stateDir = dataDir()) {
867
+ return path7.join(stateDir, "playwright-cli.json");
868
+ }
869
+ function parsePlaywrightCliPreference(raw) {
870
+ try {
871
+ const value = JSON.parse(raw);
872
+ if (!isRecord2(value)) return void 0;
873
+ const observedField = parseObservedField(value);
874
+ if (observedField === null) return void 0;
875
+ const observed = observedField === void 0 ? void 0 : observedField;
876
+ if (value.version === 1 && typeof value.enabled === "boolean") {
877
+ return observed === void 0 ? { version: 1, enabled: value.enabled } : { version: 1, enabled: value.enabled, observed };
878
+ }
879
+ if (value.version !== 2 || !isRecord2(value.enabled)) return void 0;
880
+ if (Object.entries(value.enabled).some(([runtime, enabled2]) => !PLAYWRIGHT_RUNTIMES.includes(runtime) || typeof enabled2 !== "boolean")) return void 0;
881
+ const enabled = value.enabled;
882
+ return observed === void 0 ? { version: 2, enabled } : { version: 2, enabled, observed };
883
+ } catch {
884
+ return void 0;
885
+ }
886
+ }
887
+ function playwrightCliPreferenceError(file = playwrightCliPreferenceFile()) {
888
+ const { raw, errorCode } = readPreference(file);
889
+ if (errorCode !== null) {
890
+ return `Playwright CLI: no se pudo leer la preferencia en ${file} (${errorCode}). Corrige o borra ese archivo antes de reintentar.`;
891
+ }
892
+ if (raw === null || parsePlaywrightCliPreference(raw) !== void 0) return null;
893
+ return `Playwright CLI: preferencia inv\xE1lida en ${file}. Corrige o borra ese archivo antes de reintentar.`;
894
+ }
895
+ function loadPlaywrightCliPreference(file = playwrightCliPreferenceFile(), runtime) {
896
+ const { raw } = readPreference(file);
897
+ const state = raw === null ? void 0 : parsePlaywrightCliPreference(raw);
898
+ if (state === void 0) return void 0;
899
+ if (state.version === 1) return state.enabled;
900
+ return runtime === void 0 ? Object.values(state.enabled).some(Boolean) : state.enabled[runtime] === true;
901
+ }
902
+ function loadPlaywrightCliObservation(file = playwrightCliPreferenceFile()) {
903
+ const { raw } = readPreference(file);
904
+ if (raw === null) return null;
905
+ const state = parsePlaywrightCliPreference(raw);
906
+ if (state === void 0 || state.observed === void 0) return null;
907
+ return { ...state.observed };
908
+ }
909
+ function isRecord2(value) {
910
+ return value !== null && typeof value === "object" && !Array.isArray(value);
911
+ }
912
+
913
+ // src/lib/external-tools.ts
914
+ import fs6 from "fs";
915
+ import os2 from "os";
916
+ import path8 from "path";
917
+ import { execFileSync as execFileSync2 } from "child_process";
918
+
919
+ // src/lib/browser-command.ts
920
+ function runManagedPlaywrightCommand(args, stateDir = dataDir()) {
921
+ const preferenceFile = playwrightCliPreferenceFile(stateDir);
922
+ const preferenceError = playwrightCliPreferenceError(preferenceFile);
923
+ if (preferenceError !== null) throw new Error(preferenceError);
924
+ if (loadPlaywrightCliPreference(preferenceFile) !== true) {
925
+ throw new Error("Playwright CLI: opt-in no habilitado; ejecuta install --playwright.");
926
+ }
927
+ const observed = loadPlaywrightCliObservation(preferenceFile);
928
+ const receipt = loadVerifiedManagedBrowserReceipt(stateDir, "@playwright/cli");
929
+ if (observed === null || receipt === null || observed.version !== receipt.version || observed.integrity !== receipt.integrity) {
930
+ throw new Error("Playwright CLI: falta un receipt gestionado coincidente con la observaci\xF3n.");
931
+ }
932
+ const result = runVerifiedManagedPlaywright(stateDir, args);
933
+ if (result.error !== void 0) throw result.error;
934
+ return result.status ?? 1;
935
+ }
936
+ function runVerifiedManagedPlaywright(stateDir, args, options = {}) {
937
+ const plan = planManagedBrowserInvocation(stateDir, "@playwright/cli", args);
938
+ const result = spawnSync2(plan.command, [...plan.args], {
939
+ encoding: "utf8",
940
+ stdio: options.captureOutput ? "pipe" : "inherit",
941
+ shell: false,
942
+ timeout: options.timeoutMs ?? 12e4,
943
+ maxBuffer: 2 * 1024 * 1024,
944
+ env: { ...process.env, NO_UPDATE_NOTIFIER: "1" }
945
+ });
946
+ return result;
947
+ }
948
+
949
+ // src/browser-playwright.ts
950
+ try {
951
+ process.exitCode = runManagedPlaywrightCommand(process.argv.slice(2));
952
+ } catch (error) {
953
+ console.error(error instanceof Error ? error.message : String(error));
954
+ process.exitCode = 1;
955
+ }