pake-cli 3.11.1 โ†’ 3.11.3

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.
package/dist/cli.js CHANGED
@@ -13,16 +13,17 @@ import ora from 'ora';
13
13
  import dns from 'dns';
14
14
  import http from 'http';
15
15
  import { promisify } from 'util';
16
- import fs from 'fs';
16
+ import fs from 'fs/promises';
17
17
  import { dir } from 'tmp-promise';
18
18
  import { fileTypeFromBuffer } from 'file-type';
19
19
  import icongen from 'icon-gen';
20
20
  import sharp from 'sharp';
21
21
  import * as psl from 'psl';
22
22
  import { InvalidArgumentError, program as program$1, Option } from 'commander';
23
+ import fs$1 from 'fs';
23
24
 
24
25
  var name = "pake-cli";
25
- var version = "3.11.1";
26
+ var version = "3.11.3";
26
27
  var description = "๐Ÿคฑ๐Ÿป Turn any webpage into a desktop app with one command. ๐Ÿคฑ๐Ÿป ไธ€้”ฎๆ‰“ๅŒ…็ฝ‘้กต็”Ÿๆˆ่ฝป้‡ๆกŒ้ขๅบ”็”จใ€‚";
27
28
  var engines = {
28
29
  node: ">=18.0.0"
@@ -95,7 +96,6 @@ var devDependencies = {
95
96
  "@rollup/plugin-terser": "^0.4.4",
96
97
  "@types/fs-extra": "^11.0.4",
97
98
  "@types/node": "^25.3.2",
98
- "@types/page-icon": "^0.3.6",
99
99
  "@types/prompts": "^2.4.9",
100
100
  "@types/tmp": "^0.2.6",
101
101
  "@types/update-notifier": "^6.0.8",
@@ -110,7 +110,8 @@ var devDependencies = {
110
110
  };
111
111
  var pnpm = {
112
112
  overrides: {
113
- sharp: "^0.34.5"
113
+ sharp: "^0.34.5",
114
+ "@img/sharp-libvips-darwin-arm64": "1.2.4"
114
115
  },
115
116
  onlyBuiltDependencies: [
116
117
  "esbuild",
@@ -289,7 +290,7 @@ const logger = {
289
290
  log.error(...msg.map((m) => chalk.red(m)));
290
291
  },
291
292
  warn(...msg) {
292
- log.info(...msg.map((m) => chalk.yellow(m)));
293
+ log.warn(...msg.map((m) => chalk.yellow(m)));
293
294
  },
294
295
  success(...msg) {
295
296
  log.info(...msg.map((m) => chalk.green(m)));
@@ -423,22 +424,22 @@ function checkRustInstalled() {
423
424
  }
424
425
 
425
426
  async function combineFiles(files, output) {
426
- const contents = files.map((file) => {
427
+ const contents = await Promise.all(files.map(async (file) => {
427
428
  if (file.endsWith('.css')) {
428
- const fileContent = fs.readFileSync(file, 'utf-8');
429
+ const fileContent = await fs.readFile(file, 'utf-8');
429
430
  return `window.addEventListener('DOMContentLoaded', (_event) => {
430
431
  const css = ${JSON.stringify(fileContent)};
431
432
  const style = document.createElement('style');
432
- style.innerHTML = css;
433
+ style.textContent = css;
433
434
  document.head.appendChild(style);
434
435
  });`;
435
436
  }
436
- const fileContent = fs.readFileSync(file);
437
+ const fileContent = await fs.readFile(file);
437
438
  return ("window.addEventListener('DOMContentLoaded', (_event) => { " +
438
439
  fileContent +
439
440
  ' });');
440
- });
441
- fs.writeFileSync(output, contents.join('\n'));
441
+ }));
442
+ await fs.writeFile(output, contents.join('\n'));
442
443
  return files;
443
444
  }
444
445
 
@@ -479,11 +480,15 @@ function generateIdentifierSafeName(name) {
479
480
  return cleaned;
480
481
  }
481
482
 
482
- async function mergeConfig(url, options, tauriConf) {
483
- // Ensure .pake directory exists and copy source templates if needed
483
+ function asSupportedPlatform(platform) {
484
+ if (platform !== 'win32' && platform !== 'darwin' && platform !== 'linux') {
485
+ throw new Error(`Pake only supports win32, darwin, and linux; detected '${platform}'.`);
486
+ }
487
+ return platform;
488
+ }
489
+ async function copyTemplateConfigs() {
484
490
  const srcTauriDir = path.join(npmDirectory, 'src-tauri');
485
491
  await fsExtra.ensureDir(tauriConfigDirectory);
486
- // Copy source config files to .pake directory (as templates)
487
492
  const sourceFiles = [
488
493
  'tauri.conf.json',
489
494
  'tauri.macos.conf.json',
@@ -499,51 +504,11 @@ async function mergeConfig(url, options, tauriConf) {
499
504
  await fsExtra.copy(sourcePath, destPath);
500
505
  }
501
506
  }));
502
- const { width, height, fullscreen, maximize, hideTitleBar, alwaysOnTop, appVersion, darkMode, disabledWebShortcuts, activationShortcut, userAgent, showSystemTray, systemTrayIcon, useLocalFile, identifier, name = 'pake-app', resizable = true, inject, proxyUrl, installerLanguage, hideOnClose, incognito, title, wasm, enableDragDrop, multiInstance, multiWindow, startToTray, forceInternalNavigation, internalUrlRegex, zoom, minWidth, minHeight, ignoreCertificateErrors, newWindow, camera, microphone, } = options;
503
- const { platform } = process;
504
- const platformHideOnClose = hideOnClose ?? platform === 'darwin';
505
- const tauriConfWindowOptions = {
506
- width,
507
- height,
508
- fullscreen,
509
- maximize,
510
- resizable,
511
- hide_title_bar: hideTitleBar,
512
- activation_shortcut: activationShortcut,
513
- always_on_top: alwaysOnTop,
514
- dark_mode: darkMode,
515
- disabled_web_shortcuts: disabledWebShortcuts,
516
- hide_on_close: platformHideOnClose,
517
- incognito: incognito,
518
- title: title,
519
- enable_wasm: wasm,
520
- enable_drag_drop: enableDragDrop,
521
- start_to_tray: startToTray && showSystemTray,
522
- force_internal_navigation: forceInternalNavigation,
523
- internal_url_regex: internalUrlRegex,
524
- zoom,
525
- min_width: minWidth,
526
- min_height: minHeight,
527
- ignore_certificate_errors: ignoreCertificateErrors,
528
- new_window: newWindow,
529
- };
530
- Object.assign(tauriConf.pake.windows[0], { url, ...tauriConfWindowOptions });
531
- tauriConf.productName = name;
532
- tauriConf.identifier = identifier;
533
- tauriConf.version = appVersion;
534
- // Always set mainBinaryName to ensure binary uniqueness
535
- const linuxBinaryName = `pake-${generateLinuxPackageName(name)}`;
536
- tauriConf.mainBinaryName =
537
- platform === 'linux'
538
- ? linuxBinaryName
539
- : `pake-${generateIdentifierSafeName(name)}`;
540
- if (platform == 'win32') {
541
- tauriConf.bundle.windows.wix.language[0] = installerLanguage;
542
- }
507
+ }
508
+ async function handleLocalFile(url, useLocalFile, tauriConf) {
543
509
  const pathExists = await fsExtra.pathExists(url);
544
510
  if (pathExists) {
545
511
  logger.warn('โœผ Your input might be a local file.');
546
- tauriConf.pake.windows[0].url_type = 'local';
547
512
  const fileName = path.basename(url);
548
513
  const dirName = path.dirname(url);
549
514
  const distDir = path.join(npmDirectory, 'dist');
@@ -555,8 +520,6 @@ async function mergeConfig(url, options, tauriConf) {
555
520
  else {
556
521
  fsExtra.moveSync(distDir, distBakDir, { overwrite: true });
557
522
  fsExtra.copySync(dirName, distDir, { overwrite: true });
558
- // ignore it, because about_pake.html have be erased.
559
- // const filesToCopyBack = ['cli.js', 'about_pake.html'];
560
523
  const filesToCopyBack = ['cli.js'];
561
524
  await Promise.all(filesToCopyBack.map((file) => fsExtra.copy(path.join(distBakDir, file), path.join(distDir, file))));
562
525
  }
@@ -566,28 +529,19 @@ async function mergeConfig(url, options, tauriConf) {
566
529
  else {
567
530
  tauriConf.pake.windows[0].url_type = 'web';
568
531
  }
569
- const platformMap = {
570
- win32: 'windows',
571
- linux: 'linux',
572
- darwin: 'macos',
573
- };
574
- const currentPlatform = platformMap[platform];
575
- if (userAgent.length > 0) {
576
- tauriConf.pake.user_agent[currentPlatform] = userAgent;
577
- }
578
- tauriConf.pake.system_tray[currentPlatform] = showSystemTray;
579
- // Processing targets are currently only open to Linux.
580
- if (platform === 'linux') {
581
- // Remove hardcoded desktop files and regenerate with correct app name
582
- delete tauriConf.bundle.linux.deb.files;
583
- // Generate correct desktop file configuration
584
- const linuxName = generateLinuxPackageName(name);
585
- const desktopFileName = `com.pake.${linuxName}.desktop`;
586
- const iconName = `${linuxName}_512`;
587
- // Create desktop file content
588
- // Determine if title contains Chinese characters for Name[zh_CN]
589
- const chineseName = title && /[\u4e00-\u9fa5]/.test(title) ? title : null;
590
- const desktopContent = `[Desktop Entry]
532
+ }
533
+ async function mergeLinuxConfig(options, name, tauriConf, linuxBinaryName) {
534
+ const linuxBundle = tauriConf.bundle.linux;
535
+ if (!linuxBundle) {
536
+ throw new Error('Linux bundle configuration is missing from tauri.linux.conf.json; cannot build Linux target.');
537
+ }
538
+ delete linuxBundle.deb.files;
539
+ const linuxName = generateLinuxPackageName(name);
540
+ const desktopFileName = `com.pake.${linuxName}.desktop`;
541
+ const iconName = `${linuxName}_512`;
542
+ const { title } = options;
543
+ const chineseName = title && /[\u4e00-\u9fa5]/.test(title) ? title : null;
544
+ const desktopContent = `[Desktop Entry]
591
545
  Version=1.0
592
546
  Type=Application
593
547
  Name=${name}
@@ -600,51 +554,39 @@ MimeType=text/html;text/xml;application/xhtml_xml;
600
554
  StartupNotify=true
601
555
  Terminal=false
602
556
  `;
603
- // Write desktop file to src-tauri/assets directory where Tauri expects it
604
- const srcAssetsDir = path.join(npmDirectory, 'src-tauri/assets');
605
- const srcDesktopFilePath = path.join(srcAssetsDir, desktopFileName);
606
- await fsExtra.ensureDir(srcAssetsDir);
607
- await fsExtra.writeFile(srcDesktopFilePath, desktopContent);
608
- // Set up desktop file in bundle configuration
609
- // Use absolute path from src-tauri directory to assets
610
- const desktopInstallPath = `/usr/share/applications/${desktopFileName}`;
611
- tauriConf.bundle.linux.deb.files = {
612
- [desktopInstallPath]: `assets/${desktopFileName}`,
613
- };
614
- // Add desktop file support for RPM
615
- if (!tauriConf.bundle.linux.rpm) {
616
- tauriConf.bundle.linux.rpm = {};
617
- }
618
- tauriConf.bundle.linux.rpm.files = {
619
- [desktopInstallPath]: `assets/${desktopFileName}`,
620
- };
621
- const validTargets = [
622
- 'deb',
623
- 'appimage',
624
- 'rpm',
625
- 'deb-arm64',
626
- 'appimage-arm64',
627
- 'rpm-arm64',
628
- ];
629
- const baseTarget = options.targets.includes('-arm64')
630
- ? options.targets.replace('-arm64', '')
631
- : options.targets;
632
- if (validTargets.includes(options.targets)) {
633
- tauriConf.bundle.targets = [baseTarget];
634
- }
635
- else {
636
- logger.warn(`โœผ The target must be one of ${validTargets.join(', ')}, the default 'deb' will be used.`);
637
- }
557
+ const srcAssetsDir = path.join(npmDirectory, 'src-tauri/assets');
558
+ const srcDesktopFilePath = path.join(srcAssetsDir, desktopFileName);
559
+ await fsExtra.ensureDir(srcAssetsDir);
560
+ await fsExtra.writeFile(srcDesktopFilePath, desktopContent);
561
+ const desktopInstallPath = `/usr/share/applications/${desktopFileName}`;
562
+ linuxBundle.deb.files = {
563
+ [desktopInstallPath]: `assets/${desktopFileName}`,
564
+ };
565
+ if (!linuxBundle.rpm) {
566
+ linuxBundle.rpm = {};
638
567
  }
639
- // Set macOS bundle targets (for app vs dmg)
640
- if (platform === 'darwin') {
641
- const validMacTargets = ['app', 'dmg'];
642
- if (validMacTargets.includes(options.targets)) {
643
- tauriConf.bundle.targets = [options.targets];
644
- }
568
+ linuxBundle.rpm.files = {
569
+ [desktopInstallPath]: `assets/${desktopFileName}`,
570
+ };
571
+ const validTargets = [
572
+ 'deb',
573
+ 'appimage',
574
+ 'rpm',
575
+ 'deb-arm64',
576
+ 'appimage-arm64',
577
+ 'rpm-arm64',
578
+ ];
579
+ const baseTarget = options.targets.includes('-arm64')
580
+ ? options.targets.replace('-arm64', '')
581
+ : options.targets;
582
+ if (validTargets.includes(options.targets)) {
583
+ tauriConf.bundle.targets = [baseTarget];
645
584
  }
646
- // Set icon.
647
- const safeAppName = getSafeAppName(name);
585
+ else {
586
+ logger.warn(`โœผ The target must be one of ${validTargets.join(', ')}, the default 'deb' will be used.`);
587
+ }
588
+ }
589
+ async function mergeIcons(options, name, tauriConf, platform, safeAppName) {
648
590
  const platformIconMap = {
649
591
  win32: {
650
592
  fileExt: '.ico',
@@ -670,7 +612,7 @@ Terminal=false
670
612
  const exists = resolvedIconPath && (await fsExtra.pathExists(resolvedIconPath));
671
613
  if (exists) {
672
614
  let updateIconPath = true;
673
- let customIconExt = path.extname(resolvedIconPath).toLowerCase();
615
+ const customIconExt = path.extname(resolvedIconPath).toLowerCase();
674
616
  if (customIconExt !== iconInfo.fileExt) {
675
617
  updateIconPath = false;
676
618
  logger.warn(`โœผ ${iconInfo.message}, but you give ${customIconExt}`);
@@ -679,7 +621,6 @@ Terminal=false
679
621
  else {
680
622
  const iconPath = path.join(npmDirectory, 'src-tauri/', iconInfo.path);
681
623
  tauriConf.bundle.resources = [iconInfo.path];
682
- // Avoid copying if source and destination are the same
683
624
  const absoluteDestPath = path.resolve(iconPath);
684
625
  if (resolvedIconPath !== absoluteDestPath) {
685
626
  try {
@@ -706,37 +647,32 @@ Terminal=false
706
647
  }
707
648
  // Set tray icon path.
708
649
  let trayIconPath = platform === 'darwin' ? 'png/icon_512.png' : tauriConf.bundle.icon[0];
709
- if (systemTrayIcon.length > 0) {
650
+ if (options.systemTrayIcon.length > 0) {
710
651
  try {
711
- await fsExtra.pathExists(systemTrayIcon);
712
- // ้œ€่ฆๅˆคๆ–ญๅ›พๆ ‡ๆ ผๅผ๏ผŒ้ป˜่ฎคๅชๆ”ฏๆŒicoๅ’Œpngไธค็ง
713
- let iconExt = path.extname(systemTrayIcon).toLowerCase();
714
- if (iconExt == '.png' || iconExt == '.ico') {
652
+ await fsExtra.pathExists(options.systemTrayIcon);
653
+ const iconExt = path.extname(options.systemTrayIcon).toLowerCase();
654
+ if (iconExt === '.png' || iconExt === '.ico') {
715
655
  const trayIcoPath = path.join(npmDirectory, `src-tauri/png/${safeAppName}${iconExt}`);
716
656
  trayIconPath = `png/${safeAppName}${iconExt}`;
717
- await fsExtra.copy(systemTrayIcon, trayIcoPath);
657
+ await fsExtra.copy(options.systemTrayIcon, trayIcoPath);
718
658
  }
719
659
  else {
720
660
  logger.warn(`โœผ System tray icon must be .ico or .png, but you provided ${iconExt}.`);
721
661
  logger.warn(`โœผ Default system tray icon will be used.`);
722
662
  }
723
663
  }
724
- catch {
725
- logger.warn(`โœผ ${systemTrayIcon} not exists!`);
664
+ catch (err) {
665
+ logger.warn(`โœผ Failed to apply system tray icon "${options.systemTrayIcon}": ${err instanceof Error ? err.message : String(err)}`);
726
666
  logger.warn(`โœผ Default system tray icon will remain unchanged.`);
727
667
  }
728
668
  }
729
- // Ensure trayIcon object exists before setting iconPath
730
- if (!tauriConf.app.trayIcon) {
731
- tauriConf.app.trayIcon = {};
732
- }
733
- tauriConf.app.trayIcon.iconPath = trayIconPath;
734
669
  tauriConf.pake.system_tray_path = trayIconPath;
735
670
  delete tauriConf.app.trayIcon;
736
- const injectFilePath = path.join(npmDirectory, `src-tauri/src/inject/custom.js`);
737
- // inject js or css files
671
+ }
672
+ async function injectCustomCode(options, tauriConf) {
673
+ const { inject, proxyUrl, multiInstance, multiWindow, wasm } = options;
674
+ const injectFilePath = path.join(npmDirectory, 'src-tauri/src/inject/custom.js');
738
675
  if (inject?.length > 0) {
739
- // Ensure inject is an array before calling .every()
740
676
  const injectArray = Array.isArray(inject) ? inject : [inject];
741
677
  if (!injectArray.every((item) => item.endsWith('.css') || item.endsWith('.js'))) {
742
678
  logger.error('The injected file must be in either CSS or JS format.');
@@ -753,7 +689,6 @@ Terminal=false
753
689
  tauriConf.pake.proxy_url = proxyUrl || '';
754
690
  tauriConf.pake.multi_instance = multiInstance;
755
691
  tauriConf.pake.multi_window = multiWindow;
756
- // Configure WASM support with required HTTP headers
757
692
  if (wasm) {
758
693
  tauriConf.app.security = {
759
694
  headers: {
@@ -762,16 +697,16 @@ Terminal=false
762
697
  },
763
698
  };
764
699
  }
765
- // Write entitlements dynamically on macOS so camera/microphone are opt-in
766
- if (platform === 'darwin') {
767
- const entitlementEntries = [];
768
- if (camera) {
769
- entitlementEntries.push(' <key>com.apple.security.device.camera</key>\n <true/>');
770
- }
771
- if (microphone) {
772
- entitlementEntries.push(' <key>com.apple.security.device.audio-input</key>\n <true/>');
773
- }
774
- const entitlementsContent = `<?xml version="1.0" encoding="UTF-8"?>
700
+ }
701
+ async function generateMacEntitlements(camera, microphone) {
702
+ const entitlementEntries = [];
703
+ if (camera) {
704
+ entitlementEntries.push(' <key>com.apple.security.device.camera</key>\n <true/>');
705
+ }
706
+ if (microphone) {
707
+ entitlementEntries.push(' <key>com.apple.security.device.audio-input</key>\n <true/>');
708
+ }
709
+ const entitlementsContent = `<?xml version="1.0" encoding="UTF-8"?>
775
710
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
776
711
  <plist version="1.0">
777
712
  <dict>
@@ -779,10 +714,10 @@ ${entitlementEntries.join('\n')}
779
714
  </dict>
780
715
  </plist>
781
716
  `;
782
- const entitlementsPath = path.join(npmDirectory, 'src-tauri', 'entitlements.plist');
783
- await fsExtra.writeFile(entitlementsPath, entitlementsContent);
784
- }
785
- // Save config file.
717
+ const entitlementsPath = path.join(npmDirectory, 'src-tauri', 'entitlements.plist');
718
+ await fsExtra.writeFile(entitlementsPath, entitlementsContent);
719
+ }
720
+ async function writeAllConfigs(tauriConf, platform) {
786
721
  const platformConfigPaths = {
787
722
  win32: 'tauri.windows.conf.json',
788
723
  darwin: 'tauri.macos.conf.json',
@@ -793,11 +728,85 @@ ${entitlementEntries.join('\n')}
793
728
  await fsExtra.outputJSON(configPath, bundleConf, { spaces: 4 });
794
729
  const pakeConfigPath = path.join(tauriConfigDirectory, 'pake.json');
795
730
  await fsExtra.outputJSON(pakeConfigPath, tauriConf.pake, { spaces: 4 });
796
- let tauriConf2 = JSON.parse(JSON.stringify(tauriConf));
731
+ const tauriConf2 = JSON.parse(JSON.stringify(tauriConf));
797
732
  delete tauriConf2.pake;
798
733
  const configJsonPath = path.join(tauriConfigDirectory, 'tauri.conf.json');
799
734
  await fsExtra.outputJSON(configJsonPath, tauriConf2, { spaces: 4 });
800
735
  }
736
+ async function mergeConfig(url, options, tauriConf) {
737
+ await copyTemplateConfigs();
738
+ const { width, height, fullscreen, maximize, hideTitleBar, alwaysOnTop, appVersion, darkMode, disabledWebShortcuts, activationShortcut, userAgent, showSystemTray, useLocalFile, identifier, name = 'pake-app', resizable = true, installerLanguage, hideOnClose, incognito, title, wasm, enableDragDrop, startToTray, forceInternalNavigation, internalUrlRegex, zoom, minWidth, minHeight, ignoreCertificateErrors, newWindow, camera, microphone, } = options;
739
+ const platform = asSupportedPlatform(process.platform);
740
+ const platformHideOnClose = hideOnClose ?? platform === 'darwin';
741
+ const tauriConfWindowOptions = {
742
+ width,
743
+ height,
744
+ fullscreen,
745
+ maximize,
746
+ resizable,
747
+ hide_title_bar: hideTitleBar,
748
+ activation_shortcut: activationShortcut,
749
+ always_on_top: alwaysOnTop,
750
+ dark_mode: darkMode,
751
+ disabled_web_shortcuts: disabledWebShortcuts,
752
+ hide_on_close: platformHideOnClose,
753
+ incognito,
754
+ title,
755
+ enable_wasm: wasm,
756
+ enable_drag_drop: enableDragDrop,
757
+ start_to_tray: startToTray && showSystemTray,
758
+ force_internal_navigation: forceInternalNavigation,
759
+ internal_url_regex: internalUrlRegex,
760
+ zoom,
761
+ min_width: minWidth,
762
+ min_height: minHeight,
763
+ ignore_certificate_errors: ignoreCertificateErrors,
764
+ new_window: newWindow,
765
+ };
766
+ Object.assign(tauriConf.pake.windows[0], { url, ...tauriConfWindowOptions });
767
+ tauriConf.productName = name;
768
+ tauriConf.identifier = identifier;
769
+ tauriConf.version = appVersion;
770
+ const linuxBinaryName = `pake-${generateLinuxPackageName(name)}`;
771
+ tauriConf.mainBinaryName =
772
+ platform === 'linux'
773
+ ? linuxBinaryName
774
+ : `pake-${generateIdentifierSafeName(name)}`;
775
+ if (platform === 'win32') {
776
+ const windowsBundle = tauriConf.bundle.windows;
777
+ if (!windowsBundle) {
778
+ throw new Error('Windows bundle configuration is missing from tauri.windows.conf.json; cannot build Windows target.');
779
+ }
780
+ windowsBundle.wix.language[0] = installerLanguage;
781
+ }
782
+ await handleLocalFile(url, useLocalFile, tauriConf);
783
+ const platformMap = {
784
+ win32: 'windows',
785
+ linux: 'linux',
786
+ darwin: 'macos',
787
+ };
788
+ const currentPlatform = platformMap[platform];
789
+ if (userAgent.length > 0) {
790
+ tauriConf.pake.user_agent[currentPlatform] = userAgent;
791
+ }
792
+ tauriConf.pake.system_tray[currentPlatform] = showSystemTray;
793
+ if (platform === 'linux') {
794
+ await mergeLinuxConfig(options, name, tauriConf, linuxBinaryName);
795
+ }
796
+ if (platform === 'darwin') {
797
+ const validMacTargets = ['app', 'dmg'];
798
+ if (validMacTargets.includes(options.targets)) {
799
+ tauriConf.bundle.targets = [options.targets];
800
+ }
801
+ }
802
+ const safeAppName = getSafeAppName(name);
803
+ await mergeIcons(options, name, tauriConf, platform, safeAppName);
804
+ await injectCustomCode(options, tauriConf);
805
+ if (platform === 'darwin') {
806
+ await generateMacEntitlements(camera, microphone);
807
+ }
808
+ await writeAllConfigs(tauriConf, platform);
809
+ }
801
810
 
802
811
  class BaseBuilder {
803
812
  constructor(options) {
@@ -884,7 +893,7 @@ class BaseBuilder {
884
893
  }
885
894
  else {
886
895
  logger.error('โœ• Rust required to package your webapp.');
887
- process.exit(0);
896
+ process.exit(1);
888
897
  }
889
898
  }
890
899
  const isChina = await isChinaDomain('www.npmjs.com');
@@ -1093,6 +1102,10 @@ class BaseBuilder {
1093
1102
  if (this.options.debug) {
1094
1103
  fullCommand += ' --verbose';
1095
1104
  }
1105
+ const features = this.getBuildFeatures();
1106
+ if (features.length > 0) {
1107
+ fullCommand += ` --features ${features.join(',')}`;
1108
+ }
1096
1109
  return fullCommand;
1097
1110
  }
1098
1111
  getBuildFeatures() {
@@ -1114,11 +1127,6 @@ class BaseBuilder {
1114
1127
  if (IS_MAC && this.options.targets === 'app') {
1115
1128
  fullCommand += ' --bundles app';
1116
1129
  }
1117
- // Add features
1118
- const features = this.getBuildFeatures();
1119
- if (features.length > 0) {
1120
- fullCommand += ` --features ${features.join(',')}`;
1121
- }
1122
1130
  return fullCommand;
1123
1131
  }
1124
1132
  getMacOSMajorVersion() {
@@ -1280,12 +1288,7 @@ class MacBuilder extends BaseBuilder {
1280
1288
  if (!buildTarget) {
1281
1289
  throw new Error(`Unsupported architecture: ${actualArch} for macOS`);
1282
1290
  }
1283
- let fullCommand = this.buildBaseCommand(packageManager, configPath, buildTarget);
1284
- const features = this.getBuildFeatures();
1285
- if (features.length > 0) {
1286
- fullCommand += ` --features ${features.join(',')}`;
1287
- }
1288
- return fullCommand;
1291
+ return this.buildBaseCommand(packageManager, configPath, buildTarget);
1289
1292
  }
1290
1293
  getBasePath() {
1291
1294
  const basePath = this.options.debug ? 'debug' : 'release';
@@ -1325,12 +1328,7 @@ class WinBuilder extends BaseBuilder {
1325
1328
  if (!buildTarget) {
1326
1329
  throw new Error(`Unsupported architecture: ${this.buildArch} for Windows`);
1327
1330
  }
1328
- let fullCommand = this.buildBaseCommand(packageManager, configPath, buildTarget);
1329
- const features = this.getBuildFeatures();
1330
- if (features.length > 0) {
1331
- fullCommand += ` --features ${features.join(',')}`;
1332
- }
1333
- return fullCommand;
1331
+ return this.buildBaseCommand(packageManager, configPath, buildTarget);
1334
1332
  }
1335
1333
  getBasePath() {
1336
1334
  const basePath = this.options.debug ? 'debug' : 'release';
@@ -1410,10 +1408,6 @@ class LinuxBuilder extends BaseBuilder {
1410
1408
  ? (this.getTauriTarget(this.buildArch, 'linux') ?? undefined)
1411
1409
  : undefined;
1412
1410
  let fullCommand = this.buildBaseCommand(packageManager, configPath, buildTarget);
1413
- const features = this.getBuildFeatures();
1414
- if (features.length > 0) {
1415
- fullCommand += ` --features ${features.join(',')}`;
1416
- }
1417
1411
  if (this.currentBuildType) {
1418
1412
  fullCommand += ` --bundles ${this.currentBuildType}`;
1419
1413
  }
@@ -1470,6 +1464,85 @@ class BuilderProvider {
1470
1464
  }
1471
1465
  }
1472
1466
 
1467
+ const LOCAL_HOST_SUFFIXES = [
1468
+ '.local',
1469
+ '.lan',
1470
+ '.internal',
1471
+ '.home',
1472
+ '.localdomain',
1473
+ ];
1474
+ const IPV4_ADDRESS_PATTERN = /^(\d{1,3}\.){3}\d{1,3}$/;
1475
+ function normalize(value) {
1476
+ return value.trim().toLowerCase();
1477
+ }
1478
+ function simplify(value) {
1479
+ return normalize(value).replace(/[\s._-]+/g, '');
1480
+ }
1481
+ function generateDashboardIconSlugs(appName) {
1482
+ const normalizedName = normalize(appName);
1483
+ if (!normalizedName) {
1484
+ return [];
1485
+ }
1486
+ const slugs = new Set([
1487
+ normalizedName,
1488
+ normalizedName.replace(/\s+/g, '-'),
1489
+ ]);
1490
+ return [...slugs].filter(Boolean);
1491
+ }
1492
+ function isLikelyLocalHostname(hostname) {
1493
+ const normalizedHostname = normalize(hostname);
1494
+ if (!normalizedHostname) {
1495
+ return false;
1496
+ }
1497
+ return (normalizedHostname === 'localhost' ||
1498
+ IPV4_ADDRESS_PATTERN.test(normalizedHostname) ||
1499
+ normalizedHostname.includes(':') ||
1500
+ !normalizedHostname.includes('.') ||
1501
+ LOCAL_HOST_SUFFIXES.some((suffix) => normalizedHostname.endsWith(suffix)));
1502
+ }
1503
+ function shouldPreferDashboardIcons(url, appName) {
1504
+ if (!appName) {
1505
+ return false;
1506
+ }
1507
+ try {
1508
+ const hostname = new URL(url).hostname.toLowerCase();
1509
+ if (!hostname) {
1510
+ return false;
1511
+ }
1512
+ if (isLikelyLocalHostname(hostname)) {
1513
+ return true;
1514
+ }
1515
+ const parsed = psl.parse(hostname);
1516
+ if (!('domain' in parsed) || !parsed.domain) {
1517
+ return true;
1518
+ }
1519
+ const registrableDomain = parsed.domain.toLowerCase();
1520
+ if (hostname === registrableDomain) {
1521
+ return false;
1522
+ }
1523
+ const subdomain = 'subdomain' in parsed && typeof parsed.subdomain === 'string'
1524
+ ? parsed.subdomain
1525
+ : '';
1526
+ if (!subdomain) {
1527
+ return false;
1528
+ }
1529
+ const productLabel = subdomain.split('.').pop() || '';
1530
+ const rootLabel = registrableDomain.split('.')[0] || '';
1531
+ const normalizedAppName = simplify(appName);
1532
+ return (normalizedAppName.length > 0 &&
1533
+ simplify(productLabel) === normalizedAppName &&
1534
+ simplify(rootLabel) !== normalizedAppName);
1535
+ }
1536
+ catch {
1537
+ return false;
1538
+ }
1539
+ }
1540
+ function getIconSourcePriority(url, appName) {
1541
+ return shouldPreferDashboardIcons(url, appName)
1542
+ ? ['dashboard', 'domain']
1543
+ : ['domain', 'dashboard'];
1544
+ }
1545
+
1473
1546
  const ICO_HEADER_SIZE = 6;
1474
1547
  const ICO_DIR_ENTRY_SIZE = 16;
1475
1548
  const ICO_TYPE_ICON = 1;
@@ -1571,11 +1644,48 @@ async function writeIcoWithPreferredSize(sourcePath, outputPath, preferredSize)
1571
1644
  return false;
1572
1645
  }
1573
1646
  }
1647
+ /**
1648
+ * Builds an ICO file from an array of PNG buffers using the PNG-in-ICO format
1649
+ * (supported since Windows Vista). This preserves alpha transparency.
1650
+ */
1651
+ function buildIcoFromPngBuffers(frames) {
1652
+ const count = frames.length;
1653
+ const headerSize = ICO_HEADER_SIZE + count * ICO_DIR_ENTRY_SIZE;
1654
+ const totalPayload = frames.reduce((acc, f) => acc + f.png.length, 0);
1655
+ const output = Buffer.alloc(headerSize + totalPayload);
1656
+ output.writeUInt16LE(0, 0);
1657
+ output.writeUInt16LE(ICO_TYPE_ICON, 2);
1658
+ output.writeUInt16LE(count, 4);
1659
+ let currentOffset = headerSize;
1660
+ for (let i = 0; i < count; i++) {
1661
+ const { size, png } = frames[i];
1662
+ const entryOffset = ICO_HEADER_SIZE + i * ICO_DIR_ENTRY_SIZE;
1663
+ const sizeByte = size >= 256 ? 0 : size;
1664
+ output.writeUInt8(sizeByte, entryOffset);
1665
+ output.writeUInt8(sizeByte, entryOffset + 1);
1666
+ output.writeUInt8(0, entryOffset + 2);
1667
+ output.writeUInt8(0, entryOffset + 3);
1668
+ output.writeUInt16LE(1, entryOffset + 4);
1669
+ output.writeUInt16LE(32, entryOffset + 6);
1670
+ output.writeUInt32LE(png.length, entryOffset + 8);
1671
+ output.writeUInt32LE(currentOffset, entryOffset + 12);
1672
+ png.copy(output, currentOffset);
1673
+ currentOffset += png.length;
1674
+ }
1675
+ return output;
1676
+ }
1574
1677
 
1575
1678
  const ICON_CONFIG = {
1576
1679
  minFileSize: 100,
1577
- supportedFormats: ['png', 'ico', 'jpeg', 'jpg', 'webp', 'icns'],
1578
- whiteBackground: { r: 255, g: 255, b: 255 },
1680
+ supportedFormats: [
1681
+ 'png',
1682
+ 'ico',
1683
+ 'jpeg',
1684
+ 'jpg',
1685
+ 'webp',
1686
+ 'icns',
1687
+ 'svg',
1688
+ ],
1579
1689
  transparentBackground: { r: 255, g: 255, b: 255, alpha: 0 },
1580
1690
  downloadTimeout: {
1581
1691
  ci: 5000,
@@ -1591,6 +1701,9 @@ const API_KEYS = {
1591
1701
  logoDev: ['pk_JLLMUKGZRpaG5YclhXaTkg', 'pk_Ph745P8mQSeYFfW2Wk039A'],
1592
1702
  brandfetch: ['1idqvJC0CeFSeyp3Yf7', '1idej-yhU_ThggIHFyG'],
1593
1703
  };
1704
+ /**
1705
+ * Generates platform-specific icon paths and handles copying for Windows
1706
+ */
1594
1707
  function generateIconPath(appName, isDefault = false) {
1595
1708
  const safeName = isDefault ? 'icon' : getIconBaseName(appName);
1596
1709
  const baseName = safeName;
@@ -1605,7 +1718,7 @@ function generateIconPath(appName, isDefault = false) {
1605
1718
  function getIconBaseName(appName) {
1606
1719
  const baseName = IS_LINUX
1607
1720
  ? generateLinuxPackageName(appName)
1608
- : generateSafeFilename(appName).toLowerCase();
1721
+ : getSafeAppName(appName);
1609
1722
  return baseName || 'pake-app';
1610
1723
  }
1611
1724
  async function copyWindowsIconIfNeeded(convertedPath, appName) {
@@ -1628,31 +1741,23 @@ async function copyWindowsIconIfNeeded(convertedPath, appName) {
1628
1741
  }
1629
1742
  }
1630
1743
  /**
1631
- * Adds white background to transparent icons only
1744
+ * Normalizes icon inputs to PNG while preserving alpha.
1632
1745
  */
1633
1746
  async function preprocessIcon(inputPath) {
1634
1747
  try {
1635
- const metadata = await sharp(inputPath).metadata();
1636
- if (metadata.channels !== 4)
1637
- return inputPath; // No transparency
1748
+ const extension = path.extname(inputPath).toLowerCase();
1749
+ const shouldNormalize = ['.png', '.jpeg', '.jpg', '.webp', '.svg'].includes(extension);
1750
+ if (!shouldNormalize) {
1751
+ return inputPath;
1752
+ }
1638
1753
  const { path: tempDir } = await dir();
1639
- const outputPath = path.join(tempDir, 'icon-with-background.png');
1640
- await sharp({
1641
- create: {
1642
- width: metadata.width || 512,
1643
- height: metadata.height || 512,
1644
- channels: 4,
1645
- background: { ...ICON_CONFIG.whiteBackground, alpha: 1 },
1646
- },
1647
- })
1648
- .composite([{ input: inputPath }])
1649
- .png()
1650
- .toFile(outputPath);
1754
+ const outputPath = path.join(tempDir, 'icon-normalized.png');
1755
+ await sharp(inputPath).ensureAlpha().png().toFile(outputPath);
1651
1756
  return outputPath;
1652
1757
  }
1653
1758
  catch (error) {
1654
1759
  if (error instanceof Error) {
1655
- logger.warn(`Failed to add background to icon: ${error.message}`);
1760
+ logger.warn(`Failed to normalize icon: ${error.message}`);
1656
1761
  }
1657
1762
  return inputPath;
1658
1763
  }
@@ -1719,15 +1824,22 @@ async function convertIconFormat(inputPath, appName) {
1719
1824
  const iconName = getIconBaseName(appName);
1720
1825
  // Generate platform-specific format
1721
1826
  if (IS_WIN) {
1722
- // Support multiple sizes for better Windows compatibility
1723
- await icongen(processedInputPath, platformOutputDir, {
1724
- report: false,
1725
- ico: {
1726
- name: `${iconName}_256`,
1727
- sizes: PLATFORM_CONFIG.win.sizes,
1728
- },
1729
- });
1730
- return path.join(platformOutputDir, `${iconName}_256${PLATFORM_CONFIG.win.format}`);
1827
+ const icoPath = path.join(platformOutputDir, `${iconName}_256${PLATFORM_CONFIG.win.format}`);
1828
+ const sourceBuffer = await fsExtra.readFile(processedInputPath);
1829
+ const frames = await Promise.all(PLATFORM_CONFIG.win.sizes.map(async (size) => {
1830
+ const png = await sharp(sourceBuffer)
1831
+ .resize(size, size, {
1832
+ fit: 'contain',
1833
+ background: { r: 0, g: 0, b: 0, alpha: 0 },
1834
+ })
1835
+ .ensureAlpha()
1836
+ .png()
1837
+ .toBuffer();
1838
+ return { size, png };
1839
+ }));
1840
+ const icoBuffer = buildIcoFromPngBuffers(frames);
1841
+ await fsExtra.outputFile(icoPath, icoBuffer);
1842
+ return icoPath;
1731
1843
  }
1732
1844
  if (IS_LINUX) {
1733
1845
  const outputPath = path.join(platformOutputDir, `${iconName}_${PLATFORM_CONFIG.linux.size}${PLATFORM_CONFIG.linux.format}`);
@@ -1882,15 +1994,74 @@ function generateIconServiceUrls(domain) {
1882
1994
  */
1883
1995
  function generateDashboardIconUrls(appName) {
1884
1996
  const baseUrl = 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png';
1885
- const name = appName.toLowerCase().trim();
1886
- const slugs = new Set();
1887
- // Exact name
1888
- slugs.add(name);
1889
- // Replace spaces with hyphens
1890
- slugs.add(name.replace(/\s+/g, '-'));
1891
- return [...slugs]
1892
- .filter((s) => s.length > 0)
1893
- .map((slug) => `${baseUrl}/${slug}.png`);
1997
+ return generateDashboardIconSlugs(appName).map((slug) => `${baseUrl}/${slug}.png`);
1998
+ }
1999
+ function isSupportedIconFormat(extension) {
2000
+ return ICON_CONFIG.supportedFormats.includes(extension);
2001
+ }
2002
+ function looksLikeSvg(arrayBuffer) {
2003
+ const sample = Buffer.from(arrayBuffer)
2004
+ .toString('utf-8', 0, Math.min(arrayBuffer.byteLength, 512))
2005
+ .trimStart()
2006
+ .toLowerCase();
2007
+ return (sample.startsWith('<svg') ||
2008
+ (sample.startsWith('<?xml') && sample.includes('<svg')));
2009
+ }
2010
+ function getUrlExtension(iconUrl) {
2011
+ try {
2012
+ return path.extname(new URL(iconUrl).pathname).slice(1).toLowerCase();
2013
+ }
2014
+ catch {
2015
+ return path.extname(iconUrl).slice(1).toLowerCase();
2016
+ }
2017
+ }
2018
+ async function detectDownloadedIconExtension(response, arrayBuffer, iconUrl) {
2019
+ const fileDetails = await fileTypeFromBuffer(arrayBuffer);
2020
+ if (fileDetails && isSupportedIconFormat(fileDetails.ext)) {
2021
+ return fileDetails.ext;
2022
+ }
2023
+ const contentType = response.headers
2024
+ .get('content-type')
2025
+ ?.split(';')[0]
2026
+ .trim();
2027
+ if (contentType === 'image/svg+xml' && looksLikeSvg(arrayBuffer)) {
2028
+ return 'svg';
2029
+ }
2030
+ if (getUrlExtension(iconUrl) === 'svg' && looksLikeSvg(arrayBuffer)) {
2031
+ return 'svg';
2032
+ }
2033
+ return null;
2034
+ }
2035
+ async function resolveIconFromUrl(iconUrl, appName, downloadTimeout) {
2036
+ const iconPath = await downloadIcon(iconUrl, false, downloadTimeout);
2037
+ if (!iconPath) {
2038
+ return null;
2039
+ }
2040
+ const convertedPath = await convertIconFormat(iconPath, appName);
2041
+ if (!convertedPath) {
2042
+ return null;
2043
+ }
2044
+ return await copyWindowsIconIfNeeded(convertedPath, appName);
2045
+ }
2046
+ async function tryResolveIconSource(source, domain, appName, downloadTimeout) {
2047
+ const iconUrls = source === 'dashboard'
2048
+ ? generateDashboardIconUrls(appName)
2049
+ : generateIconServiceUrls(domain);
2050
+ for (const iconUrl of iconUrls) {
2051
+ try {
2052
+ const resolvedPath = await resolveIconFromUrl(iconUrl, appName, downloadTimeout);
2053
+ if (resolvedPath) {
2054
+ return resolvedPath;
2055
+ }
2056
+ }
2057
+ catch (error) {
2058
+ if (error instanceof Error) {
2059
+ const label = source === 'dashboard' ? 'Dashboard icon' : 'Icon service';
2060
+ logger.debug(`${label} ${iconUrl} failed: ${error.message}`);
2061
+ }
2062
+ }
2063
+ }
2064
+ return null;
1894
2065
  }
1895
2066
  /**
1896
2067
  * Attempts to fetch favicon from website
@@ -1903,49 +2074,16 @@ async function tryGetFavicon(url, appName) {
1903
2074
  const downloadTimeout = isCI
1904
2075
  ? ICON_CONFIG.downloadTimeout.ci
1905
2076
  : ICON_CONFIG.downloadTimeout.default;
1906
- const serviceUrls = generateIconServiceUrls(domain);
1907
- for (const serviceUrl of serviceUrls) {
1908
- try {
1909
- const faviconPath = await downloadIcon(serviceUrl, false, downloadTimeout);
1910
- if (!faviconPath)
1911
- continue;
1912
- const convertedPath = await convertIconFormat(faviconPath, appName);
1913
- if (convertedPath) {
1914
- const finalPath = await copyWindowsIconIfNeeded(convertedPath, appName);
1915
- spinner.succeed(chalk.green('Icon fetched and converted successfully!'));
1916
- return finalPath;
1917
- }
1918
- }
1919
- catch (error) {
1920
- if (error instanceof Error) {
1921
- logger.debug(`Icon service ${serviceUrl} failed: ${error.message}`);
1922
- }
2077
+ const sourcePriority = getIconSourcePriority(url, appName);
2078
+ for (const source of sourcePriority) {
2079
+ const resolvedIconPath = await tryResolveIconSource(source, domain, appName, downloadTimeout);
2080
+ if (!resolvedIconPath) {
1923
2081
  continue;
1924
2082
  }
1925
- }
1926
- // Final fallback for selfhosted apps behind auth where domain-based
1927
- // services cannot access the site favicon.
1928
- if (appName) {
1929
- const dashboardIconUrls = generateDashboardIconUrls(appName);
1930
- for (const iconUrl of dashboardIconUrls) {
1931
- try {
1932
- const iconPath = await downloadIcon(iconUrl, false, downloadTimeout);
1933
- if (!iconPath)
1934
- continue;
1935
- const convertedPath = await convertIconFormat(iconPath, appName);
1936
- if (convertedPath) {
1937
- const finalPath = await copyWindowsIconIfNeeded(convertedPath, appName);
1938
- spinner.succeed(chalk.green(`Icon found via dashboard-icons fallback for "${appName}"!`));
1939
- return finalPath;
1940
- }
1941
- }
1942
- catch (error) {
1943
- if (error instanceof Error) {
1944
- logger.debug(`Dashboard icon ${iconUrl} failed: ${error.message}`);
1945
- }
1946
- continue;
1947
- }
1948
- }
2083
+ spinner.succeed(chalk.green(source === 'dashboard'
2084
+ ? `Icon found via dashboard-icons for "${appName}"!`
2085
+ : 'Icon fetched and converted successfully!'));
2086
+ return resolvedIconPath;
1949
2087
  }
1950
2088
  spinner.warn(`No favicon found for ${domain}. Using default.`);
1951
2089
  return null;
@@ -1979,12 +2117,11 @@ async function downloadIcon(iconUrl, showSpinner = true, customTimeout) {
1979
2117
  const arrayBuffer = await response.arrayBuffer();
1980
2118
  if (!arrayBuffer || arrayBuffer.byteLength < ICON_CONFIG.minFileSize)
1981
2119
  return null;
1982
- const fileDetails = await fileTypeFromBuffer(arrayBuffer);
1983
- if (!fileDetails ||
1984
- !ICON_CONFIG.supportedFormats.includes(fileDetails.ext)) {
2120
+ const extension = await detectDownloadedIconExtension(response, arrayBuffer, iconUrl);
2121
+ if (!extension) {
1985
2122
  return null;
1986
2123
  }
1987
- return await saveIconFile(arrayBuffer, fileDetails.ext);
2124
+ return await saveIconFile(arrayBuffer, extension);
1988
2125
  }
1989
2126
  catch (error) {
1990
2127
  clearTimeout(timeoutId);
@@ -2068,11 +2205,9 @@ function resolveLocalAppName(filePath, platform) {
2068
2205
  return normalized || 'pake-app';
2069
2206
  }
2070
2207
  function isValidName(name, platform) {
2071
- const platformRegexMapping = {
2072
- linux: /^[a-z0-9\u4e00-\u9fff][a-z0-9\u4e00-\u9fff-]*$/,
2073
- default: /^[a-zA-Z0-9\u4e00-\u9fff][a-zA-Z0-9\u4e00-\u9fff- ]*$/,
2074
- };
2075
- const reg = platformRegexMapping[platform] || platformRegexMapping.default;
2208
+ const reg = platform === 'linux'
2209
+ ? /^[a-z0-9\u4e00-\u9fff][a-z0-9\u4e00-\u9fff-]*$/
2210
+ : /^[a-zA-Z0-9\u4e00-\u9fff][a-zA-Z0-9\u4e00-\u9fff- ]*$/;
2076
2211
  return !!name && reg.test(name);
2077
2212
  }
2078
2213
  async function handleOptions(options, url) {
@@ -2177,7 +2312,7 @@ function validateNumberInput(value) {
2177
2312
  return parsedValue;
2178
2313
  }
2179
2314
  function validateUrlInput(url) {
2180
- const isFile = fs.existsSync(url);
2315
+ const isFile = fs$1.existsSync(url);
2181
2316
  if (!isFile) {
2182
2317
  try {
2183
2318
  return normalizeUrl(url);
@@ -2327,7 +2462,9 @@ ${green('|_| \\__,_|_|\\_\\___| can turn any webpage into a desktop app with
2327
2462
  .addOption(new Option('--new-window', 'Allow sites to open new windows (for auth flows, tabs, branches)')
2328
2463
  .default(DEFAULT_PAKE_OPTIONS.newWindow)
2329
2464
  .hideHelp())
2330
- .option('--install', 'Auto-install app to /Applications (macOS) after build and remove local bundle', DEFAULT_PAKE_OPTIONS.install)
2465
+ .addOption(new Option('--install', 'Auto-install app to /Applications (macOS) after build and remove local bundle')
2466
+ .default(DEFAULT_PAKE_OPTIONS.install)
2467
+ .hideHelp())
2331
2468
  .addOption(new Option('--camera', 'Request camera permission on macOS')
2332
2469
  .default(DEFAULT_PAKE_OPTIONS.camera)
2333
2470
  .hideHelp())