pake-cli 3.11.2 โ†’ 3.11.4

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
@@ -10,30 +10,28 @@ 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
- import fs from 'fs';
13
+ import fs from 'fs/promises';
17
14
  import { dir } from 'tmp-promise';
18
15
  import { fileTypeFromBuffer } from 'file-type';
19
16
  import icongen from 'icon-gen';
20
17
  import sharp from 'sharp';
21
18
  import * as psl from 'psl';
22
19
  import { InvalidArgumentError, program as program$1, Option } from 'commander';
20
+ import fs$1 from 'fs';
23
21
 
24
22
  var name = "pake-cli";
25
- var version = "3.11.2";
23
+ var version = "3.11.4";
26
24
  var description = "๐Ÿคฑ๐Ÿป Turn any webpage into a desktop app with one command. ๐Ÿคฑ๐Ÿป ไธ€้”ฎๆ‰“ๅŒ…็ฝ‘้กต็”Ÿๆˆ่ฝป้‡ๆกŒ้ขๅบ”็”จใ€‚";
27
25
  var engines = {
28
26
  node: ">=18.0.0"
29
27
  };
30
28
  var packageManager = "pnpm@10.26.2";
31
29
  var bin = {
32
- pake: "./dist/cli.js"
30
+ pake: "dist/cli.js"
33
31
  };
34
32
  var repository = {
35
33
  type: "git",
36
- url: "https://github.com/tw93/pake.git"
34
+ url: "git+https://github.com/tw93/pake.git"
37
35
  };
38
36
  var author = {
39
37
  name: "Tw93",
@@ -95,7 +93,6 @@ var devDependencies = {
95
93
  "@rollup/plugin-terser": "^0.4.4",
96
94
  "@types/fs-extra": "^11.0.4",
97
95
  "@types/node": "^25.3.2",
98
- "@types/page-icon": "^0.3.6",
99
96
  "@types/prompts": "^2.4.9",
100
97
  "@types/tmp": "^0.2.6",
101
98
  "@types/update-notifier": "^6.0.8",
@@ -110,7 +107,8 @@ var devDependencies = {
110
107
  };
111
108
  var pnpm = {
112
109
  overrides: {
113
- sharp: "^0.34.5"
110
+ sharp: "^0.34.5",
111
+ "@img/sharp-libvips-darwin-arm64": "1.2.4"
114
112
  },
115
113
  onlyBuiltDependencies: [
116
114
  "esbuild",
@@ -219,6 +217,12 @@ function getSpinner(text) {
219
217
  }).start();
220
218
  }
221
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
+
222
226
  const { platform: platform$1 } = process;
223
227
  const IS_MAC = platform$1 === 'darwin';
224
228
  const IS_WIN = platform$1 === 'win32';
@@ -278,69 +282,6 @@ async function shellExec(command, timeout = 300000, env) {
278
282
  }
279
283
  }
280
284
 
281
- const logger = {
282
- info(...msg) {
283
- log.info(...msg.map((m) => chalk.white(m)));
284
- },
285
- debug(...msg) {
286
- log.debug(...msg);
287
- },
288
- error(...msg) {
289
- log.error(...msg.map((m) => chalk.red(m)));
290
- },
291
- warn(...msg) {
292
- log.info(...msg.map((m) => chalk.yellow(m)));
293
- },
294
- success(...msg) {
295
- log.info(...msg.map((m) => chalk.green(m)));
296
- },
297
- };
298
-
299
- const resolve = promisify(dns.resolve);
300
- const ping = async (host) => {
301
- const lookup = promisify(dns.lookup);
302
- const ip = await lookup(host);
303
- const start = new Date();
304
- // Prevent timeouts from affecting user experience.
305
- const requestPromise = new Promise((resolve, reject) => {
306
- const req = http.get(`http://${ip.address}`, (res) => {
307
- const delay = new Date().getTime() - start.getTime();
308
- res.resume();
309
- resolve(delay);
310
- });
311
- req.on('error', (err) => {
312
- reject(err);
313
- });
314
- });
315
- const timeoutPromise = new Promise((_, reject) => {
316
- setTimeout(() => {
317
- reject(new Error('Request timed out after 3 seconds'));
318
- }, 1000);
319
- });
320
- return Promise.race([requestPromise, timeoutPromise]);
321
- };
322
- async function isChinaDomain(domain) {
323
- try {
324
- const [ip] = await resolve(domain);
325
- return await isChinaIP(ip, domain);
326
- }
327
- catch (error) {
328
- logger.debug(`${domain} can't be parse!`);
329
- return true;
330
- }
331
- }
332
- async function isChinaIP(ip, domain) {
333
- try {
334
- const delay = await ping(ip);
335
- logger.debug(`${domain} latency is ${delay} ms`);
336
- return delay > 1000;
337
- }
338
- catch (error) {
339
- logger.debug(`ping ${domain} failed!`);
340
- return true;
341
- }
342
- }
343
-
344
285
  function normalizePathForComparison(targetPath) {
345
286
  const normalized = path.normalize(targetPath);
346
287
  return IS_WIN ? normalized.toLowerCase() : normalized;
@@ -388,15 +329,13 @@ function ensureRustEnv() {
388
329
  ensureCargoBinOnPath();
389
330
  }
390
331
  async function installRust() {
391
- const isActions = process.env.GITHUB_ACTIONS;
392
- const isInChina = await isChinaDomain('sh.rustup.rs');
393
- const rustInstallScriptForMac = isInChina && !isActions
332
+ const rustInstallScriptForUnix = isCnMirrorEnabled()
394
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'
395
334
  : "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y";
396
335
  const rustInstallScriptForWindows = 'winget install --id Rustlang.Rustup';
397
336
  const spinner = getSpinner('Downloading Rust...');
398
337
  try {
399
- await shellExec(IS_WIN ? rustInstallScriptForWindows : rustInstallScriptForMac, 300000, undefined);
338
+ await shellExec(IS_WIN ? rustInstallScriptForWindows : rustInstallScriptForUnix, 300000, undefined);
400
339
  spinner.succeed(chalk.green('โœ” Rust installed successfully!'));
401
340
  ensureRustEnv();
402
341
  }
@@ -423,25 +362,43 @@ function checkRustInstalled() {
423
362
  }
424
363
 
425
364
  async function combineFiles(files, output) {
426
- const contents = files.map((file) => {
365
+ const contents = await Promise.all(files.map(async (file) => {
427
366
  if (file.endsWith('.css')) {
428
- const fileContent = fs.readFileSync(file, 'utf-8');
367
+ const fileContent = await fs.readFile(file, 'utf-8');
429
368
  return `window.addEventListener('DOMContentLoaded', (_event) => {
430
369
  const css = ${JSON.stringify(fileContent)};
431
370
  const style = document.createElement('style');
432
- style.innerHTML = css;
371
+ style.textContent = css;
433
372
  document.head.appendChild(style);
434
373
  });`;
435
374
  }
436
- const fileContent = fs.readFileSync(file);
375
+ const fileContent = await fs.readFile(file);
437
376
  return ("window.addEventListener('DOMContentLoaded', (_event) => { " +
438
377
  fileContent +
439
378
  ' });');
440
- });
441
- fs.writeFileSync(output, contents.join('\n'));
379
+ }));
380
+ await fs.writeFile(output, contents.join('\n'));
442
381
  return files;
443
382
  }
444
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
+
445
402
  function generateSafeFilename(name) {
446
403
  return name
447
404
  .replace(/[<>:"/\\|?*]/g, '_')
@@ -479,11 +436,15 @@ function generateIdentifierSafeName(name) {
479
436
  return cleaned;
480
437
  }
481
438
 
482
- async function mergeConfig(url, options, tauriConf) {
483
- // Ensure .pake directory exists and copy source templates if needed
439
+ function asSupportedPlatform(platform) {
440
+ if (platform !== 'win32' && platform !== 'darwin' && platform !== 'linux') {
441
+ throw new Error(`Pake only supports win32, darwin, and linux; detected '${platform}'.`);
442
+ }
443
+ return platform;
444
+ }
445
+ async function copyTemplateConfigs() {
484
446
  const srcTauriDir = path.join(npmDirectory, 'src-tauri');
485
447
  await fsExtra.ensureDir(tauriConfigDirectory);
486
- // Copy source config files to .pake directory (as templates)
487
448
  const sourceFiles = [
488
449
  'tauri.conf.json',
489
450
  'tauri.macos.conf.json',
@@ -499,51 +460,11 @@ async function mergeConfig(url, options, tauriConf) {
499
460
  await fsExtra.copy(sourcePath, destPath);
500
461
  }
501
462
  }));
502
- const { width, height, fullscreen, maximize, hideTitleBar, alwaysOnTop, appVersion, darkMode, disabledWebShortcuts, activationShortcut, userAgent, showSystemTray, systemTrayIcon, useLocalFile, identifier, name = 'pake-app', resizable = true, inject, proxyUrl, installerLanguage, hideOnClose, incognito, title, wasm, enableDragDrop, multiInstance, multiWindow, startToTray, forceInternalNavigation, internalUrlRegex, zoom, minWidth, minHeight, ignoreCertificateErrors, newWindow, camera, microphone, } = options;
503
- const { platform } = process;
504
- const platformHideOnClose = hideOnClose ?? platform === 'darwin';
505
- const tauriConfWindowOptions = {
506
- width,
507
- height,
508
- fullscreen,
509
- maximize,
510
- resizable,
511
- hide_title_bar: hideTitleBar,
512
- activation_shortcut: activationShortcut,
513
- always_on_top: alwaysOnTop,
514
- dark_mode: darkMode,
515
- disabled_web_shortcuts: disabledWebShortcuts,
516
- hide_on_close: platformHideOnClose,
517
- incognito: incognito,
518
- title: title,
519
- enable_wasm: wasm,
520
- enable_drag_drop: enableDragDrop,
521
- start_to_tray: startToTray && showSystemTray,
522
- force_internal_navigation: forceInternalNavigation,
523
- internal_url_regex: internalUrlRegex,
524
- zoom,
525
- min_width: minWidth,
526
- min_height: minHeight,
527
- ignore_certificate_errors: ignoreCertificateErrors,
528
- new_window: newWindow,
529
- };
530
- Object.assign(tauriConf.pake.windows[0], { url, ...tauriConfWindowOptions });
531
- tauriConf.productName = name;
532
- tauriConf.identifier = identifier;
533
- tauriConf.version = appVersion;
534
- // Always set mainBinaryName to ensure binary uniqueness
535
- const linuxBinaryName = `pake-${generateLinuxPackageName(name)}`;
536
- tauriConf.mainBinaryName =
537
- platform === 'linux'
538
- ? linuxBinaryName
539
- : `pake-${generateIdentifierSafeName(name)}`;
540
- if (platform == 'win32') {
541
- tauriConf.bundle.windows.wix.language[0] = installerLanguage;
542
- }
463
+ }
464
+ async function handleLocalFile(url, useLocalFile, tauriConf) {
543
465
  const pathExists = await fsExtra.pathExists(url);
544
466
  if (pathExists) {
545
467
  logger.warn('โœผ Your input might be a local file.');
546
- tauriConf.pake.windows[0].url_type = 'local';
547
468
  const fileName = path.basename(url);
548
469
  const dirName = path.dirname(url);
549
470
  const distDir = path.join(npmDirectory, 'dist');
@@ -555,8 +476,6 @@ async function mergeConfig(url, options, tauriConf) {
555
476
  else {
556
477
  fsExtra.moveSync(distDir, distBakDir, { overwrite: true });
557
478
  fsExtra.copySync(dirName, distDir, { overwrite: true });
558
- // ignore it, because about_pake.html have be erased.
559
- // const filesToCopyBack = ['cli.js', 'about_pake.html'];
560
479
  const filesToCopyBack = ['cli.js'];
561
480
  await Promise.all(filesToCopyBack.map((file) => fsExtra.copy(path.join(distBakDir, file), path.join(distDir, file))));
562
481
  }
@@ -566,28 +485,19 @@ async function mergeConfig(url, options, tauriConf) {
566
485
  else {
567
486
  tauriConf.pake.windows[0].url_type = 'web';
568
487
  }
569
- const platformMap = {
570
- win32: 'windows',
571
- linux: 'linux',
572
- darwin: 'macos',
573
- };
574
- const currentPlatform = platformMap[platform];
575
- if (userAgent.length > 0) {
576
- tauriConf.pake.user_agent[currentPlatform] = userAgent;
577
- }
578
- tauriConf.pake.system_tray[currentPlatform] = showSystemTray;
579
- // Processing targets are currently only open to Linux.
580
- if (platform === 'linux') {
581
- // Remove hardcoded desktop files and regenerate with correct app name
582
- delete tauriConf.bundle.linux.deb.files;
583
- // Generate correct desktop file configuration
584
- const linuxName = generateLinuxPackageName(name);
585
- const desktopFileName = `com.pake.${linuxName}.desktop`;
586
- const iconName = `${linuxName}_512`;
587
- // Create desktop file content
588
- // Determine if title contains Chinese characters for Name[zh_CN]
589
- const chineseName = title && /[\u4e00-\u9fa5]/.test(title) ? title : null;
590
- const desktopContent = `[Desktop Entry]
488
+ }
489
+ async function mergeLinuxConfig(options, name, tauriConf, linuxBinaryName) {
490
+ const linuxBundle = tauriConf.bundle.linux;
491
+ if (!linuxBundle) {
492
+ throw new Error('Linux bundle configuration is missing from tauri.linux.conf.json; cannot build Linux target.');
493
+ }
494
+ delete linuxBundle.deb.files;
495
+ const linuxName = generateLinuxPackageName(name);
496
+ const desktopFileName = `com.pake.${linuxName}.desktop`;
497
+ const iconName = `${linuxName}_512`;
498
+ const { title } = options;
499
+ const chineseName = title && /[\u4e00-\u9fa5]/.test(title) ? title : null;
500
+ const desktopContent = `[Desktop Entry]
591
501
  Version=1.0
592
502
  Type=Application
593
503
  Name=${name}
@@ -600,51 +510,39 @@ MimeType=text/html;text/xml;application/xhtml_xml;
600
510
  StartupNotify=true
601
511
  Terminal=false
602
512
  `;
603
- // Write desktop file to src-tauri/assets directory where Tauri expects it
604
- const srcAssetsDir = path.join(npmDirectory, 'src-tauri/assets');
605
- const srcDesktopFilePath = path.join(srcAssetsDir, desktopFileName);
606
- await fsExtra.ensureDir(srcAssetsDir);
607
- await fsExtra.writeFile(srcDesktopFilePath, desktopContent);
608
- // Set up desktop file in bundle configuration
609
- // Use absolute path from src-tauri directory to assets
610
- const desktopInstallPath = `/usr/share/applications/${desktopFileName}`;
611
- tauriConf.bundle.linux.deb.files = {
612
- [desktopInstallPath]: `assets/${desktopFileName}`,
613
- };
614
- // Add desktop file support for RPM
615
- if (!tauriConf.bundle.linux.rpm) {
616
- tauriConf.bundle.linux.rpm = {};
617
- }
618
- tauriConf.bundle.linux.rpm.files = {
619
- [desktopInstallPath]: `assets/${desktopFileName}`,
620
- };
621
- const validTargets = [
622
- 'deb',
623
- 'appimage',
624
- 'rpm',
625
- 'deb-arm64',
626
- 'appimage-arm64',
627
- 'rpm-arm64',
628
- ];
629
- const baseTarget = options.targets.includes('-arm64')
630
- ? options.targets.replace('-arm64', '')
631
- : options.targets;
632
- if (validTargets.includes(options.targets)) {
633
- tauriConf.bundle.targets = [baseTarget];
634
- }
635
- else {
636
- logger.warn(`โœผ The target must be one of ${validTargets.join(', ')}, the default 'deb' will be used.`);
637
- }
513
+ const srcAssetsDir = path.join(npmDirectory, 'src-tauri/assets');
514
+ const srcDesktopFilePath = path.join(srcAssetsDir, desktopFileName);
515
+ await fsExtra.ensureDir(srcAssetsDir);
516
+ await fsExtra.writeFile(srcDesktopFilePath, desktopContent);
517
+ const desktopInstallPath = `/usr/share/applications/${desktopFileName}`;
518
+ linuxBundle.deb.files = {
519
+ [desktopInstallPath]: `assets/${desktopFileName}`,
520
+ };
521
+ if (!linuxBundle.rpm) {
522
+ linuxBundle.rpm = {};
638
523
  }
639
- // Set macOS bundle targets (for app vs dmg)
640
- if (platform === 'darwin') {
641
- const validMacTargets = ['app', 'dmg'];
642
- if (validMacTargets.includes(options.targets)) {
643
- tauriConf.bundle.targets = [options.targets];
644
- }
524
+ linuxBundle.rpm.files = {
525
+ [desktopInstallPath]: `assets/${desktopFileName}`,
526
+ };
527
+ const validTargets = [
528
+ 'deb',
529
+ 'appimage',
530
+ 'rpm',
531
+ 'deb-arm64',
532
+ 'appimage-arm64',
533
+ 'rpm-arm64',
534
+ ];
535
+ const baseTarget = options.targets.includes('-arm64')
536
+ ? options.targets.replace('-arm64', '')
537
+ : options.targets;
538
+ if (validTargets.includes(options.targets)) {
539
+ tauriConf.bundle.targets = [baseTarget];
645
540
  }
646
- // Set icon.
647
- const safeAppName = getSafeAppName(name);
541
+ else {
542
+ logger.warn(`โœผ The target must be one of ${validTargets.join(', ')}, the default 'deb' will be used.`);
543
+ }
544
+ }
545
+ async function mergeIcons(options, name, tauriConf, platform, safeAppName) {
648
546
  const platformIconMap = {
649
547
  win32: {
650
548
  fileExt: '.ico',
@@ -670,7 +568,7 @@ Terminal=false
670
568
  const exists = resolvedIconPath && (await fsExtra.pathExists(resolvedIconPath));
671
569
  if (exists) {
672
570
  let updateIconPath = true;
673
- let customIconExt = path.extname(resolvedIconPath).toLowerCase();
571
+ const customIconExt = path.extname(resolvedIconPath).toLowerCase();
674
572
  if (customIconExt !== iconInfo.fileExt) {
675
573
  updateIconPath = false;
676
574
  logger.warn(`โœผ ${iconInfo.message}, but you give ${customIconExt}`);
@@ -679,7 +577,6 @@ Terminal=false
679
577
  else {
680
578
  const iconPath = path.join(npmDirectory, 'src-tauri/', iconInfo.path);
681
579
  tauriConf.bundle.resources = [iconInfo.path];
682
- // Avoid copying if source and destination are the same
683
580
  const absoluteDestPath = path.resolve(iconPath);
684
581
  if (resolvedIconPath !== absoluteDestPath) {
685
582
  try {
@@ -706,37 +603,32 @@ Terminal=false
706
603
  }
707
604
  // Set tray icon path.
708
605
  let trayIconPath = platform === 'darwin' ? 'png/icon_512.png' : tauriConf.bundle.icon[0];
709
- if (systemTrayIcon.length > 0) {
606
+ if (options.systemTrayIcon.length > 0) {
710
607
  try {
711
- await fsExtra.pathExists(systemTrayIcon);
712
- // ้œ€่ฆๅˆคๆ–ญๅ›พๆ ‡ๆ ผๅผ๏ผŒ้ป˜่ฎคๅชๆ”ฏๆŒicoๅ’Œpngไธค็ง
713
- let iconExt = path.extname(systemTrayIcon).toLowerCase();
714
- if (iconExt == '.png' || iconExt == '.ico') {
608
+ await fsExtra.pathExists(options.systemTrayIcon);
609
+ const iconExt = path.extname(options.systemTrayIcon).toLowerCase();
610
+ if (iconExt === '.png' || iconExt === '.ico') {
715
611
  const trayIcoPath = path.join(npmDirectory, `src-tauri/png/${safeAppName}${iconExt}`);
716
612
  trayIconPath = `png/${safeAppName}${iconExt}`;
717
- await fsExtra.copy(systemTrayIcon, trayIcoPath);
613
+ await fsExtra.copy(options.systemTrayIcon, trayIcoPath);
718
614
  }
719
615
  else {
720
616
  logger.warn(`โœผ System tray icon must be .ico or .png, but you provided ${iconExt}.`);
721
617
  logger.warn(`โœผ Default system tray icon will be used.`);
722
618
  }
723
619
  }
724
- catch {
725
- logger.warn(`โœผ ${systemTrayIcon} not exists!`);
620
+ catch (err) {
621
+ logger.warn(`โœผ Failed to apply system tray icon "${options.systemTrayIcon}": ${err instanceof Error ? err.message : String(err)}`);
726
622
  logger.warn(`โœผ Default system tray icon will remain unchanged.`);
727
623
  }
728
624
  }
729
- // Ensure trayIcon object exists before setting iconPath
730
- if (!tauriConf.app.trayIcon) {
731
- tauriConf.app.trayIcon = {};
732
- }
733
- tauriConf.app.trayIcon.iconPath = trayIconPath;
734
625
  tauriConf.pake.system_tray_path = trayIconPath;
735
626
  delete tauriConf.app.trayIcon;
736
- const injectFilePath = path.join(npmDirectory, `src-tauri/src/inject/custom.js`);
737
- // inject js or css files
627
+ }
628
+ async function injectCustomCode(options, tauriConf) {
629
+ const { inject, proxyUrl, multiInstance, multiWindow, wasm } = options;
630
+ const injectFilePath = path.join(npmDirectory, 'src-tauri/src/inject/custom.js');
738
631
  if (inject?.length > 0) {
739
- // Ensure inject is an array before calling .every()
740
632
  const injectArray = Array.isArray(inject) ? inject : [inject];
741
633
  if (!injectArray.every((item) => item.endsWith('.css') || item.endsWith('.js'))) {
742
634
  logger.error('The injected file must be in either CSS or JS format.');
@@ -753,7 +645,6 @@ Terminal=false
753
645
  tauriConf.pake.proxy_url = proxyUrl || '';
754
646
  tauriConf.pake.multi_instance = multiInstance;
755
647
  tauriConf.pake.multi_window = multiWindow;
756
- // Configure WASM support with required HTTP headers
757
648
  if (wasm) {
758
649
  tauriConf.app.security = {
759
650
  headers: {
@@ -762,16 +653,16 @@ Terminal=false
762
653
  },
763
654
  };
764
655
  }
765
- // Write entitlements dynamically on macOS so camera/microphone are opt-in
766
- if (platform === 'darwin') {
767
- const entitlementEntries = [];
768
- if (camera) {
769
- entitlementEntries.push(' <key>com.apple.security.device.camera</key>\n <true/>');
770
- }
771
- if (microphone) {
772
- entitlementEntries.push(' <key>com.apple.security.device.audio-input</key>\n <true/>');
773
- }
774
- const entitlementsContent = `<?xml version="1.0" encoding="UTF-8"?>
656
+ }
657
+ async function generateMacEntitlements(camera, microphone) {
658
+ const entitlementEntries = [];
659
+ if (camera) {
660
+ entitlementEntries.push(' <key>com.apple.security.device.camera</key>\n <true/>');
661
+ }
662
+ if (microphone) {
663
+ entitlementEntries.push(' <key>com.apple.security.device.audio-input</key>\n <true/>');
664
+ }
665
+ const entitlementsContent = `<?xml version="1.0" encoding="UTF-8"?>
775
666
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
776
667
  <plist version="1.0">
777
668
  <dict>
@@ -779,10 +670,10 @@ ${entitlementEntries.join('\n')}
779
670
  </dict>
780
671
  </plist>
781
672
  `;
782
- const entitlementsPath = path.join(npmDirectory, 'src-tauri', 'entitlements.plist');
783
- await fsExtra.writeFile(entitlementsPath, entitlementsContent);
784
- }
785
- // Save config file.
673
+ const entitlementsPath = path.join(npmDirectory, 'src-tauri', 'entitlements.plist');
674
+ await fsExtra.writeFile(entitlementsPath, entitlementsContent);
675
+ }
676
+ async function writeAllConfigs(tauriConf, platform) {
786
677
  const platformConfigPaths = {
787
678
  win32: 'tauri.windows.conf.json',
788
679
  darwin: 'tauri.macos.conf.json',
@@ -793,11 +684,85 @@ ${entitlementEntries.join('\n')}
793
684
  await fsExtra.outputJSON(configPath, bundleConf, { spaces: 4 });
794
685
  const pakeConfigPath = path.join(tauriConfigDirectory, 'pake.json');
795
686
  await fsExtra.outputJSON(pakeConfigPath, tauriConf.pake, { spaces: 4 });
796
- let tauriConf2 = JSON.parse(JSON.stringify(tauriConf));
687
+ const tauriConf2 = JSON.parse(JSON.stringify(tauriConf));
797
688
  delete tauriConf2.pake;
798
689
  const configJsonPath = path.join(tauriConfigDirectory, 'tauri.conf.json');
799
690
  await fsExtra.outputJSON(configJsonPath, tauriConf2, { spaces: 4 });
800
691
  }
692
+ async function mergeConfig(url, options, tauriConf) {
693
+ 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;
695
+ 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
+ };
722
+ Object.assign(tauriConf.pake.windows[0], { url, ...tauriConfWindowOptions });
723
+ tauriConf.productName = name;
724
+ tauriConf.identifier = identifier;
725
+ tauriConf.version = appVersion;
726
+ const linuxBinaryName = `pake-${generateLinuxPackageName(name)}`;
727
+ tauriConf.mainBinaryName =
728
+ platform === 'linux'
729
+ ? linuxBinaryName
730
+ : `pake-${generateIdentifierSafeName(name)}`;
731
+ if (platform === 'win32') {
732
+ const windowsBundle = tauriConf.bundle.windows;
733
+ if (!windowsBundle) {
734
+ throw new Error('Windows bundle configuration is missing from tauri.windows.conf.json; cannot build Windows target.');
735
+ }
736
+ windowsBundle.wix.language[0] = installerLanguage;
737
+ }
738
+ await handleLocalFile(url, useLocalFile, tauriConf);
739
+ const platformMap = {
740
+ win32: 'windows',
741
+ linux: 'linux',
742
+ darwin: 'macos',
743
+ };
744
+ const currentPlatform = platformMap[platform];
745
+ if (userAgent.length > 0) {
746
+ tauriConf.pake.user_agent[currentPlatform] = userAgent;
747
+ }
748
+ tauriConf.pake.system_tray[currentPlatform] = showSystemTray;
749
+ if (platform === 'linux') {
750
+ await mergeLinuxConfig(options, name, tauriConf, linuxBinaryName);
751
+ }
752
+ if (platform === 'darwin') {
753
+ const validMacTargets = ['app', 'dmg'];
754
+ if (validMacTargets.includes(options.targets)) {
755
+ tauriConf.bundle.targets = [options.targets];
756
+ }
757
+ }
758
+ const safeAppName = getSafeAppName(name);
759
+ await mergeIcons(options, name, tauriConf, platform, safeAppName);
760
+ await injectCustomCode(options, tauriConf);
761
+ if (platform === 'darwin') {
762
+ await generateMacEntitlements(camera, microphone);
763
+ }
764
+ await writeAllConfigs(tauriConf, platform);
765
+ }
801
766
 
802
767
  class BaseBuilder {
803
768
  constructor(options) {
@@ -864,6 +829,40 @@ class BaseBuilder {
864
829
  throw error;
865
830
  }
866
831
  }
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}`;
838
+ }
839
+ isGeneratedCnMirrorConfig(projectConfig, cnMirrorConfig) {
840
+ return projectConfig.trim() === cnMirrorConfig.trim();
841
+ }
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
+ }
865
+ }
867
866
  async prepare() {
868
867
  const tauriSrcPath = path.join(npmDirectory, 'src-tauri');
869
868
  const tauriTargetPath = path.join(tauriSrcPath, 'target');
@@ -884,18 +883,14 @@ class BaseBuilder {
884
883
  }
885
884
  else {
886
885
  logger.error('โœ• Rust required to package your webapp.');
887
- process.exit(0);
886
+ process.exit(1);
888
887
  }
889
888
  }
890
- const isChina = await isChinaDomain('www.npmjs.com');
891
889
  const spinner = getSpinner('Installing package...');
892
- const rustProjectDir = path.join(tauriSrcPath, '.cargo');
893
- const projectConf = path.join(rustProjectDir, 'config.toml');
894
- await fsExtra.ensureDir(rustProjectDir);
890
+ const useCnMirror = isCnMirrorEnabled();
891
+ await this.configureCargoRegistry(tauriSrcPath, useCnMirror);
895
892
  // Detect available package manager
896
893
  const packageManager = await this.detectPackageManager();
897
- const registryOption = ' --registry=https://registry.npmmirror.com';
898
- const peerDepsOption = packageManager === 'npm' ? ' --legacy-peer-deps' : '';
899
894
  const timeout = this.getInstallTimeout();
900
895
  const buildEnv = this.getBuildEnvironment();
901
896
  // Show helpful message for first-time users
@@ -904,43 +899,22 @@ class BaseBuilder {
904
899
  ? 'โœบ First-time setup may take 10-15 minutes on Windows (compiling dependencies)...'
905
900
  : 'โœบ First-time setup may take 5-10 minutes (installing dependencies)...');
906
901
  }
907
- let usedMirror = isChina;
902
+ if (useCnMirror) {
903
+ logger.info(`โœบ ${CN_MIRROR_ENV}=1 detected, using ${packageManager}/rsProxy CN mirror.`);
904
+ }
908
905
  try {
909
- if (isChina) {
910
- logger.info(`โœบ Located in China, using ${packageManager}/rsProxy CN mirror.`);
911
- const projectCnConf = path.join(tauriSrcPath, 'rust_proxy.toml');
912
- await this.copyFileWithSamePathGuard(projectCnConf, projectConf);
913
- await shellExec(`cd "${npmDirectory}" && ${packageManager} install${registryOption}${peerDepsOption}`, timeout, { ...buildEnv, CI: 'true' });
914
- }
915
- else {
916
- await shellExec(`cd "${npmDirectory}" && ${packageManager} install${peerDepsOption}`, timeout, { ...buildEnv, CI: 'true' });
917
- }
906
+ await shellExec(this.getInstallCommand(packageManager, useCnMirror), timeout, {
907
+ ...buildEnv,
908
+ CI: 'true',
909
+ });
918
910
  spinner.succeed(chalk.green('Package installed!'));
919
911
  }
920
912
  catch (error) {
921
- // If installation times out and we haven't tried the mirror yet, retry with mirror
922
- if (error instanceof Error &&
923
- error.message.includes('timed out') &&
924
- !usedMirror) {
925
- spinner.fail(chalk.yellow('Installation timed out, retrying with CN mirror...'));
926
- logger.info('โœบ Retrying installation with CN mirror for better speed...');
927
- const retrySpinner = getSpinner('Retrying installation...');
928
- usedMirror = true;
929
- try {
930
- const projectCnConf = path.join(tauriSrcPath, 'rust_proxy.toml');
931
- await this.copyFileWithSamePathGuard(projectCnConf, projectConf);
932
- await shellExec(`cd "${npmDirectory}" && ${packageManager} install${registryOption}${peerDepsOption}`, timeout, { ...buildEnv, CI: 'true' });
933
- retrySpinner.succeed(chalk.green('Package installed with CN mirror!'));
934
- }
935
- catch (retryError) {
936
- retrySpinner.fail(chalk.red('Installation failed'));
937
- throw retryError;
938
- }
939
- }
940
- else {
941
- spinner.fail(chalk.red('Installation failed'));
942
- throw error;
913
+ spinner.fail(chalk.red('Installation failed'));
914
+ if (!useCnMirror) {
915
+ logger.info(`โœบ If downloads are slow in China, retry with ${CN_MIRROR_ENV}=1 to use CN mirrors.`);
943
916
  }
917
+ throw error;
944
918
  }
945
919
  if (!tauriTargetPathExists) {
946
920
  logger.warn('โœผ The first packaging may be slow, please be patient and wait, it will be faster afterwards.');
@@ -1093,6 +1067,10 @@ class BaseBuilder {
1093
1067
  if (this.options.debug) {
1094
1068
  fullCommand += ' --verbose';
1095
1069
  }
1070
+ const features = this.getBuildFeatures();
1071
+ if (features.length > 0) {
1072
+ fullCommand += ` --features ${features.join(',')}`;
1073
+ }
1096
1074
  return fullCommand;
1097
1075
  }
1098
1076
  getBuildFeatures() {
@@ -1114,11 +1092,6 @@ class BaseBuilder {
1114
1092
  if (IS_MAC && this.options.targets === 'app') {
1115
1093
  fullCommand += ' --bundles app';
1116
1094
  }
1117
- // Add features
1118
- const features = this.getBuildFeatures();
1119
- if (features.length > 0) {
1120
- fullCommand += ` --features ${features.join(',')}`;
1121
- }
1122
1095
  return fullCommand;
1123
1096
  }
1124
1097
  getMacOSMajorVersion() {
@@ -1280,12 +1253,7 @@ class MacBuilder extends BaseBuilder {
1280
1253
  if (!buildTarget) {
1281
1254
  throw new Error(`Unsupported architecture: ${actualArch} for macOS`);
1282
1255
  }
1283
- let fullCommand = this.buildBaseCommand(packageManager, configPath, buildTarget);
1284
- const features = this.getBuildFeatures();
1285
- if (features.length > 0) {
1286
- fullCommand += ` --features ${features.join(',')}`;
1287
- }
1288
- return fullCommand;
1256
+ return this.buildBaseCommand(packageManager, configPath, buildTarget);
1289
1257
  }
1290
1258
  getBasePath() {
1291
1259
  const basePath = this.options.debug ? 'debug' : 'release';
@@ -1325,12 +1293,7 @@ class WinBuilder extends BaseBuilder {
1325
1293
  if (!buildTarget) {
1326
1294
  throw new Error(`Unsupported architecture: ${this.buildArch} for Windows`);
1327
1295
  }
1328
- let fullCommand = this.buildBaseCommand(packageManager, configPath, buildTarget);
1329
- const features = this.getBuildFeatures();
1330
- if (features.length > 0) {
1331
- fullCommand += ` --features ${features.join(',')}`;
1332
- }
1333
- return fullCommand;
1296
+ return this.buildBaseCommand(packageManager, configPath, buildTarget);
1334
1297
  }
1335
1298
  getBasePath() {
1336
1299
  const basePath = this.options.debug ? 'debug' : 'release';
@@ -1410,10 +1373,6 @@ class LinuxBuilder extends BaseBuilder {
1410
1373
  ? (this.getTauriTarget(this.buildArch, 'linux') ?? undefined)
1411
1374
  : undefined;
1412
1375
  let fullCommand = this.buildBaseCommand(packageManager, configPath, buildTarget);
1413
- const features = this.getBuildFeatures();
1414
- if (features.length > 0) {
1415
- fullCommand += ` --features ${features.join(',')}`;
1416
- }
1417
1376
  if (this.currentBuildType) {
1418
1377
  fullCommand += ` --bundles ${this.currentBuildType}`;
1419
1378
  }
@@ -1470,6 +1429,85 @@ class BuilderProvider {
1470
1429
  }
1471
1430
  }
1472
1431
 
1432
+ const LOCAL_HOST_SUFFIXES = [
1433
+ '.local',
1434
+ '.lan',
1435
+ '.internal',
1436
+ '.home',
1437
+ '.localdomain',
1438
+ ];
1439
+ const IPV4_ADDRESS_PATTERN = /^(\d{1,3}\.){3}\d{1,3}$/;
1440
+ function normalize(value) {
1441
+ return value.trim().toLowerCase();
1442
+ }
1443
+ function simplify(value) {
1444
+ return normalize(value).replace(/[\s._-]+/g, '');
1445
+ }
1446
+ function generateDashboardIconSlugs(appName) {
1447
+ const normalizedName = normalize(appName);
1448
+ if (!normalizedName) {
1449
+ return [];
1450
+ }
1451
+ const slugs = new Set([
1452
+ normalizedName,
1453
+ normalizedName.replace(/\s+/g, '-'),
1454
+ ]);
1455
+ return [...slugs].filter(Boolean);
1456
+ }
1457
+ function isLikelyLocalHostname(hostname) {
1458
+ const normalizedHostname = normalize(hostname);
1459
+ if (!normalizedHostname) {
1460
+ return false;
1461
+ }
1462
+ return (normalizedHostname === 'localhost' ||
1463
+ IPV4_ADDRESS_PATTERN.test(normalizedHostname) ||
1464
+ normalizedHostname.includes(':') ||
1465
+ !normalizedHostname.includes('.') ||
1466
+ LOCAL_HOST_SUFFIXES.some((suffix) => normalizedHostname.endsWith(suffix)));
1467
+ }
1468
+ function shouldPreferDashboardIcons(url, appName) {
1469
+ if (!appName) {
1470
+ return false;
1471
+ }
1472
+ try {
1473
+ const hostname = new URL(url).hostname.toLowerCase();
1474
+ if (!hostname) {
1475
+ return false;
1476
+ }
1477
+ if (isLikelyLocalHostname(hostname)) {
1478
+ return true;
1479
+ }
1480
+ const parsed = psl.parse(hostname);
1481
+ if (!('domain' in parsed) || !parsed.domain) {
1482
+ return true;
1483
+ }
1484
+ const registrableDomain = parsed.domain.toLowerCase();
1485
+ if (hostname === registrableDomain) {
1486
+ return false;
1487
+ }
1488
+ const subdomain = 'subdomain' in parsed && typeof parsed.subdomain === 'string'
1489
+ ? parsed.subdomain
1490
+ : '';
1491
+ if (!subdomain) {
1492
+ return false;
1493
+ }
1494
+ const productLabel = subdomain.split('.').pop() || '';
1495
+ const rootLabel = registrableDomain.split('.')[0] || '';
1496
+ const normalizedAppName = simplify(appName);
1497
+ return (normalizedAppName.length > 0 &&
1498
+ simplify(productLabel) === normalizedAppName &&
1499
+ simplify(rootLabel) !== normalizedAppName);
1500
+ }
1501
+ catch {
1502
+ return false;
1503
+ }
1504
+ }
1505
+ function getIconSourcePriority(url, appName) {
1506
+ return shouldPreferDashboardIcons(url, appName)
1507
+ ? ['dashboard', 'domain']
1508
+ : ['domain', 'dashboard'];
1509
+ }
1510
+
1473
1511
  const ICO_HEADER_SIZE = 6;
1474
1512
  const ICO_DIR_ENTRY_SIZE = 16;
1475
1513
  const ICO_TYPE_ICON = 1;
@@ -1571,11 +1609,48 @@ async function writeIcoWithPreferredSize(sourcePath, outputPath, preferredSize)
1571
1609
  return false;
1572
1610
  }
1573
1611
  }
1612
+ /**
1613
+ * Builds an ICO file from an array of PNG buffers using the PNG-in-ICO format
1614
+ * (supported since Windows Vista). This preserves alpha transparency.
1615
+ */
1616
+ function buildIcoFromPngBuffers(frames) {
1617
+ const count = frames.length;
1618
+ const headerSize = ICO_HEADER_SIZE + count * ICO_DIR_ENTRY_SIZE;
1619
+ const totalPayload = frames.reduce((acc, f) => acc + f.png.length, 0);
1620
+ const output = Buffer.alloc(headerSize + totalPayload);
1621
+ output.writeUInt16LE(0, 0);
1622
+ output.writeUInt16LE(ICO_TYPE_ICON, 2);
1623
+ output.writeUInt16LE(count, 4);
1624
+ let currentOffset = headerSize;
1625
+ for (let i = 0; i < count; i++) {
1626
+ const { size, png } = frames[i];
1627
+ const entryOffset = ICO_HEADER_SIZE + i * ICO_DIR_ENTRY_SIZE;
1628
+ const sizeByte = size >= 256 ? 0 : size;
1629
+ output.writeUInt8(sizeByte, entryOffset);
1630
+ output.writeUInt8(sizeByte, entryOffset + 1);
1631
+ output.writeUInt8(0, entryOffset + 2);
1632
+ output.writeUInt8(0, entryOffset + 3);
1633
+ output.writeUInt16LE(1, entryOffset + 4);
1634
+ output.writeUInt16LE(32, entryOffset + 6);
1635
+ output.writeUInt32LE(png.length, entryOffset + 8);
1636
+ output.writeUInt32LE(currentOffset, entryOffset + 12);
1637
+ png.copy(output, currentOffset);
1638
+ currentOffset += png.length;
1639
+ }
1640
+ return output;
1641
+ }
1574
1642
 
1575
1643
  const ICON_CONFIG = {
1576
1644
  minFileSize: 100,
1577
- supportedFormats: ['png', 'ico', 'jpeg', 'jpg', 'webp', 'icns'],
1578
- whiteBackground: { r: 255, g: 255, b: 255 },
1645
+ supportedFormats: [
1646
+ 'png',
1647
+ 'ico',
1648
+ 'jpeg',
1649
+ 'jpg',
1650
+ 'webp',
1651
+ 'icns',
1652
+ 'svg',
1653
+ ],
1579
1654
  transparentBackground: { r: 255, g: 255, b: 255, alpha: 0 },
1580
1655
  downloadTimeout: {
1581
1656
  ci: 5000,
@@ -1591,6 +1666,9 @@ const API_KEYS = {
1591
1666
  logoDev: ['pk_JLLMUKGZRpaG5YclhXaTkg', 'pk_Ph745P8mQSeYFfW2Wk039A'],
1592
1667
  brandfetch: ['1idqvJC0CeFSeyp3Yf7', '1idej-yhU_ThggIHFyG'],
1593
1668
  };
1669
+ /**
1670
+ * Generates platform-specific icon paths and handles copying for Windows
1671
+ */
1594
1672
  function generateIconPath(appName, isDefault = false) {
1595
1673
  const safeName = isDefault ? 'icon' : getIconBaseName(appName);
1596
1674
  const baseName = safeName;
@@ -1605,7 +1683,7 @@ function generateIconPath(appName, isDefault = false) {
1605
1683
  function getIconBaseName(appName) {
1606
1684
  const baseName = IS_LINUX
1607
1685
  ? generateLinuxPackageName(appName)
1608
- : generateSafeFilename(appName).toLowerCase();
1686
+ : getSafeAppName(appName);
1609
1687
  return baseName || 'pake-app';
1610
1688
  }
1611
1689
  async function copyWindowsIconIfNeeded(convertedPath, appName) {
@@ -1628,31 +1706,23 @@ async function copyWindowsIconIfNeeded(convertedPath, appName) {
1628
1706
  }
1629
1707
  }
1630
1708
  /**
1631
- * Adds white background to transparent icons only
1709
+ * Normalizes icon inputs to PNG while preserving alpha.
1632
1710
  */
1633
1711
  async function preprocessIcon(inputPath) {
1634
1712
  try {
1635
- const metadata = await sharp(inputPath).metadata();
1636
- if (metadata.channels !== 4)
1637
- return inputPath; // No transparency
1713
+ const extension = path.extname(inputPath).toLowerCase();
1714
+ const shouldNormalize = ['.png', '.jpeg', '.jpg', '.webp', '.svg'].includes(extension);
1715
+ if (!shouldNormalize) {
1716
+ return inputPath;
1717
+ }
1638
1718
  const { path: tempDir } = await dir();
1639
- const outputPath = path.join(tempDir, 'icon-with-background.png');
1640
- await sharp({
1641
- create: {
1642
- width: metadata.width || 512,
1643
- height: metadata.height || 512,
1644
- channels: 4,
1645
- background: { ...ICON_CONFIG.whiteBackground, alpha: 1 },
1646
- },
1647
- })
1648
- .composite([{ input: inputPath }])
1649
- .png()
1650
- .toFile(outputPath);
1719
+ const outputPath = path.join(tempDir, 'icon-normalized.png');
1720
+ await sharp(inputPath).ensureAlpha().png().toFile(outputPath);
1651
1721
  return outputPath;
1652
1722
  }
1653
1723
  catch (error) {
1654
1724
  if (error instanceof Error) {
1655
- logger.warn(`Failed to add background to icon: ${error.message}`);
1725
+ logger.warn(`Failed to normalize icon: ${error.message}`);
1656
1726
  }
1657
1727
  return inputPath;
1658
1728
  }
@@ -1719,15 +1789,22 @@ async function convertIconFormat(inputPath, appName) {
1719
1789
  const iconName = getIconBaseName(appName);
1720
1790
  // Generate platform-specific format
1721
1791
  if (IS_WIN) {
1722
- // Support multiple sizes for better Windows compatibility
1723
- await icongen(processedInputPath, platformOutputDir, {
1724
- report: false,
1725
- ico: {
1726
- name: `${iconName}_256`,
1727
- sizes: PLATFORM_CONFIG.win.sizes,
1728
- },
1729
- });
1730
- return path.join(platformOutputDir, `${iconName}_256${PLATFORM_CONFIG.win.format}`);
1792
+ const icoPath = path.join(platformOutputDir, `${iconName}_256${PLATFORM_CONFIG.win.format}`);
1793
+ const sourceBuffer = await fsExtra.readFile(processedInputPath);
1794
+ const frames = await Promise.all(PLATFORM_CONFIG.win.sizes.map(async (size) => {
1795
+ const png = await sharp(sourceBuffer)
1796
+ .resize(size, size, {
1797
+ fit: 'contain',
1798
+ background: { r: 0, g: 0, b: 0, alpha: 0 },
1799
+ })
1800
+ .ensureAlpha()
1801
+ .png()
1802
+ .toBuffer();
1803
+ return { size, png };
1804
+ }));
1805
+ const icoBuffer = buildIcoFromPngBuffers(frames);
1806
+ await fsExtra.outputFile(icoPath, icoBuffer);
1807
+ return icoPath;
1731
1808
  }
1732
1809
  if (IS_LINUX) {
1733
1810
  const outputPath = path.join(platformOutputDir, `${iconName}_${PLATFORM_CONFIG.linux.size}${PLATFORM_CONFIG.linux.format}`);
@@ -1882,15 +1959,74 @@ function generateIconServiceUrls(domain) {
1882
1959
  */
1883
1960
  function generateDashboardIconUrls(appName) {
1884
1961
  const baseUrl = 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png';
1885
- const name = appName.toLowerCase().trim();
1886
- const slugs = new Set();
1887
- // Exact name
1888
- slugs.add(name);
1889
- // Replace spaces with hyphens
1890
- slugs.add(name.replace(/\s+/g, '-'));
1891
- return [...slugs]
1892
- .filter((s) => s.length > 0)
1893
- .map((slug) => `${baseUrl}/${slug}.png`);
1962
+ return generateDashboardIconSlugs(appName).map((slug) => `${baseUrl}/${slug}.png`);
1963
+ }
1964
+ function isSupportedIconFormat(extension) {
1965
+ return ICON_CONFIG.supportedFormats.includes(extension);
1966
+ }
1967
+ function looksLikeSvg(arrayBuffer) {
1968
+ const sample = Buffer.from(arrayBuffer)
1969
+ .toString('utf-8', 0, Math.min(arrayBuffer.byteLength, 512))
1970
+ .trimStart()
1971
+ .toLowerCase();
1972
+ return (sample.startsWith('<svg') ||
1973
+ (sample.startsWith('<?xml') && sample.includes('<svg')));
1974
+ }
1975
+ function getUrlExtension(iconUrl) {
1976
+ try {
1977
+ return path.extname(new URL(iconUrl).pathname).slice(1).toLowerCase();
1978
+ }
1979
+ catch {
1980
+ return path.extname(iconUrl).slice(1).toLowerCase();
1981
+ }
1982
+ }
1983
+ async function detectDownloadedIconExtension(response, arrayBuffer, iconUrl) {
1984
+ const fileDetails = await fileTypeFromBuffer(arrayBuffer);
1985
+ if (fileDetails && isSupportedIconFormat(fileDetails.ext)) {
1986
+ return fileDetails.ext;
1987
+ }
1988
+ const contentType = response.headers
1989
+ .get('content-type')
1990
+ ?.split(';')[0]
1991
+ .trim();
1992
+ if (contentType === 'image/svg+xml' && looksLikeSvg(arrayBuffer)) {
1993
+ return 'svg';
1994
+ }
1995
+ if (getUrlExtension(iconUrl) === 'svg' && looksLikeSvg(arrayBuffer)) {
1996
+ return 'svg';
1997
+ }
1998
+ return null;
1999
+ }
2000
+ async function resolveIconFromUrl(iconUrl, appName, downloadTimeout) {
2001
+ const iconPath = await downloadIcon(iconUrl, false, downloadTimeout);
2002
+ if (!iconPath) {
2003
+ return null;
2004
+ }
2005
+ const convertedPath = await convertIconFormat(iconPath, appName);
2006
+ if (!convertedPath) {
2007
+ return null;
2008
+ }
2009
+ return await copyWindowsIconIfNeeded(convertedPath, appName);
2010
+ }
2011
+ async function tryResolveIconSource(source, domain, appName, downloadTimeout) {
2012
+ const iconUrls = source === 'dashboard'
2013
+ ? generateDashboardIconUrls(appName)
2014
+ : generateIconServiceUrls(domain);
2015
+ for (const iconUrl of iconUrls) {
2016
+ try {
2017
+ const resolvedPath = await resolveIconFromUrl(iconUrl, appName, downloadTimeout);
2018
+ if (resolvedPath) {
2019
+ return resolvedPath;
2020
+ }
2021
+ }
2022
+ catch (error) {
2023
+ if (error instanceof Error) {
2024
+ const label = source === 'dashboard' ? 'Dashboard icon' : 'Icon service';
2025
+ logger.debug(`${label} ${iconUrl} failed: ${error.message}`);
2026
+ }
2027
+ }
2028
+ }
2029
+ return null;
1894
2030
  }
1895
2031
  /**
1896
2032
  * Attempts to fetch favicon from website
@@ -1903,49 +2039,16 @@ async function tryGetFavicon(url, appName) {
1903
2039
  const downloadTimeout = isCI
1904
2040
  ? ICON_CONFIG.downloadTimeout.ci
1905
2041
  : ICON_CONFIG.downloadTimeout.default;
1906
- const serviceUrls = generateIconServiceUrls(domain);
1907
- for (const serviceUrl of serviceUrls) {
1908
- try {
1909
- const faviconPath = await downloadIcon(serviceUrl, false, downloadTimeout);
1910
- if (!faviconPath)
1911
- continue;
1912
- const convertedPath = await convertIconFormat(faviconPath, appName);
1913
- if (convertedPath) {
1914
- const finalPath = await copyWindowsIconIfNeeded(convertedPath, appName);
1915
- spinner.succeed(chalk.green('Icon fetched and converted successfully!'));
1916
- return finalPath;
1917
- }
1918
- }
1919
- catch (error) {
1920
- if (error instanceof Error) {
1921
- logger.debug(`Icon service ${serviceUrl} failed: ${error.message}`);
1922
- }
2042
+ const sourcePriority = getIconSourcePriority(url, appName);
2043
+ for (const source of sourcePriority) {
2044
+ const resolvedIconPath = await tryResolveIconSource(source, domain, appName, downloadTimeout);
2045
+ if (!resolvedIconPath) {
1923
2046
  continue;
1924
2047
  }
1925
- }
1926
- // Final fallback for selfhosted apps behind auth where domain-based
1927
- // services cannot access the site favicon.
1928
- if (appName) {
1929
- const dashboardIconUrls = generateDashboardIconUrls(appName);
1930
- for (const iconUrl of dashboardIconUrls) {
1931
- try {
1932
- const iconPath = await downloadIcon(iconUrl, false, downloadTimeout);
1933
- if (!iconPath)
1934
- continue;
1935
- const convertedPath = await convertIconFormat(iconPath, appName);
1936
- if (convertedPath) {
1937
- const finalPath = await copyWindowsIconIfNeeded(convertedPath, appName);
1938
- spinner.succeed(chalk.green(`Icon found via dashboard-icons fallback for "${appName}"!`));
1939
- return finalPath;
1940
- }
1941
- }
1942
- catch (error) {
1943
- if (error instanceof Error) {
1944
- logger.debug(`Dashboard icon ${iconUrl} failed: ${error.message}`);
1945
- }
1946
- continue;
1947
- }
1948
- }
2048
+ spinner.succeed(chalk.green(source === 'dashboard'
2049
+ ? `Icon found via dashboard-icons for "${appName}"!`
2050
+ : 'Icon fetched and converted successfully!'));
2051
+ return resolvedIconPath;
1949
2052
  }
1950
2053
  spinner.warn(`No favicon found for ${domain}. Using default.`);
1951
2054
  return null;
@@ -1979,12 +2082,11 @@ async function downloadIcon(iconUrl, showSpinner = true, customTimeout) {
1979
2082
  const arrayBuffer = await response.arrayBuffer();
1980
2083
  if (!arrayBuffer || arrayBuffer.byteLength < ICON_CONFIG.minFileSize)
1981
2084
  return null;
1982
- const fileDetails = await fileTypeFromBuffer(arrayBuffer);
1983
- if (!fileDetails ||
1984
- !ICON_CONFIG.supportedFormats.includes(fileDetails.ext)) {
2085
+ const extension = await detectDownloadedIconExtension(response, arrayBuffer, iconUrl);
2086
+ if (!extension) {
1985
2087
  return null;
1986
2088
  }
1987
- return await saveIconFile(arrayBuffer, fileDetails.ext);
2089
+ return await saveIconFile(arrayBuffer, extension);
1988
2090
  }
1989
2091
  catch (error) {
1990
2092
  clearTimeout(timeoutId);
@@ -2068,11 +2170,9 @@ function resolveLocalAppName(filePath, platform) {
2068
2170
  return normalized || 'pake-app';
2069
2171
  }
2070
2172
  function isValidName(name, platform) {
2071
- const platformRegexMapping = {
2072
- linux: /^[a-z0-9\u4e00-\u9fff][a-z0-9\u4e00-\u9fff-]*$/,
2073
- default: /^[a-zA-Z0-9\u4e00-\u9fff][a-zA-Z0-9\u4e00-\u9fff- ]*$/,
2074
- };
2075
- const reg = platformRegexMapping[platform] || platformRegexMapping.default;
2173
+ const reg = platform === 'linux'
2174
+ ? /^[a-z0-9\u4e00-\u9fff][a-z0-9\u4e00-\u9fff-]*$/
2175
+ : /^[a-zA-Z0-9\u4e00-\u9fff][a-zA-Z0-9\u4e00-\u9fff- ]*$/;
2076
2176
  return !!name && reg.test(name);
2077
2177
  }
2078
2178
  async function handleOptions(options, url) {
@@ -2177,7 +2277,7 @@ function validateNumberInput(value) {
2177
2277
  return parsedValue;
2178
2278
  }
2179
2279
  function validateUrlInput(url) {
2180
- const isFile = fs.existsSync(url);
2280
+ const isFile = fs$1.existsSync(url);
2181
2281
  if (!isFile) {
2182
2282
  try {
2183
2283
  return normalizeUrl(url);
@@ -2327,7 +2427,9 @@ ${green('|_| \\__,_|_|\\_\\___| can turn any webpage into a desktop app with
2327
2427
  .addOption(new Option('--new-window', 'Allow sites to open new windows (for auth flows, tabs, branches)')
2328
2428
  .default(DEFAULT_PAKE_OPTIONS.newWindow)
2329
2429
  .hideHelp())
2330
- .option('--install', 'Auto-install app to /Applications (macOS) after build and remove local bundle', DEFAULT_PAKE_OPTIONS.install)
2430
+ .addOption(new Option('--install', 'Auto-install app to /Applications (macOS) after build and remove local bundle')
2431
+ .default(DEFAULT_PAKE_OPTIONS.install)
2432
+ .hideHelp())
2331
2433
  .addOption(new Option('--camera', 'Request camera permission on macOS')
2332
2434
  .default(DEFAULT_PAKE_OPTIONS.camera)
2333
2435
  .hideHelp())