extension 4.1.20 → 4.1.22

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.
Files changed (27) hide show
  1. package/README.md +18 -16
  2. package/dist/{84.cjs → 597.cjs} +80 -2
  3. package/dist/browsers.cjs +408 -103
  4. package/dist/cli.cjs +558 -148
  5. package/dist/extension/browsers/__spec__/safari-fake-tools.d.ts +27 -0
  6. package/dist/extension/browsers/browsers-lib/messages.d.ts +2 -1
  7. package/dist/extension/browsers/browsers-lib/process-teardown.d.ts +3 -0
  8. package/dist/extension/browsers/browsers-lib/ready-stamp.d.ts +1 -0
  9. package/dist/extension/browsers/browsers-lib/resolve-live-pid.d.ts +25 -0
  10. package/dist/extension/browsers/browsers-lib/write-json-atomic.d.ts +1 -0
  11. package/dist/extension/browsers/browsers-types.d.ts +1 -1
  12. package/dist/extension/browsers/index.d.ts +1 -1
  13. package/dist/extension/browsers/run-chromium/cdp/cdp-extension-controller/index.d.ts +2 -0
  14. package/dist/extension/browsers/run-chromium/cdp/ensure-developer-mode.d.ts +12 -0
  15. package/dist/extension/browsers/run-firefox/firefox-launch/index.d.ts +9 -0
  16. package/dist/extension/browsers/run-firefox/firefox-launch/process-handlers.d.ts +1 -1
  17. package/dist/extension/browsers/run-safari/safari-launch/index.d.ts +1 -1
  18. package/dist/extension/browsers/run-safari/safari-launch/tools.d.ts +17 -0
  19. package/dist/extension/browsers/run-safari/safari-packager.d.ts +2 -1
  20. package/dist/extension/browsers/run-safari/safari-types.d.ts +2 -0
  21. package/dist/extension/config-types.d.ts +5 -2
  22. package/dist/extension/helpers/messages.d.ts +7 -0
  23. package/dist/extension/helpers/messaging.d.ts +1 -0
  24. package/dist/extension/helpers/node-version-guard.d.ts +3 -1
  25. package/dist/extension/helpers/template-catalog.d.ts +1 -1
  26. package/dist/extension/helpers/template-corpus.generated.d.ts +1 -1
  27. package/package.json +5 -7
package/dist/cli.cjs CHANGED
@@ -230,7 +230,7 @@ var __webpack_modules__ = {
230
230
  (0, _helpers_messaging__rspack_import_3._w)(_messages__rspack_import_5.io());
231
231
  }
232
232
  } catch {
233
- const provenanceNote = _messages__rspack_import_5.xX(opts.binaryProvenance);
233
+ const provenanceNote = _messages__rspack_import_5.xX(opts.binaryProvenance, opts.browser);
234
234
  (0, _helpers_messaging__rspack_import_3._w)(_messages__rspack_import_5.io());
235
235
  (0, _helpers_messaging__rspack_import_3._w)((0, _helpers_messaging__rspack_import_3.Nr)({
236
236
  rows: [
@@ -421,6 +421,7 @@ var __webpack_modules__ = {
421
421
  var pintor__rspack_import_8 = __webpack_require__("pintor");
422
422
  var pintor__rspack_import_8_default = /*#__PURE__*/ __webpack_require__.n(pintor__rspack_import_8);
423
423
  var _helpers_messaging__rspack_import_9 = __webpack_require__("./helpers/messaging.ts");
424
+ var _browser_family__rspack_import_10 = __webpack_require__("./browsers/browsers-lib/browser-family.ts");
424
425
  const require1 = (0, node_module__rspack_import_1.createRequire)(__rslib_import_meta_url__);
425
426
  function getLoggingPrefix(type) {
426
427
  return (0, _helpers_messaging__rspack_import_9.Pl)(type);
@@ -750,7 +751,7 @@ var __webpack_modules__ = {
750
751
  return `${getLoggingPrefix('warn')} safari-web-extension-converter reported ${pintor__rspack_import_8_default().yellow(String(warnings.length))} ${1 === warnings.length ? 'warning' : 'warnings'}, some manifest keys/APIs may not be supported by Safari:\n` + warnings.map((line)=>` ${pintor__rspack_import_8_default().gray('•')} ${line}`).join('\n') + (kept.length ? `\nExtension.js kept ${1 === kept.length ? 'one of these keys' : 'some of these keys'} on purpose:\n${keptLines}` : '');
751
752
  }
752
753
  function safariDefaultBundleIdNote(bundleId) {
753
- return `${getLoggingPrefix('warn')} The bundle id ${pintor__rspack_import_8_default().gray(bundleId)} is generated from the app name.\nEvery project built from the same source shares this id, and the first team to register it takes it.\nSet your own with ${pintor__rspack_import_8_default().blue('--bundle-id')} before your first submission.\nAfter a submission, a new id is a new extension with none of your users.`;
754
+ return `${getLoggingPrefix('info')} Bundle id ${pintor__rspack_import_8_default().gray(bundleId)} (generated, set ${pintor__rspack_import_8_default().blue('--bundle-id')} to own it).`;
754
755
  }
755
756
  function safariOpenHint(appPath, appName) {
756
757
  return `${getLoggingPrefix('info')} Launch it once to register with Safari: ${pintor__rspack_import_8_default().blue('open')} ${pintor__rspack_import_8_default().underline(`"${appPath}"`)}\nThen turn on ${pintor__rspack_import_8_default().yellow(appName)} in Safari ▸ Settings ▸ Extensions.`;
@@ -772,6 +773,9 @@ var __webpack_modules__ = {
772
773
  function safariRegistered(appName) {
773
774
  return `${getLoggingPrefix('success')} Safari recognizes ${pintor__rspack_import_8_default().yellow(appName)}, finish enabling it with the steps above.`;
774
775
  }
776
+ function safariPidUnresolved(appName) {
777
+ return `${getLoggingPrefix('warn')} Opened ${pintor__rspack_import_8_default().yellow(appName)} but could not find its process id.\nThe session's ready.json names the app and the extension without a ${pintor__rspack_import_8_default().gray('browserPid')}.`;
778
+ }
775
779
  function safariNotYetRegistered(appName) {
776
780
  return `${getLoggingPrefix('info')} Safari hasn't picked up ${pintor__rspack_import_8_default().yellow(appName)} yet.\nOpen the app once, then check Safari ▸ Settings ▸ Extensions.`;
777
781
  }
@@ -826,9 +830,15 @@ var __webpack_modules__ = {
826
830
  if (rest.startsWith(node_path__rspack_import_3.sep) || rest.startsWith('/')) return `~${rest}`;
827
831
  return raw;
828
832
  }
829
- function binaryProvenanceNote(provenance) {
830
- if ('pinned' === provenance) return '(pinned with --chromium-binary)';
831
- return '';
833
+ function binaryProvenanceNote(provenance, browser) {
834
+ if ('pinned' !== provenance) return '';
835
+ return `(pinned with ${pinnedBinaryFlag(browser)})`;
836
+ }
837
+ function pinnedBinaryFlag(browser) {
838
+ const name = String(browser || '');
839
+ if ((0, _browser_family__rspack_import_10.M_)(name)) return '--gecko-binary';
840
+ if ('safari' === name || 'webkit-based' === name) return '--safari-binary';
841
+ return '--chromium-binary';
832
842
  }
833
843
  function runningInDevelopment(manifest, browser, message, browserVersionLine, updateSuffix, opts) {
834
844
  const capitalize = (str)=>str.charAt(0).toUpperCase() + str.slice(1);
@@ -864,7 +874,7 @@ var __webpack_modules__ = {
864
874
  const baseBrowserLabel = (0, _helpers_messaging__rspack_import_9.A6)(String(browser || 'unknown'), resolveBrowserVersionLine(browser, browserVersionLine, {
865
875
  pinned: opts?.binaryProvenance === 'pinned'
866
876
  }));
867
- const provenanceNote = binaryProvenanceNote(opts?.binaryProvenance);
877
+ const provenanceNote = binaryProvenanceNote(opts?.binaryProvenance, browser);
868
878
  const browserLabel = provenanceNote ? `${baseBrowserLabel} ${provenanceNote}` : baseBrowserLabel;
869
879
  const cleanId = String(id || '').trim();
870
880
  const includeExtensionId = opts?.includeExtensionId !== false;
@@ -1033,6 +1043,7 @@ var __webpack_modules__ = {
1033
1043
  jR: ()=>safariBuilding,
1034
1044
  jc: ()=>safariProjectStale,
1035
1045
  jk: ()=>requireGeckoBinaryForGeckoBased,
1046
+ jn: ()=>safariPidUnresolved,
1036
1047
  kK: ()=>preferringSystemBrowserOverSnapshot,
1037
1048
  l9: ()=>parsingPacketError,
1038
1049
  lO: ()=>firefoxDryRunArgs,
@@ -1322,17 +1333,21 @@ var __webpack_modules__ = {
1322
1333
  var _messages__rspack_import_2 = __webpack_require__("./browsers/browsers-lib/messages.ts");
1323
1334
  const FORCE_KILL_GRACE_MS = 5000;
1324
1335
  const terminatedByUs = new WeakSet();
1336
+ const terminatedPids = new Set();
1325
1337
  function wasTerminatedByUs(child) {
1326
1338
  return !!child && terminatedByUs.has(child);
1327
1339
  }
1340
+ function wasPidTerminatedByUs(pid) {
1341
+ return 'number' == typeof pid && terminatedPids.has(pid);
1342
+ }
1328
1343
  function authorLog(line) {
1329
1344
  if (line && (0, _helpers_messaging__rspack_import_1._o)()) (0, _helpers_messaging__rspack_import_1._w)(line);
1330
1345
  }
1331
- function killWindowsTree(child, sync) {
1332
- if ('win32' !== process.platform) return;
1346
+ function killWindowsTree(pid, sync) {
1347
+ if ('win32' !== process.platform || !pid) return;
1333
1348
  const args = [
1334
1349
  '/PID',
1335
- String(child.pid),
1350
+ String(pid),
1336
1351
  '/T',
1337
1352
  '/F'
1338
1353
  ];
@@ -1347,10 +1362,37 @@ var __webpack_modules__ = {
1347
1362
  }).on('error', ()=>{});
1348
1363
  } catch {}
1349
1364
  }
1365
+ function signalPid(pid, signal) {
1366
+ try {
1367
+ process.kill(pid, signal);
1368
+ return true;
1369
+ } catch {
1370
+ return false;
1371
+ }
1372
+ }
1373
+ function gracefulTerminatePid(pid, browser) {
1374
+ if (!pid || terminatedPids.has(pid)) return;
1375
+ terminatedPids.add(pid);
1376
+ killWindowsTree(pid, false);
1377
+ authorLog(_messages__rspack_import_2.XH(browser));
1378
+ signalPid(pid, 'SIGTERM');
1379
+ const killTimer = setTimeout(()=>{
1380
+ authorLog(_messages__rspack_import_2.AG(browser));
1381
+ signalPid(pid, 'SIGKILL');
1382
+ }, FORCE_KILL_GRACE_MS);
1383
+ killTimer.unref?.();
1384
+ }
1385
+ function forceKillPidOnExit(pid, browser) {
1386
+ if (!pid) return;
1387
+ terminatedPids.add(pid);
1388
+ killWindowsTree(pid, true);
1389
+ authorLog(_messages__rspack_import_2.AG(browser));
1390
+ signalPid(pid, 'SIGKILL');
1391
+ }
1350
1392
  function gracefulTerminateChild(child, browser) {
1351
1393
  if (!child || child.killed) return;
1352
1394
  terminatedByUs.add(child);
1353
- killWindowsTree(child, false);
1395
+ killWindowsTree(child.pid, false);
1354
1396
  authorLog(_messages__rspack_import_2.XH(browser));
1355
1397
  child.kill('SIGTERM');
1356
1398
  const killTimer = setTimeout(()=>{
@@ -1364,7 +1406,7 @@ var __webpack_modules__ = {
1364
1406
  function forceKillChildOnExit(child, browser) {
1365
1407
  if (!child) return;
1366
1408
  terminatedByUs.add(child);
1367
- killWindowsTree(child, true);
1409
+ killWindowsTree(child.pid, true);
1368
1410
  try {
1369
1411
  authorLog(_messages__rspack_import_2.AG(browser));
1370
1412
  child.kill('SIGKILL');
@@ -1385,7 +1427,10 @@ var __webpack_modules__ = {
1385
1427
  $Y: ()=>forceKillChildOnExit,
1386
1428
  Df: ()=>gracefulTerminateChild,
1387
1429
  FA: ()=>isBenignSocketTeardown,
1388
- Op: ()=>wasTerminatedByUs
1430
+ Op: ()=>wasTerminatedByUs,
1431
+ PU: ()=>wasPidTerminatedByUs,
1432
+ Qx: ()=>forceKillPidOnExit,
1433
+ _T: ()=>gracefulTerminatePid
1389
1434
  });
1390
1435
  },
1391
1436
  "./browsers/browsers-lib/ready-message.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) {
@@ -1405,6 +1450,7 @@ var __webpack_modules__ = {
1405
1450
  "./browsers/browsers-lib/ready-stamp.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) {
1406
1451
  var node_fs__rspack_import_0 = __webpack_require__("node:fs");
1407
1452
  var node_path__rspack_import_1 = __webpack_require__("node:path");
1453
+ var _write_json_atomic__rspack_import_2 = __webpack_require__("./browsers/browsers-lib/write-json-atomic.ts");
1408
1454
  function readyPathFor(extensionOutputPath) {
1409
1455
  return node_path__rspack_import_1.join(node_path__rspack_import_1.dirname(extensionOutputPath), 'extension-js', node_path__rspack_import_1.basename(extensionOutputPath), 'ready.json');
1410
1456
  }
@@ -1416,7 +1462,7 @@ var __webpack_modules__ = {
1416
1462
  const ready = JSON.parse(node_fs__rspack_import_0.readFileSync(readyPath, 'utf-8'));
1417
1463
  if (ready.rdpPort === rdpPort) return;
1418
1464
  ready.rdpPort = rdpPort;
1419
- node_fs__rspack_import_0.writeFileSync(readyPath, JSON.stringify(ready, null, 2));
1465
+ (0, _write_json_atomic__rspack_import_2.Z)(readyPath, ready);
1420
1466
  } catch {}
1421
1467
  }
1422
1468
  function stampReadyBrowserLaunch(extensionOutputPath, details) {
@@ -1428,13 +1474,14 @@ var __webpack_modules__ = {
1428
1474
  const profilePath = String(details?.profilePath || '').trim();
1429
1475
  if (profilePath) ready.profilePath = profilePath;
1430
1476
  if ('number' == typeof details?.browserPid && Number.isFinite(details.browserPid)) ready.browserPid = details.browserPid;
1477
+ if ('number' == typeof details?.launcherPid && Number.isFinite(details.launcherPid)) ready.launcherPid = details.launcherPid;
1431
1478
  const extensionId = String(details?.extensionId || '').trim();
1432
1479
  if (extensionId) ready.extensionId = extensionId;
1433
1480
  const binary = String(details?.binary || '').trim();
1434
1481
  if (binary) ready.binary = binary;
1435
1482
  const provenance = String(details?.binaryProvenance || '').trim();
1436
1483
  if (provenance) ready.binaryProvenance = provenance;
1437
- node_fs__rspack_import_0.writeFileSync(readyPath, JSON.stringify(ready, null, 2));
1484
+ (0, _write_json_atomic__rspack_import_2.Z)(readyPath, ready);
1438
1485
  } catch {}
1439
1486
  }
1440
1487
  function stampReadyExtensionId(extensionOutputPath, extensionId) {
@@ -1446,7 +1493,7 @@ var __webpack_modules__ = {
1446
1493
  const ready = JSON.parse(node_fs__rspack_import_0.readFileSync(readyPath, 'utf-8'));
1447
1494
  if (ready.extensionId === id) return;
1448
1495
  ready.extensionId = id;
1449
- node_fs__rspack_import_0.writeFileSync(readyPath, JSON.stringify(ready, null, 2));
1496
+ (0, _write_json_atomic__rspack_import_2.Z)(readyPath, ready);
1450
1497
  } catch {}
1451
1498
  }
1452
1499
  function stampReadyExtensionLoadRefused(extensionOutputPath, reason) {
@@ -1461,7 +1508,7 @@ var __webpack_modules__ = {
1461
1508
  ready.message = `${browserLabel.charAt(0).toUpperCase() + browserLabel.slice(1)} refused to load the extension at ${extensionOutputPath}${reason ? `: ${reason}` : ''}`;
1462
1509
  ready.extensionLoadRefusedAt = new Date().toISOString();
1463
1510
  if (reason) ready.extensionLoadRefusedReason = reason;
1464
- node_fs__rspack_import_0.writeFileSync(readyPath, JSON.stringify(ready, null, 2));
1511
+ (0, _write_json_atomic__rspack_import_2.Z)(readyPath, ready);
1465
1512
  } catch {}
1466
1513
  }
1467
1514
  function stampReadyProfileLocked(extensionOutputPath, details) {
@@ -1475,7 +1522,7 @@ var __webpack_modules__ = {
1475
1522
  ready.message = String(details?.message || '').trim() || 'the browser profile is already in use by another session';
1476
1523
  ready.profileLockedAt = new Date().toISOString();
1477
1524
  if (details?.owner) ready.profileLockOwner = details.owner;
1478
- node_fs__rspack_import_0.writeFileSync(readyPath, JSON.stringify(ready, null, 2));
1525
+ (0, _write_json_atomic__rspack_import_2.Z)(readyPath, ready);
1479
1526
  } catch {}
1480
1527
  }
1481
1528
  function stampReadyBrowserExited(extensionOutputPath, code) {
@@ -1491,7 +1538,7 @@ var __webpack_modules__ = {
1491
1538
  ready.code = 'browser_exited';
1492
1539
  ready.message = `the ${ready.browser || 'browser'} process exited (code ${code ?? 'unknown'}); nothing is running`;
1493
1540
  }
1494
- node_fs__rspack_import_0.writeFileSync(readyPath, JSON.stringify(ready, null, 2));
1541
+ (0, _write_json_atomic__rspack_import_2.Z)(readyPath, ready);
1495
1542
  } catch {}
1496
1543
  }
1497
1544
  __webpack_require__.d(__webpack_exports__, {
@@ -1955,6 +2002,27 @@ var __webpack_modules__ = {
1955
2002
  yO: ()=>probeChromiumBinaryVersion
1956
2003
  });
1957
2004
  },
2005
+ "./browsers/browsers-lib/write-json-atomic.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) {
2006
+ var node_fs__rspack_import_0 = __webpack_require__("node:fs");
2007
+ function writeJsonAtomic(filePath, value) {
2008
+ const tmpPath = `${filePath}.tmp-${process.pid}`;
2009
+ try {
2010
+ node_fs__rspack_import_0.writeFileSync(tmpPath, JSON.stringify(value, null, 2), 'utf-8');
2011
+ node_fs__rspack_import_0.renameSync(tmpPath, filePath);
2012
+ return true;
2013
+ } catch {
2014
+ try {
2015
+ node_fs__rspack_import_0.rmSync(tmpPath, {
2016
+ force: true
2017
+ });
2018
+ } catch {}
2019
+ return false;
2020
+ }
2021
+ }
2022
+ __webpack_require__.d(__webpack_exports__, {
2023
+ Z: ()=>writeJsonAtomic
2024
+ });
2025
+ },
1958
2026
  "./browsers/browsers-lib/wsl-support.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) {
1959
2027
  __webpack_require__.d(__webpack_exports__, {
1960
2028
  qc: ()=>external_wsl_support_namespaceObject.hasGuiDisplay,
@@ -2660,6 +2728,7 @@ var __webpack_modules__ = {
2660
2728
  '--enable-features=SidePanelUpdates',
2661
2729
  '--disable-features=DisableLoadExtensionCommandLineSwitch',
2662
2730
  '--disable-features=ExtensionDisableUnsupportedDeveloper',
2731
+ '--disable-features=SafetyHubExtensionsOffStoreTrigger',
2663
2732
  '--enable-unsafe-extension-debugging',
2664
2733
  '--silent-debugger-extension-api'
2665
2734
  ];
@@ -3488,7 +3557,7 @@ var __webpack_modules__ = {
3488
3557
  binaryProvenance
3489
3558
  };
3490
3559
  if (enableCdp) {
3491
- const mod = await __webpack_require__.e(84).then(__webpack_require__.bind(__webpack_require__, "./browsers/run-chromium/chromium-launch/setup-cdp-after-launch.ts"));
3560
+ const mod = await __webpack_require__.e(597).then(__webpack_require__.bind(__webpack_require__, "./browsers/run-chromium/chromium-launch/setup-cdp-after-launch.ts"));
3492
3561
  const CDP_SETUP_TIMEOUT_MS = 45000;
3493
3562
  await Promise.race([
3494
3563
  mod.setupCdpAfterLaunch(compilation, cdpConfig, chromiumConfig, pipeStreams),
@@ -3779,6 +3848,150 @@ var __webpack_modules__ = {
3779
3848
  var process_teardown = __webpack_require__("./browsers/browsers-lib/process-teardown.ts");
3780
3849
  var ready_message = __webpack_require__("./browsers/browsers-lib/ready-message.ts");
3781
3850
  var ready_stamp = __webpack_require__("./browsers/browsers-lib/ready-stamp.ts");
3851
+ var external_node_child_process_ = __webpack_require__("node:child_process");
3852
+ const PS_ROW = /^\s*(\d+)\s+(\d+)\s+(.*)$/;
3853
+ const HELPER_PROCESS = /plugin-container|(^|\s)-contentproc(\s|$)|(^|\s)-childID(\s|$)|--type=/;
3854
+ function runQuiet(bin, args) {
3855
+ try {
3856
+ const result = (0, external_node_child_process_.spawnSync)(bin, args, {
3857
+ encoding: 'utf-8',
3858
+ stdio: [
3859
+ 'ignore',
3860
+ 'pipe',
3861
+ 'ignore'
3862
+ ],
3863
+ windowsHide: true,
3864
+ maxBuffer: 67108864
3865
+ });
3866
+ if (result.error || 0 !== result.status) return null;
3867
+ return String(result.stdout || '');
3868
+ } catch {
3869
+ return null;
3870
+ }
3871
+ }
3872
+ function parseProcessRows(output) {
3873
+ const rows = [];
3874
+ for (const line of String(output || '').split(/\r?\n/)){
3875
+ const match = PS_ROW.exec(line.replace(/\r$/, ''));
3876
+ if (!match) continue;
3877
+ const pid = Number(match[1]);
3878
+ const ppid = Number(match[2]);
3879
+ if (Number.isFinite(pid) && Number.isFinite(ppid)) rows.push({
3880
+ pid,
3881
+ ppid,
3882
+ command: match[3].trim()
3883
+ });
3884
+ }
3885
+ return rows;
3886
+ }
3887
+ function appendLinuxProfileEnv(rows) {
3888
+ return rows.map((row)=>{
3889
+ try {
3890
+ const environ = external_node_fs_.readFileSync(`/proc/${row.pid}/environ`, 'utf-8');
3891
+ const entry = environ.split('\0').find((pair)=>pair.startsWith('XRE_PROFILE_PATH='));
3892
+ return entry ? {
3893
+ ...row,
3894
+ command: `${row.command} ${entry}`
3895
+ } : row;
3896
+ } catch {
3897
+ return row;
3898
+ }
3899
+ });
3900
+ }
3901
+ function listProcesses(platform = process.platform) {
3902
+ if ('win32' === platform) {
3903
+ const script = 'Get-CimInstance Win32_Process | ForEach-Object { "$($_.ProcessId) $($_.ParentProcessId) $($_.CommandLine)" }';
3904
+ const output = runQuiet('powershell.exe', [
3905
+ '-NoProfile',
3906
+ '-NonInteractive',
3907
+ '-Command',
3908
+ script
3909
+ ]);
3910
+ return output ? parseProcessRows(output) : [];
3911
+ }
3912
+ if ('darwin' === platform) {
3913
+ const output = runQuiet('ps', [
3914
+ '-axEo',
3915
+ 'pid=,ppid=,command='
3916
+ ]);
3917
+ return output ? parseProcessRows(output) : [];
3918
+ }
3919
+ const output = runQuiet('ps', [
3920
+ '-axo',
3921
+ 'pid=,ppid=,command='
3922
+ ]);
3923
+ const rows = output ? parseProcessRows(output) : [];
3924
+ return 'linux' === platform ? appendLinuxProfileEnv(rows) : rows;
3925
+ }
3926
+ function resolve_live_pid_normalizePath(value) {
3927
+ return String(value || '').replace(/\\/g, '/').toLowerCase();
3928
+ }
3929
+ function carriesProfile(command, profilePath) {
3930
+ const haystack = resolve_live_pid_normalizePath(command);
3931
+ const needle = resolve_live_pid_normalizePath(profilePath);
3932
+ let from = 0;
3933
+ for(;;){
3934
+ const at = haystack.indexOf(needle, from);
3935
+ if (at < 0) return false;
3936
+ const before = 0 === at ? ' ' : haystack[at - 1];
3937
+ const after = haystack[at + needle.length] ?? ' ';
3938
+ if (/[\s="']/.test(before) && /[\s"']/.test(after)) return true;
3939
+ from = at + 1;
3940
+ }
3941
+ }
3942
+ function executableOf(command) {
3943
+ const trimmed = resolve_live_pid_normalizePath(command).trim();
3944
+ if (trimmed.startsWith('"')) {
3945
+ const end = trimmed.indexOf('"', 1);
3946
+ return end > 0 ? trimmed.slice(1, end) : trimmed;
3947
+ }
3948
+ const cut = trimmed.search(/\s(-|[a-z_][a-z0-9_]*=)/);
3949
+ return cut > 0 ? trimmed.slice(0, cut) : trimmed;
3950
+ }
3951
+ function findLiveBrowserPid(input) {
3952
+ const profilePath = String(input.profilePath || '').trim();
3953
+ const binary = resolve_live_pid_normalizePath(input.binary).trim();
3954
+ if (!profilePath || !binary) return null;
3955
+ const candidates = input.rows.filter((row)=>executableOf(row.command) === binary && carriesProfile(row.command, profilePath) && !HELPER_PROCESS.test(row.command));
3956
+ if (0 === candidates.length) return null;
3957
+ const launcher = input.launcherPid ? candidates.find((row)=>row.pid === input.launcherPid) : void 0;
3958
+ if (launcher) {
3959
+ const twin = candidates.find((row)=>row.pid !== launcher.pid);
3960
+ return twin ? twin.pid : launcher.pid;
3961
+ }
3962
+ const pids = new Set(candidates.map((row)=>row.pid));
3963
+ const roots = candidates.filter((row)=>!pids.has(row.ppid));
3964
+ return (roots[0] || candidates[0]).pid;
3965
+ }
3966
+ function isPidAlive(pid) {
3967
+ if (!pid || !Number.isFinite(pid)) return false;
3968
+ try {
3969
+ process.kill(pid, 0);
3970
+ return true;
3971
+ } catch (error) {
3972
+ return error?.code === 'EPERM';
3973
+ }
3974
+ }
3975
+ async function resolveLiveBrowserPid(input) {
3976
+ const list = input.list || listProcesses;
3977
+ const attempts = Math.max(1, input.attempts ?? 6);
3978
+ const intervalMs = input.intervalMs ?? 500;
3979
+ const sleep = input.sleep || ((ms)=>new Promise((r)=>{
3980
+ setTimeout(r, ms).unref?.();
3981
+ }));
3982
+ let last = null;
3983
+ for(let attempt = 0; attempt < attempts; attempt += 1){
3984
+ last = findLiveBrowserPid({
3985
+ profilePath: input.profilePath,
3986
+ binary: input.binary,
3987
+ launcherPid: input.launcherPid,
3988
+ rows: list()
3989
+ });
3990
+ if (last && last !== input.launcherPid) break;
3991
+ if (attempt < attempts - 1) await sleep(intervalMs);
3992
+ }
3993
+ return last;
3994
+ }
3782
3995
  var runtime_options = __webpack_require__("./browsers/browsers-lib/runtime-options.ts");
3783
3996
  var shared_utils = __webpack_require__("./browsers/browsers-lib/shared-utils.ts");
3784
3997
  function parseFlatpakBinary(binary) {
@@ -4056,7 +4269,12 @@ var __webpack_modules__ = {
4056
4269
  for (const instance of activeInstances)attemptCleanup(instance);
4057
4270
  }
4058
4271
  function forceKillAllOnExit() {
4059
- for (const instance of activeInstances)(0, process_teardown.$Y)(instance.childRef(), instance.browser);
4272
+ for (const instance of activeInstances){
4273
+ const child = instance.childRef();
4274
+ const livePid = instance.livePidRef();
4275
+ (0, process_teardown.$Y)(child, instance.browser);
4276
+ if (livePid && livePid !== child?.pid) (0, process_teardown.Qx)(livePid, instance.browser);
4277
+ }
4060
4278
  }
4061
4279
  function firstBrowserLabel() {
4062
4280
  for (const instance of activeInstances)return instance.browser;
@@ -4088,10 +4306,11 @@ var __webpack_modules__ = {
4088
4306
  process.exit(1);
4089
4307
  });
4090
4308
  }
4091
- function setupFirefoxProcessHandlers(browser, childRef, cleanupInstance) {
4309
+ function setupFirefoxProcessHandlers(browser, childRef, cleanupInstance, livePidRef = ()=>null) {
4092
4310
  const instance = {
4093
4311
  browser,
4094
4312
  childRef,
4313
+ livePidRef,
4095
4314
  cleanupInstance,
4096
4315
  isCleaningUp: false
4097
4316
  };
@@ -5177,7 +5396,6 @@ var __webpack_modules__ = {
5177
5396
  } catch {}
5178
5397
  return controller;
5179
5398
  }
5180
- var external_node_child_process_ = __webpack_require__("node:child_process");
5181
5399
  var wsl_support = __webpack_require__("./browsers/browsers-lib/wsl-support.ts");
5182
5400
  const LINUX_FIREFOX_PATHS = [
5183
5401
  '/usr/bin/firefox',
@@ -5447,13 +5665,12 @@ var __webpack_modules__ = {
5447
5665
  browserPid: this.child?.pid,
5448
5666
  extensionId: this.extensionOutputPath ? (0, banner.MT)(this.extensionOutputPath) : void 0
5449
5667
  });
5450
- this.child.on('close', ()=>{
5451
- (0, shared_utils.sW)(profilePath);
5452
- });
5453
5668
  this.wireChildLifecycle();
5669
+ this.trackLiveBrowserPid();
5454
5670
  let ctrl;
5455
5671
  try {
5456
5672
  ctrl = await setupRdpAfterLaunch(this.host, compilation, debugPort);
5673
+ this.trackLiveBrowserPid(1);
5457
5674
  } catch (error) {
5458
5675
  const reason = error?.extensionLoadRefusedReason;
5459
5676
  if (!reason) {
@@ -5548,24 +5765,89 @@ var __webpack_modules__ = {
5548
5765
  if (process.env.VITEST || process.env.VITEST_WORKER_ID) throw new Error('Firefox startup timed out');
5549
5766
  process.exit(1);
5550
5767
  });
5551
- let disposeProcessHandlers;
5552
5768
  child.on('close', (code)=>{
5553
- if (this.watchTimeout) {
5554
- clearTimeout(this.watchTimeout);
5555
- this.watchTimeout = void 0;
5556
- }
5557
- if ((0, messaging._o)()) this.ctx.logger?.info?.(messages.Th(this.host.browser));
5558
- if (!(0, process_teardown.Op)(child)) {
5559
- this.ctx.logger?.error?.(`[browser] ${this.host.browser} exited (code ${code ?? 'unknown'}) without being asked to. The add-on may have been rejected or the browser crashed; the session cannot be driven.`);
5560
- (0, ready_stamp.Wi)(this.extensionOutputPath, code);
5561
- }
5562
- this.cleanupInstance().catch((err)=>{
5563
- if ((0, messaging._o)()) this.ctx.logger?.error?.(`[browser] Cleanup error on child close: ${err?.message || err}`);
5564
- });
5565
- disposeProcessHandlers?.();
5769
+ const expected = (0, process_teardown.Op)(child);
5770
+ if (!expected && this.adoptHandedOffBrowser()) return;
5771
+ this.onBrowserGone(code, expected);
5566
5772
  });
5567
5773
  this.pipeChildOutput(child);
5568
- disposeProcessHandlers = setupFirefoxProcessHandlers(this.host.browser, ()=>this.child, ()=>this.cleanupInstance());
5774
+ this.disposeProcessHandlers = setupFirefoxProcessHandlers(this.host.browser, ()=>this.child, ()=>this.cleanupInstance(), ()=>this.livePid);
5775
+ }
5776
+ onBrowserGone(code, expected) {
5777
+ if (this.browserGone) return;
5778
+ this.browserGone = true;
5779
+ if (this.watchTimeout) {
5780
+ clearTimeout(this.watchTimeout);
5781
+ this.watchTimeout = void 0;
5782
+ }
5783
+ if (this.liveExitWatcher) {
5784
+ clearInterval(this.liveExitWatcher);
5785
+ this.liveExitWatcher = void 0;
5786
+ }
5787
+ if (this.host.launchProfilePath) (0, shared_utils.sW)(this.host.launchProfilePath);
5788
+ if ((0, messaging._o)()) this.ctx.logger?.info?.(messages.Th(this.host.browser));
5789
+ if (!expected) {
5790
+ this.ctx.logger?.error?.(`[browser] ${this.host.browser} exited (code ${code ?? 'unknown'}) without being asked to. The add-on may have been rejected or the browser crashed; the session cannot be driven.`);
5791
+ (0, ready_stamp.Wi)(this.extensionOutputPath, code);
5792
+ }
5793
+ this.cleanupInstance().catch((err)=>{
5794
+ if ((0, messaging._o)()) this.ctx.logger?.error?.(`[browser] Cleanup error on child close: ${err?.message || err}`);
5795
+ });
5796
+ this.disposeProcessHandlers?.();
5797
+ this.disposeProcessHandlers = void 0;
5798
+ }
5799
+ async trackLiveBrowserPid(attempts) {
5800
+ const child = this.child;
5801
+ const profilePath = this.host.launchProfilePath;
5802
+ if (!child?.pid || !profilePath || this.browserGone) return;
5803
+ try {
5804
+ const pid = await resolveLiveBrowserPid({
5805
+ profilePath,
5806
+ binary: child.spawnfile,
5807
+ launcherPid: child.pid,
5808
+ attempts
5809
+ });
5810
+ if (pid && pid !== child.pid && this.child === child) this.adoptLivePid(pid);
5811
+ } catch {}
5812
+ }
5813
+ adoptHandedOffBrowser() {
5814
+ const profilePath = this.host.launchProfilePath;
5815
+ const child = this.child;
5816
+ if (!profilePath || !child || this.browserGone) return false;
5817
+ try {
5818
+ const pid = this.livePid || findLiveBrowserPid({
5819
+ profilePath,
5820
+ binary: child.spawnfile,
5821
+ launcherPid: child.pid,
5822
+ rows: listProcesses()
5823
+ });
5824
+ if (!pid || pid === this.child?.pid || !isPidAlive(pid)) return false;
5825
+ this.adoptLivePid(pid);
5826
+ return true;
5827
+ } catch {
5828
+ return false;
5829
+ }
5830
+ }
5831
+ adoptLivePid(pid) {
5832
+ if (this.livePid === pid) return;
5833
+ this.livePid = pid;
5834
+ (0, ready_stamp.M)(this.extensionOutputPath, {
5835
+ browserPid: pid,
5836
+ launcherPid: this.child?.pid
5837
+ });
5838
+ if ((0, messaging._o)()) this.ctx.logger?.info?.(`[browser] ${this.host.browser} handed the session to pid ${pid} (launcher pid ${this.child?.pid}).`);
5839
+ this.watchLivePidExit();
5840
+ }
5841
+ watchLivePidExit() {
5842
+ if (this.liveExitWatcher) clearInterval(this.liveExitWatcher);
5843
+ this.liveExitWatcher = setInterval(()=>{
5844
+ const pid = this.livePid;
5845
+ if (!pid || isPidAlive(pid)) return;
5846
+ clearInterval(this.liveExitWatcher);
5847
+ this.liveExitWatcher = void 0;
5848
+ this.onBrowserGone(null, (0, process_teardown.PU)(pid));
5849
+ }, 1000);
5850
+ this.liveExitWatcher.unref?.();
5569
5851
  }
5570
5852
  async retryAddonInstall(compilation, debugPort) {
5571
5853
  try {
@@ -5600,6 +5882,7 @@ var __webpack_modules__ = {
5600
5882
  }
5601
5883
  async cleanupInstance() {
5602
5884
  (0, process_teardown.Df)(this.child, this.host.browser);
5885
+ if (this.livePid && this.livePid !== this.child?.pid) (0, process_teardown._T)(this.livePid, this.host.browser);
5603
5886
  }
5604
5887
  scheduleWatchTimeout() {
5605
5888
  if (this.watchTimeout) return;
@@ -5634,6 +5917,10 @@ var __webpack_modules__ = {
5634
5917
  firefox_launch_define_property(this, "child", null);
5635
5918
  firefox_launch_define_property(this, "watchTimeout", void 0);
5636
5919
  firefox_launch_define_property(this, "extensionOutputPath", void 0);
5920
+ firefox_launch_define_property(this, "livePid", null);
5921
+ firefox_launch_define_property(this, "liveExitWatcher", void 0);
5922
+ firefox_launch_define_property(this, "browserGone", false);
5923
+ firefox_launch_define_property(this, "disposeProcessHandlers", void 0);
5637
5924
  this.host = host;
5638
5925
  this.ctx = ctx;
5639
5926
  }
@@ -5807,7 +6094,6 @@ var __webpack_modules__ = {
5807
6094
  QW: ()=>safariBuildPreflight,
5808
6095
  iF: ()=>safariPreflightError
5809
6096
  });
5810
- var external_node_child_process_ = __webpack_require__("node:child_process");
5811
6097
  var external_node_fs_ = __webpack_require__("node:fs");
5812
6098
  var messaging = __webpack_require__("./helpers/messaging.ts");
5813
6099
  var banner = __webpack_require__("./browsers/browsers-lib/banner.ts");
@@ -5821,6 +6107,7 @@ var __webpack_modules__ = {
5821
6107
  (0, messaging._w)(messages.Aj(xcodebuildCmd));
5822
6108
  }
5823
6109
  var safari_config = __webpack_require__("./browsers/run-safari/safari-launch/safari-config.ts");
6110
+ var external_node_child_process_ = __webpack_require__("node:child_process");
5824
6111
  function isMacOS() {
5825
6112
  return 'darwin' === process.platform;
5826
6113
  }
@@ -5878,20 +6165,6 @@ var __webpack_modules__ = {
5878
6165
  ok: Boolean(converter && xcodebuild)
5879
6166
  };
5880
6167
  }
5881
- function fallbackLogger() {
5882
- return {
5883
- info: (...a)=>(0, messaging._w)(...a),
5884
- warn: (...a)=>(0, messaging.mR)(...a),
5885
- error: (...a)=>(0, messaging.Gg)(...a),
5886
- debug: (...a)=>console?.debug?.(...a)
5887
- };
5888
- }
5889
- function isTestEnv() {
5890
- return Boolean(process.env.VITEST || process.env.VITEST_WORKER_ID);
5891
- }
5892
- function delay(ms) {
5893
- return new Promise((resolve)=>setTimeout(resolve, ms));
5894
- }
5895
6168
  const TOOL_TAIL_LINES = 50;
5896
6169
  const TOOL_TAIL_BYTES = 8192;
5897
6170
  function toolOutputTail(output) {
@@ -5930,6 +6203,60 @@ var __webpack_modules__ = {
5930
6203
  }));
5931
6204
  });
5932
6205
  }
6206
+ async function resolvePidForBundle(bundleId) {
6207
+ for(let attempt = 0; attempt < 10; attempt += 1){
6208
+ const pid = await new Promise((resolve)=>{
6209
+ const child = (0, external_node_child_process_.spawn)("osascript", [
6210
+ '-e',
6211
+ `tell application "System Events" to get unix id of first process whose bundle identifier is "${bundleId}"`
6212
+ ]);
6213
+ let out = '';
6214
+ child.stdout.on('data', (d)=>out += String(d));
6215
+ child.on('error', ()=>resolve(null));
6216
+ child.on('close', ()=>{
6217
+ const n = Number(String(out).trim());
6218
+ resolve(Number.isFinite(n) && n > 0 ? n : null);
6219
+ });
6220
+ });
6221
+ if (pid) return pid;
6222
+ await new Promise((r)=>setTimeout(r, 500));
6223
+ }
6224
+ return null;
6225
+ }
6226
+ function createSafariTools() {
6227
+ return {
6228
+ detectToolchain: ()=>detectSafariToolchain(),
6229
+ runConverter: (args)=>runTool('xcrun', args),
6230
+ runXcodebuild: (args)=>runTool('xcodebuild', args),
6231
+ openApp: (target)=>runTool('open', [
6232
+ target
6233
+ ]),
6234
+ openSafari: (binary)=>runTool('open', [
6235
+ '-a',
6236
+ binary
6237
+ ]),
6238
+ resolvePid: (bundleId)=>resolvePidForBundle(bundleId),
6239
+ pluginkitList: async ()=>{
6240
+ const { ok, output } = await runTool('pluginkit', [
6241
+ '-m'
6242
+ ], {
6243
+ quiet: true
6244
+ });
6245
+ return ok ? output : '';
6246
+ }
6247
+ };
6248
+ }
6249
+ function fallbackLogger() {
6250
+ return {
6251
+ info: (...a)=>(0, messaging._w)(...a),
6252
+ warn: (...a)=>(0, messaging.mR)(...a),
6253
+ error: (...a)=>(0, messaging.Gg)(...a),
6254
+ debug: (...a)=>console?.debug?.(...a)
6255
+ };
6256
+ }
6257
+ function delay(ms) {
6258
+ return new Promise((resolve)=>setTimeout(resolve, ms));
6259
+ }
5933
6260
  function converterWarnings(output) {
5934
6261
  const lines = output.split(/\r?\n/);
5935
6262
  const collected = [];
@@ -5950,15 +6277,11 @@ var __webpack_modules__ = {
5950
6277
  }
5951
6278
  return collected;
5952
6279
  }
5953
- async function confirmRegisteredWithSafari(bundleIdentifier) {
6280
+ async function confirmRegisteredWithSafari(tools, bundleIdentifier) {
5954
6281
  const needle = `${bundleIdentifier}.Extension`;
5955
6282
  for(let attempt = 0; attempt < 6; attempt += 1){
5956
- const { ok, output } = await runTool('pluginkit', [
5957
- '-m'
5958
- ], {
5959
- quiet: true
5960
- });
5961
- if (ok && output.includes(needle)) return true;
6283
+ const listing = await tools.pluginkitList();
6284
+ if (listing.includes(needle)) return true;
5962
6285
  await delay(800);
5963
6286
  }
5964
6287
  return false;
@@ -5973,6 +6296,10 @@ var __webpack_modules__ = {
5973
6296
  macOsOnly: config.macOsOnly
5974
6297
  };
5975
6298
  }
6299
+ function completePackage(config, logger, mode) {
6300
+ if ('full' === mode && config.bundleIdDerived) logger.info?.(messages.$q(config.bundleIdentifier));
6301
+ return describePackage(config);
6302
+ }
5976
6303
  function safariPreflightError() {
5977
6304
  const tc = detectSafariToolchain();
5978
6305
  if (!tc.platformOk) return messages.G3(process.platform);
@@ -5996,26 +6323,6 @@ var __webpack_modules__ = {
5996
6323
  severity: 'ok'
5997
6324
  };
5998
6325
  }
5999
- async function resolvePidForBundle(bundleId) {
6000
- for(let attempt = 0; attempt < 10; attempt += 1){
6001
- const pid = await new Promise((resolve)=>{
6002
- const child = (0, external_node_child_process_.spawn)("osascript", [
6003
- '-e',
6004
- `tell application "System Events" to get unix id of first process whose bundle identifier is "${bundleId}"`
6005
- ]);
6006
- let out = '';
6007
- child.stdout.on('data', (d)=>out += String(d));
6008
- child.on('error', ()=>resolve(null));
6009
- child.on('close', ()=>{
6010
- const n = Number(String(out).trim());
6011
- resolve(Number.isFinite(n) && n > 0 ? n : null);
6012
- });
6013
- });
6014
- if (pid) return pid;
6015
- await new Promise((r)=>setTimeout(r, 500));
6016
- }
6017
- return null;
6018
- }
6019
6326
  async function announceSafariDevSession(host, config, appPath) {
6020
6327
  try {
6021
6328
  await (0, banner.ai)({
@@ -6031,16 +6338,16 @@ var __webpack_modules__ = {
6031
6338
  } catch {}
6032
6339
  }
6033
6340
  async function runSafariPipeline(compilation, host, logger, mode) {
6341
+ const tools = host.tools || createSafariTools();
6034
6342
  const config = (0, safari_config.F7)(compilation, host);
6035
6343
  const converterArgs = (0, safari_config.w7)(config);
6036
6344
  const xcodebuildArgs = (0, safari_config.vx)(config);
6037
6345
  const isSigned = Boolean(config.developmentTeam);
6038
- if ('full' === mode && config.bundleIdDerived) logger.warn?.(messages.$q(config.bundleIdentifier));
6039
- if (host.dryRun || isTestEnv()) {
6346
+ if (host.dryRun) {
6040
6347
  logSafariDryRun(`xcrun ${converterArgs.join(' ')}`, `xcodebuild ${xcodebuildArgs.join(' ')}`);
6041
- return describePackage(config);
6348
+ return completePackage(config, logger, mode);
6042
6349
  }
6043
- const toolchain = detectSafariToolchain();
6350
+ const toolchain = tools.detectToolchain();
6044
6351
  if (!toolchain.platformOk) {
6045
6352
  logger.warn?.(messages.G3(process.platform));
6046
6353
  return describePackage(config);
@@ -6064,7 +6371,7 @@ var __webpack_modules__ = {
6064
6371
  }
6065
6372
  const { saved, restore } = (0, safari_config._D)(config);
6066
6373
  logger.info?.(messages.uK(config.extensionDir));
6067
- const converted = await runTool('xcrun', converterArgs);
6374
+ const converted = await tools.runConverter(converterArgs);
6068
6375
  if (!converted.ok) {
6069
6376
  const tail = toolOutputTail(converted.output);
6070
6377
  logger.error?.(messages.A('safari-web-extension-converter', converted.code, tail));
@@ -6081,7 +6388,7 @@ var __webpack_modules__ = {
6081
6388
  logger.info?.(messages.l_(config.projectLocation));
6082
6389
  } else if ('full' === mode) logger.info?.(messages.oM());
6083
6390
  if ('full' === mode) logger.info?.(messages.jR((0, safari_config.aD)(config)));
6084
- const built = await runTool('xcodebuild', xcodebuildArgs);
6391
+ const built = await tools.runXcodebuild(xcodebuildArgs);
6085
6392
  if (!built.ok) {
6086
6393
  const tail = toolOutputTail(built.output);
6087
6394
  logger.error?.(messages.A('xcodebuild', built.code, tail));
@@ -6096,30 +6403,26 @@ var __webpack_modules__ = {
6096
6403
  if (!config.open) {
6097
6404
  logger.info?.(messages.aO(appPath, config.appName));
6098
6405
  if (host.announceDevReady) await announceSafariDevSession(host, config, appPath);
6099
- return describePackage(config);
6406
+ return completePackage(config, logger, mode);
6100
6407
  }
6101
6408
  const target = external_node_fs_.existsSync(appPath) ? appPath : (0, safari_config.Gi)(config);
6102
6409
  logger.info?.(messages.fw(target));
6103
- await runTool('open', [
6104
- target
6105
- ]);
6106
- if (config.safariBinary) await runTool('open', [
6107
- '-a',
6108
- config.safariBinary
6109
- ]);
6410
+ await tools.openApp(target);
6411
+ if (config.safariBinary) await tools.openSafari(config.safariBinary);
6110
6412
  const raisedBundleId = config.safariBinary ? 'com.apple.Safari' : config.bundleIdentifier;
6111
- const browserPid = await resolvePidForBundle(raisedBundleId);
6112
- if (browserPid) (0, ready_stamp.M)(config.extensionDir, {
6113
- browserPid,
6413
+ const browserPid = await tools.resolvePid(raisedBundleId);
6414
+ (0, ready_stamp.M)(config.extensionDir, {
6415
+ browserPid: browserPid || void 0,
6114
6416
  binary: config.safariBinary || appPath,
6115
6417
  binaryProvenance: config.safariBinary ? 'pinned' : 'system',
6116
6418
  extensionId: `${config.bundleIdentifier}.Extension`
6117
6419
  });
6420
+ if (!browserPid) logger.warn?.(messages.jn(config.appName));
6118
6421
  logger.info?.(messages.fO(config.appName, isSigned));
6119
- if (await confirmRegisteredWithSafari(config.bundleIdentifier)) logger.info?.(messages.fd(config.appName));
6422
+ if (await confirmRegisteredWithSafari(tools, config.bundleIdentifier)) logger.info?.(messages.fd(config.appName));
6120
6423
  else logger.info?.(messages.qY(config.appName));
6121
6424
  if (host.announceDevReady) await announceSafariDevSession(host, config, appPath);
6122
- return describePackage(config);
6425
+ return completePackage(config, logger, mode);
6123
6426
  }
6124
6427
  async function packageSafariExtension(host, outputPath, logger, mode = 'full') {
6125
6428
  const compilation = {
@@ -6239,9 +6542,10 @@ var __webpack_modules__ = {
6239
6542
  function manifestFingerprintPath(config) {
6240
6543
  return node_path__rspack_import_1.join(config.projectLocation, '.manifest-fingerprint');
6241
6544
  }
6545
+ const HOT_UPDATE_DIR = 'hot';
6242
6546
  function topLevelEntries(extensionDir) {
6243
6547
  try {
6244
- return node_fs__rspack_import_0.readdirSync(extensionDir).sort();
6548
+ return node_fs__rspack_import_0.readdirSync(extensionDir).filter((entry)=>entry !== HOT_UPDATE_DIR).sort();
6245
6549
  } catch {
6246
6550
  return [];
6247
6551
  }
@@ -6370,7 +6674,7 @@ var __webpack_modules__ = {
6370
6674
  return Object.fromEntries(Object.entries(input).filter(([, value])=>void 0 !== value));
6371
6675
  }
6372
6676
  function createSafariPackager(options = {}) {
6373
- const { browser = 'safari', noOpen = true, dryRun = false, logger, ...identity } = options;
6677
+ const { browser = 'safari', noOpen = true, dryRun = false, logger, tools, ...identity } = options;
6374
6678
  return async (distPath, mode = 'full', overrides)=>await (0, _safari_launch__rspack_import_0.Ah)({
6375
6679
  extension: [
6376
6680
  distPath
@@ -6378,6 +6682,7 @@ var __webpack_modules__ = {
6378
6682
  browser,
6379
6683
  noOpen,
6380
6684
  dryRun,
6685
+ tools,
6381
6686
  ...withoutUndefined(identity),
6382
6687
  ...withoutUndefined(overrides)
6383
6688
  }, distPath, logger, mode);
@@ -6705,6 +7010,8 @@ var __webpack_modules__ = {
6705
7010
  const MIN_NODE_MINOR = 12;
6706
7011
  const MIN_DENO_MAJOR = 2;
6707
7012
  const MIN_DENO_MINOR = 5;
7013
+ const MIN_BUN_MAJOR = 1;
7014
+ const MIN_BUN_MINOR = 2;
6708
7015
  const BLOCKED_DENO_VERSIONS = [
6709
7016
  '2.8.0'
6710
7017
  ];
@@ -6721,6 +7028,9 @@ var __webpack_modules__ = {
6721
7028
  if (BLOCKED_DENO_VERSIONS.includes(version)) return false;
6722
7029
  return meetsFloor(version, MIN_DENO_MAJOR, MIN_DENO_MINOR);
6723
7030
  }
7031
+ function isSupportedBunVersion(version) {
7032
+ return meetsFloor(version, MIN_BUN_MAJOR, MIN_BUN_MINOR);
7033
+ }
6724
7034
  function detectBunVersion(versions = process.versions) {
6725
7035
  const bunVersion = versions.bun;
6726
7036
  return 'string' == typeof bunVersion && bunVersion.length > 0 ? bunVersion : void 0;
@@ -6733,8 +7043,10 @@ var __webpack_modules__ = {
6733
7043
  if (BLOCKED_DENO_VERSIONS.includes(version)) return `[Extension.js] Deno ${version} cannot load node:querystring inside the bundler, so every build fails on it. Deno fixed that in 2.8.1. Run deno upgrade to build the extension on Deno.`;
6734
7044
  return `[Extension.js] Requires Deno >= ${MIN_DENO_MAJOR}.${MIN_DENO_MINOR} (you are on ${version}). Run deno upgrade, or run the extension CLI on Node.js >= ${MIN_NODE_MAJOR}.${MIN_NODE_MINOR} instead.`;
6735
7045
  }
6736
- function unsupportedNodeVersionMessage(version, bunVersion) {
6737
- if (bunVersion) return `[Extension.js] The extension CLI runs on Node.js, not on the Bun runtime (Bun ${bunVersion}, reporting Node.js ${version}), so the Node.js you have installed is not the problem. Run it on Node.js instead: drop --bun from bunx or bun run, or unset run.bun in bunfig.toml, plain bunx runs the extension CLI on Node.js.`;
7046
+ function unsupportedBunVersionMessage(version) {
7047
+ return `[Extension.js] Requires Bun >= ${MIN_BUN_MAJOR}.${MIN_BUN_MINOR} (you are on ${version}). Run bun upgrade, or run the extension CLI on Node.js >= ${MIN_NODE_MAJOR}.${MIN_NODE_MINOR} instead.`;
7048
+ }
7049
+ function unsupportedNodeVersionMessage(version) {
6738
7050
  return `[Extension.js] Requires Node.js >= ${MIN_NODE_MAJOR}.${MIN_NODE_MINOR} (you are on ${version}). Upgrade Node.js to run the extension CLI.`;
6739
7051
  }
6740
7052
  function enforceSupportedNodeVersion(version = process.versions.node, bunVersion = detectBunVersion(), denoVersion = detectDenoVersion()) {
@@ -6744,8 +7056,14 @@ var __webpack_modules__ = {
6744
7056
  process.exit(1);
6745
7057
  return;
6746
7058
  }
6747
- if (!bunVersion && isSupportedNodeVersion(version)) return;
6748
- console.error(unsupportedNodeVersionMessage(version, bunVersion));
7059
+ if (bunVersion) {
7060
+ if (isSupportedBunVersion(bunVersion)) return;
7061
+ console.error(unsupportedBunVersionMessage(bunVersion));
7062
+ process.exit(1);
7063
+ return;
7064
+ }
7065
+ if (isSupportedNodeVersion(version)) return;
7066
+ console.error(unsupportedNodeVersionMessage(version));
6749
7067
  process.exit(1);
6750
7068
  }
6751
7069
  enforceSupportedNodeVersion();
@@ -7141,7 +7459,7 @@ var __webpack_modules__ = {
7141
7459
  return Math.min(Math.max(n, min), max);
7142
7460
  }
7143
7461
  const TEMPLATE_CORPUS_REPO = 'extension-js/examples';
7144
- const TEMPLATE_CORPUS_REF = '39448a235ea1b02516e6d555bcae409db2258b7f';
7462
+ const TEMPLATE_CORPUS_REF = 'f8087f012cf195cba9aa2e3c32fd5a6729da33f0';
7145
7463
  const TEMPLATE_CORPUS_SLUGS = [
7146
7464
  'action',
7147
7465
  'action-locales',
@@ -8254,7 +8572,10 @@ Docker / Devcontainers / Codespaces
8254
8572
  - Use ${messages_code('--host 0.0.0.0')} to bind the dev server on all interfaces so HMR is reachable from the host.
8255
8573
  - Use ${messages_code('--no-browser')} inside the container and load the extension manually from ${messages_code('dist/<browser>/')} in your host browser.
8256
8574
  - Chromium sandbox flags (${messages_code('--no-sandbox')}) are added automatically when Docker, Podman, devcontainers, or Codespaces are detected.
8257
- - File watching uses polling by default (1 s interval), which works across bind-mounted volumes.
8575
+ - File watching uses native file events by default, which keeps rebuilds fast.
8576
+ - Set ${messages_code(messages_arg('EXTENSION_WATCH_POLL=true'))} to poll instead when native events do not fire,
8577
+ as in a container, a virtual machine, or a network or bind mount.
8578
+ Tune it with ${messages_code(messages_arg('EXTENSION_WATCH_POLL_INTERVAL'))} (default 1000 ms).
8258
8579
  - Example: ${messages_code('extension dev ./my-ext --host 0.0.0.0 --no-browser --port 8080')}
8259
8580
 
8260
8581
  Flatpak Firefox
@@ -8427,7 +8748,9 @@ Cross-browser compatibility
8427
8748
  ],
8428
8749
  notes: [
8429
8750
  'Use --no-browser inside the container and load dist/<browser>/ in the host browser',
8430
- 'File watching uses polling (1s interval) for bind-mount compatibility',
8751
+ 'File watching uses native file events by default',
8752
+ 'Set EXTENSION_WATCH_POLL=true to poll instead when native events do not fire, as in a container, a virtual machine, or a network or bind mount',
8753
+ 'EXTENSION_WATCH_POLL_INTERVAL sets the poll interval in ms (default 1000)',
8431
8754
  '--no-sandbox is added automatically when a container environment is detected'
8432
8755
  ]
8433
8756
  }
@@ -8458,12 +8781,45 @@ Cross-browser compatibility
8458
8781
  }
8459
8782
  const OPEN_SURFACE_NOUN = {
8460
8783
  popup: 'popup',
8784
+ options: 'options page',
8461
8785
  sidebar: 'side panel',
8462
8786
  action: 'action popup'
8463
8787
  };
8464
8788
  function openSurfaceNoun(surface) {
8465
8789
  return OPEN_SURFACE_NOUN[surface] || surface;
8466
8790
  }
8791
+ function openSurfaceAction(surface) {
8792
+ return 'command' === surface ? 'trigger the command' : `open the ${openSurfaceNoun(surface)}`;
8793
+ }
8794
+ function asSentence(text) {
8795
+ const trimmed = text.trim();
8796
+ const capitalized = trimmed.charAt(0).toUpperCase() + trimmed.slice(1);
8797
+ return capitalized.endsWith('.') ? capitalized : `${capitalized}.`;
8798
+ }
8799
+ function listenerCount(count, event) {
8800
+ return `${count} ${event} ${1 === count ? 'listener' : 'listeners'}`;
8801
+ }
8802
+ function openedSurface(surface) {
8803
+ return `${getLoggingPrefix('success')} Opened the ${openSurfaceNoun(surface)}.`;
8804
+ }
8805
+ function replayedActionClick(listeners) {
8806
+ return `${getLoggingPrefix('success')} Replayed ${listenerCount(listeners, 'onClicked')} without a user gesture.`;
8807
+ }
8808
+ function replayedCommand(listeners, command) {
8809
+ const name = command ? ` for ${messages_arg(command)}` : '';
8810
+ return `${getLoggingPrefix('success')} Replayed ${listenerCount(listeners, 'onCommand')}${name} without a user gesture.`;
8811
+ }
8812
+ function openSurfaceWarning(warning) {
8813
+ return `${getLoggingPrefix('warn')} ${asSentence(warning)}`;
8814
+ }
8815
+ function openSurfaceFailed(surface, reason, engine, remedy) {
8816
+ const engineNote = engine ? messages_arg(` (engine ${engine})`) : '';
8817
+ return [
8818
+ `${getLoggingPrefix('error')} Can't ${openSurfaceAction(surface)}.`,
8819
+ `${messaging.Sc.label('REASON')} ${messaging.Sc.val(reason)}${engineNote}`,
8820
+ remedy
8821
+ ].filter(Boolean).join('\n');
8822
+ }
8467
8823
  function openSurfaceGestureStep(surface) {
8468
8824
  return `Click the extension's toolbar entry, or its row in the extensions menu, to open the ${openSurfaceNoun(surface)}.`;
8469
8825
  }
@@ -8473,6 +8829,12 @@ Cross-browser compatibility
8473
8829
  function openSurfaceNeedsGesture(surface) {
8474
8830
  return `${getLoggingPrefix('error')} Chromium opens the ${openSurfaceNoun(surface)} only in response to a user gesture, so ${messages_code(`extension open ${surface}`)} can't open it from the command line.\n` + openSurfaceGestureStep(surface);
8475
8831
  }
8832
+ function doctorHeader(browser, passes, total) {
8833
+ return `doctor (${browser}), ${passes}/${total} checks passed`;
8834
+ }
8835
+ function doctorRemedy(check, remediation) {
8836
+ return `\n${check}: ${remediation}`;
8837
+ }
8476
8838
  function sessionOnPort(browser, port) {
8477
8839
  return port ? `for ${browser} on port ${port}` : `for ${browser}`;
8478
8840
  }
@@ -8735,11 +9097,20 @@ Cross-browser compatibility
8735
9097
  if ('json' === format) return void console.log(JSON.stringify(event, null, 2));
8736
9098
  console.log(formatPrettyLogLine(event));
8737
9099
  }
9100
+ function levelToken(level) {
9101
+ const word = level.toUpperCase();
9102
+ if ('error' === level) return external_pintor_default().red(word);
9103
+ if ('warn' === level) return external_pintor_default().brightYellow(word);
9104
+ if ('info' === level) return external_pintor_default().gray(word);
9105
+ if ('debug' === level) return external_pintor_default().dim(external_pintor_default().gray(word));
9106
+ return word;
9107
+ }
8738
9108
  function formatPrettyLogLine(event) {
8739
9109
  const parts = Array.isArray(event.messageParts) ? event.messageParts.map((p)=>'string' == typeof p ? p : JSON.stringify(p)).join(' ') : '';
8740
9110
  const code = event.code ? ` ${event.code}` : '';
8741
9111
  const remediation = event.remediation ? `\n ↳ ${event.remediation}` : '';
8742
- return `[${event.seq ?? '-'}] ${String(event.level || 'log').toUpperCase()} (${event.context})${code} ${parts}${remediation}`;
9112
+ const level = String(event.level || 'log').toLowerCase();
9113
+ return `[${event.seq ?? '-'}] ${levelToken(level)} ${external_pintor_default().dim(`(${event.context})`)}${code} ${parts}${remediation}`;
8743
9114
  }
8744
9115
  function writeFrame(frame) {
8745
9116
  try {
@@ -8822,7 +9193,7 @@ Cross-browser compatibility
8822
9193
  if (matches(event)) printEvent(event, format);
8823
9194
  },
8824
9195
  onGap: (gap)=>{
8825
- console.error(`… ${gap.dropped} event(s) dropped (${gap.reason}), stream is behind`);
9196
+ console.error(external_pintor_default().dim(external_pintor_default().gray(`… ${gap.dropped} event(s) dropped (${gap.reason}), stream is behind`)));
8826
9197
  },
8827
9198
  onClose: (close)=>{
8828
9199
  if (!refusals.has(close.code) && sessionStillNamed()) return;
@@ -8994,11 +9365,33 @@ Cross-browser compatibility
8994
9365
  hint: openSurfaceGestureStep(surface)
8995
9366
  };
8996
9367
  }
8997
- function printResult(result, output, command) {
9368
+ function openResultLines(value) {
9369
+ if (!value || 'object' != typeof value) return;
9370
+ const reply = value;
9371
+ if ('string' == typeof reply.opened) return [
9372
+ openedSurface(reply.opened)
9373
+ ];
9374
+ if ('popup' === reply.triggered) return [
9375
+ openedSurface('action')
9376
+ ];
9377
+ if ('number' != typeof reply.listeners) return;
9378
+ const lines = [];
9379
+ if ('onClicked' === reply.triggered) lines.push(replayedActionClick(reply.listeners));
9380
+ else {
9381
+ if ('command' !== reply.triggered) return;
9382
+ lines.push(replayedCommand(reply.listeners, 'string' == typeof reply.command ? reply.command : void 0));
9383
+ }
9384
+ if ('string' == typeof reply.warning && reply.warning) lines.push(openSurfaceWarning(reply.warning));
9385
+ return lines;
9386
+ }
9387
+ function printResult(result, output, command, pretty) {
8998
9388
  if ('json' === normalizeOutputFormat(output)) return void console.log(JSON.stringify(buildActEnvelope(command, result)));
8999
9389
  if (result.ok) {
9000
9390
  const value = result.value;
9001
- console.log('string' == typeof value ? value : JSON.stringify(value, null, 2));
9391
+ const lines = pretty?.value(value) ?? [
9392
+ 'string' == typeof value ? value : JSON.stringify(value, null, 2)
9393
+ ];
9394
+ for (const line of lines)console.log(line);
9002
9395
  const consoleRecords = result.console;
9003
9396
  if (Array.isArray(consoleRecords)) {
9004
9397
  if (0 === consoleRecords.length) console.log('(no console lines captured)');
@@ -9007,6 +9400,11 @@ Cross-browser compatibility
9007
9400
  if (result.truncated) console.error('… result truncated (byte cap)');
9008
9401
  return;
9009
9402
  }
9403
+ if (pretty) {
9404
+ const { error } = buildActEnvelope(command, result);
9405
+ console.error(pretty.failure(error));
9406
+ return;
9407
+ }
9010
9408
  const err = result.error || {
9011
9409
  name: 'Error',
9012
9410
  message: 'command failed'
@@ -9080,7 +9478,7 @@ Cross-browser compatibility
9080
9478
  if (result?.ok && input.augment) try {
9081
9479
  Object.assign(result, input.augment(projectPath, browser, result));
9082
9480
  } catch {}
9083
- printResult(result, input.opts.output, input.command);
9481
+ printResult(result, input.opts.output, input.command, input.pretty);
9084
9482
  await exitAfterDrain(result.ok ? 0 : 1);
9085
9483
  }
9086
9484
  function targetFrom(opts, fallback = 'background') {
@@ -9225,7 +9623,11 @@ Cross-browser compatibility
9225
9623
  },
9226
9624
  args,
9227
9625
  opts,
9228
- preflight: (bridge, projectPath, browser)=>gestureRefusal(surface, bridge, projectPath, browser)
9626
+ preflight: (bridge, projectPath, browser)=>gestureRefusal(surface, bridge, projectPath, browser),
9627
+ pretty: {
9628
+ value: openResultLines,
9629
+ failure: (error)=>openSurfaceFailed(surface, error.message, error.engine, error.hint)
9630
+ }
9229
9631
  });
9230
9632
  });
9231
9633
  }
@@ -9264,7 +9666,7 @@ Cross-browser compatibility
9264
9666
  return values.length > 0 ? values : void 0;
9265
9667
  }
9266
9668
  function registerBuildCommand(program) {
9267
- program.command('build').arguments('[project-name]').usage('[path-to-remote-extension] [options]').description(commandDescriptions.build).option(`--browser <${BROWSER_TARGETS_HELP}>`, 'specify a browser/engine to run. Defaults to `chromium`. `safari` builds a Safari app via Xcode (macOS only)').option('--polyfill [boolean]', 'whether or not to apply the cross-browser polyfill. Defaults to `false`', parseOptionalBoolean).option('--no-polyfill', 'disable the cross-browser polyfill').option('--zip [boolean]', 'whether or not to compress the extension into a ZIP file. Defaults to `false`', parseOptionalBoolean).option('--zip-source [boolean]', 'whether or not to include the source files in the ZIP file. Defaults to `false`', parseOptionalBoolean).option('--zip-filename <string>', 'specify the name of the ZIP file. Defaults to the extension name and version').option('--silent [boolean]', 'suppress the build summary output. Defaults to `false`', parseOptionalBoolean).option('--addon-lint [boolean]', 'check Firefox builds against addons.mozilla.org rules with addons-linter when it is installed. Findings print as warnings. Defaults to `true`', parseOptionalBoolean).option('--no-addon-lint', 'skip the addons.mozilla.org lint of Firefox builds').option('--install [boolean]', '[internal] install project dependencies when missing', parseOptionalBoolean).option('--extensions <list>', 'comma-separated list of companion extensions or store URLs to load').option('--mode <development|production|none>', 'bundler mode override (also sets NODE_ENV). Defaults to `production`').option('--open [boolean]', 'open the built Safari app after packaging (safari targets only). Defaults to `false`', parseOptionalBoolean).option('--app-name <name>', 'override the Safari app name (safari targets only). Defaults to the manifest `name`').option('--bundle-id <reverse.dns>', 'set a user-owned Safari bundle identifier (safari targets only). Defaults to a generated dev.extensionjs.* id').option('--development-team <id>', 'sign the Safari app with an Apple Developer team id (safari targets only). Without it the build is ad-hoc signed, which Safari loads locally with no per-launch toggle. Use a team id for a stable identity you can distribute').option('--macos-only [boolean]', 'generate a macOS-only Safari Xcode project (safari targets only). Pass `false` for a universal macOS + iOS project. Defaults to `true`', parseOptionalBoolean).option('--force-regenerate', 'regenerate the Safari Xcode project even when up to date (safari targets only)').option('--output <pretty|json>', 'result format. Use json for a schema-1 envelope on stdout').addOption(new external_commander_namespaceObject.Option('--debug', 'print maintainer diagnostics alongside normal output')).addOption(new external_commander_namespaceObject.Option('--author, --author-mode', 'deprecated alias for --debug').hideHelp()).action(async (pathOrRemoteUrl, { browser: cliBrowser, ...buildOptions })=>{
9669
+ program.command('build').arguments('[project-name]').usage('[path-to-remote-extension] [options]').description(commandDescriptions.build).option(`--browser <${BROWSER_TARGETS_HELP}>`, 'specify a browser/engine to run. Defaults to `chromium`. `safari` builds a Safari app via Xcode (macOS only)').option('--polyfill [boolean]', 'whether or not to apply the cross-browser polyfill. Defaults to `false`', parseOptionalBoolean).option('--no-polyfill', 'disable the cross-browser polyfill').option('--zip [boolean]', 'whether or not to compress the extension into a ZIP file. Defaults to `false`', parseOptionalBoolean).option('--zip-source [boolean]', 'whether or not to include the source files in the ZIP file. Defaults to `false`', parseOptionalBoolean).option('--zip-filename <string>', 'specify the name of the ZIP file. Defaults to the extension name and version').option('--silent [boolean]', 'suppress the build summary output. Defaults to `false`', parseOptionalBoolean).option('--addon-lint [boolean]', 'check Firefox builds against addons.mozilla.org rules with addons-linter when it is installed. Findings print as warnings. Defaults to `true`', parseOptionalBoolean).option('--no-addon-lint', 'skip the addons.mozilla.org lint of Firefox builds').option('--install [boolean]', '[internal] install project dependencies when missing', parseOptionalBoolean).option('--extensions <list>', 'comma-separated list of companion extensions or store URLs to load').option('--mode <development|production|none>', 'bundler mode override (also sets NODE_ENV). Defaults to `production`').option('--open [boolean]', 'open the built Safari app after packaging (safari targets only). Defaults to `false`', parseOptionalBoolean).option('--app-name <name>', 'override the Safari app name (safari targets only). Defaults to the manifest `name`').option('--bundle-id <reverse.dns>', 'set a user-owned Safari bundle identifier (safari targets only). Defaults to a generated dev.extensionjs.* id shared by every build of the same source, so set your own before the first submission').option('--development-team <id>', 'sign the Safari app with an Apple Developer team id (safari targets only). Without it the build is ad-hoc signed, which Safari loads locally with no per-launch toggle. Use a team id for a stable identity you can distribute').option('--macos-only [boolean]', 'generate a macOS-only Safari Xcode project (safari targets only). Pass `false` for a universal macOS + iOS project. Defaults to `true`', parseOptionalBoolean).option('--force-regenerate', 'regenerate the Safari Xcode project even when up to date (safari targets only)').option('--output <pretty|json>', 'result format. Use json for a schema-1 envelope on stdout').addOption(new external_commander_namespaceObject.Option('--debug', 'print maintainer diagnostics alongside normal output')).addOption(new external_commander_namespaceObject.Option('--author, --author-mode', 'deprecated alias for --debug').hideHelp()).action(async (pathOrRemoteUrl, { browser: cliBrowser, ...buildOptions })=>{
9268
9670
  const browser = cliBrowser ?? await resolveConfigBrowser(pathOrRemoteUrl || process.cwd(), 'build') ?? 'chromium';
9269
9671
  if (buildOptions.debug || buildOptions.author || buildOptions.authorMode) {
9270
9672
  process.env.EXTENSION_DEBUG = '1';
@@ -9826,37 +10228,45 @@ Cross-browser compatibility
9826
10228
  async function waitForReadyContract(options) {
9827
10229
  const readyPath = sessionReadyPath(options.bridge, options.projectPath, options.browser);
9828
10230
  const start = Date.now();
10231
+ let lastReadFailure = '';
9829
10232
  while(Date.now() - start < options.timeoutMs){
9830
- if (external_node_fs_.existsSync(readyPath)) try {
9831
- const payload = JSON.parse(external_node_fs_.readFileSync(readyPath, 'utf8'));
9832
- const isLive = isProcessLikelyAlive(payload.pid);
9833
- const isFresh = isFreshContractPayload(payload);
9834
- if (payload.command !== options.command) {
9835
- await new Promise((resolve)=>setTimeout(resolve, 250));
9836
- continue;
10233
+ if (external_node_fs_.existsSync(readyPath)) {
10234
+ let payload = null;
10235
+ try {
10236
+ payload = JSON.parse(external_node_fs_.readFileSync(readyPath, 'utf8'));
10237
+ lastReadFailure = '';
10238
+ } catch (error) {
10239
+ payload = null;
10240
+ lastReadFailure = error instanceof Error ? error.message : String(error);
9837
10241
  }
9838
- if (!isLive) {
9839
- if ('start' !== options.command || !isFresh) {
10242
+ if (payload) {
10243
+ const isLive = isProcessLikelyAlive(payload.pid);
10244
+ const isFresh = isFreshContractPayload(payload);
10245
+ if (payload.command !== options.command) {
9840
10246
  await new Promise((resolve)=>setTimeout(resolve, 250));
9841
10247
  continue;
9842
10248
  }
9843
- }
9844
- if ('ready' === payload.status) return payload;
9845
- if ('error' === payload.status) {
9846
- if (!isLive && 'start' !== options.command) {
9847
- await new Promise((resolve)=>setTimeout(resolve, 250));
9848
- continue;
10249
+ if (!isLive) {
10250
+ if ('start' !== options.command || !isFresh) {
10251
+ await new Promise((resolve)=>setTimeout(resolve, 250));
10252
+ continue;
10253
+ }
10254
+ }
10255
+ if ('ready' === payload.status) return payload;
10256
+ if ('error' === payload.status) {
10257
+ if (!isLive && 'start' !== options.command) {
10258
+ await new Promise((resolve)=>setTimeout(resolve, 250));
10259
+ continue;
10260
+ }
10261
+ const detail = payload.message || payload.errors?.[0] || 'unknown error';
10262
+ throw new WaitModeError(String(detail), messaging.Lp.E_INTERNAL);
9849
10263
  }
9850
- const detail = payload.message || payload.errors?.[0] || 'unknown error';
9851
- throw new WaitModeError(String(detail), messaging.Lp.E_INTERNAL);
9852
10264
  }
9853
- } catch (error) {
9854
- if (error instanceof Error) throw error;
9855
- throw new Error(String(error));
9856
10265
  }
9857
10266
  await new Promise((resolve)=>setTimeout(resolve, 250));
9858
10267
  }
9859
- throw new WaitModeError(`Timed out waiting for ready contract at ${readyPath} (${options.timeoutMs} ms)`, messaging.Lp.E_READY_TIMEOUT);
10268
+ const timeoutDetail = lastReadFailure ? `. The last read of the file failed to parse as JSON: ${lastReadFailure}` : '';
10269
+ throw new WaitModeError(`Timed out waiting for ready contract at ${readyPath} (${options.timeoutMs} ms)${timeoutDetail}`, messaging.Lp.E_READY_TIMEOUT);
9860
10270
  }
9861
10271
  async function runWaitMode(options) {
9862
10272
  if (isHttpUrl(options.pathOrRemoteUrl)) throw new WaitModeError('--wait requires a local project path (remote URLs are not supported)', messaging.Lp.E_ARGS);
@@ -9900,7 +10310,7 @@ Cross-browser compatibility
9900
10310
  return Number.isFinite(parsed) && parsed >= 0 ? Math.floor(parsed) : 8080;
9901
10311
  }
9902
10312
  function registerDevCommand(program) {
9903
- program.command('dev').arguments('[project-path|remote-url]').usage('[project-path|remote-url] [options]').description(commandDescriptions.dev).addHelpText('after', "\nAdditional options:\n --no-browser stop the browser launch, the dev server still starts\n --no-reload emit a dev-mode dist without the content-script reload runtime, tabs need a manual reload to see changes\n --wait wait for ready contract and exit, pair with --output json for machine output\n" + BROWSER_LAUNCH_HELP_FOOTER).option('--profile <path-to-file | boolean>', 'what path to use for the browser profile. A boolean value of false sets the profile to the default user profile. Defaults to a fresh profile').option(`-b, --browser <${BROWSER_TARGETS_HELP}>`, 'specify a browser/engine to run. Defaults to `chromium`. `safari` builds and opens a Safari app via Xcode (macOS only)').option('--chromium-binary <path-to-binary>', 'specify a path to the Chromium binary. This option overrides the --browser setting. Defaults to the system default').addOption(geckoBinaryOption()).addOption(firefoxBinaryAliasOption()).option('--safari-binary <path-to-binary>', 'specify the Safari binary to open after packaging (safari targets only)').option('--app-name <name>', 'override the Safari app name (safari targets only). Defaults to the manifest `name`').option('--bundle-id <reverse.dns>', 'set a user-owned Safari bundle identifier (safari targets only). Defaults to a generated dev.extensionjs.* id').option('--development-team <id>', 'sign the Safari app with an Apple Developer team id (safari targets only). Without it the build is ad-hoc signed, which Safari loads locally with no per-launch toggle. Use a team id for a stable identity you can distribute').option('--macos-only [boolean]', 'generate a macOS-only Safari Xcode project (safari targets only). Pass `false` for a universal macOS + iOS project. Defaults to `true`', parseOptionalBoolean).option('--force-regenerate', 'regenerate the Safari Xcode project even when up to date (safari targets only)').option('--polyfill [boolean]', 'whether or not to apply the cross-browser polyfill. Defaults to `true`', parseOptionalBoolean).option('--no-polyfill', 'disable the cross-browser polyfill').option('--no-open', NO_OPEN_FLAG_DESCRIPTION).option('--starting-url <url>', 'specify the starting URL for the browser. Defaults to `undefined`').option('--port <port>', 'specify the port to use for the development server. Defaults to `8080`').option('--host <host>', 'specify the host to bind the dev server to. Use 0.0.0.0 for Docker/devcontainers. Defaults to `127.0.0.1`').option('--public-host <host>', 'connectable host the browser (HMR + reload bridge) dials when it differs from the bind host (e.g. a remote/devcontainer). Defaults to the bind host, or 127.0.0.1 when bound to 0.0.0.0').option('--log-context <list>', '[experimental] comma-separated contexts to include (background,content,page,sidebar,popup,options,devtools). Use `all` to include all contexts (default)').option('--logs <off|error|warn|info|debug|trace|all>', '[experimental] minimum centralized logger level to display in terminal (default: off)').option('--log-format <pretty|json|ndjson>', '[experimental] output format for logger events. Defaults to `pretty`').option('--no-log-timestamps', 'disable ISO timestamps in pretty output').option('--no-log-color', 'disable color in pretty output').option('--log-url <pattern>', '[experimental] only show logs where event.url matches this substring or regex (/re/i)').option('--log-tab <id>', 'only show logs for a specific tabId (number)').option('--extensions <list>', 'comma-separated list of companion extensions or store URLs to load').option('--install [boolean]', '[internal] install project dependencies when missing', parseOptionalBoolean).option('--wait [boolean]', 'wait for dist/extension-js/<browser>/ready.json and exit', parseOptionalBoolean).option('--wait-timeout <ms>', 'timeout in milliseconds when using --wait (default: 60000)').option('--output <pretty|json>', 'result format. Use json for a schema-1 envelope on stdout').addOption(new external_commander_namespaceObject.Option('--wait-format <pretty|json>').hideHelp()).addOption(new external_commander_namespaceObject.Option('--debug', 'print maintainer diagnostics alongside normal output')).addOption(new external_commander_namespaceObject.Option('--author, --author-mode', 'deprecated alias for --debug').hideHelp()).option('--allow-control', 'enable the agent-bridge control channel for bounded act (storage/reload/open): see `extension reload|storage|open`').option('--allow-eval', 'additionally enable `extension eval` (implies --allow-control, runs arbitrary code in a context, writes a 0600 session token)').option('--parent-pid <pid>', 'exit when the given process dies. For wrappers that spawn `extension dev`, so a leaked dev server can never outlive its owner').action(async (pathOrRemoteUrl, options, command)=>{
10313
+ program.command('dev').arguments('[project-path|remote-url]').usage('[project-path|remote-url] [options]').description(commandDescriptions.dev).addHelpText('after', "\nAdditional options:\n --no-browser stop the browser launch, the dev server still starts\n --no-reload emit a dev-mode dist without the content-script reload runtime, tabs need a manual reload to see changes\n --wait wait for ready contract and exit, pair with --output json for machine output\n" + BROWSER_LAUNCH_HELP_FOOTER).option('--profile <path-to-file | boolean>', 'what path to use for the browser profile. A boolean value of false sets the profile to the default user profile. Defaults to a fresh profile').option(`-b, --browser <${BROWSER_TARGETS_HELP}>`, 'specify a browser/engine to run. Defaults to `chromium`. `safari` builds and opens a Safari app via Xcode (macOS only)').option('--chromium-binary <path-to-binary>', 'specify a path to the Chromium binary. This option overrides the --browser setting. Defaults to the system default').addOption(geckoBinaryOption()).addOption(firefoxBinaryAliasOption()).option('--safari-binary <path-to-binary>', 'specify the Safari binary to open after packaging (safari targets only)').option('--app-name <name>', 'override the Safari app name (safari targets only). Defaults to the manifest `name`').option('--bundle-id <reverse.dns>', 'set a user-owned Safari bundle identifier (safari targets only). Defaults to a generated dev.extensionjs.* id shared by every build of the same source, so set your own before the first submission').option('--development-team <id>', 'sign the Safari app with an Apple Developer team id (safari targets only). Without it the build is ad-hoc signed, which Safari loads locally with no per-launch toggle. Use a team id for a stable identity you can distribute').option('--macos-only [boolean]', 'generate a macOS-only Safari Xcode project (safari targets only). Pass `false` for a universal macOS + iOS project. Defaults to `true`', parseOptionalBoolean).option('--force-regenerate', 'regenerate the Safari Xcode project even when up to date (safari targets only)').option('--polyfill [boolean]', 'whether or not to apply the cross-browser polyfill. Defaults to `true`', parseOptionalBoolean).option('--no-polyfill', 'disable the cross-browser polyfill').option('--no-open', NO_OPEN_FLAG_DESCRIPTION).option('--starting-url <url>', 'specify the starting URL for the browser. Defaults to `undefined`').option('--port <port>', 'specify the port to use for the development server. Defaults to `8080`').option('--host <host>', 'specify the host to bind the dev server to. Use 0.0.0.0 for Docker/devcontainers. Defaults to `127.0.0.1`').option('--public-host <host>', 'connectable host the browser (HMR + reload bridge) dials when it differs from the bind host (e.g. a remote/devcontainer). Defaults to the bind host, or 127.0.0.1 when bound to 0.0.0.0').option('--log-context <list>', '[experimental] comma-separated contexts to include (background,content,page,sidebar,popup,options,devtools). Use `all` to include all contexts (default)').option('--logs <off|error|warn|info|debug|trace|all>', '[experimental] minimum centralized logger level to display in terminal (default: off)').option('--log-format <pretty|json|ndjson>', '[experimental] output format for logger events. Defaults to `pretty`').option('--no-log-timestamps', 'disable ISO timestamps in pretty output').option('--no-log-color', 'disable color in pretty output').option('--log-url <pattern>', '[experimental] only show logs where event.url matches this substring or regex (/re/i)').option('--log-tab <id>', 'only show logs for a specific tabId (number)').option('--extensions <list>', 'comma-separated list of companion extensions or store URLs to load').option('--install [boolean]', '[internal] install project dependencies when missing', parseOptionalBoolean).option('--wait [boolean]', 'wait for dist/extension-js/<browser>/ready.json and exit', parseOptionalBoolean).option('--wait-timeout <ms>', 'timeout in milliseconds when using --wait (default: 60000)').option('--output <pretty|json>', 'result format. Use json for a schema-1 envelope on stdout').addOption(new external_commander_namespaceObject.Option('--wait-format <pretty|json>').hideHelp()).addOption(new external_commander_namespaceObject.Option('--debug', 'print maintainer diagnostics alongside normal output')).addOption(new external_commander_namespaceObject.Option('--author, --author-mode', 'deprecated alias for --debug').hideHelp()).option('--allow-control', 'enable the agent-bridge control channel for bounded act (storage/reload/open): see `extension reload|storage|open`').option('--allow-eval', 'additionally enable `extension eval` (implies --allow-control, runs arbitrary code in a context, writes a 0600 session token)').option('--parent-pid <pid>', 'exit when the given process dies. For wrappers that spawn `extension dev`, so a leaked dev server can never outlive its owner').action(async (pathOrRemoteUrl, options, command)=>{
9904
10314
  const { browser: cliBrowser, ...devOptions } = options;
9905
10315
  const browser = cliBrowser ?? await resolveConfigBrowser(pathOrRemoteUrl || process.cwd(), 'dev') ?? 'chromium';
9906
10316
  if (devOptions.debug || devOptions.author || devOptions.authorMode) {
@@ -10356,19 +10766,19 @@ Cross-browser compatibility
10356
10766
  });
10357
10767
  return results;
10358
10768
  }
10769
+ function checkGlyph(status) {
10770
+ if ('pass' === status) return external_pintor_default().green('✓');
10771
+ if ('fail' === status) return external_pintor_default().red('✗');
10772
+ if ('warn' === status) return external_pintor_default().brightYellow('!');
10773
+ return external_pintor_default().gray('–');
10774
+ }
10359
10775
  function printPretty(results, browser) {
10360
10776
  const passes = results.filter((r)=>'pass' === r.status).length;
10361
- const glyph = {
10362
- pass: '✓',
10363
- fail: '✗',
10364
- warn: '!',
10365
- skip: '–'
10366
- };
10367
- console.log(`doctor (${browser}), ${passes}/${results.length} checks passed`);
10777
+ console.log(doctorHeader(browser, passes, results.length));
10368
10778
  const width = Math.max(...results.map((r)=>r.check.length));
10369
- for (const r of results)console.log(` ${glyph[r.status]} ${r.check.padEnd(width)} ${r.detail}`);
10779
+ for (const r of results)console.log(` ${checkGlyph(r.status)} ${r.check.padEnd(width)} ${r.detail}`);
10370
10780
  const advisory = results.find((r)=>'fail' === r.status) ?? results.find((r)=>'warn' === r.status);
10371
- if (advisory?.remediation) console.log(`\n${advisory.check}: ${advisory.remediation}`);
10781
+ if (advisory?.remediation) console.log(doctorRemedy(advisory.check, advisory.remediation));
10372
10782
  }
10373
10783
  const CHECK_CODES = {
10374
10784
  'ready-contract': messaging.Lp.E_SESSION_NOT_FOUND,