pake-cli 3.3.7-beta โ†’ 3.4.1

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/README.md CHANGED
@@ -29,6 +29,7 @@
29
29
  - **Beginners**: Download ready-made [Popular Packages](#popular-packages) or use [Online Building](docs/github-actions-usage.md) with no environment setup required
30
30
  - **Developers**: Install [CLI Tool](docs/cli-usage.md) for one-command packaging of any website with customizable icons, window settings, and more
31
31
  - **Advanced Users**: Clone the project locally for [Custom Development](#development), or check [Advanced Usage](docs/advanced-usage.md) for style customization and feature enhancement
32
+ - **Troubleshooting**: Check [FAQ](docs/faq.md) for common issues and solutions
32
33
 
33
34
  ## Popular Packages
34
35
 
package/dist/cli.js CHANGED
@@ -6,6 +6,7 @@ import path from 'path';
6
6
  import fsExtra from 'fs-extra';
7
7
  import { fileURLToPath } from 'url';
8
8
  import prompts from 'prompts';
9
+ import os from 'os';
9
10
  import { execa, execaSync } from 'execa';
10
11
  import crypto from 'crypto';
11
12
  import ora from 'ora';
@@ -22,7 +23,7 @@ import sharp from 'sharp';
22
23
  import * as psl from 'psl';
23
24
 
24
25
  var name = "pake-cli";
25
- var version = "3.3.7-beta";
26
+ var version = "3.4.1";
26
27
  var description = "๐Ÿคฑ๐Ÿป Turn any webpage into a desktop app with one command. ๐Ÿคฑ๐Ÿป ไธ€้”ฎๆ‰“ๅŒ…็ฝ‘้กต็”Ÿๆˆ่ฝป้‡ๆกŒ้ขๅบ”็”จใ€‚";
27
28
  var engines = {
28
29
  node: ">=18.0.0"
@@ -74,12 +75,12 @@ var license = "MIT";
74
75
  var dependencies = {
75
76
  "@tauri-apps/api": "^2.8.0",
76
77
  "@tauri-apps/cli": "^2.8.4",
77
- axios: "^1.11.0",
78
- chalk: "^5.6.0",
78
+ axios: "^1.12.2",
79
+ chalk: "^5.6.2",
79
80
  commander: "^12.1.0",
80
81
  execa: "^9.6.0",
81
82
  "file-type": "^18.7.0",
82
- "fs-extra": "^11.3.1",
83
+ "fs-extra": "^11.3.2",
83
84
  "icon-gen": "^5.0.0",
84
85
  loglevel: "^1.9.2",
85
86
  ora: "^8.2.0",
@@ -96,7 +97,7 @@ var devDependencies = {
96
97
  "@rollup/plugin-replace": "^6.0.2",
97
98
  "@rollup/plugin-terser": "^0.4.4",
98
99
  "@types/fs-extra": "^11.0.4",
99
- "@types/node": "^20.19.13",
100
+ "@types/node": "^20.19.21",
100
101
  "@types/page-icon": "^0.3.6",
101
102
  "@types/prompts": "^2.4.9",
102
103
  "@types/tmp": "^0.2.6",
@@ -104,10 +105,10 @@ var devDependencies = {
104
105
  "app-root-path": "^3.1.0",
105
106
  "cross-env": "^7.0.3",
106
107
  prettier: "^3.6.2",
107
- rollup: "^4.50.0",
108
+ rollup: "^4.52.4",
108
109
  "rollup-plugin-typescript2": "^0.36.0",
109
110
  tslib: "^2.8.1",
110
- typescript: "^5.9.2"
111
+ typescript: "^5.9.3"
111
112
  };
112
113
  var packageJson = {
113
114
  name: name,
@@ -204,7 +205,9 @@ async function shellExec(command, timeout = 300000, env) {
204
205
  try {
205
206
  const { exitCode } = await execa(command, {
206
207
  cwd: npmDirectory,
207
- stdio: ['inherit', 'pipe', 'inherit'], // Hide stdout verbose, keep stderr
208
+ // Use 'inherit' to show all output directly to user in real-time.
209
+ // This ensures linuxdeploy and other tool outputs are visible during builds.
210
+ stdio: 'inherit',
208
211
  shell: true,
209
212
  timeout,
210
213
  env: env ? { ...process.env, ...env } : process.env,
@@ -217,7 +220,28 @@ async function shellExec(command, timeout = 300000, env) {
217
220
  if (error.timedOut) {
218
221
  throw new Error(`Command timed out after ${timeout}ms: "${command}". Try increasing timeout or check network connectivity.`);
219
222
  }
220
- throw new Error(`Error occurred while executing command "${command}". Exit code: ${exitCode}. Details: ${errorMessage}`);
223
+ let errorMsg = `Error occurred while executing command "${command}". Exit code: ${exitCode}. Details: ${errorMessage}`;
224
+ // Provide helpful guidance for common Linux AppImage build failures
225
+ // caused by strip tool incompatibility with modern glibc (2.38+)
226
+ if (process.platform === 'linux' &&
227
+ (errorMessage.includes('linuxdeploy') ||
228
+ errorMessage.includes('appimage') ||
229
+ errorMessage.includes('strip'))) {
230
+ errorMsg +=
231
+ '\n\nโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”\n' +
232
+ 'Linux AppImage Build Failed\n' +
233
+ 'โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”\n\n' +
234
+ 'Cause: Strip tool incompatibility with glibc 2.38+\n' +
235
+ ' (affects Debian Trixie, Arch Linux, and other modern distros)\n\n' +
236
+ 'Quick fix:\n' +
237
+ ' NO_STRIP=1 pake <url> --targets appimage --debug\n\n' +
238
+ 'Alternatives:\n' +
239
+ ' โ€ข Use DEB format: pake <url> --targets deb\n' +
240
+ ' โ€ข Update binutils: sudo apt install binutils (or pacman -S binutils)\n' +
241
+ ' โ€ข Detailed guide: https://github.com/tw93/Pake/blob/main/docs/faq.md\n' +
242
+ 'โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”';
243
+ }
244
+ throw new Error(errorMsg);
221
245
  }
222
246
  }
223
247
 
@@ -284,6 +308,52 @@ async function isChinaIP(ip, domain) {
284
308
  }
285
309
  }
286
310
 
311
+ function normalizePathForComparison(targetPath) {
312
+ const normalized = path.normalize(targetPath);
313
+ return IS_WIN ? normalized.toLowerCase() : normalized;
314
+ }
315
+ function getCargoHomeCandidates() {
316
+ const candidates = new Set();
317
+ if (process.env.CARGO_HOME) {
318
+ candidates.add(process.env.CARGO_HOME);
319
+ }
320
+ const homeDir = os.homedir();
321
+ if (homeDir) {
322
+ candidates.add(path.join(homeDir, '.cargo'));
323
+ }
324
+ if (IS_WIN && process.env.USERPROFILE) {
325
+ candidates.add(path.join(process.env.USERPROFILE, '.cargo'));
326
+ }
327
+ return Array.from(candidates).filter(Boolean);
328
+ }
329
+ function ensureCargoBinOnPath() {
330
+ const currentPath = process.env.PATH || '';
331
+ const segments = currentPath.split(path.delimiter).filter(Boolean);
332
+ const normalizedSegments = new Set(segments.map((segment) => normalizePathForComparison(segment)));
333
+ const additions = [];
334
+ let cargoHomeSet = Boolean(process.env.CARGO_HOME);
335
+ for (const cargoHome of getCargoHomeCandidates()) {
336
+ const binDir = path.join(cargoHome, 'bin');
337
+ if (fsExtra.pathExistsSync(binDir) &&
338
+ !normalizedSegments.has(normalizePathForComparison(binDir))) {
339
+ additions.push(binDir);
340
+ normalizedSegments.add(normalizePathForComparison(binDir));
341
+ }
342
+ if (!cargoHomeSet && fsExtra.pathExistsSync(cargoHome)) {
343
+ process.env.CARGO_HOME = cargoHome;
344
+ cargoHomeSet = true;
345
+ }
346
+ }
347
+ if (additions.length) {
348
+ const prefix = additions.join(path.delimiter);
349
+ process.env.PATH = segments.length
350
+ ? `${prefix}${path.delimiter}${segments.join(path.delimiter)}`
351
+ : prefix;
352
+ }
353
+ }
354
+ function ensureRustEnv() {
355
+ ensureCargoBinOnPath();
356
+ }
287
357
  async function installRust() {
288
358
  const isActions = process.env.GITHUB_ACTIONS;
289
359
  const isInChina = await isChinaDomain('sh.rustup.rs');
@@ -293,8 +363,9 @@ async function installRust() {
293
363
  const rustInstallScriptForWindows = 'winget install --id Rustlang.Rustup';
294
364
  const spinner = getSpinner('Downloading Rust...');
295
365
  try {
296
- await shellExec(IS_WIN ? rustInstallScriptForWindows : rustInstallScriptForMac);
366
+ await shellExec(IS_WIN ? rustInstallScriptForWindows : rustInstallScriptForMac, 300000, undefined);
297
367
  spinner.succeed(chalk.green('โœ” Rust installed successfully!'));
368
+ ensureRustEnv();
298
369
  }
299
370
  catch (error) {
300
371
  spinner.fail(chalk.red('โœ• Rust installation failed!'));
@@ -303,6 +374,7 @@ async function installRust() {
303
374
  }
304
375
  }
305
376
  function checkRustInstalled() {
377
+ ensureCargoBinOnPath();
306
378
  try {
307
379
  execaSync('rustc', ['--version']);
308
380
  return true;
@@ -343,14 +415,14 @@ function generateLinuxPackageName(name) {
343
415
  .replace(/-+/g, '-');
344
416
  }
345
417
  function generateIdentifierSafeName(name) {
346
- const cleaned = name
347
- .replace(/[^a-zA-Z0-9\u4e00-\u9fff]/g, '')
348
- .toLowerCase();
418
+ const cleaned = name.replace(/[^a-zA-Z0-9\u4e00-\u9fff]/g, '').toLowerCase();
349
419
  if (cleaned === '') {
350
420
  const fallback = Array.from(name)
351
- .map(char => {
421
+ .map((char) => {
352
422
  const code = char.charCodeAt(0);
353
- if ((code >= 48 && code <= 57) || (code >= 65 && code <= 90) || (code >= 97 && code <= 122)) {
423
+ if ((code >= 48 && code <= 57) ||
424
+ (code >= 65 && code <= 90) ||
425
+ (code >= 97 && code <= 122)) {
354
426
  return char.toLowerCase();
355
427
  }
356
428
  return code.toString(16);
@@ -362,6 +434,12 @@ function generateIdentifierSafeName(name) {
362
434
  return cleaned;
363
435
  }
364
436
 
437
+ /**
438
+ * Helper function to generate safe lowercase app name for file paths
439
+ */
440
+ function getSafeAppName(name) {
441
+ return generateSafeFilename(name).toLowerCase();
442
+ }
365
443
  async function mergeConfig(url, options, tauriConf) {
366
444
  // Ensure .pake directory exists and copy source templates if needed
367
445
  const srcTauriDir = path.join(npmDirectory, 'src-tauri');
@@ -382,13 +460,14 @@ async function mergeConfig(url, options, tauriConf) {
382
460
  await fsExtra.copy(sourcePath, destPath);
383
461
  }
384
462
  }));
385
- const { width, height, fullscreen, hideTitleBar, alwaysOnTop, appVersion, darkMode, disabledWebShortcuts, activationShortcut, userAgent, showSystemTray, systemTrayIcon, useLocalFile, identifier, name, resizable = true, inject, proxyUrl, installerLanguage, hideOnClose, incognito, title, wasm, enableDragDrop, } = options;
463
+ const { width, height, fullscreen, maximize, hideTitleBar, alwaysOnTop, appVersion, darkMode, disabledWebShortcuts, activationShortcut, userAgent, showSystemTray, systemTrayIcon, useLocalFile, identifier, name, resizable = true, inject, proxyUrl, installerLanguage, hideOnClose, incognito, title, wasm, enableDragDrop, multiInstance, startToTray, } = options;
386
464
  const { platform } = process;
387
465
  const platformHideOnClose = hideOnClose ?? platform === 'darwin';
388
466
  const tauriConfWindowOptions = {
389
467
  width,
390
468
  height,
391
469
  fullscreen,
470
+ maximize,
392
471
  resizable,
393
472
  hide_title_bar: hideTitleBar,
394
473
  activation_shortcut: activationShortcut,
@@ -400,6 +479,7 @@ async function mergeConfig(url, options, tauriConf) {
400
479
  title: title || null,
401
480
  enable_wasm: wasm,
402
481
  enable_drag_drop: enableDragDrop,
482
+ start_to_tray: startToTray && showSystemTray,
403
483
  };
404
484
  Object.assign(tauriConf.pake.windows[0], { url, ...tauriConfWindowOptions });
405
485
  tauriConf.productName = name;
@@ -452,7 +532,7 @@ async function mergeConfig(url, options, tauriConf) {
452
532
  // Remove hardcoded desktop files and regenerate with correct app name
453
533
  delete tauriConf.bundle.linux.deb.files;
454
534
  // Generate correct desktop file configuration
455
- const appNameSafe = generateSafeFilename(name).toLowerCase();
535
+ const appNameSafe = getSafeAppName(name);
456
536
  const identifier = `com.pake.${appNameSafe}`;
457
537
  const desktopFileName = `${identifier}.desktop`;
458
538
  // Create desktop file content
@@ -503,22 +583,23 @@ StartupNotify=true
503
583
  }
504
584
  }
505
585
  // Set icon.
586
+ const safeAppName = getSafeAppName(name);
506
587
  const platformIconMap = {
507
588
  win32: {
508
589
  fileExt: '.ico',
509
- path: `png/${generateSafeFilename(name).toLowerCase()}_256.ico`,
590
+ path: `png/${safeAppName}_256.ico`,
510
591
  defaultIcon: 'png/icon_256.ico',
511
592
  message: 'Windows icon must be .ico and 256x256px.',
512
593
  },
513
594
  linux: {
514
595
  fileExt: '.png',
515
- path: `png/${generateSafeFilename(name).toLowerCase()}_512.png`,
596
+ path: `png/${safeAppName}_512.png`,
516
597
  defaultIcon: 'png/icon_512.png',
517
598
  message: 'Linux icon must be .png and 512x512px.',
518
599
  },
519
600
  darwin: {
520
601
  fileExt: '.icns',
521
- path: `icons/${generateSafeFilename(name).toLowerCase()}.icns`,
602
+ path: `icons/${safeAppName}.icns`,
522
603
  defaultIcon: 'icons/icon.icns',
523
604
  message: 'macOS icon must be .icns type.',
524
605
  },
@@ -562,8 +643,8 @@ StartupNotify=true
562
643
  // ้œ€่ฆๅˆคๆ–ญๅ›พๆ ‡ๆ ผๅผ๏ผŒ้ป˜่ฎคๅชๆ”ฏๆŒicoๅ’Œpngไธค็ง
563
644
  let iconExt = path.extname(systemTrayIcon).toLowerCase();
564
645
  if (iconExt == '.png' || iconExt == '.ico') {
565
- const trayIcoPath = path.join(npmDirectory, `src-tauri/png/${generateSafeFilename(name).toLowerCase()}${iconExt}`);
566
- trayIconPath = `png/${generateSafeFilename(name).toLowerCase()}${iconExt}`;
646
+ const trayIcoPath = path.join(npmDirectory, `src-tauri/png/${safeAppName}${iconExt}`);
647
+ trayIconPath = `png/${safeAppName}${iconExt}`;
567
648
  await fsExtra.copy(systemTrayIcon, trayIcoPath);
568
649
  }
569
650
  else {
@@ -597,6 +678,7 @@ StartupNotify=true
597
678
  await fsExtra.writeFile(injectFilePath, '');
598
679
  }
599
680
  tauriConf.pake.proxy_url = proxyUrl || '';
681
+ tauriConf.pake.multi_instance = multiInstance;
600
682
  // Configure WASM support with required HTTP headers
601
683
  if (wasm) {
602
684
  tauriConf.app.security = {
@@ -673,6 +755,7 @@ class BaseBuilder {
673
755
  logger.warn('โœผ The first use requires installing system dependencies.');
674
756
  logger.warn('โœผ See more in https://tauri.app/start/prerequisites/.');
675
757
  }
758
+ ensureRustEnv();
676
759
  if (!checkRustInstalled()) {
677
760
  const res = await prompts({
678
761
  type: 'confirm',
@@ -705,10 +788,10 @@ class BaseBuilder {
705
788
  logger.info(`โœบ Located in China, using ${packageManager}/rsProxy CN mirror.`);
706
789
  const projectCnConf = path.join(tauriSrcPath, 'rust_proxy.toml');
707
790
  await fsExtra.copy(projectCnConf, projectConf);
708
- await shellExec(`cd "${npmDirectory}" && ${packageManager} install${registryOption}${peerDepsOption} --silent`, timeout, buildEnv);
791
+ await shellExec(`cd "${npmDirectory}" && ${packageManager} install${registryOption}${peerDepsOption}`, timeout, buildEnv);
709
792
  }
710
793
  else {
711
- await shellExec(`cd "${npmDirectory}" && ${packageManager} install${peerDepsOption} --silent`, timeout, buildEnv);
794
+ await shellExec(`cd "${npmDirectory}" && ${packageManager} install${peerDepsOption}`, timeout, buildEnv);
712
795
  }
713
796
  spinner.succeed(chalk.green('Package installed!'));
714
797
  if (!tauriTargetPathExists) {
@@ -733,7 +816,19 @@ class BaseBuilder {
733
816
  buildSpinner.stop();
734
817
  // Show static message to keep the status visible
735
818
  logger.warn('โœธ Building app...');
736
- const buildEnv = this.getBuildEnvironment();
819
+ const buildEnv = {
820
+ ...this.getBuildEnvironment(),
821
+ ...(process.env.NO_STRIP && { NO_STRIP: process.env.NO_STRIP }),
822
+ };
823
+ // Warn users about potential AppImage build failures on modern Linux systems.
824
+ // The linuxdeploy tool bundled in Tauri uses an older strip tool that doesn't
825
+ // recognize the .relr.dyn section introduced in glibc 2.38+.
826
+ if (process.platform === 'linux' && this.options.targets === 'appimage') {
827
+ if (!buildEnv.NO_STRIP) {
828
+ logger.warn('โš  Building AppImage on Linux may fail due to strip incompatibility with glibc 2.38+');
829
+ logger.warn('โš  If build fails, retry with: NO_STRIP=1 pake <url> --targets appimage');
830
+ }
831
+ }
737
832
  await shellExec(`cd "${npmDirectory}" && ${this.getBuildCommand(packageManager)}`, this.getBuildTimeout(), buildEnv);
738
833
  // Copy app
739
834
  const fileName = this.getFileName();
@@ -793,6 +888,11 @@ class BaseBuilder {
793
888
  if (target) {
794
889
  fullCommand += ` --target ${target}`;
795
890
  }
891
+ // Enable verbose output in debug mode to help diagnose build issues.
892
+ // This provides detailed logs from Tauri CLI and bundler tools.
893
+ if (this.options.debug) {
894
+ fullCommand += ' --verbose';
895
+ }
796
896
  return fullCommand;
797
897
  }
798
898
  /**
@@ -1106,6 +1206,12 @@ class LinuxBuilder extends BaseBuilder {
1106
1206
  if (features.length > 0) {
1107
1207
  fullCommand += ` --features ${features.join(',')}`;
1108
1208
  }
1209
+ // Enable verbose output for AppImage builds when debugging or PAKE_VERBOSE is set.
1210
+ // AppImage builds often fail with minimal error messages from linuxdeploy,
1211
+ // so verbose mode helps diagnose issues like strip failures and missing dependencies.
1212
+ if (this.options.targets === 'appimage' && (this.options.debug || process.env.PAKE_VERBOSE)) {
1213
+ fullCommand += ' --verbose';
1214
+ }
1109
1215
  return fullCommand;
1110
1216
  }
1111
1217
  getBasePath() {
@@ -1155,6 +1261,7 @@ const DEFAULT_PAKE_OPTIONS = {
1155
1261
  height: 780,
1156
1262
  width: 1200,
1157
1263
  fullscreen: false,
1264
+ maximize: false,
1158
1265
  hideTitleBar: false,
1159
1266
  alwaysOnTop: false,
1160
1267
  appVersion: '1.0.0',
@@ -1176,6 +1283,8 @@ const DEFAULT_PAKE_OPTIONS = {
1176
1283
  wasm: false,
1177
1284
  enableDragDrop: false,
1178
1285
  keepBinary: false,
1286
+ multiInstance: false,
1287
+ startToTray: false,
1179
1288
  };
1180
1289
 
1181
1290
  async function checkUpdateTips() {
@@ -1619,8 +1728,7 @@ ${green('|_| \\__,_|_|\\_\\___| can turn any webpage into a desktop app with
1619
1728
  program
1620
1729
  .addHelpText('beforeAll', logo)
1621
1730
  .usage(`[url] [options]`)
1622
- .showHelpAfterError()
1623
- .helpOption(false);
1731
+ .showHelpAfterError();
1624
1732
  program
1625
1733
  .argument('[url]', 'The web URL you want to package', validateUrlInput)
1626
1734
  // Refer to https://github.com/tj/commander.js#custom-option-processing, turn string array into a string connected with custom connectors.
@@ -1659,6 +1767,9 @@ program
1659
1767
  .addOption(new Option('--always-on-top', 'Always on the top level')
1660
1768
  .default(DEFAULT_PAKE_OPTIONS.alwaysOnTop)
1661
1769
  .hideHelp())
1770
+ .addOption(new Option('--maximize', 'Start window maximized')
1771
+ .default(DEFAULT_PAKE_OPTIONS.maximize)
1772
+ .hideHelp())
1662
1773
  .addOption(new Option('--dark-mode', 'Force Mac app to use dark mode')
1663
1774
  .default(DEFAULT_PAKE_OPTIONS.darkMode)
1664
1775
  .hideHelp())
@@ -1699,6 +1810,12 @@ program
1699
1810
  .addOption(new Option('--keep-binary', 'Keep raw binary file alongside installer')
1700
1811
  .default(DEFAULT_PAKE_OPTIONS.keepBinary)
1701
1812
  .hideHelp())
1813
+ .addOption(new Option('--multi-instance', 'Allow multiple app instances')
1814
+ .default(DEFAULT_PAKE_OPTIONS.multiInstance)
1815
+ .hideHelp())
1816
+ .addOption(new Option('--start-to-tray', 'Start app minimized to tray')
1817
+ .default(DEFAULT_PAKE_OPTIONS.startToTray)
1818
+ .hideHelp())
1702
1819
  .addOption(new Option('--installer-language <string>', 'Installer language')
1703
1820
  .default(DEFAULT_PAKE_OPTIONS.installerLanguage)
1704
1821
  .hideHelp())
@@ -1706,12 +1823,12 @@ program
1706
1823
  .configureHelp({
1707
1824
  sortSubcommands: true,
1708
1825
  optionTerm: (option) => {
1709
- if (option.flags === '-v, --version')
1826
+ if (option.flags === '-v, --version' || option.flags === '-h, --help')
1710
1827
  return '';
1711
1828
  return option.flags;
1712
1829
  },
1713
1830
  optionDescription: (option) => {
1714
- if (option.flags === '-v, --version')
1831
+ if (option.flags === '-v, --version' || option.flags === '-h, --help')
1715
1832
  return '';
1716
1833
  return option.description;
1717
1834
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pake-cli",
3
- "version": "3.3.7-beta",
3
+ "version": "3.4.1",
4
4
  "description": "๐Ÿคฑ๐Ÿป Turn any webpage into a desktop app with one command. ๐Ÿคฑ๐Ÿป ไธ€้”ฎๆ‰“ๅŒ…็ฝ‘้กต็”Ÿๆˆ่ฝป้‡ๆกŒ้ขๅบ”็”จใ€‚",
5
5
  "engines": {
6
6
  "node": ">=18.0.0"
@@ -52,12 +52,12 @@
52
52
  "dependencies": {
53
53
  "@tauri-apps/api": "^2.8.0",
54
54
  "@tauri-apps/cli": "^2.8.4",
55
- "axios": "^1.11.0",
56
- "chalk": "^5.6.0",
55
+ "axios": "^1.12.2",
56
+ "chalk": "^5.6.2",
57
57
  "commander": "^12.1.0",
58
58
  "execa": "^9.6.0",
59
59
  "file-type": "^18.7.0",
60
- "fs-extra": "^11.3.1",
60
+ "fs-extra": "^11.3.2",
61
61
  "icon-gen": "^5.0.0",
62
62
  "loglevel": "^1.9.2",
63
63
  "ora": "^8.2.0",
@@ -74,7 +74,7 @@
74
74
  "@rollup/plugin-replace": "^6.0.2",
75
75
  "@rollup/plugin-terser": "^0.4.4",
76
76
  "@types/fs-extra": "^11.0.4",
77
- "@types/node": "^20.19.13",
77
+ "@types/node": "^20.19.21",
78
78
  "@types/page-icon": "^0.3.6",
79
79
  "@types/prompts": "^2.4.9",
80
80
  "@types/tmp": "^0.2.6",
@@ -82,9 +82,9 @@
82
82
  "app-root-path": "^3.1.0",
83
83
  "cross-env": "^7.0.3",
84
84
  "prettier": "^3.6.2",
85
- "rollup": "^4.50.0",
85
+ "rollup": "^4.52.4",
86
86
  "rollup-plugin-typescript2": "^0.36.0",
87
87
  "tslib": "^2.8.1",
88
- "typescript": "^5.9.2"
88
+ "typescript": "^5.9.3"
89
89
  }
90
90
  }
@@ -16,6 +16,8 @@
16
16
  "incognito": false,
17
17
  "enable_wasm": false,
18
18
  "enable_drag_drop": false,
19
+ "maximize": false,
20
+ "start_to_tray": false,
19
21
  "title": null
20
22
  }
21
23
  ],
@@ -31,5 +33,6 @@
31
33
  },
32
34
  "system_tray_path": "png/icon_512.png",
33
35
  "inject": [],
34
- "proxy_url": ""
36
+ "proxy_url": "",
37
+ "multi_instance": false
35
38
  }
@@ -1,6 +1,6 @@
1
1
  # This file is automatically @generated by Cargo.
2
2
  # It is not intended for manual editing.
3
- version = 3
3
+ version = 4
4
4
 
5
5
  [[package]]
6
6
  name = "addr2line"
@@ -3546,10 +3546,11 @@ dependencies = [
3546
3546
 
3547
3547
  [[package]]
3548
3548
  name = "serde"
3549
- version = "1.0.219"
3549
+ version = "1.0.228"
3550
3550
  source = "registry+https://github.com/rust-lang/crates.io-index"
3551
- checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
3551
+ checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
3552
3552
  dependencies = [
3553
+ "serde_core",
3553
3554
  "serde_derive",
3554
3555
  ]
3555
3556
 
@@ -3564,11 +3565,20 @@ dependencies = [
3564
3565
  "typeid",
3565
3566
  ]
3566
3567
 
3568
+ [[package]]
3569
+ name = "serde_core"
3570
+ version = "1.0.228"
3571
+ source = "registry+https://github.com/rust-lang/crates.io-index"
3572
+ checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
3573
+ dependencies = [
3574
+ "serde_derive",
3575
+ ]
3576
+
3567
3577
  [[package]]
3568
3578
  name = "serde_derive"
3569
- version = "1.0.219"
3579
+ version = "1.0.228"
3570
3580
  source = "registry+https://github.com/rust-lang/crates.io-index"
3571
- checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
3581
+ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
3572
3582
  dependencies = [
3573
3583
  "proc-macro2",
3574
3584
  "quote",
@@ -3588,14 +3598,15 @@ dependencies = [
3588
3598
 
3589
3599
  [[package]]
3590
3600
  name = "serde_json"
3591
- version = "1.0.143"
3601
+ version = "1.0.145"
3592
3602
  source = "registry+https://github.com/rust-lang/crates.io-index"
3593
- checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a"
3603
+ checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
3594
3604
  dependencies = [
3595
3605
  "itoa",
3596
3606
  "memchr",
3597
3607
  "ryu",
3598
3608
  "serde",
3609
+ "serde_core",
3599
3610
  ]
3600
3611
 
3601
3612
  [[package]]
@@ -15,19 +15,19 @@ crate-type = ["staticlib", "cdylib", "lib"]
15
15
  # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
16
16
 
17
17
  [build-dependencies]
18
- tauri-build = { version = "2.4.0", features = [] }
18
+ tauri-build = { version = "2.4.1", features = [] }
19
19
 
20
20
  [dependencies]
21
- serde_json = "1.0.143"
22
- serde = { version = "1.0.219", features = ["derive"] }
21
+ serde_json = "1.0.145"
22
+ serde = { version = "1.0.228", features = ["derive"] }
23
23
  tokio = { version = "1.47.1", features = ["full"] }
24
- tauri = { version = "2.8.4", features = ["tray-icon", "image-ico", "image-png", "macos-proxy"] }
24
+ tauri = { version = "2.8.5", features = ["tray-icon", "image-ico", "image-png", "macos-proxy"] }
25
25
  tauri-plugin-window-state = "2.4.0"
26
26
  tauri-plugin-oauth = "2.0.0"
27
27
  tauri-plugin-http = "2.5.2"
28
28
  tauri-plugin-global-shortcut = { version = "2.3.0" }
29
29
  tauri-plugin-shell = "2.3.1"
30
- tauri-plugin-single-instance = "2.3.3"
30
+ tauri-plugin-single-instance = "2.3.4"
31
31
  tauri-plugin-notification = "2.3.1"
32
32
 
33
33
  [features]
@@ -15,7 +15,9 @@
15
15
  "hide_on_close": true,
16
16
  "incognito": false,
17
17
  "enable_wasm": false,
18
- "enable_drag_drop": false
18
+ "enable_drag_drop": false,
19
+ "maximize": false,
20
+ "start_to_tray": false
19
21
  }
20
22
  ],
21
23
  "user_agent": {
@@ -30,5 +32,6 @@
30
32
  },
31
33
  "system_tray_path": "icons/icon.png",
32
34
  "inject": [],
33
- "proxy_url": ""
35
+ "proxy_url": "",
36
+ "multi_instance": false
34
37
  }
@@ -5,6 +5,7 @@ pub struct WindowConfig {
5
5
  pub url: String,
6
6
  pub hide_title_bar: bool,
7
7
  pub fullscreen: bool,
8
+ pub maximize: bool,
8
9
  pub width: f64,
9
10
  pub height: f64,
10
11
  pub resizable: bool,
@@ -18,6 +19,7 @@ pub struct WindowConfig {
18
19
  pub title: Option<String>,
19
20
  pub enable_wasm: bool,
20
21
  pub enable_drag_drop: bool,
22
+ pub start_to_tray: bool,
21
23
  }
22
24
 
23
25
  #[derive(Debug, Serialize, Deserialize)]
@@ -59,6 +61,8 @@ pub struct PakeConfig {
59
61
  pub system_tray: FunctionON,
60
62
  pub system_tray_path: String,
61
63
  pub proxy_url: String,
64
+ #[serde(default)]
65
+ pub multi_instance: bool,
62
66
  }
63
67
 
64
68
  impl PakeConfig {
@@ -3,13 +3,13 @@ use std::sync::{Arc, Mutex};
3
3
  use std::time::{Duration, Instant};
4
4
  use tauri::{
5
5
  menu::{MenuBuilder, MenuItemBuilder},
6
- tray::TrayIconBuilder,
6
+ tray::{TrayIconBuilder, TrayIconEvent},
7
7
  AppHandle, Manager,
8
8
  };
9
9
  use tauri_plugin_global_shortcut::{GlobalShortcutExt, Shortcut};
10
10
  use tauri_plugin_window_state::{AppHandleExt, StateFlags};
11
11
 
12
- pub fn set_system_tray(app: &AppHandle, show_system_tray: bool) -> tauri::Result<()> {
12
+ pub fn set_system_tray(app: &AppHandle, show_system_tray: bool, tray_icon_path: &str) -> tauri::Result<()> {
13
13
  if !show_system_tray {
14
14
  app.remove_tray_by_id("pake-tray");
15
15
  return Ok(());
@@ -44,7 +44,30 @@ pub fn set_system_tray(app: &AppHandle, show_system_tray: bool) -> tauri::Result
44
44
  }
45
45
  _ => (),
46
46
  })
47
- .icon(app.default_window_icon().unwrap().clone())
47
+ .on_tray_icon_event(|tray, event| match event {
48
+ TrayIconEvent::Click { button, .. } => {
49
+ if button == tauri::tray::MouseButton::Left {
50
+ if let Some(window) = tray.app_handle().get_webview_window("pake") {
51
+ let is_visible = window.is_visible().unwrap_or(false);
52
+ if is_visible {
53
+ window.hide().unwrap();
54
+ } else {
55
+ window.show().unwrap();
56
+ window.set_focus().unwrap();
57
+ }
58
+ }
59
+ }
60
+ }
61
+ _ => {}
62
+ })
63
+ .icon(if tray_icon_path.is_empty() {
64
+ app.default_window_icon().unwrap_or_else(|| panic!("Failed to get default window icon")).clone()
65
+ } else {
66
+ tauri::image::Image::from_path(tray_icon_path).unwrap_or_else(|_| {
67
+ // If custom tray icon fails to load, fallback to default
68
+ app.default_window_icon().unwrap_or_else(|| panic!("Failed to get default window icon")).clone()
69
+ })
70
+ })
48
71
  .build(app)?;
49
72
 
50
73
  tray.set_icon_as_template(false)?;
@@ -43,6 +43,7 @@ pub fn set_window(app: &mut App, config: &PakeConfig, tauri_config: &Config) ->
43
43
  .user_agent(user_agent)
44
44
  .resizable(window_config.resizable)
45
45
  .fullscreen(window_config.fullscreen)
46
+ .maximized(window_config.maximize)
46
47
  .inner_size(window_config.width, window_config.height)
47
48
  .always_on_top(window_config.always_on_top)
48
49
  .incognito(window_config.incognito);
@@ -65,14 +66,7 @@ pub fn set_window(app: &mut App, config: &PakeConfig, tauri_config: &Config) ->
65
66
  .additional_browser_args("--enable-unsafe-webgpu");
66
67
  }
67
68
 
68
- if !config.proxy_url.is_empty() {
69
- if let Ok(proxy_url) = Url::from_str(&config.proxy_url) {
70
- window_builder = window_builder.proxy_url(proxy_url);
71
- #[cfg(debug_assertions)]
72
- println!("Proxy configured: {}", config.proxy_url);
73
- }
74
- }
75
-
69
+ // Platform-specific configuration must be set before proxy on Windows/Linux
76
70
  #[cfg(target_os = "macos")]
77
71
  {
78
72
  let title_bar_style = if window_config.hide_title_bar {
@@ -87,7 +81,7 @@ pub fn set_window(app: &mut App, config: &PakeConfig, tauri_config: &Config) ->
87
81
  }
88
82
  }
89
83
 
90
- // Windows and Linux share the same configuration
84
+ // Windows and Linux: set data_directory before proxy_url
91
85
  #[cfg(not(target_os = "macos"))]
92
86
  {
93
87
  window_builder = window_builder
@@ -96,5 +90,14 @@ pub fn set_window(app: &mut App, config: &PakeConfig, tauri_config: &Config) ->
96
90
  .theme(None);
97
91
  }
98
92
 
93
+ // Set proxy after platform-specific configs (required for Windows/Linux)
94
+ if !config.proxy_url.is_empty() {
95
+ if let Ok(proxy_url) = Url::from_str(&config.proxy_url) {
96
+ window_builder = window_builder.proxy_url(proxy_url);
97
+ #[cfg(debug_assertions)]
98
+ println!("Proxy configured: {}", config.proxy_url);
99
+ }
100
+ }
101
+
99
102
  window_builder.build().expect("Failed to build window")
100
103
  }
@@ -100,21 +100,6 @@ const DOWNLOADABLE_FILE_EXTENSIONS = {
100
100
  "scss",
101
101
  "sass",
102
102
  "less",
103
- "html",
104
- "htm",
105
- "php",
106
- "py",
107
- "java",
108
- "cpp",
109
- "c",
110
- "h",
111
- "cs",
112
- "rb",
113
- "go",
114
- "rs",
115
- "swift",
116
- "kt",
117
- "scala",
118
103
  "sh",
119
104
  "bat",
120
105
  "ps1",
@@ -163,6 +148,22 @@ function isChineseLanguage(language = getUserLanguage()) {
163
148
  );
164
149
  }
165
150
 
151
+ // User notification helper
152
+ function showDownloadError(filename) {
153
+ const isChinese = isChineseLanguage();
154
+ const message = isChinese
155
+ ? `ไธ‹่ฝฝๅคฑ่ดฅ: ${filename}`
156
+ : `Download failed: ${filename}`;
157
+
158
+ if (window.Notification && Notification.permission === "granted") {
159
+ new Notification(isChinese ? "ไธ‹่ฝฝ้”™่ฏฏ" : "Download Error", {
160
+ body: message,
161
+ });
162
+ } else {
163
+ console.error(message);
164
+ }
165
+ }
166
+
166
167
  // Unified file detection - replaces both isDownloadLink and isFileLink
167
168
  function isDownloadableFile(url) {
168
169
  try {
@@ -266,7 +267,7 @@ document.addEventListener("DOMContentLoaded", () => {
266
267
  }
267
268
 
268
269
  // write the ArrayBuffer to a binary, and you're done
269
- const userLanguage = navigator.language || navigator.userLanguage;
270
+ const userLanguage = getUserLanguage();
270
271
  invoke("download_file_by_binary", {
271
272
  params: {
272
273
  filename,
@@ -275,16 +276,18 @@ document.addEventListener("DOMContentLoaded", () => {
275
276
  },
276
277
  }).catch((error) => {
277
278
  console.error("Failed to download data URI file:", filename, error);
279
+ showDownloadError(filename);
278
280
  });
279
281
  } catch (error) {
280
282
  console.error("Failed to process data URI:", dataURI, error);
283
+ showDownloadError(filename || "file");
281
284
  }
282
285
  }
283
286
 
284
287
  function downloadFromBlobUrl(blobUrl, filename) {
285
288
  convertBlobUrlToBinary(blobUrl)
286
289
  .then((binary) => {
287
- const userLanguage = navigator.language || navigator.userLanguage;
290
+ const userLanguage = getUserLanguage();
288
291
  invoke("download_file_by_binary", {
289
292
  params: {
290
293
  filename,
@@ -293,10 +296,12 @@ document.addEventListener("DOMContentLoaded", () => {
293
296
  },
294
297
  }).catch((error) => {
295
298
  console.error("Failed to download blob file:", filename, error);
299
+ showDownloadError(filename);
296
300
  });
297
301
  })
298
302
  .catch((error) => {
299
303
  console.error("Failed to convert blob to binary:", blobUrl, error);
304
+ showDownloadError(filename);
300
305
  });
301
306
  }
302
307
 
@@ -385,20 +390,21 @@ document.addEventListener("DOMContentLoaded", () => {
385
390
 
386
391
  // Handle _blank links: same domain navigates in-app, cross-domain opens new window
387
392
  if (target === "_blank") {
388
- e.preventDefault();
389
- e.stopImmediatePropagation();
390
-
391
393
  if (isSameDomain(absoluteUrl)) {
392
- window.location.href = absoluteUrl;
393
- } else {
394
- const newWindow = originalWindowOpen.call(
395
- window,
396
- absoluteUrl,
397
- "_blank",
398
- "width=1200,height=800,scrollbars=yes,resizable=yes",
399
- );
400
- if (!newWindow) handleExternalLink(absoluteUrl);
394
+ // For same-domain links, let the browser/SPA handle it naturally
395
+ // This prevents full page reload in SPA apps like Discord
396
+ return;
401
397
  }
398
+
399
+ e.preventDefault();
400
+ e.stopImmediatePropagation();
401
+ const newWindow = originalWindowOpen.call(
402
+ window,
403
+ absoluteUrl,
404
+ "_blank",
405
+ "width=1200,height=800,scrollbars=yes,resizable=yes",
406
+ );
407
+ if (!newWindow) handleExternalLink(absoluteUrl);
402
408
  return;
403
409
  }
404
410
 
@@ -448,39 +454,24 @@ document.addEventListener("DOMContentLoaded", () => {
448
454
  // Rewrite the window.open function.
449
455
  const originalWindowOpen = window.open;
450
456
  window.open = function (url, name, specs) {
451
- // Apple login and google login
452
457
  if (name === "AppleAuthentication") {
453
- //do nothing
454
- } else if (
455
- specs &&
456
- (specs.includes("height=") || specs.includes("width="))
457
- ) {
458
- location.href = url;
459
- } else {
458
+ return originalWindowOpen.call(window, url, name, specs);
459
+ }
460
+
461
+ try {
460
462
  const baseUrl = window.location.origin + window.location.pathname;
461
463
  const hrefUrl = new URL(url, baseUrl);
462
464
  const absoluteUrl = hrefUrl.href;
463
465
 
464
- // Apply same domain logic as anchor links
465
- if (isSameDomain(absoluteUrl)) {
466
- // Same domain: navigate in app or open new window based on specs
467
- if (name === "_blank" || !name) {
468
- return originalWindowOpen.call(
469
- window,
470
- absoluteUrl,
471
- "_blank",
472
- "width=1200,height=800,scrollbars=yes,resizable=yes",
473
- );
474
- } else {
475
- location.href = absoluteUrl;
476
- }
477
- } else {
478
- // Cross domain: open in external browser
466
+ if (!isSameDomain(absoluteUrl)) {
479
467
  handleExternalLink(absoluteUrl);
468
+ return null;
480
469
  }
470
+
471
+ return originalWindowOpen.call(window, absoluteUrl, name, specs);
472
+ } catch (error) {
473
+ return originalWindowOpen.call(window, url, name, specs);
481
474
  }
482
- // Call the original window.open function to maintain its normal functionality.
483
- return originalWindowOpen.call(window, url, name, specs);
484
475
  };
485
476
 
486
477
  // Set the default zoom, There are problems with Loop without using try-catch.
@@ -688,13 +679,16 @@ document.addEventListener("DOMContentLoaded", () => {
688
679
  }
689
680
  } else {
690
681
  // Regular HTTP(S) image
691
- const userLanguage = navigator.language || navigator.userLanguage;
682
+ const userLanguage = getUserLanguage();
692
683
  invoke("download_file", {
693
684
  params: {
694
685
  url: imageUrl,
695
686
  filename: filename,
696
687
  language: userLanguage,
697
688
  },
689
+ }).catch((error) => {
690
+ console.error("Failed to download image:", filename, error);
691
+ showDownloadError(filename);
698
692
  });
699
693
  }
700
694
  }
@@ -742,7 +736,7 @@ document.addEventListener("DOMContentLoaded", () => {
742
736
 
743
737
  // Simplified menu builder
744
738
  function buildMenuItems(type, data) {
745
- const userLanguage = navigator.language || navigator.userLanguage;
739
+ const userLanguage = getUserLanguage();
746
740
  const items = [];
747
741
 
748
742
  switch (type) {
@@ -769,6 +763,9 @@ document.addEventListener("DOMContentLoaded", () => {
769
763
  const filename = getFilenameFromUrl(data.url);
770
764
  invoke("download_file", {
771
765
  params: { url: data.url, filename, language: userLanguage },
766
+ }).catch((error) => {
767
+ console.error("Failed to download file:", filename, error);
768
+ showDownloadError(filename);
772
769
  });
773
770
  }),
774
771
  );
@@ -24,6 +24,8 @@ pub fn run_app() {
24
24
  let hide_on_close = pake_config.windows[0].hide_on_close;
25
25
  let activation_shortcut = pake_config.windows[0].activation_shortcut.clone();
26
26
  let init_fullscreen = pake_config.windows[0].fullscreen;
27
+ let start_to_tray = pake_config.windows[0].start_to_tray && show_system_tray; // Only valid when tray is enabled
28
+ let multi_instance = pake_config.multi_instance;
27
29
 
28
30
  let window_state_plugin = WindowStatePlugin::default()
29
31
  .with_state_flags(if init_fullscreen {
@@ -35,19 +37,25 @@ pub fn run_app() {
35
37
  .build();
36
38
 
37
39
  #[allow(deprecated)]
38
- tauri_app
40
+ let mut app_builder = tauri_app
39
41
  .plugin(window_state_plugin)
40
42
  .plugin(tauri_plugin_oauth::init())
41
43
  .plugin(tauri_plugin_http::init())
42
44
  .plugin(tauri_plugin_shell::init())
43
- .plugin(tauri_plugin_notification::init())
44
- .plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
45
+ .plugin(tauri_plugin_notification::init());
46
+
47
+ // Only add single instance plugin if multiple instances are not allowed
48
+ if !multi_instance {
49
+ app_builder = app_builder.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
45
50
  if let Some(window) = app.get_webview_window("pake") {
46
51
  let _ = window.unminimize();
47
52
  let _ = window.show();
48
53
  let _ = window.set_focus();
49
54
  }
50
- }))
55
+ }));
56
+ }
57
+
58
+ app_builder
51
59
  .invoke_handler(tauri::generate_handler![
52
60
  download_file,
53
61
  download_file_by_binary,
@@ -55,15 +63,18 @@ pub fn run_app() {
55
63
  ])
56
64
  .setup(move |app| {
57
65
  let window = set_window(app, &pake_config, &tauri_config);
58
- set_system_tray(app.app_handle(), show_system_tray).unwrap();
66
+ set_system_tray(app.app_handle(), show_system_tray, &pake_config.system_tray_path).unwrap();
59
67
  set_global_shortcut(app.app_handle(), activation_shortcut).unwrap();
60
68
 
61
69
  // Show window after state restoration to prevent position flashing
62
- let window_clone = window.clone();
63
- tauri::async_runtime::spawn(async move {
64
- tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
65
- window_clone.show().unwrap();
66
- });
70
+ // Unless start_to_tray is enabled, then keep it hidden
71
+ if !start_to_tray {
72
+ let window_clone = window.clone();
73
+ tauri::async_runtime::spawn(async move {
74
+ tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
75
+ window_clone.show().unwrap();
76
+ });
77
+ }
67
78
 
68
79
  Ok(())
69
80
  })
@@ -1,10 +0,0 @@
1
- [source.crates-io]
2
- replace-with = 'rsproxy-sparse'
3
- [source.rsproxy]
4
- registry = "https://rsproxy.cn/crates.io-index"
5
- [source.rsproxy-sparse]
6
- registry = "sparse+https://rsproxy.cn/index/"
7
- [registries.rsproxy]
8
- index = "https://rsproxy.cn/crates.io-index"
9
- [net]
10
- git-fetch-with-cli = true