pake-cli 3.11.4 โ†’ 3.11.6

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
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import log from 'loglevel';
3
+ import chalk from 'chalk';
3
4
  import updateNotifier from 'update-notifier';
4
5
  import path from 'path';
5
6
  import fsExtra from 'fs-extra';
6
7
  import { fileURLToPath } from 'url';
7
- import chalk from 'chalk';
8
8
  import prompts from 'prompts';
9
9
  import os from 'os';
10
10
  import { execa, execaSync } from 'execa';
@@ -20,7 +20,7 @@ import { InvalidArgumentError, program as program$1, Option } from 'commander';
20
20
  import fs$1 from 'fs';
21
21
 
22
22
  var name = "pake-cli";
23
- var version = "3.11.4";
23
+ var version = "3.11.6";
24
24
  var description = "๐Ÿคฑ๐Ÿป Turn any webpage into a desktop app with one command. ๐Ÿคฑ๐Ÿป ไธ€้”ฎๆ‰“ๅŒ…็ฝ‘้กต็”Ÿๆˆ่ฝป้‡ๆกŒ้ขๅบ”็”จใ€‚";
25
25
  var engines = {
26
26
  node: ">=18.0.0"
@@ -436,6 +436,42 @@ function generateIdentifierSafeName(name) {
436
436
  return cleaned;
437
437
  }
438
438
 
439
+ /**
440
+ * Pure transform from CLI options to the window-config slice that gets
441
+ * merged into pake.json. Exposed for snapshot testing so option drift
442
+ * (e.g. a new flag added in cli-program.ts but forgotten here) is caught.
443
+ *
444
+ * Keep this function side-effect free.
445
+ */
446
+ function buildWindowConfigOverrides(options, platform = asSupportedPlatform(process.platform)) {
447
+ const platformHideOnClose = options.hideOnClose ?? platform === 'darwin';
448
+ return {
449
+ width: options.width,
450
+ height: options.height,
451
+ fullscreen: options.fullscreen,
452
+ maximize: options.maximize,
453
+ resizable: options.resizable ?? true,
454
+ hide_title_bar: options.hideTitleBar,
455
+ activation_shortcut: options.activationShortcut,
456
+ always_on_top: options.alwaysOnTop,
457
+ dark_mode: options.darkMode,
458
+ disabled_web_shortcuts: options.disabledWebShortcuts,
459
+ hide_on_close: platformHideOnClose,
460
+ incognito: options.incognito,
461
+ title: options.title,
462
+ enable_wasm: options.wasm,
463
+ enable_drag_drop: options.enableDragDrop,
464
+ start_to_tray: options.startToTray && options.showSystemTray,
465
+ force_internal_navigation: options.forceInternalNavigation,
466
+ internal_url_regex: options.internalUrlRegex,
467
+ enable_find: options.enableFind,
468
+ zoom: options.zoom,
469
+ min_width: options.minWidth,
470
+ min_height: options.minHeight,
471
+ ignore_certificate_errors: options.ignoreCertificateErrors,
472
+ new_window: options.newWindow,
473
+ };
474
+ }
439
475
  function asSupportedPlatform(platform) {
440
476
  if (platform !== 'win32' && platform !== 'darwin' && platform !== 'linux') {
441
477
  throw new Error(`Pake only supports win32, darwin, and linux; detected '${platform}'.`);
@@ -691,34 +727,9 @@ async function writeAllConfigs(tauriConf, platform) {
691
727
  }
692
728
  async function mergeConfig(url, options, tauriConf) {
693
729
  await copyTemplateConfigs();
694
- 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;
730
+ const { appVersion, userAgent, showSystemTray, useLocalFile, identifier, name = 'pake-app', installerLanguage, wasm, camera, microphone, } = options;
695
731
  const platform = asSupportedPlatform(process.platform);
696
- const platformHideOnClose = hideOnClose ?? platform === 'darwin';
697
- const tauriConfWindowOptions = {
698
- width,
699
- height,
700
- fullscreen,
701
- maximize,
702
- resizable,
703
- hide_title_bar: hideTitleBar,
704
- activation_shortcut: activationShortcut,
705
- always_on_top: alwaysOnTop,
706
- dark_mode: darkMode,
707
- disabled_web_shortcuts: disabledWebShortcuts,
708
- hide_on_close: platformHideOnClose,
709
- incognito,
710
- title,
711
- enable_wasm: wasm,
712
- enable_drag_drop: enableDragDrop,
713
- start_to_tray: startToTray && showSystemTray,
714
- force_internal_navigation: forceInternalNavigation,
715
- internal_url_regex: internalUrlRegex,
716
- zoom,
717
- min_width: minWidth,
718
- min_height: minHeight,
719
- ignore_certificate_errors: ignoreCertificateErrors,
720
- new_window: newWindow,
721
- };
732
+ const tauriConfWindowOptions = buildWindowConfigOverrides(options, platform);
722
733
  Object.assign(tauriConf.pake.windows[0], { url, ...tauriConfWindowOptions });
723
734
  tauriConf.productName = name;
724
735
  tauriConf.identifier = identifier;
@@ -764,104 +775,138 @@ async function mergeConfig(url, options, tauriConf) {
764
775
  await writeAllConfigs(tauriConf, platform);
765
776
  }
766
777
 
767
- class BaseBuilder {
768
- constructor(options) {
769
- this.options = options;
770
- }
771
- getBuildEnvironment() {
772
- if (!IS_MAC) {
773
- return undefined;
774
- }
775
- const currentPath = process.env.PATH || '';
776
- const systemToolsPath = '/usr/bin';
777
- const buildPath = currentPath.startsWith(`${systemToolsPath}:`)
778
- ? currentPath
779
- : `${systemToolsPath}:${currentPath}`;
780
- return {
781
- CFLAGS: '-fno-modules',
782
- CXXFLAGS: '-fno-modules',
783
- MACOSX_DEPLOYMENT_TARGET: '14.0',
784
- PATH: buildPath,
785
- };
778
+ /**
779
+ * Returns build environment variables overrides for macOS, where Rust crates
780
+ * sometimes need explicit C/C++ flags and a deterministic SDK target. Other
781
+ * platforms inherit `process.env` unchanged.
782
+ */
783
+ function getBuildEnvironment() {
784
+ if (!IS_MAC) {
785
+ return undefined;
786
786
  }
787
- getInstallTimeout() {
788
- // Windows needs more time due to native compilation and antivirus scanning
789
- return process.platform === 'win32' ? 900000 : 600000;
787
+ const currentPath = process.env.PATH || '';
788
+ const systemToolsPath = '/usr/bin';
789
+ const buildPath = currentPath.startsWith(`${systemToolsPath}:`)
790
+ ? currentPath
791
+ : `${systemToolsPath}:${currentPath}`;
792
+ return {
793
+ CFLAGS: '-fno-modules',
794
+ CXXFLAGS: '-fno-modules',
795
+ MACOSX_DEPLOYMENT_TARGET: '14.0',
796
+ PATH: buildPath,
797
+ };
798
+ }
799
+ /**
800
+ * Windows needs more time due to native compilation and antivirus scanning.
801
+ */
802
+ function getInstallTimeout() {
803
+ return process.platform === 'win32' ? 900000 : 600000;
804
+ }
805
+ function getBuildTimeout() {
806
+ return 900000;
807
+ }
808
+ let packageManagerCache = null;
809
+ /**
810
+ * Returns 'pnpm' when available, otherwise 'npm'. Throws if neither is found.
811
+ * Cached after the first successful detection so tests can call repeatedly.
812
+ */
813
+ async function detectPackageManager() {
814
+ if (packageManagerCache) {
815
+ return packageManagerCache;
790
816
  }
791
- getBuildTimeout() {
792
- return 900000;
817
+ const { execa } = await import('execa');
818
+ try {
819
+ await execa('pnpm', ['--version'], { stdio: 'ignore' });
820
+ logger.info('โœบ Using pnpm for package management.');
821
+ packageManagerCache = 'pnpm';
822
+ return 'pnpm';
793
823
  }
794
- async detectPackageManager() {
795
- if (BaseBuilder.packageManagerCache) {
796
- return BaseBuilder.packageManagerCache;
797
- }
798
- const { execa } = await import('execa');
824
+ catch {
799
825
  try {
800
- await execa('pnpm', ['--version'], { stdio: 'ignore' });
801
- logger.info('โœบ Using pnpm for package management.');
802
- BaseBuilder.packageManagerCache = 'pnpm';
803
- return 'pnpm';
826
+ await execa('npm', ['--version'], { stdio: 'ignore' });
827
+ logger.info('โœบ pnpm not available, using npm for package management.');
828
+ packageManagerCache = 'npm';
829
+ return 'npm';
804
830
  }
805
831
  catch {
806
- try {
807
- await execa('npm', ['--version'], { stdio: 'ignore' });
808
- logger.info('โœบ pnpm not available, using npm for package management.');
809
- BaseBuilder.packageManagerCache = 'npm';
810
- return 'npm';
811
- }
812
- catch {
813
- throw new Error('Neither pnpm nor npm is available. Please install a package manager.');
814
- }
832
+ throw new Error('Neither pnpm nor npm is available. Please install a package manager.');
815
833
  }
816
834
  }
817
- async copyFileWithSamePathGuard(sourcePath, destinationPath) {
818
- if (path.resolve(sourcePath) === path.resolve(destinationPath)) {
835
+ }
836
+ function getInstallCommand(packageManager, useCnMirror) {
837
+ const registryOption = useCnMirror
838
+ ? ' --registry=https://registry.npmmirror.com'
839
+ : '';
840
+ const peerDepsOption = packageManager === 'npm' ? ' --legacy-peer-deps' : '';
841
+ return `cd "${npmDirectory}" && ${packageManager} install${registryOption}${peerDepsOption}`;
842
+ }
843
+ async function copyFileWithSamePathGuard(sourcePath, destinationPath) {
844
+ if (path.resolve(sourcePath) === path.resolve(destinationPath)) {
845
+ return;
846
+ }
847
+ try {
848
+ await fsExtra.copy(sourcePath, destinationPath, { overwrite: true });
849
+ }
850
+ catch (error) {
851
+ if (error instanceof Error &&
852
+ error.message.includes('Source and destination must not be the same')) {
819
853
  return;
820
854
  }
821
- try {
822
- await fsExtra.copy(sourcePath, destinationPath, { overwrite: true });
823
- }
824
- catch (error) {
825
- if (error instanceof Error &&
826
- error.message.includes('Source and destination must not be the same')) {
827
- return;
828
- }
829
- throw error;
830
- }
855
+ throw error;
831
856
  }
832
- getInstallCommand(packageManager, useCnMirror) {
833
- const registryOption = useCnMirror
834
- ? ' --registry=https://registry.npmmirror.com'
835
- : '';
836
- const peerDepsOption = packageManager === 'npm' ? ' --legacy-peer-deps' : '';
837
- return `cd "${npmDirectory}" && ${packageManager} install${registryOption}${peerDepsOption}`;
857
+ }
858
+ function isGeneratedCnMirrorConfig(projectConfig, cnMirrorConfig) {
859
+ return projectConfig.trim() === cnMirrorConfig.trim();
860
+ }
861
+ /**
862
+ * Toggles `.cargo/config.toml` to point at rsproxy.cn when the user opts in
863
+ * via `PAKE_USE_CN_MIRROR=1`, and removes the auto-generated mirror config
864
+ * (or warns about a manual one) when they opt out.
865
+ */
866
+ async function configureCargoRegistry(tauriSrcPath, useCnMirror) {
867
+ const rustProjectDir = path.join(tauriSrcPath, '.cargo');
868
+ const projectConf = path.join(rustProjectDir, 'config.toml');
869
+ const projectCnConf = path.join(tauriSrcPath, 'rust_proxy.toml');
870
+ if (useCnMirror) {
871
+ await fsExtra.ensureDir(rustProjectDir);
872
+ await copyFileWithSamePathGuard(projectCnConf, projectConf);
873
+ return;
838
874
  }
839
- isGeneratedCnMirrorConfig(projectConfig, cnMirrorConfig) {
840
- return projectConfig.trim() === cnMirrorConfig.trim();
875
+ if (!(await fsExtra.pathExists(projectConf))) {
876
+ return;
841
877
  }
842
- async configureCargoRegistry(tauriSrcPath, useCnMirror) {
843
- const rustProjectDir = path.join(tauriSrcPath, '.cargo');
844
- const projectConf = path.join(rustProjectDir, 'config.toml');
845
- const projectCnConf = path.join(tauriSrcPath, 'rust_proxy.toml');
846
- if (useCnMirror) {
847
- await fsExtra.ensureDir(rustProjectDir);
848
- await this.copyFileWithSamePathGuard(projectCnConf, projectConf);
849
- return;
850
- }
851
- if (!(await fsExtra.pathExists(projectConf))) {
852
- return;
853
- }
854
- const [projectConfig, cnMirrorConfig] = await Promise.all([
855
- fsExtra.readFile(projectConf, 'utf8'),
856
- fsExtra.readFile(projectCnConf, 'utf8'),
857
- ]);
858
- if (this.isGeneratedCnMirrorConfig(projectConfig, cnMirrorConfig)) {
859
- await fsExtra.remove(projectConf);
860
- return;
861
- }
862
- if (projectConfig.includes('rsproxy.cn')) {
863
- logger.warn(`โœผ ${projectConf} still references rsproxy.cn. Remove it or set ${CN_MIRROR_ENV}=1 if you want to use the CN mirror.`);
864
- }
878
+ const [projectConfig, cnMirrorConfig] = await Promise.all([
879
+ fsExtra.readFile(projectConf, 'utf8'),
880
+ fsExtra.readFile(projectCnConf, 'utf8'),
881
+ ]);
882
+ if (isGeneratedCnMirrorConfig(projectConfig, cnMirrorConfig)) {
883
+ await fsExtra.remove(projectConf);
884
+ return;
885
+ }
886
+ if (projectConfig.includes('rsproxy.cn')) {
887
+ logger.warn(`โœผ ${projectConf} still references rsproxy.cn. Remove it or set ${CN_MIRROR_ENV}=1 if you want to use the CN mirror.`);
888
+ }
889
+ }
890
+ /**
891
+ * Returns true when an error string looks like the well-known Tauri+linuxdeploy
892
+ * strip failure that we automatically retry with NO_STRIP=1.
893
+ */
894
+ function isLinuxDeployStripError(error) {
895
+ if (!(error instanceof Error) || !error.message) {
896
+ return false;
897
+ }
898
+ const message = error.message.toLowerCase();
899
+ return (message.includes('linuxdeploy') ||
900
+ message.includes('failed to run linuxdeploy') ||
901
+ message.includes('strip:') ||
902
+ message.includes('unable to recognise the format of the input file') ||
903
+ message.includes('appimage tool failed') ||
904
+ message.includes('strip tool'));
905
+ }
906
+
907
+ class BaseBuilder {
908
+ constructor(options) {
909
+ this.options = options;
865
910
  }
866
911
  async prepare() {
867
912
  const tauriSrcPath = path.join(npmDirectory, 'src-tauri');
@@ -888,11 +933,10 @@ class BaseBuilder {
888
933
  }
889
934
  const spinner = getSpinner('Installing package...');
890
935
  const useCnMirror = isCnMirrorEnabled();
891
- await this.configureCargoRegistry(tauriSrcPath, useCnMirror);
892
- // Detect available package manager
893
- const packageManager = await this.detectPackageManager();
894
- const timeout = this.getInstallTimeout();
895
- const buildEnv = this.getBuildEnvironment();
936
+ await configureCargoRegistry(tauriSrcPath, useCnMirror);
937
+ const packageManager = await detectPackageManager();
938
+ const timeout = getInstallTimeout();
939
+ const buildEnv = getBuildEnvironment();
896
940
  // Show helpful message for first-time users
897
941
  if (!tauriTargetPathExists) {
898
942
  logger.info(process.platform === 'win32'
@@ -903,7 +947,7 @@ class BaseBuilder {
903
947
  logger.info(`โœบ ${CN_MIRROR_ENV}=1 detected, using ${packageManager}/rsProxy CN mirror.`);
904
948
  }
905
949
  try {
906
- await shellExec(this.getInstallCommand(packageManager, useCnMirror), timeout, {
950
+ await shellExec(getInstallCommand(packageManager, useCnMirror), timeout, {
907
951
  ...buildEnv,
908
952
  CI: 'true',
909
953
  });
@@ -926,7 +970,7 @@ class BaseBuilder {
926
970
  async start(url) {
927
971
  logger.info('Pake dev server starting...');
928
972
  await mergeConfig(url, this.options, tauriConfig);
929
- const packageManager = await this.detectPackageManager();
973
+ const packageManager = await detectPackageManager();
930
974
  const configPath = path.join(npmDirectory, 'src-tauri', '.pake', 'tauri.conf.json');
931
975
  const features = this.getBuildFeatures();
932
976
  const featureArgs = features.length > 0 ? `--features ${features.join(',')}` : '';
@@ -937,8 +981,7 @@ class BaseBuilder {
937
981
  async buildAndCopy(url, target) {
938
982
  const { name = 'pake-app' } = this.options;
939
983
  await mergeConfig(url, this.options, tauriConfig);
940
- // Detect available package manager
941
- const packageManager = await this.detectPackageManager();
984
+ const packageManager = await detectPackageManager();
942
985
  // Build app
943
986
  const buildSpinner = getSpinner('Building app...');
944
987
  // Let spinner run for a moment so user can see it, then stop before package manager command
@@ -946,7 +989,7 @@ class BaseBuilder {
946
989
  buildSpinner.stop();
947
990
  // Show static message to keep the status visible
948
991
  logger.warn('โœธ Building app...');
949
- const baseEnv = this.getBuildEnvironment();
992
+ const baseEnv = getBuildEnvironment();
950
993
  let buildEnv = {
951
994
  ...(baseEnv ?? {}),
952
995
  ...(process.env.NO_STRIP ? { NO_STRIP: process.env.NO_STRIP } : {}),
@@ -962,7 +1005,7 @@ class BaseBuilder {
962
1005
  }
963
1006
  }
964
1007
  const buildCommand = `cd "${npmDirectory}" && ${this.getBuildCommand(packageManager)}`;
965
- const buildTimeout = this.getBuildTimeout();
1008
+ const buildTimeout = getBuildTimeout();
966
1009
  try {
967
1010
  await shellExec(buildCommand, buildTimeout, resolveExecEnv());
968
1011
  }
@@ -970,7 +1013,7 @@ class BaseBuilder {
970
1013
  const shouldRetryWithoutStrip = process.platform === 'linux' &&
971
1014
  target === 'appimage' &&
972
1015
  !buildEnv.NO_STRIP &&
973
- this.isLinuxDeployStripError(error);
1016
+ isLinuxDeployStripError(error);
974
1017
  if (shouldRetryWithoutStrip) {
975
1018
  logger.warn('โš  AppImage build failed during linuxdeploy strip step, retrying with NO_STRIP=1 automatically.');
976
1019
  buildEnv = {
@@ -1026,18 +1069,6 @@ class BaseBuilder {
1026
1069
  getFileType(target) {
1027
1070
  return target;
1028
1071
  }
1029
- isLinuxDeployStripError(error) {
1030
- if (!(error instanceof Error) || !error.message) {
1031
- return false;
1032
- }
1033
- const message = error.message.toLowerCase();
1034
- return (message.includes('linuxdeploy') ||
1035
- message.includes('failed to run linuxdeploy') ||
1036
- message.includes('strip:') ||
1037
- message.includes('unable to recognise the format of the input file') ||
1038
- message.includes('appimage tool failed') ||
1039
- message.includes('strip tool'));
1040
- }
1041
1072
  resolveTargetArch(requestedArch) {
1042
1073
  if (requestedArch === 'auto' || !requestedArch) {
1043
1074
  return process.arch;
@@ -1175,7 +1206,6 @@ class BaseBuilder {
1175
1206
  return 'src-tauri/target'; // Override in subclasses if needed
1176
1207
  }
1177
1208
  }
1178
- BaseBuilder.packageManagerCache = null;
1179
1209
  BaseBuilder.ARCH_MAPPINGS = {
1180
1210
  darwin: {
1181
1211
  arm64: 'aarch64-apple-darwin',
@@ -1511,6 +1541,9 @@ function getIconSourcePriority(url, appName) {
1511
1541
  const ICO_HEADER_SIZE = 6;
1512
1542
  const ICO_DIR_ENTRY_SIZE = 16;
1513
1543
  const ICO_TYPE_ICON = 1;
1544
+ // Standard Windows icon sizes covering tray (16/24/32), taskbar (32/48),
1545
+ // shell (48/256) and high-DPI (128/256). Issue #1190.
1546
+ const WIN_STANDARD_ICO_SIZES = [16, 24, 32, 48, 64, 128, 256];
1514
1547
  function decodeDimension(value) {
1515
1548
  return value === 0 ? 256 : value;
1516
1549
  }
@@ -1609,6 +1642,91 @@ async function writeIcoWithPreferredSize(sourcePath, outputPath, preferredSize)
1609
1642
  return false;
1610
1643
  }
1611
1644
  }
1645
+ /**
1646
+ * PNG signature `\x89PNG`. ICO frames may carry either a BMP DIB or an
1647
+ * embedded PNG payload (PNG-in-ICO, supported since Windows Vista).
1648
+ */
1649
+ const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47]);
1650
+ function frameLooksLikePng(entry) {
1651
+ return (entry.data.length >= PNG_SIGNATURE.length &&
1652
+ entry.data.subarray(0, PNG_SIGNATURE.length).equals(PNG_SIGNATURE));
1653
+ }
1654
+ async function decodeFrameToPng(entry) {
1655
+ if (frameLooksLikePng(entry)) {
1656
+ return Buffer.from(entry.data);
1657
+ }
1658
+ // BMP DIB frames need to go through sharp's ico-to-PNG path, which only
1659
+ // works on the full ICO container. Fall back to letting the caller use a
1660
+ // sharp pipeline against the original ICO for the missing source.
1661
+ return null;
1662
+ }
1663
+ async function pickLargestFrameAsPng(buffer, entries) {
1664
+ const largest = [...entries].sort((a, b) => Math.max(b.width, b.height) - Math.max(a.width, a.height))[0];
1665
+ if (largest) {
1666
+ const decoded = await decodeFrameToPng(largest);
1667
+ if (decoded) {
1668
+ return decoded;
1669
+ }
1670
+ }
1671
+ // Fallback: let sharp render directly from the ICO buffer. sharp picks the
1672
+ // largest embedded frame on its own.
1673
+ try {
1674
+ return await sharp(buffer).png().toBuffer();
1675
+ }
1676
+ catch {
1677
+ return null;
1678
+ }
1679
+ }
1680
+ /**
1681
+ * Ensures the produced ICO carries every Windows standard size so the OS
1682
+ * never has to downsample a 256x256 frame to 16x16 for the tray.
1683
+ * Falls back to `writeIcoWithPreferredSize` if rendering fails.
1684
+ *
1685
+ * Issue #1190.
1686
+ */
1687
+ async function ensureMultiResolutionIco(sourcePath, outputPath, preferredSize = 256, desiredSizes = WIN_STANDARD_ICO_SIZES) {
1688
+ try {
1689
+ const sourceBuffer = await fsExtra.readFile(sourcePath);
1690
+ const entries = parseIcoBuffer(sourceBuffer);
1691
+ const sourcePng = await pickLargestFrameAsPng(sourceBuffer, entries);
1692
+ if (!sourcePng) {
1693
+ return await writeIcoWithPreferredSize(sourcePath, outputPath, preferredSize);
1694
+ }
1695
+ const frames = await Promise.all(desiredSizes.map(async (size) => {
1696
+ // Reuse an existing exact-size PNG frame when possible to keep any
1697
+ // hand-tuned small icon (e.g. a 16x16 with deliberate pixel hinting).
1698
+ const exact = entries.find((entry) => entry.width === size && entry.height === size);
1699
+ if (exact && frameLooksLikePng(exact)) {
1700
+ return { size, png: Buffer.from(exact.data) };
1701
+ }
1702
+ const png = await sharp(sourcePng)
1703
+ .resize(size, size, {
1704
+ fit: 'contain',
1705
+ background: { r: 0, g: 0, b: 0, alpha: 0 },
1706
+ })
1707
+ .ensureAlpha()
1708
+ .png()
1709
+ .toBuffer();
1710
+ return { size, png };
1711
+ }));
1712
+ // Order frames so the preferred size lands first (Windows shell uses the
1713
+ // first-listed frame as a quality hint when choosing which to display).
1714
+ frames.sort((a, b) => {
1715
+ const aExact = a.size === preferredSize ? 0 : 1;
1716
+ const bExact = b.size === preferredSize ? 0 : 1;
1717
+ if (aExact !== bExact)
1718
+ return aExact - bExact;
1719
+ return b.size - a.size;
1720
+ });
1721
+ const icoBuffer = buildIcoFromPngBuffers(frames);
1722
+ await fsExtra.ensureDir(path.dirname(outputPath));
1723
+ await fsExtra.outputFile(outputPath, icoBuffer);
1724
+ return true;
1725
+ }
1726
+ catch {
1727
+ return await writeIcoWithPreferredSize(sourcePath, outputPath, preferredSize);
1728
+ }
1729
+ }
1612
1730
  /**
1613
1731
  * Builds an ICO file from an array of PNG buffers using the PNG-in-ICO format
1614
1732
  * (supported since Windows Vista). This preserves alpha transparency.
@@ -1658,7 +1776,7 @@ const ICON_CONFIG = {
1658
1776
  },
1659
1777
  };
1660
1778
  const PLATFORM_CONFIG = {
1661
- win: { format: '.ico', sizes: [16, 32, 48, 64, 128, 256] },
1779
+ win: { format: '.ico', sizes: [...WIN_STANDARD_ICO_SIZES] },
1662
1780
  linux: { format: '.png', size: 512 },
1663
1781
  macos: { format: '.icns', sizes: [16, 32, 64, 128, 256, 512, 1024] },
1664
1782
  };
@@ -1693,10 +1811,15 @@ async function copyWindowsIconIfNeeded(convertedPath, appName) {
1693
1811
  try {
1694
1812
  const finalIconPath = generateIconPath(appName);
1695
1813
  await fsExtra.ensureDir(path.dirname(finalIconPath));
1696
- // Reorder ICO to prioritize 256px icons for better Windows display
1697
- const reordered = await writeIcoWithPreferredSize(convertedPath, finalIconPath, 256);
1698
- if (!reordered) {
1699
- await fsExtra.copy(convertedPath, finalIconPath);
1814
+ // Re-render ICO so every Windows standard size is present and prefer the
1815
+ // 256px frame as the leading entry; falls back to plain reordering if the
1816
+ // ICO is non-decodable, then to a raw copy. (Issue #1190)
1817
+ const upgraded = await ensureMultiResolutionIco(convertedPath, finalIconPath, 256);
1818
+ if (!upgraded) {
1819
+ const reordered = await writeIcoWithPreferredSize(convertedPath, finalIconPath, 256);
1820
+ if (!reordered) {
1821
+ await fsExtra.copy(convertedPath, finalIconPath);
1822
+ }
1700
1823
  }
1701
1824
  return finalIconPath;
1702
1825
  }
@@ -2153,6 +2276,28 @@ function normalizeUrl(urlToNormalize) {
2153
2276
  }
2154
2277
  }
2155
2278
 
2279
+ /**
2280
+ * Error class used for user-facing CLI errors.
2281
+ *
2282
+ * The top-level catch in `bin/cli.ts` prints `message` directly without a
2283
+ * stack trace and exits with code 1. Use this for predictable failures
2284
+ * (invalid names, missing files, etc.) so users see a clean message instead
2285
+ * of a Node.js stack dump.
2286
+ */
2287
+ class PakeError extends Error {
2288
+ constructor(message) {
2289
+ super(message);
2290
+ this.isUserError = true;
2291
+ this.name = 'PakeError';
2292
+ }
2293
+ }
2294
+ function isPakeError(error) {
2295
+ return (error instanceof PakeError ||
2296
+ (typeof error === 'object' &&
2297
+ error !== null &&
2298
+ error.isUserError === true));
2299
+ }
2300
+
2156
2301
  function resolveAppName(name, platform) {
2157
2302
  const domain = getDomain(name) || 'pake';
2158
2303
  return platform !== 'linux' ? capitalizeFirstLetter(domain) : domain;
@@ -2195,13 +2340,13 @@ async function handleOptions(options, url) {
2195
2340
  const LINUX_NAME_ERROR = `โœ• Name should only include lowercase letters, numbers, and dashes (not leading dashes). Examples: com-123-xxx, 123pan, pan123, weread, we-read, 123.`;
2196
2341
  const DEFAULT_NAME_ERROR = `โœ• Name should only include letters, numbers, dashes, and spaces (not leading dashes and spaces). Examples: 123pan, 123Pan, Pan123, weread, WeRead, WERead, we-read, We Read, 123.`;
2197
2342
  const errorMsg = platform === 'linux' ? LINUX_NAME_ERROR : DEFAULT_NAME_ERROR;
2198
- logger.error(errorMsg);
2199
2343
  if (isActions) {
2344
+ logger.error(errorMsg);
2200
2345
  name = resolveAppName(url, platform);
2201
2346
  logger.warn(`โœผ Inside github actions, use the default name: ${name}`);
2202
2347
  }
2203
2348
  else {
2204
- process.exit(1);
2349
+ throw new PakeError(errorMsg);
2205
2350
  }
2206
2351
  }
2207
2352
  const resolvedName = name || 'pake-app';
@@ -2258,6 +2403,7 @@ const DEFAULT_PAKE_OPTIONS = {
2258
2403
  startToTray: false,
2259
2404
  forceInternalNavigation: false,
2260
2405
  internalUrlRegex: '',
2406
+ enableFind: false,
2261
2407
  iterativeBuild: false,
2262
2408
  zoom: 100,
2263
2409
  minWidth: 0,
@@ -2397,6 +2543,9 @@ ${green('|_| \\__,_|_|\\_\\___| can turn any webpage into a desktop app with
2397
2543
  .addOption(new Option('--internal-url-regex <string>', 'Regex pattern to match URLs that should be considered internal')
2398
2544
  .default(DEFAULT_PAKE_OPTIONS.internalUrlRegex)
2399
2545
  .hideHelp())
2546
+ .addOption(new Option('--enable-find', 'Enable in-page Find UI with Cmd/Ctrl+F/G shortcuts')
2547
+ .default(DEFAULT_PAKE_OPTIONS.enableFind)
2548
+ .hideHelp())
2400
2549
  .addOption(new Option('--installer-language <string>', 'Installer language')
2401
2550
  .default(DEFAULT_PAKE_OPTIONS.installerLanguage)
2402
2551
  .hideHelp())
@@ -2459,21 +2608,46 @@ async function checkUpdateTips() {
2459
2608
  });
2460
2609
  }
2461
2610
  program.action(async (url, options) => {
2462
- await checkUpdateTips();
2463
- if (!url) {
2464
- program.help({
2465
- error: false,
2466
- });
2467
- return;
2611
+ try {
2612
+ await checkUpdateTips();
2613
+ if (!url) {
2614
+ program.help({
2615
+ error: false,
2616
+ });
2617
+ return;
2618
+ }
2619
+ log.setDefaultLevel('info');
2620
+ log.setLevel('info');
2621
+ if (options.debug) {
2622
+ log.setLevel('debug');
2623
+ }
2624
+ const appOptions = await handleOptions(options, url);
2625
+ const builder = BuilderProvider.create(appOptions);
2626
+ await builder.prepare();
2627
+ await builder.build(url);
2628
+ }
2629
+ catch (error) {
2630
+ if (isPakeError(error)) {
2631
+ console.error(chalk.red(error.message));
2632
+ }
2633
+ else if (error instanceof Error) {
2634
+ console.error(chalk.red(`โœ• ${error.message}`));
2635
+ if (options?.debug && error.stack) {
2636
+ console.error(chalk.gray(error.stack));
2637
+ }
2638
+ }
2639
+ else {
2640
+ console.error(chalk.red(`โœ• Unexpected error: ${String(error)}`));
2641
+ }
2642
+ process.exit(1);
2643
+ }
2644
+ });
2645
+ program.parseAsync().catch((error) => {
2646
+ if (error instanceof Error) {
2647
+ console.error(chalk.red(`โœ• ${error.message}`));
2468
2648
  }
2469
- log.setDefaultLevel('info');
2470
- log.setLevel('info');
2471
- if (options.debug) {
2472
- log.setLevel('debug');
2649
+ else {
2650
+ console.error(chalk.red(`โœ• Unexpected error: ${String(error)}`));
2473
2651
  }
2474
- const appOptions = await handleOptions(options, url);
2475
- const builder = BuilderProvider.create(appOptions);
2476
- await builder.prepare();
2477
- await builder.build(url);
2652
+ process.exit(1);
2478
2653
  });
2479
- program.parse();
@@ -0,0 +1 @@
1
+ <!doctype html><html><head><meta charset="utf-8"><title>Pake Badge Test</title></head><body><h1>Pake Badge Test</h1><pre id="log"></pre><script>const log=(msg)=>{document.getElementById("log").textContent+=msg+"\n"};async function run(){await new Promise(r=>setTimeout(r,1500));log("setAppBadge(3)");await navigator.setAppBadge(3);setTimeout(async()=>{log("setAppBadge() dot");await navigator.setAppBadge();},7000);setTimeout(async()=>{log("clearAppBadge()");await navigator.clearAppBadge();},14000);}run().catch(e=>log(String(e)));</script></body></html>
@@ -0,0 +1 @@
1
+ <html><body><h1>Hello Pake</h1></body></html>