extension 4.1.19 → 4.1.21

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 (30) hide show
  1. package/README.md +23 -20
  2. package/bin/extension.cjs +2 -0
  3. package/dist/84.cjs +2 -1
  4. package/dist/browsers.cjs +179 -95
  5. package/dist/cli.cjs +454 -238
  6. package/dist/extension/browsers/__spec__/safari-fake-tools.d.ts +27 -0
  7. package/dist/extension/browsers/browsers-lib/constants.d.ts +0 -2
  8. package/dist/extension/browsers/browsers-lib/messages.d.ts +2 -1
  9. package/dist/extension/browsers/browsers-lib/resolve-profile.d.ts +0 -35
  10. package/dist/extension/browsers/browsers-lib/write-json-atomic.d.ts +1 -0
  11. package/dist/extension/browsers/browsers-types.d.ts +0 -89
  12. package/dist/extension/browsers/index.d.ts +1 -29
  13. package/dist/extension/browsers/run-chromium/chromium-launch/extension-output-path.d.ts +0 -6
  14. package/dist/extension/browsers/run-chromium/chromium-launch/index.d.ts +0 -9
  15. package/dist/extension/browsers/run-chromium/chromium-types.d.ts +0 -11
  16. package/dist/extension/browsers/run-safari/safari-launch/index.d.ts +2 -7
  17. package/dist/extension/browsers/run-safari/safari-launch/tools.d.ts +17 -0
  18. package/dist/extension/browsers/run-safari/safari-packager.d.ts +2 -16
  19. package/dist/extension/browsers/run-safari/safari-types.d.ts +2 -1
  20. package/dist/extension/commands/act.d.ts +0 -6
  21. package/dist/extension/commands/doctor.d.ts +0 -7
  22. package/dist/extension/commands/publish.d.ts +0 -6
  23. package/dist/extension/config-types.d.ts +5 -2
  24. package/dist/extension/helpers/cli-explicit.d.ts +0 -15
  25. package/dist/extension/helpers/messages.d.ts +7 -0
  26. package/dist/extension/helpers/node-version-guard.d.ts +7 -2
  27. package/dist/extension/helpers/template-catalog.d.ts +1 -1
  28. package/dist/extension/helpers/template-corpus.generated.d.ts +1 -1
  29. package/dist/extension/helpers/vendors.d.ts +1 -0
  30. package/package.json +4 -6
package/dist/cli.cjs CHANGED
@@ -741,16 +741,19 @@ var __webpack_modules__ = {
741
741
  const tail = outputTail.trim().length ? `\n${pintor__rspack_import_8_default().gray('── last output ──')}\n${outputTail}` : `\n${pintor__rspack_import_8_default().gray('(no output captured)')}`;
742
742
  return `${getLoggingPrefix('error')} Safari packaging tool ${pintor__rspack_import_8_default().underline(tool)} failed (${pintor__rspack_import_8_default().red(code)}).${tail}`;
743
743
  }
744
+ const SAFARI_KEPT_ON_PURPOSE = {
745
+ world: 'Safari has honored the MAIN world since Safari 18, so the key stays'
746
+ };
744
747
  function safariConverterWarnings(warnings) {
745
- 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');
748
+ const kept = warnings.map((line)=>line.trim()).filter((line)=>line in SAFARI_KEPT_ON_PURPOSE);
749
+ const keptLines = kept.map((line)=>` ${pintor__rspack_import_8_default().yellow(line)} ${pintor__rspack_import_8_default().gray(SAFARI_KEPT_ON_PURPOSE[line])}`).join('\n');
750
+ 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}` : '');
746
751
  }
747
752
  function safariDefaultBundleIdNote(bundleId) {
748
- 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.`;
753
+ 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).`;
749
754
  }
750
- function safariOpenHint(appPath, appName, signed) {
751
- const launch = `${getLoggingPrefix('info')} Launch it once to register with Safari: ${pintor__rspack_import_8_default().blue('open')} ${pintor__rspack_import_8_default().underline(`"${appPath}"`)}\n`;
752
- if (signed) return launch + `Then turn on ${pintor__rspack_import_8_default().yellow(appName)} in Safari ▸ Settings ▸ Extensions.`;
753
- return launch + `Then enable ${pintor__rspack_import_8_default().yellow(appName)} via Safari ▸ Develop ▸ ` + `${pintor__rspack_import_8_default().yellow('Allow Unsigned Extensions')} and Safari ▸ Settings ▸ Extensions.`;
755
+ function safariOpenHint(appPath, appName) {
756
+ 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.`;
754
757
  }
755
758
  function safariDryRunNotBuilding() {
756
759
  return `${getLoggingPrefix('info')} Dry run: skip the Safari app build.`;
@@ -762,17 +765,21 @@ var __webpack_modules__ = {
762
765
  return `${getLoggingPrefix('info')} xcodebuild: ${pintor__rspack_import_8_default().gray(cmd)}.`;
763
766
  }
764
767
  function safariNextSteps(appName, signed) {
765
- if (signed) return `${getLoggingPrefix('info')} One-time setup to load ${pintor__rspack_import_8_default().yellow(appName)} in Safari:\n ${pintor__rspack_import_8_default().gray('1.')} Safari ▸ Settings ▸ Extensions ▸ turn on ${pintor__rspack_import_8_default().yellow(appName)}\n ${pintor__rspack_import_8_default().gray('→')} Signed with your team id, so this survives a restart.`;
766
- return `${getLoggingPrefix('info')} One-time setup to load ${pintor__rspack_import_8_default().yellow(appName)} in Safari:\n ${pintor__rspack_import_8_default().gray('1.')} Safari Settings Advanced check ${pintor__rspack_import_8_default().yellow('“Show features for web developers”')}\n ${pintor__rspack_import_8_default().gray('2.')} Safari ▸ Develop ▸ ${pintor__rspack_import_8_default().yellow('Allow Unsigned Extensions')} ${pintor__rspack_import_8_default().gray('(resets each launch)')}\n ${pintor__rspack_import_8_default().gray('3.')} Safari ▸ Settings ▸ Extensions ▸ turn on ${pintor__rspack_import_8_default().yellow(appName)}\n ${pintor__rspack_import_8_default().gray('→')} The app window that just opened can also take you there.`;
768
+ const steps = `${getLoggingPrefix('info')} One-time setup to load ${pintor__rspack_import_8_default().yellow(appName)} in Safari:\n ${pintor__rspack_import_8_default().gray('1.')} Safari ▸ Settings ▸ Extensions ▸ turn on ${pintor__rspack_import_8_default().yellow(appName)}\n`;
769
+ if (signed) return steps + ` ${pintor__rspack_import_8_default().gray('')} Signed with your team id, so the identity is stable for distribution.`;
770
+ return steps + ` ${pintor__rspack_import_8_default().gray('→')} Ad-hoc signed, so it stays enabled across restarts.`;
767
771
  }
768
772
  function safariRegistered(appName) {
769
773
  return `${getLoggingPrefix('success')} Safari recognizes ${pintor__rspack_import_8_default().yellow(appName)}, finish enabling it with the steps above.`;
770
774
  }
775
+ function safariPidUnresolved(appName) {
776
+ 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')}.`;
777
+ }
771
778
  function safariNotYetRegistered(appName) {
772
779
  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.`;
773
780
  }
774
781
  function safariRebuilt(appName) {
775
- return `${getLoggingPrefix('success')} Rebuilt ${pintor__rspack_import_8_default().yellow(appName)}, reload the page (or toggle the extension) in Safari to see changes.`;
782
+ return `${getLoggingPrefix('success')} Rebuilt ${pintor__rspack_import_8_default().yellow(appName)}.`;
776
783
  }
777
784
  function safariProjectStale() {
778
785
  return `${getLoggingPrefix('info')} Regenerating the Xcode project, manifest.json or identity options changed since it was generated.`;
@@ -1029,6 +1036,7 @@ var __webpack_modules__ = {
1029
1036
  jR: ()=>safariBuilding,
1030
1037
  jc: ()=>safariProjectStale,
1031
1038
  jk: ()=>requireGeckoBinaryForGeckoBased,
1039
+ jn: ()=>safariPidUnresolved,
1032
1040
  kK: ()=>preferringSystemBrowserOverSnapshot,
1033
1041
  l9: ()=>parsingPacketError,
1034
1042
  lO: ()=>firefoxDryRunArgs,
@@ -1401,6 +1409,7 @@ var __webpack_modules__ = {
1401
1409
  "./browsers/browsers-lib/ready-stamp.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) {
1402
1410
  var node_fs__rspack_import_0 = __webpack_require__("node:fs");
1403
1411
  var node_path__rspack_import_1 = __webpack_require__("node:path");
1412
+ var _write_json_atomic__rspack_import_2 = __webpack_require__("./browsers/browsers-lib/write-json-atomic.ts");
1404
1413
  function readyPathFor(extensionOutputPath) {
1405
1414
  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');
1406
1415
  }
@@ -1412,7 +1421,7 @@ var __webpack_modules__ = {
1412
1421
  const ready = JSON.parse(node_fs__rspack_import_0.readFileSync(readyPath, 'utf-8'));
1413
1422
  if (ready.rdpPort === rdpPort) return;
1414
1423
  ready.rdpPort = rdpPort;
1415
- node_fs__rspack_import_0.writeFileSync(readyPath, JSON.stringify(ready, null, 2));
1424
+ (0, _write_json_atomic__rspack_import_2.Z)(readyPath, ready);
1416
1425
  } catch {}
1417
1426
  }
1418
1427
  function stampReadyBrowserLaunch(extensionOutputPath, details) {
@@ -1430,7 +1439,7 @@ var __webpack_modules__ = {
1430
1439
  if (binary) ready.binary = binary;
1431
1440
  const provenance = String(details?.binaryProvenance || '').trim();
1432
1441
  if (provenance) ready.binaryProvenance = provenance;
1433
- node_fs__rspack_import_0.writeFileSync(readyPath, JSON.stringify(ready, null, 2));
1442
+ (0, _write_json_atomic__rspack_import_2.Z)(readyPath, ready);
1434
1443
  } catch {}
1435
1444
  }
1436
1445
  function stampReadyExtensionId(extensionOutputPath, extensionId) {
@@ -1442,7 +1451,7 @@ var __webpack_modules__ = {
1442
1451
  const ready = JSON.parse(node_fs__rspack_import_0.readFileSync(readyPath, 'utf-8'));
1443
1452
  if (ready.extensionId === id) return;
1444
1453
  ready.extensionId = id;
1445
- node_fs__rspack_import_0.writeFileSync(readyPath, JSON.stringify(ready, null, 2));
1454
+ (0, _write_json_atomic__rspack_import_2.Z)(readyPath, ready);
1446
1455
  } catch {}
1447
1456
  }
1448
1457
  function stampReadyExtensionLoadRefused(extensionOutputPath, reason) {
@@ -1457,7 +1466,7 @@ var __webpack_modules__ = {
1457
1466
  ready.message = `${browserLabel.charAt(0).toUpperCase() + browserLabel.slice(1)} refused to load the extension at ${extensionOutputPath}${reason ? `: ${reason}` : ''}`;
1458
1467
  ready.extensionLoadRefusedAt = new Date().toISOString();
1459
1468
  if (reason) ready.extensionLoadRefusedReason = reason;
1460
- node_fs__rspack_import_0.writeFileSync(readyPath, JSON.stringify(ready, null, 2));
1469
+ (0, _write_json_atomic__rspack_import_2.Z)(readyPath, ready);
1461
1470
  } catch {}
1462
1471
  }
1463
1472
  function stampReadyProfileLocked(extensionOutputPath, details) {
@@ -1471,7 +1480,7 @@ var __webpack_modules__ = {
1471
1480
  ready.message = String(details?.message || '').trim() || 'the browser profile is already in use by another session';
1472
1481
  ready.profileLockedAt = new Date().toISOString();
1473
1482
  if (details?.owner) ready.profileLockOwner = details.owner;
1474
- node_fs__rspack_import_0.writeFileSync(readyPath, JSON.stringify(ready, null, 2));
1483
+ (0, _write_json_atomic__rspack_import_2.Z)(readyPath, ready);
1475
1484
  } catch {}
1476
1485
  }
1477
1486
  function stampReadyBrowserExited(extensionOutputPath, code) {
@@ -1487,7 +1496,7 @@ var __webpack_modules__ = {
1487
1496
  ready.code = 'browser_exited';
1488
1497
  ready.message = `the ${ready.browser || 'browser'} process exited (code ${code ?? 'unknown'}); nothing is running`;
1489
1498
  }
1490
- node_fs__rspack_import_0.writeFileSync(readyPath, JSON.stringify(ready, null, 2));
1499
+ (0, _write_json_atomic__rspack_import_2.Z)(readyPath, ready);
1491
1500
  } catch {}
1492
1501
  }
1493
1502
  __webpack_require__.d(__webpack_exports__, {
@@ -1951,6 +1960,27 @@ var __webpack_modules__ = {
1951
1960
  yO: ()=>probeChromiumBinaryVersion
1952
1961
  });
1953
1962
  },
1963
+ "./browsers/browsers-lib/write-json-atomic.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) {
1964
+ var node_fs__rspack_import_0 = __webpack_require__("node:fs");
1965
+ function writeJsonAtomic(filePath, value) {
1966
+ const tmpPath = `${filePath}.tmp-${process.pid}`;
1967
+ try {
1968
+ node_fs__rspack_import_0.writeFileSync(tmpPath, JSON.stringify(value, null, 2), 'utf-8');
1969
+ node_fs__rspack_import_0.renameSync(tmpPath, filePath);
1970
+ return true;
1971
+ } catch {
1972
+ try {
1973
+ node_fs__rspack_import_0.rmSync(tmpPath, {
1974
+ force: true
1975
+ });
1976
+ } catch {}
1977
+ return false;
1978
+ }
1979
+ }
1980
+ __webpack_require__.d(__webpack_exports__, {
1981
+ Z: ()=>writeJsonAtomic
1982
+ });
1983
+ },
1954
1984
  "./browsers/browsers-lib/wsl-support.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) {
1955
1985
  __webpack_require__.d(__webpack_exports__, {
1956
1986
  qc: ()=>external_wsl_support_namespaceObject.hasGuiDisplay,
@@ -5803,7 +5833,6 @@ var __webpack_modules__ = {
5803
5833
  QW: ()=>safariBuildPreflight,
5804
5834
  iF: ()=>safariPreflightError
5805
5835
  });
5806
- var external_node_child_process_ = __webpack_require__("node:child_process");
5807
5836
  var external_node_fs_ = __webpack_require__("node:fs");
5808
5837
  var messaging = __webpack_require__("./helpers/messaging.ts");
5809
5838
  var banner = __webpack_require__("./browsers/browsers-lib/banner.ts");
@@ -5817,6 +5846,7 @@ var __webpack_modules__ = {
5817
5846
  (0, messaging._w)(messages.Aj(xcodebuildCmd));
5818
5847
  }
5819
5848
  var safari_config = __webpack_require__("./browsers/run-safari/safari-launch/safari-config.ts");
5849
+ var external_node_child_process_ = __webpack_require__("node:child_process");
5820
5850
  function isMacOS() {
5821
5851
  return 'darwin' === process.platform;
5822
5852
  }
@@ -5874,20 +5904,6 @@ var __webpack_modules__ = {
5874
5904
  ok: Boolean(converter && xcodebuild)
5875
5905
  };
5876
5906
  }
5877
- function fallbackLogger() {
5878
- return {
5879
- info: (...a)=>(0, messaging._w)(...a),
5880
- warn: (...a)=>(0, messaging.mR)(...a),
5881
- error: (...a)=>(0, messaging.Gg)(...a),
5882
- debug: (...a)=>console?.debug?.(...a)
5883
- };
5884
- }
5885
- function isTestEnv() {
5886
- return Boolean(process.env.VITEST || process.env.VITEST_WORKER_ID);
5887
- }
5888
- function delay(ms) {
5889
- return new Promise((resolve)=>setTimeout(resolve, ms));
5890
- }
5891
5907
  const TOOL_TAIL_LINES = 50;
5892
5908
  const TOOL_TAIL_BYTES = 8192;
5893
5909
  function toolOutputTail(output) {
@@ -5926,18 +5942,85 @@ var __webpack_modules__ = {
5926
5942
  }));
5927
5943
  });
5928
5944
  }
5945
+ async function resolvePidForBundle(bundleId) {
5946
+ for(let attempt = 0; attempt < 10; attempt += 1){
5947
+ const pid = await new Promise((resolve)=>{
5948
+ const child = (0, external_node_child_process_.spawn)("osascript", [
5949
+ '-e',
5950
+ `tell application "System Events" to get unix id of first process whose bundle identifier is "${bundleId}"`
5951
+ ]);
5952
+ let out = '';
5953
+ child.stdout.on('data', (d)=>out += String(d));
5954
+ child.on('error', ()=>resolve(null));
5955
+ child.on('close', ()=>{
5956
+ const n = Number(String(out).trim());
5957
+ resolve(Number.isFinite(n) && n > 0 ? n : null);
5958
+ });
5959
+ });
5960
+ if (pid) return pid;
5961
+ await new Promise((r)=>setTimeout(r, 500));
5962
+ }
5963
+ return null;
5964
+ }
5965
+ function createSafariTools() {
5966
+ return {
5967
+ detectToolchain: ()=>detectSafariToolchain(),
5968
+ runConverter: (args)=>runTool('xcrun', args),
5969
+ runXcodebuild: (args)=>runTool('xcodebuild', args),
5970
+ openApp: (target)=>runTool('open', [
5971
+ target
5972
+ ]),
5973
+ openSafari: (binary)=>runTool('open', [
5974
+ '-a',
5975
+ binary
5976
+ ]),
5977
+ resolvePid: (bundleId)=>resolvePidForBundle(bundleId),
5978
+ pluginkitList: async ()=>{
5979
+ const { ok, output } = await runTool('pluginkit', [
5980
+ '-m'
5981
+ ], {
5982
+ quiet: true
5983
+ });
5984
+ return ok ? output : '';
5985
+ }
5986
+ };
5987
+ }
5988
+ function fallbackLogger() {
5989
+ return {
5990
+ info: (...a)=>(0, messaging._w)(...a),
5991
+ warn: (...a)=>(0, messaging.mR)(...a),
5992
+ error: (...a)=>(0, messaging.Gg)(...a),
5993
+ debug: (...a)=>console?.debug?.(...a)
5994
+ };
5995
+ }
5996
+ function delay(ms) {
5997
+ return new Promise((resolve)=>setTimeout(resolve, ms));
5998
+ }
5929
5999
  function converterWarnings(output) {
5930
- return output.split(/\r?\n/).filter((line)=>/warning/i.test(line)).map((line)=>line.trim()).filter((line)=>line.length > 0);
6000
+ const lines = output.split(/\r?\n/);
6001
+ const collected = [];
6002
+ let inWarning = false;
6003
+ for (const line of lines){
6004
+ const trimmed = line.trim();
6005
+ if (/warning/i.test(line)) {
6006
+ inWarning = true;
6007
+ if (trimmed.length > 0) collected.push(trimmed);
6008
+ continue;
6009
+ }
6010
+ const isContinuation = inWarning && /^\s/.test(line) && trimmed.length > 0;
6011
+ if (isContinuation) {
6012
+ collected.push(trimmed);
6013
+ continue;
6014
+ }
6015
+ inWarning = false;
6016
+ }
6017
+ return collected;
5931
6018
  }
5932
- async function confirmRegisteredWithSafari(bundleIdentifier) {
6019
+ async function confirmRegisteredWithSafari(tools, bundleIdentifier) {
5933
6020
  const needle = `${bundleIdentifier}.Extension`;
5934
6021
  for(let attempt = 0; attempt < 6; attempt += 1){
5935
- const { ok, output } = await runTool('pluginkit', [
5936
- '-m'
5937
- ], {
5938
- quiet: true
5939
- });
5940
- if (ok && output.includes(needle)) return true;
6022
+ const listing = await tools.pluginkitList();
6023
+ if (listing.includes(needle)) return true;
5941
6024
  await delay(800);
5942
6025
  }
5943
6026
  return false;
@@ -5952,6 +6035,10 @@ var __webpack_modules__ = {
5952
6035
  macOsOnly: config.macOsOnly
5953
6036
  };
5954
6037
  }
6038
+ function completePackage(config, logger, mode) {
6039
+ if ('full' === mode && config.bundleIdDerived) logger.info?.(messages.$q(config.bundleIdentifier));
6040
+ return describePackage(config);
6041
+ }
5955
6042
  function safariPreflightError() {
5956
6043
  const tc = detectSafariToolchain();
5957
6044
  if (!tc.platformOk) return messages.G3(process.platform);
@@ -5975,26 +6062,6 @@ var __webpack_modules__ = {
5975
6062
  severity: 'ok'
5976
6063
  };
5977
6064
  }
5978
- async function resolvePidForBundle(bundleId) {
5979
- for(let attempt = 0; attempt < 10; attempt += 1){
5980
- const pid = await new Promise((resolve)=>{
5981
- const child = (0, external_node_child_process_.spawn)("osascript", [
5982
- '-e',
5983
- `tell application "System Events" to get unix id of first process whose bundle identifier is "${bundleId}"`
5984
- ]);
5985
- let out = '';
5986
- child.stdout.on('data', (d)=>out += String(d));
5987
- child.on('error', ()=>resolve(null));
5988
- child.on('close', ()=>{
5989
- const n = Number(String(out).trim());
5990
- resolve(Number.isFinite(n) && n > 0 ? n : null);
5991
- });
5992
- });
5993
- if (pid) return pid;
5994
- await new Promise((r)=>setTimeout(r, 500));
5995
- }
5996
- return null;
5997
- }
5998
6065
  async function announceSafariDevSession(host, config, appPath) {
5999
6066
  try {
6000
6067
  await (0, banner.ai)({
@@ -6010,16 +6077,16 @@ var __webpack_modules__ = {
6010
6077
  } catch {}
6011
6078
  }
6012
6079
  async function runSafariPipeline(compilation, host, logger, mode) {
6080
+ const tools = host.tools || createSafariTools();
6013
6081
  const config = (0, safari_config.F7)(compilation, host);
6014
6082
  const converterArgs = (0, safari_config.w7)(config);
6015
6083
  const xcodebuildArgs = (0, safari_config.vx)(config);
6016
6084
  const isSigned = Boolean(config.developmentTeam);
6017
- if ('full' === mode && config.bundleIdDerived) logger.warn?.(messages.$q(config.bundleIdentifier));
6018
- if (host.dryRun || isTestEnv()) {
6085
+ if (host.dryRun) {
6019
6086
  logSafariDryRun(`xcrun ${converterArgs.join(' ')}`, `xcodebuild ${xcodebuildArgs.join(' ')}`);
6020
- return describePackage(config);
6087
+ return completePackage(config, logger, mode);
6021
6088
  }
6022
- const toolchain = detectSafariToolchain();
6089
+ const toolchain = tools.detectToolchain();
6023
6090
  if (!toolchain.platformOk) {
6024
6091
  logger.warn?.(messages.G3(process.platform));
6025
6092
  return describePackage(config);
@@ -6043,7 +6110,7 @@ var __webpack_modules__ = {
6043
6110
  }
6044
6111
  const { saved, restore } = (0, safari_config._D)(config);
6045
6112
  logger.info?.(messages.uK(config.extensionDir));
6046
- const converted = await runTool('xcrun', converterArgs);
6113
+ const converted = await tools.runConverter(converterArgs);
6047
6114
  if (!converted.ok) {
6048
6115
  const tail = toolOutputTail(converted.output);
6049
6116
  logger.error?.(messages.A('safari-web-extension-converter', converted.code, tail));
@@ -6058,9 +6125,9 @@ var __webpack_modules__ = {
6058
6125
  if (preservedKeys.length > 0) logger.info?.(messages.oB(preservedKeys));
6059
6126
  (0, safari_config.x7)(config);
6060
6127
  logger.info?.(messages.l_(config.projectLocation));
6061
- } else logger.info?.(messages.oM());
6128
+ } else if ('full' === mode) logger.info?.(messages.oM());
6062
6129
  if ('full' === mode) logger.info?.(messages.jR((0, safari_config.aD)(config)));
6063
- const built = await runTool('xcodebuild', xcodebuildArgs);
6130
+ const built = await tools.runXcodebuild(xcodebuildArgs);
6064
6131
  if (!built.ok) {
6065
6132
  const tail = toolOutputTail(built.output);
6066
6133
  logger.error?.(messages.A('xcodebuild', built.code, tail));
@@ -6073,32 +6140,28 @@ var __webpack_modules__ = {
6073
6140
  }
6074
6141
  logger.info?.(messages.Rl(appPath));
6075
6142
  if (!config.open) {
6076
- logger.info?.(messages.aO(appPath, config.appName, isSigned));
6143
+ logger.info?.(messages.aO(appPath, config.appName));
6077
6144
  if (host.announceDevReady) await announceSafariDevSession(host, config, appPath);
6078
- return describePackage(config);
6145
+ return completePackage(config, logger, mode);
6079
6146
  }
6080
6147
  const target = external_node_fs_.existsSync(appPath) ? appPath : (0, safari_config.Gi)(config);
6081
6148
  logger.info?.(messages.fw(target));
6082
- await runTool('open', [
6083
- target
6084
- ]);
6085
- if (config.safariBinary) await runTool('open', [
6086
- '-a',
6087
- config.safariBinary
6088
- ]);
6149
+ await tools.openApp(target);
6150
+ if (config.safariBinary) await tools.openSafari(config.safariBinary);
6089
6151
  const raisedBundleId = config.safariBinary ? 'com.apple.Safari' : config.bundleIdentifier;
6090
- const browserPid = await resolvePidForBundle(raisedBundleId);
6091
- if (browserPid) (0, ready_stamp.M)(config.extensionDir, {
6092
- browserPid,
6152
+ const browserPid = await tools.resolvePid(raisedBundleId);
6153
+ (0, ready_stamp.M)(config.extensionDir, {
6154
+ browserPid: browserPid || void 0,
6093
6155
  binary: config.safariBinary || appPath,
6094
6156
  binaryProvenance: config.safariBinary ? 'pinned' : 'system',
6095
6157
  extensionId: `${config.bundleIdentifier}.Extension`
6096
6158
  });
6159
+ if (!browserPid) logger.warn?.(messages.jn(config.appName));
6097
6160
  logger.info?.(messages.fO(config.appName, isSigned));
6098
- if (await confirmRegisteredWithSafari(config.bundleIdentifier)) logger.info?.(messages.fd(config.appName));
6161
+ if (await confirmRegisteredWithSafari(tools, config.bundleIdentifier)) logger.info?.(messages.fd(config.appName));
6099
6162
  else logger.info?.(messages.qY(config.appName));
6100
6163
  if (host.announceDevReady) await announceSafariDevSession(host, config, appPath);
6101
- return describePackage(config);
6164
+ return completePackage(config, logger, mode);
6102
6165
  }
6103
6166
  async function packageSafariExtension(host, outputPath, logger, mode = 'full') {
6104
6167
  const compilation = {
@@ -6218,36 +6281,56 @@ var __webpack_modules__ = {
6218
6281
  function manifestFingerprintPath(config) {
6219
6282
  return node_path__rspack_import_1.join(config.projectLocation, '.manifest-fingerprint');
6220
6283
  }
6221
- function stableStringify(obj) {
6222
- if (null == obj) return JSON.stringify(obj);
6223
- if ('object' != typeof obj) return JSON.stringify(obj);
6224
- if (Array.isArray(obj)) return `[${obj.map(stableStringify).join(',')}]`;
6225
- const keys = Object.keys(obj).sort();
6226
- return `{${keys.map((k)=>`${JSON.stringify(k)}:${stableStringify(obj[k])}`).join(',')}}`;
6227
- }
6228
- function readManifestRaw(extensionDir) {
6284
+ const HOT_UPDATE_DIR = 'hot';
6285
+ function topLevelEntries(extensionDir) {
6229
6286
  try {
6230
- return node_fs__rspack_import_0.readFileSync(node_path__rspack_import_1.join(extensionDir, 'manifest.json'), 'utf8');
6287
+ return node_fs__rspack_import_0.readdirSync(extensionDir).filter((entry)=>entry !== HOT_UPDATE_DIR).sort();
6231
6288
  } catch {
6232
- return '';
6289
+ return [];
6233
6290
  }
6234
6291
  }
6235
- function normalizeManifest(raw) {
6292
+ function iconsFingerprint(extensionDir) {
6236
6293
  try {
6237
- return stableStringify(JSON.parse(raw));
6294
+ const raw = node_fs__rspack_import_0.readFileSync(node_path__rspack_import_1.join(extensionDir, 'manifest.json'), 'utf8');
6295
+ const icons = JSON.parse(raw).icons;
6296
+ if (!icons || 'object' != typeof icons) return '';
6297
+ return Object.keys(icons).sort().map((size)=>`${size}:${String(icons[size])}`).join(',');
6238
6298
  } catch {
6239
- return raw;
6299
+ return '';
6300
+ }
6301
+ }
6302
+ function asRecord(value) {
6303
+ return value && 'object' == typeof value && !Array.isArray(value) ? value : {};
6304
+ }
6305
+ function judgedManifestSurface(extensionDir) {
6306
+ const manifest = readManifest(extensionDir);
6307
+ const parts = [
6308
+ Object.keys(manifest).sort().join(',')
6309
+ ];
6310
+ for (const listName of [
6311
+ 'permissions',
6312
+ 'optional_permissions'
6313
+ ]){
6314
+ const list = manifest[listName];
6315
+ const names = Array.isArray(list) ? list.filter((entry)=>'string' == typeof entry).sort() : [];
6316
+ parts.push(`${listName}:${names.join(',')}`);
6240
6317
  }
6318
+ const contentScripts = manifest.content_scripts;
6319
+ if (Array.isArray(contentScripts)) parts.push(contentScripts.map((entry)=>Object.keys(asRecord(entry)).sort().join('+')).join(';'));
6320
+ parts.push(Object.keys(asRecord(manifest.options_ui)).sort().join(','));
6321
+ return parts.join('|');
6241
6322
  }
6242
6323
  function composeProjectFingerprint(config) {
6243
6324
  return JSON.stringify({
6244
- v: 2,
6325
+ v: 4,
6245
6326
  identity: {
6246
6327
  appName: config.appName,
6247
6328
  bundleId: config.bundleIdentifier,
6248
6329
  macOsOnly: config.macOsOnly
6249
6330
  },
6250
- manifest: normalizeManifest(readManifestRaw(config.extensionDir))
6331
+ entries: topLevelEntries(config.extensionDir),
6332
+ icons: iconsFingerprint(config.extensionDir),
6333
+ judged: judgedManifestSurface(config.extensionDir)
6251
6334
  });
6252
6335
  }
6253
6336
  function saveManifestFingerprint(config) {
@@ -6330,7 +6413,7 @@ var __webpack_modules__ = {
6330
6413
  return Object.fromEntries(Object.entries(input).filter(([, value])=>void 0 !== value));
6331
6414
  }
6332
6415
  function createSafariPackager(options = {}) {
6333
- const { browser = 'safari', noOpen = true, dryRun = false, logger, ...identity } = options;
6416
+ const { browser = 'safari', noOpen = true, dryRun = false, logger, tools, ...identity } = options;
6334
6417
  return async (distPath, mode = 'full', overrides)=>await (0, _safari_launch__rspack_import_0.Ah)({
6335
6418
  extension: [
6336
6419
  distPath
@@ -6338,6 +6421,7 @@ var __webpack_modules__ = {
6338
6421
  browser,
6339
6422
  noOpen,
6340
6423
  dryRun,
6424
+ tools,
6341
6425
  ...withoutUndefined(identity),
6342
6426
  ...withoutUndefined(overrides)
6343
6427
  }, distPath, logger, mode);
@@ -6663,23 +6747,62 @@ var __webpack_modules__ = {
6663
6747
  "./index.ts" (__unused_rspack_module, __unused_rspack___webpack_exports__, __webpack_require__) {
6664
6748
  const MIN_NODE_MAJOR = 22;
6665
6749
  const MIN_NODE_MINOR = 12;
6750
+ const MIN_DENO_MAJOR = 2;
6751
+ const MIN_DENO_MINOR = 5;
6752
+ const MIN_BUN_MAJOR = 1;
6753
+ const MIN_BUN_MINOR = 2;
6754
+ const BLOCKED_DENO_VERSIONS = [
6755
+ '2.8.0'
6756
+ ];
6757
+ function meetsFloor(version, major, minor) {
6758
+ const [actualMajor, actualMinor] = version.split('.').map((part)=>parseInt(part, 10));
6759
+ if (!Number.isFinite(actualMajor)) return true;
6760
+ if (actualMajor !== major) return actualMajor > major;
6761
+ return (Number.isFinite(actualMinor) ? actualMinor : 0) >= minor;
6762
+ }
6666
6763
  function isSupportedNodeVersion(version) {
6667
- const [major, minor] = version.split('.').map((part)=>parseInt(part, 10));
6668
- if (!Number.isFinite(major)) return true;
6669
- if (major !== MIN_NODE_MAJOR) return major > MIN_NODE_MAJOR;
6670
- return (Number.isFinite(minor) ? minor : 0) >= MIN_NODE_MINOR;
6764
+ return meetsFloor(version, MIN_NODE_MAJOR, MIN_NODE_MINOR);
6765
+ }
6766
+ function isSupportedDenoVersion(version) {
6767
+ if (BLOCKED_DENO_VERSIONS.includes(version)) return false;
6768
+ return meetsFloor(version, MIN_DENO_MAJOR, MIN_DENO_MINOR);
6769
+ }
6770
+ function isSupportedBunVersion(version) {
6771
+ return meetsFloor(version, MIN_BUN_MAJOR, MIN_BUN_MINOR);
6671
6772
  }
6672
6773
  function detectBunVersion(versions = process.versions) {
6673
6774
  const bunVersion = versions.bun;
6674
6775
  return 'string' == typeof bunVersion && bunVersion.length > 0 ? bunVersion : void 0;
6675
6776
  }
6676
- function unsupportedNodeVersionMessage(version, bunVersion) {
6677
- 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.`;
6777
+ function detectDenoVersion(versions = process.versions) {
6778
+ const denoVersion = versions.deno;
6779
+ return 'string' == typeof denoVersion && denoVersion.length > 0 ? denoVersion : void 0;
6780
+ }
6781
+ function unsupportedDenoVersionMessage(version) {
6782
+ 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.`;
6783
+ 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.`;
6784
+ }
6785
+ function unsupportedBunVersionMessage(version) {
6786
+ 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.`;
6787
+ }
6788
+ function unsupportedNodeVersionMessage(version) {
6678
6789
  return `[Extension.js] Requires Node.js >= ${MIN_NODE_MAJOR}.${MIN_NODE_MINOR} (you are on ${version}). Upgrade Node.js to run the extension CLI.`;
6679
6790
  }
6680
- function enforceSupportedNodeVersion(version = process.versions.node, bunVersion = detectBunVersion()) {
6681
- if (!bunVersion && isSupportedNodeVersion(version)) return;
6682
- console.error(unsupportedNodeVersionMessage(version, bunVersion));
6791
+ function enforceSupportedNodeVersion(version = process.versions.node, bunVersion = detectBunVersion(), denoVersion = detectDenoVersion()) {
6792
+ if (denoVersion) {
6793
+ if (isSupportedDenoVersion(denoVersion)) return;
6794
+ console.error(unsupportedDenoVersionMessage(denoVersion));
6795
+ process.exit(1);
6796
+ return;
6797
+ }
6798
+ if (bunVersion) {
6799
+ if (isSupportedBunVersion(bunVersion)) return;
6800
+ console.error(unsupportedBunVersionMessage(bunVersion));
6801
+ process.exit(1);
6802
+ return;
6803
+ }
6804
+ if (isSupportedNodeVersion(version)) return;
6805
+ console.error(unsupportedNodeVersionMessage(version));
6683
6806
  process.exit(1);
6684
6807
  }
6685
6808
  enforceSupportedNodeVersion();
@@ -7075,7 +7198,7 @@ var __webpack_modules__ = {
7075
7198
  return Math.min(Math.max(n, min), max);
7076
7199
  }
7077
7200
  const TEMPLATE_CORPUS_REPO = 'extension-js/examples';
7078
- const TEMPLATE_CORPUS_REF = 'e552f6db6f2862093ba9d7026e0d376f002d95cc';
7201
+ const TEMPLATE_CORPUS_REF = '06d33e35894516a460409dca06d1614e291e6252';
7079
7202
  const TEMPLATE_CORPUS_SLUGS = [
7080
7203
  'action',
7081
7204
  'action-locales',
@@ -8066,7 +8189,7 @@ AI assistants
8066
8189
  return `${getLoggingPrefix('error')} Can't use ${messages_code(bundleId)} as a bundle identifier.\nUse reverse-DNS form: dot-separated segments of letters, digits and hyphens, each starting with a letter (e.g. ${messages_code('com.example.my-extension')}).`;
8067
8190
  }
8068
8191
  function safariCommandNotSupported(command) {
8069
- return `${getLoggingPrefix('error')} ${messages_code(command)} can't load an extension into Safari automatically.\nSafari extensions ship inside a signed app and are enabled by hand, so there's no live browser session to load into, unlike Chromium and Firefox.\nBuild the Safari app instead: ${messages_code('extension build --browser safari')}\nThen open the generated app and enable it in Safari Settings Extensions.`;
8192
+ return `${getLoggingPrefix('error')} ${messages_code(command)} can't load an extension into Safari.\nSafari loads an extension only from an app it has registered, and that app is enabled by hand once installed.\nIts automation route can load a folder but grants no website access, so content scripts never run.\nRun a live Safari session: ${messages_code('extension dev --browser safari')}\nOr package the app and open it: ${messages_code('extension build --browser safari --open')}\nBoth need macOS with Xcode, and Safari takes the enable gesture once in Safari Settings Extensions.`;
8070
8193
  }
8071
8194
  function programAIHelp() {
8072
8195
  return `\n${getLoggingPrefix('info')} ${external_pintor_default().gray('Development tips for extension developers and AI assistants')}
@@ -8188,7 +8311,10 @@ Docker / Devcontainers / Codespaces
8188
8311
  - Use ${messages_code('--host 0.0.0.0')} to bind the dev server on all interfaces so HMR is reachable from the host.
8189
8312
  - Use ${messages_code('--no-browser')} inside the container and load the extension manually from ${messages_code('dist/<browser>/')} in your host browser.
8190
8313
  - Chromium sandbox flags (${messages_code('--no-sandbox')}) are added automatically when Docker, Podman, devcontainers, or Codespaces are detected.
8191
- - File watching uses polling by default (1 s interval), which works across bind-mounted volumes.
8314
+ - File watching uses native file events by default, which keeps rebuilds fast.
8315
+ - Set ${messages_code(messages_arg('EXTENSION_WATCH_POLL=true'))} to poll instead when native events do not fire,
8316
+ as in a container, a virtual machine, or a network or bind mount.
8317
+ Tune it with ${messages_code(messages_arg('EXTENSION_WATCH_POLL_INTERVAL'))} (default 1000 ms).
8192
8318
  - Example: ${messages_code('extension dev ./my-ext --host 0.0.0.0 --no-browser --port 8080')}
8193
8319
 
8194
8320
  Flatpak Firefox
@@ -8361,7 +8487,9 @@ Cross-browser compatibility
8361
8487
  ],
8362
8488
  notes: [
8363
8489
  'Use --no-browser inside the container and load dist/<browser>/ in the host browser',
8364
- 'File watching uses polling (1s interval) for bind-mount compatibility',
8490
+ 'File watching uses native file events by default',
8491
+ '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',
8492
+ 'EXTENSION_WATCH_POLL_INTERVAL sets the poll interval in ms (default 1000)',
8365
8493
  '--no-sandbox is added automatically when a container environment is detected'
8366
8494
  ]
8367
8495
  }
@@ -8392,12 +8520,45 @@ Cross-browser compatibility
8392
8520
  }
8393
8521
  const OPEN_SURFACE_NOUN = {
8394
8522
  popup: 'popup',
8523
+ options: 'options page',
8395
8524
  sidebar: 'side panel',
8396
8525
  action: 'action popup'
8397
8526
  };
8398
8527
  function openSurfaceNoun(surface) {
8399
8528
  return OPEN_SURFACE_NOUN[surface] || surface;
8400
8529
  }
8530
+ function openSurfaceAction(surface) {
8531
+ return 'command' === surface ? 'trigger the command' : `open the ${openSurfaceNoun(surface)}`;
8532
+ }
8533
+ function asSentence(text) {
8534
+ const trimmed = text.trim();
8535
+ const capitalized = trimmed.charAt(0).toUpperCase() + trimmed.slice(1);
8536
+ return capitalized.endsWith('.') ? capitalized : `${capitalized}.`;
8537
+ }
8538
+ function listenerCount(count, event) {
8539
+ return `${count} ${event} ${1 === count ? 'listener' : 'listeners'}`;
8540
+ }
8541
+ function openedSurface(surface) {
8542
+ return `${getLoggingPrefix('success')} Opened the ${openSurfaceNoun(surface)}.`;
8543
+ }
8544
+ function replayedActionClick(listeners) {
8545
+ return `${getLoggingPrefix('success')} Replayed ${listenerCount(listeners, 'onClicked')} without a user gesture.`;
8546
+ }
8547
+ function replayedCommand(listeners, command) {
8548
+ const name = command ? ` for ${messages_arg(command)}` : '';
8549
+ return `${getLoggingPrefix('success')} Replayed ${listenerCount(listeners, 'onCommand')}${name} without a user gesture.`;
8550
+ }
8551
+ function openSurfaceWarning(warning) {
8552
+ return `${getLoggingPrefix('warn')} ${asSentence(warning)}`;
8553
+ }
8554
+ function openSurfaceFailed(surface, reason, engine, remedy) {
8555
+ const engineNote = engine ? messages_arg(` (engine ${engine})`) : '';
8556
+ return [
8557
+ `${getLoggingPrefix('error')} Can't ${openSurfaceAction(surface)}.`,
8558
+ `${messaging.Sc.label('REASON')} ${messaging.Sc.val(reason)}${engineNote}`,
8559
+ remedy
8560
+ ].filter(Boolean).join('\n');
8561
+ }
8401
8562
  function openSurfaceGestureStep(surface) {
8402
8563
  return `Click the extension's toolbar entry, or its row in the extensions menu, to open the ${openSurfaceNoun(surface)}.`;
8403
8564
  }
@@ -8407,6 +8568,12 @@ Cross-browser compatibility
8407
8568
  function openSurfaceNeedsGesture(surface) {
8408
8569
  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);
8409
8570
  }
8571
+ function doctorHeader(browser, passes, total) {
8572
+ return `doctor (${browser}), ${passes}/${total} checks passed`;
8573
+ }
8574
+ function doctorRemedy(check, remediation) {
8575
+ return `\n${check}: ${remediation}`;
8576
+ }
8410
8577
  function sessionOnPort(browser, port) {
8411
8578
  return port ? `for ${browser} on port ${port}` : `for ${browser}`;
8412
8579
  }
@@ -8483,6 +8650,96 @@ Cross-browser compatibility
8483
8650
  function sessionLogsPath(bridge, projectPath, browser) {
8484
8651
  return sessionFilePath(bridge, 'logsPath', projectPath, browser, 'logs.ndjson');
8485
8652
  }
8653
+ function isSafariVendor(value) {
8654
+ return 'safari' === value || 'webkit-based' === value;
8655
+ }
8656
+ const SUPPORTED_BROWSER_TARGETS = [
8657
+ 'chrome',
8658
+ 'chromium',
8659
+ 'edge',
8660
+ 'firefox',
8661
+ 'brave',
8662
+ 'opera',
8663
+ 'vivaldi',
8664
+ 'yandex',
8665
+ 'waterfox',
8666
+ 'librewolf',
8667
+ 'chromium-based',
8668
+ 'gecko-based',
8669
+ 'firefox-based',
8670
+ 'safari',
8671
+ 'webkit-based'
8672
+ ];
8673
+ const BROWSER_TARGETS_HELP = SUPPORTED_BROWSER_TARGETS.join(' | ');
8674
+ const SESSION_BROWSER_TARGETS_HELP = "chrome | chromium | edge | firefox | safari";
8675
+ const NO_SAFARI_BROWSER_TARGETS_HELP = SUPPORTED_BROWSER_TARGETS.filter((target)=>!isSafariVendor(target)).join(' | ');
8676
+ function parseOptionalBoolean(value) {
8677
+ if (void 0 === value) return true;
8678
+ const normalized = String(value).trim().toLowerCase();
8679
+ return ![
8680
+ 'false',
8681
+ '0',
8682
+ 'no',
8683
+ 'off'
8684
+ ].includes(normalized);
8685
+ }
8686
+ const vendors = (browser)=>{
8687
+ const value = browser ?? 'chromium';
8688
+ return 'all' === value ? [
8689
+ 'chrome',
8690
+ 'edge',
8691
+ 'firefox'
8692
+ ] : String(value).split(',').map((part)=>part.trim()).filter(Boolean);
8693
+ };
8694
+ const MANAGED_INSTALL_TARGETS = [
8695
+ 'chrome',
8696
+ 'chromium',
8697
+ 'edge',
8698
+ 'firefox',
8699
+ 'chromium-based',
8700
+ 'gecko-based',
8701
+ 'firefox-based'
8702
+ ];
8703
+ const MANAGED_INSTALL_TARGETS_HELP = [
8704
+ ...MANAGED_INSTALL_TARGETS,
8705
+ 'all'
8706
+ ].join(' | ');
8707
+ const MANAGED_INSTALL_BINARIES = [
8708
+ 'chrome',
8709
+ 'chromium',
8710
+ 'edge',
8711
+ 'firefox'
8712
+ ];
8713
+ const installTargets = (browser)=>'all' === browser ? [
8714
+ ...MANAGED_INSTALL_BINARIES
8715
+ ] : vendors(browser);
8716
+ function validateVendors(vendorsList, onInvalid) {
8717
+ const supported = SUPPORTED_BROWSER_TARGETS;
8718
+ for (const v of vendorsList)if (!supported.includes(v)) {
8719
+ onInvalid(v, supported);
8720
+ return false;
8721
+ }
8722
+ return true;
8723
+ }
8724
+ const MANAGED_INSTALL_TARGET_SET = new Set(MANAGED_INSTALL_TARGETS);
8725
+ const SUPPORTED_BROWSER_TARGET_SET = new Set(SUPPORTED_BROWSER_TARGETS);
8726
+ function classifyManagedInstallTarget(name) {
8727
+ const value = String(name || '').trim().toLowerCase();
8728
+ if (!value) return 'unknown';
8729
+ if (MANAGED_INSTALL_TARGET_SET.has(value)) return 'managed';
8730
+ if (SUPPORTED_BROWSER_TARGET_SET.has(value)) return 'not-installable';
8731
+ return 'unknown';
8732
+ }
8733
+ function firstNonManagedInstallTarget(targetsList) {
8734
+ for (const name of targetsList){
8735
+ const kind = classifyManagedInstallTarget(name);
8736
+ if ('managed' !== kind) return {
8737
+ name,
8738
+ kind
8739
+ };
8740
+ }
8741
+ return null;
8742
+ }
8486
8743
  function parseLogSinceLocal(value) {
8487
8744
  if (null == value || '' === value) return null;
8488
8745
  if ('number' == typeof value) return Number.isFinite(value) ? {
@@ -8579,11 +8836,20 @@ Cross-browser compatibility
8579
8836
  if ('json' === format) return void console.log(JSON.stringify(event, null, 2));
8580
8837
  console.log(formatPrettyLogLine(event));
8581
8838
  }
8839
+ function levelToken(level) {
8840
+ const word = level.toUpperCase();
8841
+ if ('error' === level) return external_pintor_default().red(word);
8842
+ if ('warn' === level) return external_pintor_default().brightYellow(word);
8843
+ if ('info' === level) return external_pintor_default().gray(word);
8844
+ if ('debug' === level) return external_pintor_default().dim(external_pintor_default().gray(word));
8845
+ return word;
8846
+ }
8582
8847
  function formatPrettyLogLine(event) {
8583
8848
  const parts = Array.isArray(event.messageParts) ? event.messageParts.map((p)=>'string' == typeof p ? p : JSON.stringify(p)).join(' ') : '';
8584
8849
  const code = event.code ? ` ${event.code}` : '';
8585
8850
  const remediation = event.remediation ? `\n ↳ ${event.remediation}` : '';
8586
- return `[${event.seq ?? '-'}] ${String(event.level || 'log').toUpperCase()} (${event.context})${code} ${parts}${remediation}`;
8851
+ const level = String(event.level || 'log').toLowerCase();
8852
+ return `[${event.seq ?? '-'}] ${levelToken(level)} ${external_pintor_default().dim(`(${event.context})`)}${code} ${parts}${remediation}`;
8587
8853
  }
8588
8854
  function writeFrame(frame) {
8589
8855
  try {
@@ -8593,7 +8859,7 @@ Cross-browser compatibility
8593
8859
  }
8594
8860
  }
8595
8861
  function registerLogsCommand(program) {
8596
- program.command('logs').arguments('[project-path]').usage('[project-path] [options]').description(commandDescriptions.logs).option('--browser <chrome | chromium | edge | firefox>', 'which dist/extension-js/<browser> to read. Defaults to `chromium`').option('--follow', 'stream live via the control channel instead of printing and exiting').option('--context <list>', 'comma-separated contexts (background, content, popup, options, sidebar, devtools, page)').option('--level <off|error|warn|info|debug|trace|all>', 'minimum severity to show. Defaults to `all`').option('--signals-only', '[experimental] show only structured dx.signal diagnostics. No emitter ships yet, so this currently prints nothing').option('--since <seq|iso>', 'only show events after this sequence number or ISO timestamp').option('--url <glob|substring>', 'only events whose url/hostname matches (glob with * or plain substring)').option('--tab <id>', 'only events from this tab id').option('--output <pretty|json|ndjson>', 'output format. Defaults to pretty on a TTY, ndjson when piped').action(async (projectPathArg, options)=>{
8862
+ program.command('logs').arguments('[project-path]').usage('[project-path] [options]').description(commandDescriptions.logs).option(`--browser <${SESSION_BROWSER_TARGETS_HELP}>`, 'which dist/extension-js/<browser> to read. Defaults to `chromium`').option('--follow', 'stream live via the control channel instead of printing and exiting').option('--context <list>', 'comma-separated contexts (background, content, popup, options, sidebar, devtools, page)').option('--level <off|error|warn|info|debug|trace|all>', 'minimum severity to show. Defaults to `all`').option('--signals-only', '[experimental] show only structured dx.signal diagnostics. No emitter ships yet, so this currently prints nothing').option('--since <seq|iso>', 'only show events after this sequence number or ISO timestamp').option('--url <glob|substring>', 'only events whose url/hostname matches (glob with * or plain substring)').option('--tab <id>', 'only events from this tab id').option('--output <pretty|json|ndjson>', 'output format. Defaults to pretty on a TTY, ndjson when piped').action(async (projectPathArg, options)=>{
8597
8863
  const bridge = await loadExtensionDevelopBridgeModule();
8598
8864
  const projectPath = resolveSessionProjectPath(bridge, projectPathArg);
8599
8865
  const browser = options.browser || 'chromium';
@@ -8666,7 +8932,7 @@ Cross-browser compatibility
8666
8932
  if (matches(event)) printEvent(event, format);
8667
8933
  },
8668
8934
  onGap: (gap)=>{
8669
- console.error(`… ${gap.dropped} event(s) dropped (${gap.reason}), stream is behind`);
8935
+ console.error(external_pintor_default().dim(external_pintor_default().gray(`… ${gap.dropped} event(s) dropped (${gap.reason}), stream is behind`)));
8670
8936
  },
8671
8937
  onClose: (close)=>{
8672
8938
  if (!refusals.has(close.code) && sessionStillNamed()) return;
@@ -8838,11 +9104,33 @@ Cross-browser compatibility
8838
9104
  hint: openSurfaceGestureStep(surface)
8839
9105
  };
8840
9106
  }
8841
- function printResult(result, output, command) {
9107
+ function openResultLines(value) {
9108
+ if (!value || 'object' != typeof value) return;
9109
+ const reply = value;
9110
+ if ('string' == typeof reply.opened) return [
9111
+ openedSurface(reply.opened)
9112
+ ];
9113
+ if ('popup' === reply.triggered) return [
9114
+ openedSurface('action')
9115
+ ];
9116
+ if ('number' != typeof reply.listeners) return;
9117
+ const lines = [];
9118
+ if ('onClicked' === reply.triggered) lines.push(replayedActionClick(reply.listeners));
9119
+ else {
9120
+ if ('command' !== reply.triggered) return;
9121
+ lines.push(replayedCommand(reply.listeners, 'string' == typeof reply.command ? reply.command : void 0));
9122
+ }
9123
+ if ('string' == typeof reply.warning && reply.warning) lines.push(openSurfaceWarning(reply.warning));
9124
+ return lines;
9125
+ }
9126
+ function printResult(result, output, command, pretty) {
8842
9127
  if ('json' === normalizeOutputFormat(output)) return void console.log(JSON.stringify(buildActEnvelope(command, result)));
8843
9128
  if (result.ok) {
8844
9129
  const value = result.value;
8845
- console.log('string' == typeof value ? value : JSON.stringify(value, null, 2));
9130
+ const lines = pretty?.value(value) ?? [
9131
+ 'string' == typeof value ? value : JSON.stringify(value, null, 2)
9132
+ ];
9133
+ for (const line of lines)console.log(line);
8846
9134
  const consoleRecords = result.console;
8847
9135
  if (Array.isArray(consoleRecords)) {
8848
9136
  if (0 === consoleRecords.length) console.log('(no console lines captured)');
@@ -8851,6 +9139,11 @@ Cross-browser compatibility
8851
9139
  if (result.truncated) console.error('… result truncated (byte cap)');
8852
9140
  return;
8853
9141
  }
9142
+ if (pretty) {
9143
+ const { error } = buildActEnvelope(command, result);
9144
+ console.error(pretty.failure(error));
9145
+ return;
9146
+ }
8854
9147
  const err = result.error || {
8855
9148
  name: 'Error',
8856
9149
  message: 'command failed'
@@ -8924,7 +9217,7 @@ Cross-browser compatibility
8924
9217
  if (result?.ok && input.augment) try {
8925
9218
  Object.assign(result, input.augment(projectPath, browser, result));
8926
9219
  } catch {}
8927
- printResult(result, input.opts.output, input.command);
9220
+ printResult(result, input.opts.output, input.command, input.pretty);
8928
9221
  await exitAfterDrain(result.ok ? 0 : 1);
8929
9222
  }
8930
9223
  function targetFrom(opts, fallback = 'background') {
@@ -8936,7 +9229,7 @@ Cross-browser compatibility
8936
9229
  if (null != opts.tab && '' !== opts.tab) target.tabId = Number(opts.tab);
8937
9230
  return target;
8938
9231
  }
8939
- const commonOptions = (cmd)=>cmd.option('--browser <chrome | chromium | edge | firefox>', 'which session to target (default chromium)').option('--timeout <ms>', 'command timeout in milliseconds (default 5000)').option('--output <pretty|json>', 'output format (default pretty)');
9232
+ const commonOptions = (cmd)=>cmd.option(`--browser <${SESSION_BROWSER_TARGETS_HELP}>`, 'which session to target (default chromium)').option('--timeout <ms>', 'command timeout in milliseconds (default 5000)').option('--output <pretty|json>', 'output format (default pretty)');
8940
9233
  function registerActCommands(program) {
8941
9234
  commonOptions(program.command('eval').arguments('<expression> [project-path]').description(commandDescriptions.eval).option('--context <background|popup|options|sidebar|devtools|newtab|history|bookmarks|content|page>', 'target context (default background). Extension pages (popup/options/sidebar/devtools/newtab/history/bookmarks) answer via their own in-page relay and must be open').option('--url <glob|substring>', 'for content/page: document to target (resolved to its tab)').option('--tab <id>', 'for content/page: a specific tab (default: the --url match, else the active tab)')).action(async (expression, projectPathArg, opts)=>{
8942
9235
  await runCommand({
@@ -9069,7 +9362,11 @@ Cross-browser compatibility
9069
9362
  },
9070
9363
  args,
9071
9364
  opts,
9072
- preflight: (bridge, projectPath, browser)=>gestureRefusal(surface, bridge, projectPath, browser)
9365
+ preflight: (bridge, projectPath, browser)=>gestureRefusal(surface, bridge, projectPath, browser),
9366
+ pretty: {
9367
+ value: openResultLines,
9368
+ failure: (error)=>openSurfaceFailed(surface, error.message, error.engine, error.hint)
9369
+ }
9073
9370
  });
9074
9371
  });
9075
9372
  }
@@ -9107,97 +9404,8 @@ Cross-browser compatibility
9107
9404
  const values = String(raw).split(',').map((value)=>value.trim()).filter((value)=>value.length > 0);
9108
9405
  return values.length > 0 ? values : void 0;
9109
9406
  }
9110
- function isSafariVendor(value) {
9111
- return 'safari' === value || 'webkit-based' === value;
9112
- }
9113
- const SUPPORTED_BROWSER_TARGETS = [
9114
- 'chrome',
9115
- 'chromium',
9116
- 'edge',
9117
- 'firefox',
9118
- 'brave',
9119
- 'opera',
9120
- 'vivaldi',
9121
- 'yandex',
9122
- 'waterfox',
9123
- 'librewolf',
9124
- 'chromium-based',
9125
- 'gecko-based',
9126
- 'firefox-based',
9127
- 'safari',
9128
- 'webkit-based'
9129
- ];
9130
- const BROWSER_TARGETS_HELP = SUPPORTED_BROWSER_TARGETS.join(' | ');
9131
- const NO_SAFARI_BROWSER_TARGETS_HELP = SUPPORTED_BROWSER_TARGETS.filter((target)=>!isSafariVendor(target)).join(' | ');
9132
- function parseOptionalBoolean(value) {
9133
- if (void 0 === value) return true;
9134
- const normalized = String(value).trim().toLowerCase();
9135
- return ![
9136
- 'false',
9137
- '0',
9138
- 'no',
9139
- 'off'
9140
- ].includes(normalized);
9141
- }
9142
- const vendors = (browser)=>{
9143
- const value = browser ?? 'chromium';
9144
- return 'all' === value ? [
9145
- 'chrome',
9146
- 'edge',
9147
- 'firefox'
9148
- ] : String(value).split(',').map((part)=>part.trim()).filter(Boolean);
9149
- };
9150
- const MANAGED_INSTALL_TARGETS = [
9151
- 'chrome',
9152
- 'chromium',
9153
- 'edge',
9154
- 'firefox',
9155
- 'chromium-based',
9156
- 'gecko-based',
9157
- 'firefox-based'
9158
- ];
9159
- const MANAGED_INSTALL_TARGETS_HELP = [
9160
- ...MANAGED_INSTALL_TARGETS,
9161
- 'all'
9162
- ].join(' | ');
9163
- const MANAGED_INSTALL_BINARIES = [
9164
- 'chrome',
9165
- 'chromium',
9166
- 'edge',
9167
- 'firefox'
9168
- ];
9169
- const installTargets = (browser)=>'all' === browser ? [
9170
- ...MANAGED_INSTALL_BINARIES
9171
- ] : vendors(browser);
9172
- function validateVendors(vendorsList, onInvalid) {
9173
- const supported = SUPPORTED_BROWSER_TARGETS;
9174
- for (const v of vendorsList)if (!supported.includes(v)) {
9175
- onInvalid(v, supported);
9176
- return false;
9177
- }
9178
- return true;
9179
- }
9180
- const MANAGED_INSTALL_TARGET_SET = new Set(MANAGED_INSTALL_TARGETS);
9181
- const SUPPORTED_BROWSER_TARGET_SET = new Set(SUPPORTED_BROWSER_TARGETS);
9182
- function classifyManagedInstallTarget(name) {
9183
- const value = String(name || '').trim().toLowerCase();
9184
- if (!value) return 'unknown';
9185
- if (MANAGED_INSTALL_TARGET_SET.has(value)) return 'managed';
9186
- if (SUPPORTED_BROWSER_TARGET_SET.has(value)) return 'not-installable';
9187
- return 'unknown';
9188
- }
9189
- function firstNonManagedInstallTarget(targetsList) {
9190
- for (const name of targetsList){
9191
- const kind = classifyManagedInstallTarget(name);
9192
- if ('managed' !== kind) return {
9193
- name,
9194
- kind
9195
- };
9196
- }
9197
- return null;
9198
- }
9199
9407
  function registerBuildCommand(program) {
9200
- 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 treats as unsigned: the extension then needs Develop \u25b8 Allow Unsigned Extensions re-ticked on every launch. A signed build is listed and stays enabled across restarts').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 })=>{
9408
+ 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 })=>{
9201
9409
  const browser = cliBrowser ?? await resolveConfigBrowser(pathOrRemoteUrl || process.cwd(), 'build') ?? 'chromium';
9202
9410
  if (buildOptions.debug || buildOptions.author || buildOptions.authorMode) {
9203
9411
  process.env.EXTENSION_DEBUG = '1';
@@ -9759,37 +9967,45 @@ Cross-browser compatibility
9759
9967
  async function waitForReadyContract(options) {
9760
9968
  const readyPath = sessionReadyPath(options.bridge, options.projectPath, options.browser);
9761
9969
  const start = Date.now();
9970
+ let lastReadFailure = '';
9762
9971
  while(Date.now() - start < options.timeoutMs){
9763
- if (external_node_fs_.existsSync(readyPath)) try {
9764
- const payload = JSON.parse(external_node_fs_.readFileSync(readyPath, 'utf8'));
9765
- const isLive = isProcessLikelyAlive(payload.pid);
9766
- const isFresh = isFreshContractPayload(payload);
9767
- if (payload.command !== options.command) {
9768
- await new Promise((resolve)=>setTimeout(resolve, 250));
9769
- continue;
9972
+ if (external_node_fs_.existsSync(readyPath)) {
9973
+ let payload = null;
9974
+ try {
9975
+ payload = JSON.parse(external_node_fs_.readFileSync(readyPath, 'utf8'));
9976
+ lastReadFailure = '';
9977
+ } catch (error) {
9978
+ payload = null;
9979
+ lastReadFailure = error instanceof Error ? error.message : String(error);
9770
9980
  }
9771
- if (!isLive) {
9772
- if ('start' !== options.command || !isFresh) {
9981
+ if (payload) {
9982
+ const isLive = isProcessLikelyAlive(payload.pid);
9983
+ const isFresh = isFreshContractPayload(payload);
9984
+ if (payload.command !== options.command) {
9773
9985
  await new Promise((resolve)=>setTimeout(resolve, 250));
9774
9986
  continue;
9775
9987
  }
9776
- }
9777
- if ('ready' === payload.status) return payload;
9778
- if ('error' === payload.status) {
9779
- if (!isLive && 'start' !== options.command) {
9780
- await new Promise((resolve)=>setTimeout(resolve, 250));
9781
- continue;
9988
+ if (!isLive) {
9989
+ if ('start' !== options.command || !isFresh) {
9990
+ await new Promise((resolve)=>setTimeout(resolve, 250));
9991
+ continue;
9992
+ }
9993
+ }
9994
+ if ('ready' === payload.status) return payload;
9995
+ if ('error' === payload.status) {
9996
+ if (!isLive && 'start' !== options.command) {
9997
+ await new Promise((resolve)=>setTimeout(resolve, 250));
9998
+ continue;
9999
+ }
10000
+ const detail = payload.message || payload.errors?.[0] || 'unknown error';
10001
+ throw new WaitModeError(String(detail), messaging.Lp.E_INTERNAL);
9782
10002
  }
9783
- const detail = payload.message || payload.errors?.[0] || 'unknown error';
9784
- throw new WaitModeError(String(detail), messaging.Lp.E_INTERNAL);
9785
10003
  }
9786
- } catch (error) {
9787
- if (error instanceof Error) throw error;
9788
- throw new Error(String(error));
9789
10004
  }
9790
10005
  await new Promise((resolve)=>setTimeout(resolve, 250));
9791
10006
  }
9792
- throw new WaitModeError(`Timed out waiting for ready contract at ${readyPath} (${options.timeoutMs} ms)`, messaging.Lp.E_READY_TIMEOUT);
10007
+ const timeoutDetail = lastReadFailure ? `. The last read of the file failed to parse as JSON: ${lastReadFailure}` : '';
10008
+ throw new WaitModeError(`Timed out waiting for ready contract at ${readyPath} (${options.timeoutMs} ms)${timeoutDetail}`, messaging.Lp.E_READY_TIMEOUT);
9793
10009
  }
9794
10010
  async function runWaitMode(options) {
9795
10011
  if (isHttpUrl(options.pathOrRemoteUrl)) throw new WaitModeError('--wait requires a local project path (remote URLs are not supported)', messaging.Lp.E_ARGS);
@@ -9833,7 +10049,7 @@ Cross-browser compatibility
9833
10049
  return Number.isFinite(parsed) && parsed >= 0 ? Math.floor(parsed) : 8080;
9834
10050
  }
9835
10051
  function registerDevCommand(program) {
9836
- 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; no live reload)').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 treats as unsigned: the extension then needs Develop \u25b8 Allow Unsigned Extensions re-ticked on every launch. A signed build is listed and stays enabled across restarts').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)=>{
10052
+ 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)=>{
9837
10053
  const { browser: cliBrowser, ...devOptions } = options;
9838
10054
  const browser = cliBrowser ?? await resolveConfigBrowser(pathOrRemoteUrl || process.cwd(), 'dev') ?? 'chromium';
9839
10055
  if (devOptions.debug || devOptions.author || devOptions.authorMode) {
@@ -10289,19 +10505,19 @@ Cross-browser compatibility
10289
10505
  });
10290
10506
  return results;
10291
10507
  }
10508
+ function checkGlyph(status) {
10509
+ if ('pass' === status) return external_pintor_default().green('✓');
10510
+ if ('fail' === status) return external_pintor_default().red('✗');
10511
+ if ('warn' === status) return external_pintor_default().brightYellow('!');
10512
+ return external_pintor_default().gray('–');
10513
+ }
10292
10514
  function printPretty(results, browser) {
10293
10515
  const passes = results.filter((r)=>'pass' === r.status).length;
10294
- const glyph = {
10295
- pass: '✓',
10296
- fail: '✗',
10297
- warn: '!',
10298
- skip: '–'
10299
- };
10300
- console.log(`doctor (${browser}), ${passes}/${results.length} checks passed`);
10516
+ console.log(doctorHeader(browser, passes, results.length));
10301
10517
  const width = Math.max(...results.map((r)=>r.check.length));
10302
- for (const r of results)console.log(` ${glyph[r.status]} ${r.check.padEnd(width)} ${r.detail}`);
10518
+ for (const r of results)console.log(` ${checkGlyph(r.status)} ${r.check.padEnd(width)} ${r.detail}`);
10303
10519
  const advisory = results.find((r)=>'fail' === r.status) ?? results.find((r)=>'warn' === r.status);
10304
- if (advisory?.remediation) console.log(`\n${advisory.check}: ${advisory.remediation}`);
10520
+ if (advisory?.remediation) console.log(doctorRemedy(advisory.check, advisory.remediation));
10305
10521
  }
10306
10522
  const CHECK_CODES = {
10307
10523
  'ready-contract': messaging.Lp.E_SESSION_NOT_FOUND,
@@ -10313,7 +10529,7 @@ Cross-browser compatibility
10313
10529
  browser: messaging.Lp.E_BROWSER_LAUNCH
10314
10530
  };
10315
10531
  function registerDoctorCommand(program) {
10316
- program.command('doctor').argument('[project-path]', 'path to the extension project root or the folder holding its manifest').option('--browser <chrome | chromium | edge | firefox>', 'which session to diagnose (defaults to the single live session, else chromium)').option('--output <pretty|json>', 'result format. Use json for a schema-1 envelope on stdout').description(commandDescriptions.doctor).action(async (projectPathArg, opts)=>{
10532
+ program.command('doctor').argument('[project-path]', 'path to the extension project root or the folder holding its manifest').option(`--browser <${SESSION_BROWSER_TARGETS_HELP}>`, 'which session to diagnose (defaults to the single live session, else chromium)').option('--output <pretty|json>', 'result format. Use json for a schema-1 envelope on stdout').description(commandDescriptions.doctor).action(async (projectPathArg, opts)=>{
10317
10533
  const asJson = isJsonOutput(opts);
10318
10534
  let results;
10319
10535
  try {