pake-cli 3.11.3 โ†’ 3.11.5

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,18 +1,15 @@
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';
11
11
  import crypto from 'crypto';
12
12
  import ora from 'ora';
13
- import dns from 'dns';
14
- import http from 'http';
15
- import { promisify } from 'util';
16
13
  import fs from 'fs/promises';
17
14
  import { dir } from 'tmp-promise';
18
15
  import { fileTypeFromBuffer } from 'file-type';
@@ -23,18 +20,18 @@ import { InvalidArgumentError, program as program$1, Option } from 'commander';
23
20
  import fs$1 from 'fs';
24
21
 
25
22
  var name = "pake-cli";
26
- var version = "3.11.3";
23
+ var version = "3.11.5";
27
24
  var description = "๐Ÿคฑ๐Ÿป Turn any webpage into a desktop app with one command. ๐Ÿคฑ๐Ÿป ไธ€้”ฎๆ‰“ๅŒ…็ฝ‘้กต็”Ÿๆˆ่ฝป้‡ๆกŒ้ขๅบ”็”จใ€‚";
28
25
  var engines = {
29
26
  node: ">=18.0.0"
30
27
  };
31
28
  var packageManager = "pnpm@10.26.2";
32
29
  var bin = {
33
- pake: "./dist/cli.js"
30
+ pake: "dist/cli.js"
34
31
  };
35
32
  var repository = {
36
33
  type: "git",
37
- url: "https://github.com/tw93/pake.git"
34
+ url: "git+https://github.com/tw93/pake.git"
38
35
  };
39
36
  var author = {
40
37
  name: "Tw93",
@@ -220,6 +217,12 @@ function getSpinner(text) {
220
217
  }).start();
221
218
  }
222
219
 
220
+ const TRUE_VALUES = new Set(['1', 'true', 'yes', 'on']);
221
+ const CN_MIRROR_ENV = 'PAKE_USE_CN_MIRROR';
222
+ function isCnMirrorEnabled(value = process.env[CN_MIRROR_ENV]) {
223
+ return TRUE_VALUES.has((value ?? '').trim().toLowerCase());
224
+ }
225
+
223
226
  const { platform: platform$1 } = process;
224
227
  const IS_MAC = platform$1 === 'darwin';
225
228
  const IS_WIN = platform$1 === 'win32';
@@ -279,69 +282,6 @@ async function shellExec(command, timeout = 300000, env) {
279
282
  }
280
283
  }
281
284
 
282
- const logger = {
283
- info(...msg) {
284
- log.info(...msg.map((m) => chalk.white(m)));
285
- },
286
- debug(...msg) {
287
- log.debug(...msg);
288
- },
289
- error(...msg) {
290
- log.error(...msg.map((m) => chalk.red(m)));
291
- },
292
- warn(...msg) {
293
- log.warn(...msg.map((m) => chalk.yellow(m)));
294
- },
295
- success(...msg) {
296
- log.info(...msg.map((m) => chalk.green(m)));
297
- },
298
- };
299
-
300
- const resolve = promisify(dns.resolve);
301
- const ping = async (host) => {
302
- const lookup = promisify(dns.lookup);
303
- const ip = await lookup(host);
304
- const start = new Date();
305
- // Prevent timeouts from affecting user experience.
306
- const requestPromise = new Promise((resolve, reject) => {
307
- const req = http.get(`http://${ip.address}`, (res) => {
308
- const delay = new Date().getTime() - start.getTime();
309
- res.resume();
310
- resolve(delay);
311
- });
312
- req.on('error', (err) => {
313
- reject(err);
314
- });
315
- });
316
- const timeoutPromise = new Promise((_, reject) => {
317
- setTimeout(() => {
318
- reject(new Error('Request timed out after 3 seconds'));
319
- }, 1000);
320
- });
321
- return Promise.race([requestPromise, timeoutPromise]);
322
- };
323
- async function isChinaDomain(domain) {
324
- try {
325
- const [ip] = await resolve(domain);
326
- return await isChinaIP(ip, domain);
327
- }
328
- catch (error) {
329
- logger.debug(`${domain} can't be parse!`);
330
- return true;
331
- }
332
- }
333
- async function isChinaIP(ip, domain) {
334
- try {
335
- const delay = await ping(ip);
336
- logger.debug(`${domain} latency is ${delay} ms`);
337
- return delay > 1000;
338
- }
339
- catch (error) {
340
- logger.debug(`ping ${domain} failed!`);
341
- return true;
342
- }
343
- }
344
-
345
285
  function normalizePathForComparison(targetPath) {
346
286
  const normalized = path.normalize(targetPath);
347
287
  return IS_WIN ? normalized.toLowerCase() : normalized;
@@ -389,15 +329,13 @@ function ensureRustEnv() {
389
329
  ensureCargoBinOnPath();
390
330
  }
391
331
  async function installRust() {
392
- const isActions = process.env.GITHUB_ACTIONS;
393
- const isInChina = await isChinaDomain('sh.rustup.rs');
394
- const rustInstallScriptForMac = isInChina && !isActions
332
+ const rustInstallScriptForUnix = isCnMirrorEnabled()
395
333
  ? 'export RUSTUP_DIST_SERVER="https://rsproxy.cn" && export RUSTUP_UPDATE_ROOT="https://rsproxy.cn/rustup" && curl --proto "=https" --tlsv1.2 -sSf https://rsproxy.cn/rustup-init.sh | sh'
396
334
  : "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y";
397
335
  const rustInstallScriptForWindows = 'winget install --id Rustlang.Rustup';
398
336
  const spinner = getSpinner('Downloading Rust...');
399
337
  try {
400
- await shellExec(IS_WIN ? rustInstallScriptForWindows : rustInstallScriptForMac, 300000, undefined);
338
+ await shellExec(IS_WIN ? rustInstallScriptForWindows : rustInstallScriptForUnix, 300000, undefined);
401
339
  spinner.succeed(chalk.green('โœ” Rust installed successfully!'));
402
340
  ensureRustEnv();
403
341
  }
@@ -443,6 +381,24 @@ async function combineFiles(files, output) {
443
381
  return files;
444
382
  }
445
383
 
384
+ const logger = {
385
+ info(...msg) {
386
+ log.info(...msg.map((m) => chalk.white(m)));
387
+ },
388
+ debug(...msg) {
389
+ log.debug(...msg);
390
+ },
391
+ error(...msg) {
392
+ log.error(...msg.map((m) => chalk.red(m)));
393
+ },
394
+ warn(...msg) {
395
+ log.warn(...msg.map((m) => chalk.yellow(m)));
396
+ },
397
+ success(...msg) {
398
+ log.info(...msg.map((m) => chalk.green(m)));
399
+ },
400
+ };
401
+
446
402
  function generateSafeFilename(name) {
447
403
  return name
448
404
  .replace(/[<>:"/\\|?*]/g, '_')
@@ -480,6 +436,41 @@ function generateIdentifierSafeName(name) {
480
436
  return cleaned;
481
437
  }
482
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
+ zoom: options.zoom,
468
+ min_width: options.minWidth,
469
+ min_height: options.minHeight,
470
+ ignore_certificate_errors: options.ignoreCertificateErrors,
471
+ new_window: options.newWindow,
472
+ };
473
+ }
483
474
  function asSupportedPlatform(platform) {
484
475
  if (platform !== 'win32' && platform !== 'darwin' && platform !== 'linux') {
485
476
  throw new Error(`Pake only supports win32, darwin, and linux; detected '${platform}'.`);
@@ -735,34 +726,9 @@ async function writeAllConfigs(tauriConf, platform) {
735
726
  }
736
727
  async function mergeConfig(url, options, tauriConf) {
737
728
  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;
729
+ const { appVersion, userAgent, showSystemTray, useLocalFile, identifier, name = 'pake-app', installerLanguage, wasm, camera, microphone, } = options;
739
730
  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
- };
731
+ const tauriConfWindowOptions = buildWindowConfigOverrides(options, platform);
766
732
  Object.assign(tauriConf.pake.windows[0], { url, ...tauriConfWindowOptions });
767
733
  tauriConf.productName = name;
768
734
  tauriConf.identifier = identifier;
@@ -808,70 +774,138 @@ async function mergeConfig(url, options, tauriConf) {
808
774
  await writeAllConfigs(tauriConf, platform);
809
775
  }
810
776
 
811
- class BaseBuilder {
812
- constructor(options) {
813
- this.options = options;
814
- }
815
- getBuildEnvironment() {
816
- if (!IS_MAC) {
817
- return undefined;
818
- }
819
- const currentPath = process.env.PATH || '';
820
- const systemToolsPath = '/usr/bin';
821
- const buildPath = currentPath.startsWith(`${systemToolsPath}:`)
822
- ? currentPath
823
- : `${systemToolsPath}:${currentPath}`;
824
- return {
825
- CFLAGS: '-fno-modules',
826
- CXXFLAGS: '-fno-modules',
827
- MACOSX_DEPLOYMENT_TARGET: '14.0',
828
- PATH: buildPath,
829
- };
777
+ /**
778
+ * Returns build environment variables overrides for macOS, where Rust crates
779
+ * sometimes need explicit C/C++ flags and a deterministic SDK target. Other
780
+ * platforms inherit `process.env` unchanged.
781
+ */
782
+ function getBuildEnvironment() {
783
+ if (!IS_MAC) {
784
+ return undefined;
830
785
  }
831
- getInstallTimeout() {
832
- // Windows needs more time due to native compilation and antivirus scanning
833
- return process.platform === 'win32' ? 900000 : 600000;
786
+ const currentPath = process.env.PATH || '';
787
+ const systemToolsPath = '/usr/bin';
788
+ const buildPath = currentPath.startsWith(`${systemToolsPath}:`)
789
+ ? currentPath
790
+ : `${systemToolsPath}:${currentPath}`;
791
+ return {
792
+ CFLAGS: '-fno-modules',
793
+ CXXFLAGS: '-fno-modules',
794
+ MACOSX_DEPLOYMENT_TARGET: '14.0',
795
+ PATH: buildPath,
796
+ };
797
+ }
798
+ /**
799
+ * Windows needs more time due to native compilation and antivirus scanning.
800
+ */
801
+ function getInstallTimeout() {
802
+ return process.platform === 'win32' ? 900000 : 600000;
803
+ }
804
+ function getBuildTimeout() {
805
+ return 900000;
806
+ }
807
+ let packageManagerCache = null;
808
+ /**
809
+ * Returns 'pnpm' when available, otherwise 'npm'. Throws if neither is found.
810
+ * Cached after the first successful detection so tests can call repeatedly.
811
+ */
812
+ async function detectPackageManager() {
813
+ if (packageManagerCache) {
814
+ return packageManagerCache;
834
815
  }
835
- getBuildTimeout() {
836
- return 900000;
816
+ const { execa } = await import('execa');
817
+ try {
818
+ await execa('pnpm', ['--version'], { stdio: 'ignore' });
819
+ logger.info('โœบ Using pnpm for package management.');
820
+ packageManagerCache = 'pnpm';
821
+ return 'pnpm';
837
822
  }
838
- async detectPackageManager() {
839
- if (BaseBuilder.packageManagerCache) {
840
- return BaseBuilder.packageManagerCache;
841
- }
842
- const { execa } = await import('execa');
823
+ catch {
843
824
  try {
844
- await execa('pnpm', ['--version'], { stdio: 'ignore' });
845
- logger.info('โœบ Using pnpm for package management.');
846
- BaseBuilder.packageManagerCache = 'pnpm';
847
- return 'pnpm';
825
+ await execa('npm', ['--version'], { stdio: 'ignore' });
826
+ logger.info('โœบ pnpm not available, using npm for package management.');
827
+ packageManagerCache = 'npm';
828
+ return 'npm';
848
829
  }
849
830
  catch {
850
- try {
851
- await execa('npm', ['--version'], { stdio: 'ignore' });
852
- logger.info('โœบ pnpm not available, using npm for package management.');
853
- BaseBuilder.packageManagerCache = 'npm';
854
- return 'npm';
855
- }
856
- catch {
857
- throw new Error('Neither pnpm nor npm is available. Please install a package manager.');
858
- }
831
+ throw new Error('Neither pnpm nor npm is available. Please install a package manager.');
859
832
  }
860
833
  }
861
- async copyFileWithSamePathGuard(sourcePath, destinationPath) {
862
- if (path.resolve(sourcePath) === path.resolve(destinationPath)) {
834
+ }
835
+ function getInstallCommand(packageManager, useCnMirror) {
836
+ const registryOption = useCnMirror
837
+ ? ' --registry=https://registry.npmmirror.com'
838
+ : '';
839
+ const peerDepsOption = packageManager === 'npm' ? ' --legacy-peer-deps' : '';
840
+ return `cd "${npmDirectory}" && ${packageManager} install${registryOption}${peerDepsOption}`;
841
+ }
842
+ async function copyFileWithSamePathGuard(sourcePath, destinationPath) {
843
+ if (path.resolve(sourcePath) === path.resolve(destinationPath)) {
844
+ return;
845
+ }
846
+ try {
847
+ await fsExtra.copy(sourcePath, destinationPath, { overwrite: true });
848
+ }
849
+ catch (error) {
850
+ if (error instanceof Error &&
851
+ error.message.includes('Source and destination must not be the same')) {
863
852
  return;
864
853
  }
865
- try {
866
- await fsExtra.copy(sourcePath, destinationPath, { overwrite: true });
867
- }
868
- catch (error) {
869
- if (error instanceof Error &&
870
- error.message.includes('Source and destination must not be the same')) {
871
- return;
872
- }
873
- throw error;
874
- }
854
+ throw error;
855
+ }
856
+ }
857
+ function isGeneratedCnMirrorConfig(projectConfig, cnMirrorConfig) {
858
+ return projectConfig.trim() === cnMirrorConfig.trim();
859
+ }
860
+ /**
861
+ * Toggles `.cargo/config.toml` to point at rsproxy.cn when the user opts in
862
+ * via `PAKE_USE_CN_MIRROR=1`, and removes the auto-generated mirror config
863
+ * (or warns about a manual one) when they opt out.
864
+ */
865
+ async function configureCargoRegistry(tauriSrcPath, useCnMirror) {
866
+ const rustProjectDir = path.join(tauriSrcPath, '.cargo');
867
+ const projectConf = path.join(rustProjectDir, 'config.toml');
868
+ const projectCnConf = path.join(tauriSrcPath, 'rust_proxy.toml');
869
+ if (useCnMirror) {
870
+ await fsExtra.ensureDir(rustProjectDir);
871
+ await copyFileWithSamePathGuard(projectCnConf, projectConf);
872
+ return;
873
+ }
874
+ if (!(await fsExtra.pathExists(projectConf))) {
875
+ return;
876
+ }
877
+ const [projectConfig, cnMirrorConfig] = await Promise.all([
878
+ fsExtra.readFile(projectConf, 'utf8'),
879
+ fsExtra.readFile(projectCnConf, 'utf8'),
880
+ ]);
881
+ if (isGeneratedCnMirrorConfig(projectConfig, cnMirrorConfig)) {
882
+ await fsExtra.remove(projectConf);
883
+ return;
884
+ }
885
+ if (projectConfig.includes('rsproxy.cn')) {
886
+ logger.warn(`โœผ ${projectConf} still references rsproxy.cn. Remove it or set ${CN_MIRROR_ENV}=1 if you want to use the CN mirror.`);
887
+ }
888
+ }
889
+ /**
890
+ * Returns true when an error string looks like the well-known Tauri+linuxdeploy
891
+ * strip failure that we automatically retry with NO_STRIP=1.
892
+ */
893
+ function isLinuxDeployStripError(error) {
894
+ if (!(error instanceof Error) || !error.message) {
895
+ return false;
896
+ }
897
+ const message = error.message.toLowerCase();
898
+ return (message.includes('linuxdeploy') ||
899
+ message.includes('failed to run linuxdeploy') ||
900
+ message.includes('strip:') ||
901
+ message.includes('unable to recognise the format of the input file') ||
902
+ message.includes('appimage tool failed') ||
903
+ message.includes('strip tool'));
904
+ }
905
+
906
+ class BaseBuilder {
907
+ constructor(options) {
908
+ this.options = options;
875
909
  }
876
910
  async prepare() {
877
911
  const tauriSrcPath = path.join(npmDirectory, 'src-tauri');
@@ -896,60 +930,34 @@ class BaseBuilder {
896
930
  process.exit(1);
897
931
  }
898
932
  }
899
- const isChina = await isChinaDomain('www.npmjs.com');
900
933
  const spinner = getSpinner('Installing package...');
901
- const rustProjectDir = path.join(tauriSrcPath, '.cargo');
902
- const projectConf = path.join(rustProjectDir, 'config.toml');
903
- await fsExtra.ensureDir(rustProjectDir);
904
- // Detect available package manager
905
- const packageManager = await this.detectPackageManager();
906
- const registryOption = ' --registry=https://registry.npmmirror.com';
907
- const peerDepsOption = packageManager === 'npm' ? ' --legacy-peer-deps' : '';
908
- const timeout = this.getInstallTimeout();
909
- const buildEnv = this.getBuildEnvironment();
934
+ const useCnMirror = isCnMirrorEnabled();
935
+ await configureCargoRegistry(tauriSrcPath, useCnMirror);
936
+ const packageManager = await detectPackageManager();
937
+ const timeout = getInstallTimeout();
938
+ const buildEnv = getBuildEnvironment();
910
939
  // Show helpful message for first-time users
911
940
  if (!tauriTargetPathExists) {
912
941
  logger.info(process.platform === 'win32'
913
942
  ? 'โœบ First-time setup may take 10-15 minutes on Windows (compiling dependencies)...'
914
943
  : 'โœบ First-time setup may take 5-10 minutes (installing dependencies)...');
915
944
  }
916
- let usedMirror = isChina;
945
+ if (useCnMirror) {
946
+ logger.info(`โœบ ${CN_MIRROR_ENV}=1 detected, using ${packageManager}/rsProxy CN mirror.`);
947
+ }
917
948
  try {
918
- if (isChina) {
919
- logger.info(`โœบ Located in China, using ${packageManager}/rsProxy CN mirror.`);
920
- const projectCnConf = path.join(tauriSrcPath, 'rust_proxy.toml');
921
- await this.copyFileWithSamePathGuard(projectCnConf, projectConf);
922
- await shellExec(`cd "${npmDirectory}" && ${packageManager} install${registryOption}${peerDepsOption}`, timeout, { ...buildEnv, CI: 'true' });
923
- }
924
- else {
925
- await shellExec(`cd "${npmDirectory}" && ${packageManager} install${peerDepsOption}`, timeout, { ...buildEnv, CI: 'true' });
926
- }
949
+ await shellExec(getInstallCommand(packageManager, useCnMirror), timeout, {
950
+ ...buildEnv,
951
+ CI: 'true',
952
+ });
927
953
  spinner.succeed(chalk.green('Package installed!'));
928
954
  }
929
955
  catch (error) {
930
- // If installation times out and we haven't tried the mirror yet, retry with mirror
931
- if (error instanceof Error &&
932
- error.message.includes('timed out') &&
933
- !usedMirror) {
934
- spinner.fail(chalk.yellow('Installation timed out, retrying with CN mirror...'));
935
- logger.info('โœบ Retrying installation with CN mirror for better speed...');
936
- const retrySpinner = getSpinner('Retrying installation...');
937
- usedMirror = true;
938
- try {
939
- const projectCnConf = path.join(tauriSrcPath, 'rust_proxy.toml');
940
- await this.copyFileWithSamePathGuard(projectCnConf, projectConf);
941
- await shellExec(`cd "${npmDirectory}" && ${packageManager} install${registryOption}${peerDepsOption}`, timeout, { ...buildEnv, CI: 'true' });
942
- retrySpinner.succeed(chalk.green('Package installed with CN mirror!'));
943
- }
944
- catch (retryError) {
945
- retrySpinner.fail(chalk.red('Installation failed'));
946
- throw retryError;
947
- }
948
- }
949
- else {
950
- spinner.fail(chalk.red('Installation failed'));
951
- throw error;
956
+ spinner.fail(chalk.red('Installation failed'));
957
+ if (!useCnMirror) {
958
+ logger.info(`โœบ If downloads are slow in China, retry with ${CN_MIRROR_ENV}=1 to use CN mirrors.`);
952
959
  }
960
+ throw error;
953
961
  }
954
962
  if (!tauriTargetPathExists) {
955
963
  logger.warn('โœผ The first packaging may be slow, please be patient and wait, it will be faster afterwards.');
@@ -961,7 +969,7 @@ class BaseBuilder {
961
969
  async start(url) {
962
970
  logger.info('Pake dev server starting...');
963
971
  await mergeConfig(url, this.options, tauriConfig);
964
- const packageManager = await this.detectPackageManager();
972
+ const packageManager = await detectPackageManager();
965
973
  const configPath = path.join(npmDirectory, 'src-tauri', '.pake', 'tauri.conf.json');
966
974
  const features = this.getBuildFeatures();
967
975
  const featureArgs = features.length > 0 ? `--features ${features.join(',')}` : '';
@@ -972,8 +980,7 @@ class BaseBuilder {
972
980
  async buildAndCopy(url, target) {
973
981
  const { name = 'pake-app' } = this.options;
974
982
  await mergeConfig(url, this.options, tauriConfig);
975
- // Detect available package manager
976
- const packageManager = await this.detectPackageManager();
983
+ const packageManager = await detectPackageManager();
977
984
  // Build app
978
985
  const buildSpinner = getSpinner('Building app...');
979
986
  // Let spinner run for a moment so user can see it, then stop before package manager command
@@ -981,7 +988,7 @@ class BaseBuilder {
981
988
  buildSpinner.stop();
982
989
  // Show static message to keep the status visible
983
990
  logger.warn('โœธ Building app...');
984
- const baseEnv = this.getBuildEnvironment();
991
+ const baseEnv = getBuildEnvironment();
985
992
  let buildEnv = {
986
993
  ...(baseEnv ?? {}),
987
994
  ...(process.env.NO_STRIP ? { NO_STRIP: process.env.NO_STRIP } : {}),
@@ -997,7 +1004,7 @@ class BaseBuilder {
997
1004
  }
998
1005
  }
999
1006
  const buildCommand = `cd "${npmDirectory}" && ${this.getBuildCommand(packageManager)}`;
1000
- const buildTimeout = this.getBuildTimeout();
1007
+ const buildTimeout = getBuildTimeout();
1001
1008
  try {
1002
1009
  await shellExec(buildCommand, buildTimeout, resolveExecEnv());
1003
1010
  }
@@ -1005,7 +1012,7 @@ class BaseBuilder {
1005
1012
  const shouldRetryWithoutStrip = process.platform === 'linux' &&
1006
1013
  target === 'appimage' &&
1007
1014
  !buildEnv.NO_STRIP &&
1008
- this.isLinuxDeployStripError(error);
1015
+ isLinuxDeployStripError(error);
1009
1016
  if (shouldRetryWithoutStrip) {
1010
1017
  logger.warn('โš  AppImage build failed during linuxdeploy strip step, retrying with NO_STRIP=1 automatically.');
1011
1018
  buildEnv = {
@@ -1061,18 +1068,6 @@ class BaseBuilder {
1061
1068
  getFileType(target) {
1062
1069
  return target;
1063
1070
  }
1064
- isLinuxDeployStripError(error) {
1065
- if (!(error instanceof Error) || !error.message) {
1066
- return false;
1067
- }
1068
- const message = error.message.toLowerCase();
1069
- return (message.includes('linuxdeploy') ||
1070
- message.includes('failed to run linuxdeploy') ||
1071
- message.includes('strip:') ||
1072
- message.includes('unable to recognise the format of the input file') ||
1073
- message.includes('appimage tool failed') ||
1074
- message.includes('strip tool'));
1075
- }
1076
1071
  resolveTargetArch(requestedArch) {
1077
1072
  if (requestedArch === 'auto' || !requestedArch) {
1078
1073
  return process.arch;
@@ -1210,7 +1205,6 @@ class BaseBuilder {
1210
1205
  return 'src-tauri/target'; // Override in subclasses if needed
1211
1206
  }
1212
1207
  }
1213
- BaseBuilder.packageManagerCache = null;
1214
1208
  BaseBuilder.ARCH_MAPPINGS = {
1215
1209
  darwin: {
1216
1210
  arm64: 'aarch64-apple-darwin',
@@ -1546,6 +1540,9 @@ function getIconSourcePriority(url, appName) {
1546
1540
  const ICO_HEADER_SIZE = 6;
1547
1541
  const ICO_DIR_ENTRY_SIZE = 16;
1548
1542
  const ICO_TYPE_ICON = 1;
1543
+ // Standard Windows icon sizes covering tray (16/24/32), taskbar (32/48),
1544
+ // shell (48/256) and high-DPI (128/256). Issue #1190.
1545
+ const WIN_STANDARD_ICO_SIZES = [16, 24, 32, 48, 64, 128, 256];
1549
1546
  function decodeDimension(value) {
1550
1547
  return value === 0 ? 256 : value;
1551
1548
  }
@@ -1644,6 +1641,91 @@ async function writeIcoWithPreferredSize(sourcePath, outputPath, preferredSize)
1644
1641
  return false;
1645
1642
  }
1646
1643
  }
1644
+ /**
1645
+ * PNG signature `\x89PNG`. ICO frames may carry either a BMP DIB or an
1646
+ * embedded PNG payload (PNG-in-ICO, supported since Windows Vista).
1647
+ */
1648
+ const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47]);
1649
+ function frameLooksLikePng(entry) {
1650
+ return (entry.data.length >= PNG_SIGNATURE.length &&
1651
+ entry.data.subarray(0, PNG_SIGNATURE.length).equals(PNG_SIGNATURE));
1652
+ }
1653
+ async function decodeFrameToPng(entry) {
1654
+ if (frameLooksLikePng(entry)) {
1655
+ return Buffer.from(entry.data);
1656
+ }
1657
+ // BMP DIB frames need to go through sharp's ico-to-PNG path, which only
1658
+ // works on the full ICO container. Fall back to letting the caller use a
1659
+ // sharp pipeline against the original ICO for the missing source.
1660
+ return null;
1661
+ }
1662
+ async function pickLargestFrameAsPng(buffer, entries) {
1663
+ const largest = [...entries].sort((a, b) => Math.max(b.width, b.height) - Math.max(a.width, a.height))[0];
1664
+ if (largest) {
1665
+ const decoded = await decodeFrameToPng(largest);
1666
+ if (decoded) {
1667
+ return decoded;
1668
+ }
1669
+ }
1670
+ // Fallback: let sharp render directly from the ICO buffer. sharp picks the
1671
+ // largest embedded frame on its own.
1672
+ try {
1673
+ return await sharp(buffer).png().toBuffer();
1674
+ }
1675
+ catch {
1676
+ return null;
1677
+ }
1678
+ }
1679
+ /**
1680
+ * Ensures the produced ICO carries every Windows standard size so the OS
1681
+ * never has to downsample a 256x256 frame to 16x16 for the tray.
1682
+ * Falls back to `writeIcoWithPreferredSize` if rendering fails.
1683
+ *
1684
+ * Issue #1190.
1685
+ */
1686
+ async function ensureMultiResolutionIco(sourcePath, outputPath, preferredSize = 256, desiredSizes = WIN_STANDARD_ICO_SIZES) {
1687
+ try {
1688
+ const sourceBuffer = await fsExtra.readFile(sourcePath);
1689
+ const entries = parseIcoBuffer(sourceBuffer);
1690
+ const sourcePng = await pickLargestFrameAsPng(sourceBuffer, entries);
1691
+ if (!sourcePng) {
1692
+ return await writeIcoWithPreferredSize(sourcePath, outputPath, preferredSize);
1693
+ }
1694
+ const frames = await Promise.all(desiredSizes.map(async (size) => {
1695
+ // Reuse an existing exact-size PNG frame when possible to keep any
1696
+ // hand-tuned small icon (e.g. a 16x16 with deliberate pixel hinting).
1697
+ const exact = entries.find((entry) => entry.width === size && entry.height === size);
1698
+ if (exact && frameLooksLikePng(exact)) {
1699
+ return { size, png: Buffer.from(exact.data) };
1700
+ }
1701
+ const png = await sharp(sourcePng)
1702
+ .resize(size, size, {
1703
+ fit: 'contain',
1704
+ background: { r: 0, g: 0, b: 0, alpha: 0 },
1705
+ })
1706
+ .ensureAlpha()
1707
+ .png()
1708
+ .toBuffer();
1709
+ return { size, png };
1710
+ }));
1711
+ // Order frames so the preferred size lands first (Windows shell uses the
1712
+ // first-listed frame as a quality hint when choosing which to display).
1713
+ frames.sort((a, b) => {
1714
+ const aExact = a.size === preferredSize ? 0 : 1;
1715
+ const bExact = b.size === preferredSize ? 0 : 1;
1716
+ if (aExact !== bExact)
1717
+ return aExact - bExact;
1718
+ return b.size - a.size;
1719
+ });
1720
+ const icoBuffer = buildIcoFromPngBuffers(frames);
1721
+ await fsExtra.ensureDir(path.dirname(outputPath));
1722
+ await fsExtra.outputFile(outputPath, icoBuffer);
1723
+ return true;
1724
+ }
1725
+ catch {
1726
+ return await writeIcoWithPreferredSize(sourcePath, outputPath, preferredSize);
1727
+ }
1728
+ }
1647
1729
  /**
1648
1730
  * Builds an ICO file from an array of PNG buffers using the PNG-in-ICO format
1649
1731
  * (supported since Windows Vista). This preserves alpha transparency.
@@ -1693,7 +1775,7 @@ const ICON_CONFIG = {
1693
1775
  },
1694
1776
  };
1695
1777
  const PLATFORM_CONFIG = {
1696
- win: { format: '.ico', sizes: [16, 32, 48, 64, 128, 256] },
1778
+ win: { format: '.ico', sizes: [...WIN_STANDARD_ICO_SIZES] },
1697
1779
  linux: { format: '.png', size: 512 },
1698
1780
  macos: { format: '.icns', sizes: [16, 32, 64, 128, 256, 512, 1024] },
1699
1781
  };
@@ -1728,10 +1810,15 @@ async function copyWindowsIconIfNeeded(convertedPath, appName) {
1728
1810
  try {
1729
1811
  const finalIconPath = generateIconPath(appName);
1730
1812
  await fsExtra.ensureDir(path.dirname(finalIconPath));
1731
- // Reorder ICO to prioritize 256px icons for better Windows display
1732
- const reordered = await writeIcoWithPreferredSize(convertedPath, finalIconPath, 256);
1733
- if (!reordered) {
1734
- await fsExtra.copy(convertedPath, finalIconPath);
1813
+ // Re-render ICO so every Windows standard size is present and prefer the
1814
+ // 256px frame as the leading entry; falls back to plain reordering if the
1815
+ // ICO is non-decodable, then to a raw copy. (Issue #1190)
1816
+ const upgraded = await ensureMultiResolutionIco(convertedPath, finalIconPath, 256);
1817
+ if (!upgraded) {
1818
+ const reordered = await writeIcoWithPreferredSize(convertedPath, finalIconPath, 256);
1819
+ if (!reordered) {
1820
+ await fsExtra.copy(convertedPath, finalIconPath);
1821
+ }
1735
1822
  }
1736
1823
  return finalIconPath;
1737
1824
  }
@@ -2188,6 +2275,28 @@ function normalizeUrl(urlToNormalize) {
2188
2275
  }
2189
2276
  }
2190
2277
 
2278
+ /**
2279
+ * Error class used for user-facing CLI errors.
2280
+ *
2281
+ * The top-level catch in `bin/cli.ts` prints `message` directly without a
2282
+ * stack trace and exits with code 1. Use this for predictable failures
2283
+ * (invalid names, missing files, etc.) so users see a clean message instead
2284
+ * of a Node.js stack dump.
2285
+ */
2286
+ class PakeError extends Error {
2287
+ constructor(message) {
2288
+ super(message);
2289
+ this.isUserError = true;
2290
+ this.name = 'PakeError';
2291
+ }
2292
+ }
2293
+ function isPakeError(error) {
2294
+ return (error instanceof PakeError ||
2295
+ (typeof error === 'object' &&
2296
+ error !== null &&
2297
+ error.isUserError === true));
2298
+ }
2299
+
2191
2300
  function resolveAppName(name, platform) {
2192
2301
  const domain = getDomain(name) || 'pake';
2193
2302
  return platform !== 'linux' ? capitalizeFirstLetter(domain) : domain;
@@ -2230,13 +2339,13 @@ async function handleOptions(options, url) {
2230
2339
  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.`;
2231
2340
  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.`;
2232
2341
  const errorMsg = platform === 'linux' ? LINUX_NAME_ERROR : DEFAULT_NAME_ERROR;
2233
- logger.error(errorMsg);
2234
2342
  if (isActions) {
2343
+ logger.error(errorMsg);
2235
2344
  name = resolveAppName(url, platform);
2236
2345
  logger.warn(`โœผ Inside github actions, use the default name: ${name}`);
2237
2346
  }
2238
2347
  else {
2239
- process.exit(1);
2348
+ throw new PakeError(errorMsg);
2240
2349
  }
2241
2350
  }
2242
2351
  const resolvedName = name || 'pake-app';
@@ -2494,21 +2603,46 @@ async function checkUpdateTips() {
2494
2603
  });
2495
2604
  }
2496
2605
  program.action(async (url, options) => {
2497
- await checkUpdateTips();
2498
- if (!url) {
2499
- program.help({
2500
- error: false,
2501
- });
2502
- return;
2606
+ try {
2607
+ await checkUpdateTips();
2608
+ if (!url) {
2609
+ program.help({
2610
+ error: false,
2611
+ });
2612
+ return;
2613
+ }
2614
+ log.setDefaultLevel('info');
2615
+ log.setLevel('info');
2616
+ if (options.debug) {
2617
+ log.setLevel('debug');
2618
+ }
2619
+ const appOptions = await handleOptions(options, url);
2620
+ const builder = BuilderProvider.create(appOptions);
2621
+ await builder.prepare();
2622
+ await builder.build(url);
2623
+ }
2624
+ catch (error) {
2625
+ if (isPakeError(error)) {
2626
+ console.error(chalk.red(error.message));
2627
+ }
2628
+ else if (error instanceof Error) {
2629
+ console.error(chalk.red(`โœ• ${error.message}`));
2630
+ if (options?.debug && error.stack) {
2631
+ console.error(chalk.gray(error.stack));
2632
+ }
2633
+ }
2634
+ else {
2635
+ console.error(chalk.red(`โœ• Unexpected error: ${String(error)}`));
2636
+ }
2637
+ process.exit(1);
2503
2638
  }
2504
- log.setDefaultLevel('info');
2505
- log.setLevel('info');
2506
- if (options.debug) {
2507
- log.setLevel('debug');
2639
+ });
2640
+ program.parseAsync().catch((error) => {
2641
+ if (error instanceof Error) {
2642
+ console.error(chalk.red(`โœ• ${error.message}`));
2643
+ }
2644
+ else {
2645
+ console.error(chalk.red(`โœ• Unexpected error: ${String(error)}`));
2508
2646
  }
2509
- const appOptions = await handleOptions(options, url);
2510
- const builder = BuilderProvider.create(appOptions);
2511
- await builder.prepare();
2512
- await builder.build(url);
2647
+ process.exit(1);
2513
2648
  });
2514
- program.parse();