pake-cli 3.15.6 โ†’ 3.16.0

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
@@ -4,26 +4,26 @@ import chalk from 'chalk';
4
4
  import updateNotifier from 'update-notifier';
5
5
  import path from 'path';
6
6
  import fsExtra from 'fs-extra';
7
- import { fileURLToPath } from 'url';
8
7
  import prompts from 'prompts';
9
8
  import os from 'os';
10
9
  import { execa, execaSync } from 'execa';
11
10
  import crypto from 'crypto';
12
11
  import ora from 'ora';
13
12
  import fs from 'fs';
13
+ import { fileURLToPath } from 'url';
14
+ import { setTimeout as setTimeout$1 } from 'timers/promises';
14
15
  import fs$1 from 'fs/promises';
15
16
  import { dir } from 'tmp-promise';
16
17
  import { fileTypeFromBuffer } from 'file-type';
17
- import icongen from 'icon-gen';
18
- import sharp from 'sharp';
19
18
  import * as psl from 'psl';
20
19
  import { InvalidArgumentError, program as program$1, Option } from 'commander';
21
20
 
22
21
  var name = "pake-cli";
23
- var version = "3.15.6";
22
+ var version = "3.16.0";
24
23
  var description = "๐Ÿคฑ๐Ÿป Turn any webpage into a desktop app with one command. ๐Ÿคฑ๐Ÿป ไธ€้”ฎๆ‰“ๅŒ…็ฝ‘้กต็”Ÿๆˆ่ฝป้‡ๆกŒ้ขๅบ”็”จใ€‚";
24
+ var homepage = "https://faberon.io/projects/pake";
25
25
  var engines = {
26
- node: ">=20.0.0"
26
+ node: ">=20.9.0"
27
27
  };
28
28
  var packageManager = "pnpm@10.26.2";
29
29
  var bin = {
@@ -80,7 +80,6 @@ var dependencies = {
80
80
  execa: "^9.6.1",
81
81
  "file-type": "^21.3.4",
82
82
  "fs-extra": "^11.3.3",
83
- "icon-gen": "^5.0.0",
84
83
  loglevel: "^1.9.2",
85
84
  ora: "^9.3.0",
86
85
  prompts: "^2.4.2",
@@ -100,7 +99,6 @@ var devDependencies = {
100
99
  "@types/prompts": "^2.4.9",
101
100
  "@types/tmp": "^0.2.6",
102
101
  "@types/update-notifier": "^6.0.8",
103
- "app-root-path": "^3.1.0",
104
102
  "cross-env": "^10.1.0",
105
103
  prettier: "^3.8.1",
106
104
  rollup: "^4.59.0",
@@ -124,6 +122,7 @@ var packageJson = {
124
122
  name: name,
125
123
  version: version,
126
124
  description: description,
125
+ homepage: homepage,
127
126
  engines: engines,
128
127
  packageManager: packageManager,
129
128
  bin: bin,
@@ -140,40 +139,6 @@ var packageJson = {
140
139
  pnpm: pnpm
141
140
  };
142
141
 
143
- // Convert the current module URL to a file path
144
- const currentModulePath = fileURLToPath(import.meta.url);
145
- // Resolve the parent directory of the current module
146
- const npmDirectory = path.join(path.dirname(currentModulePath), '..');
147
- const tauriConfigDirectory = path.join(npmDirectory, 'src-tauri', '.pake');
148
-
149
- // Load configs from npm package directory, not from project source
150
- const tauriSrcDir = path.join(npmDirectory, 'src-tauri');
151
- const pakeConf = fsExtra.readJSONSync(path.join(tauriSrcDir, 'pake.json'));
152
- const CommonConf = fsExtra.readJSONSync(path.join(tauriSrcDir, 'tauri.conf.json'));
153
- const WinConf = fsExtra.readJSONSync(path.join(tauriSrcDir, 'tauri.windows.conf.json'));
154
- const MacConf = fsExtra.readJSONSync(path.join(tauriSrcDir, 'tauri.macos.conf.json'));
155
- const LinuxConf = fsExtra.readJSONSync(path.join(tauriSrcDir, 'tauri.linux.conf.json'));
156
- const platformConfigs = {
157
- win32: WinConf,
158
- darwin: MacConf,
159
- linux: LinuxConf,
160
- };
161
- const { platform: platform$2 } = process;
162
- // @ts-ignore
163
- const platformConfig = platformConfigs[platform$2];
164
- let tauriConfig = {
165
- ...CommonConf,
166
- bundle: platformConfig.bundle,
167
- app: {
168
- ...CommonConf.app,
169
- trayIcon: {
170
- ...(platformConfig?.app?.trayIcon ?? {}),
171
- },
172
- },
173
- build: CommonConf.build,
174
- pake: pakeConf,
175
- };
176
-
177
142
  // Stable exit-code contract: 0 success, 2 invalid input, 3 build/network
178
143
  // failure, 4 missing environment, 1 unexpected. Documented in cli-usage docs.
179
144
  const ERROR_EXIT_CODES = {
@@ -282,10 +247,10 @@ function isCnMirrorEnabled(value = process.env[CN_MIRROR_ENV]) {
282
247
  return TRUE_VALUES.has((value ?? '').trim().toLowerCase());
283
248
  }
284
249
 
285
- const { platform: platform$1 } = process;
286
- const IS_MAC = platform$1 === 'darwin';
287
- const IS_WIN = platform$1 === 'win32';
288
- const IS_LINUX = platform$1 === 'linux';
250
+ const { platform: platform$2 } = process;
251
+ const IS_MAC = platform$2 === 'darwin';
252
+ const IS_WIN = platform$2 === 'win32';
253
+ const IS_LINUX = platform$2 === 'linux';
289
254
  // Distro IDs / ID_LIKE families that ship an RPM-based package manager.
290
255
  const RPM_FAMILY_IDS = new Set([
291
256
  'rhel',
@@ -374,10 +339,295 @@ function getDefaultLinuxTargets() {
374
339
  return detectLinuxPackageFamily() === 'rpm' ? 'rpm,appimage' : 'deb,appimage';
375
340
  }
376
341
 
342
+ // Convert the current module URL to a file path
343
+ const currentModulePath = fileURLToPath(import.meta.url);
344
+ // Resolve the parent directory of the current module
345
+ const packageDirectory = path.join(path.dirname(currentModulePath), '..');
346
+ let npmDirectory = packageDirectory;
347
+ let tauriConfigDirectory = path.join(npmDirectory, 'src-tauri', '.pake');
348
+ // A CLI invocation owns one build workspace. Keep template resolution separate
349
+ // from generated paths so packaging never rewrites the installed package.
350
+ function setBuildDirectory(directory) {
351
+ npmDirectory = directory;
352
+ tauriConfigDirectory = path.join(directory, 'src-tauri', '.pake');
353
+ }
354
+
355
+ const logger = {
356
+ info(...msg) {
357
+ log.info(...msg.map((m) => chalk.white(m)));
358
+ },
359
+ debug(...msg) {
360
+ log.debug(...msg);
361
+ },
362
+ error(...msg) {
363
+ log.error(...msg.map((m) => chalk.red(m)));
364
+ },
365
+ warn(...msg) {
366
+ log.warn(...msg.map((m) => chalk.yellow(m)));
367
+ },
368
+ success(...msg) {
369
+ log.info(...msg.map((m) => chalk.green(m)));
370
+ },
371
+ };
372
+
373
+ /**
374
+ * Error class used for user-facing CLI errors.
375
+ *
376
+ * The top-level catch in `bin/cli.ts` prints `message` directly without a
377
+ * stack trace and exits with the code mapped from `code` (see
378
+ * ERROR_EXIT_CODES in utils/output.ts). Use this for predictable failures
379
+ * (invalid names, missing files, etc.) so users see a clean message instead
380
+ * of a Node.js stack dump. `code` and `hint` also feed the `--json` result.
381
+ */
382
+ class PakeError extends Error {
383
+ constructor(message, options) {
384
+ super(message);
385
+ this.isUserError = true;
386
+ this.name = 'PakeError';
387
+ this.code = options?.code;
388
+ this.hint = options?.hint;
389
+ }
390
+ }
391
+ function isPakeError(error) {
392
+ return (error instanceof PakeError ||
393
+ (typeof error === 'object' &&
394
+ error !== null &&
395
+ error.isUserError === true));
396
+ }
397
+
398
+ /** Check the local launcher and native binding, not just an installed manifest. */
399
+ async function hasReadyTauriCli(directory) {
400
+ const modules = path.join(directory, 'node_modules');
401
+ const launcher = path.join(modules, '.bin', process.platform === 'win32' ? 'tauri.cmd' : 'tauri');
402
+ const entry = path.join(modules, '@tauri-apps', 'cli', 'tauri.js');
403
+ try {
404
+ await fsExtra.access(launcher, process.platform === 'win32' ? fsExtra.constants.F_OK : fsExtra.constants.X_OK);
405
+ await execa(process.execPath, [entry, '--version'], {
406
+ cwd: directory,
407
+ stdio: 'ignore',
408
+ timeout: 10000,
409
+ });
410
+ return true;
411
+ }
412
+ catch {
413
+ return false;
414
+ }
415
+ }
416
+
417
+ let cancellation;
418
+ function beginBuildCancellation() {
419
+ const scope = { controller: new AbortController() };
420
+ cancellation = scope;
421
+ const cancel = (signal) => scope.controller.abort(new PakeError(`Build cancelled (${signal}).`, { code: 'BUILD_FAILED' }));
422
+ const interrupt = () => cancel('SIGINT');
423
+ const terminate = () => cancel('SIGTERM');
424
+ process.on('SIGINT', interrupt);
425
+ process.on('SIGTERM', terminate);
426
+ return () => {
427
+ process.off('SIGINT', interrupt);
428
+ process.off('SIGTERM', terminate);
429
+ if (cancellation === scope)
430
+ cancellation = undefined;
431
+ };
432
+ }
433
+ function getBuildCancellationSignal() {
434
+ return cancellation?.controller.signal;
435
+ }
436
+ function preventBuildWorkspaceCleanup(reason) {
437
+ if (cancellation)
438
+ cancellation.unsafeCleanup = reason;
439
+ }
440
+ function throwIfBuildCancelled() {
441
+ getBuildCancellationSignal()?.throwIfAborted();
442
+ }
443
+ /** Copy only build inputs; cached or previously generated user content is not a template. */
444
+ async function createBuildWorkspace(sourceDirectory) {
445
+ const directory = await fsExtra.mkdtemp(path.join(os.tmpdir(), 'pake-build-'));
446
+ try {
447
+ for (const file of [
448
+ 'package.json',
449
+ 'pnpm-lock.yaml',
450
+ 'package-lock.json',
451
+ 'rust-toolchain.toml',
452
+ 'rust-toolchain',
453
+ ]) {
454
+ const source = path.join(sourceDirectory, file);
455
+ if (await fsExtra.pathExists(source))
456
+ await fsExtra.copy(source, path.join(directory, file));
457
+ }
458
+ const sourceTauri = path.join(sourceDirectory, 'src-tauri');
459
+ await fsExtra.copy(sourceTauri, path.join(directory, 'src-tauri'), {
460
+ filter: (source) => !['target', '.pake', 'gen'].includes(path.relative(sourceTauri, source).split(path.sep)[0]),
461
+ });
462
+ await fsExtra.ensureDir(path.join(directory, 'dist'));
463
+ // Keep the CLI runnable while local input staging replaces this workspace's dist.
464
+ await fsExtra.copy(path.join(sourceDirectory, 'dist', 'cli.js'), path.join(directory, 'dist', 'cli.js'));
465
+ const modules = path.join(sourceDirectory, 'node_modules');
466
+ if (await hasReadyTauriCli(sourceDirectory)) {
467
+ await fsExtra.symlink(modules, path.join(directory, 'node_modules'), process.platform === 'win32' ? 'junction' : 'dir');
468
+ }
469
+ return directory;
470
+ }
471
+ catch (error) {
472
+ await fsExtra.remove(directory);
473
+ throw error;
474
+ }
475
+ }
476
+ /** Hold the cache through artifact copying, beyond Cargo's own compile lock. */
477
+ async function acquireBuildCache(targetDirectory) {
478
+ await fsExtra.ensureDir(targetDirectory);
479
+ const lock = path.join(targetDirectory, '.pake-build.lock');
480
+ const started = Date.now();
481
+ let announced = false;
482
+ for (;;) {
483
+ throwIfBuildCancelled();
484
+ try {
485
+ const handle = await fsExtra.open(lock, 'wx');
486
+ try {
487
+ fsExtra.writeFileSync(handle, String(process.pid));
488
+ }
489
+ finally {
490
+ fsExtra.closeSync(handle);
491
+ }
492
+ let released = false;
493
+ return async () => {
494
+ if (!released) {
495
+ await fsExtra.remove(lock);
496
+ released = true;
497
+ }
498
+ };
499
+ }
500
+ catch (error) {
501
+ if (error.code !== 'EEXIST')
502
+ throw error;
503
+ }
504
+ try {
505
+ const owner = Number(await fsExtra.readFile(lock, 'utf8'));
506
+ if (Number.isInteger(owner) && owner > 0) {
507
+ try {
508
+ process.kill(owner, 0);
509
+ }
510
+ catch (error) {
511
+ if (error.code === 'ESRCH') {
512
+ // Read-then-unlink cannot atomically reclaim a dead owner's lock:
513
+ // another waiter may already have acquired a new one at this path.
514
+ throw new PakeError('A previous Pake process left a compilation cache lock.', {
515
+ code: 'BUILD_FAILED',
516
+ hint: `After stopping other Pake builds, remove ${lock} and retry.`,
517
+ });
518
+ }
519
+ }
520
+ }
521
+ }
522
+ catch (error) {
523
+ if (error.code === 'ENOENT')
524
+ continue;
525
+ throw error;
526
+ }
527
+ if (Date.now() - started > 900000) {
528
+ throw new PakeError('Another Pake build is still using the compilation cache.', {
529
+ code: 'BUILD_FAILED',
530
+ hint: 'Wait for that build to finish, then retry.',
531
+ });
532
+ }
533
+ if (!announced) {
534
+ logger.info('Waiting for another Pake build to finish using the compilation cache...');
535
+ announced = true;
536
+ }
537
+ await setTimeout$1(200);
538
+ }
539
+ }
540
+ async function enterBuildWorkspace() {
541
+ const previousTarget = process.env.CARGO_TARGET_DIR;
542
+ const targetDirectory = path.resolve(packageDirectory, previousTarget || 'src-tauri/target');
543
+ const release = await acquireBuildCache(targetDirectory);
544
+ let directory;
545
+ try {
546
+ directory = await createBuildWorkspace(packageDirectory);
547
+ }
548
+ catch (error) {
549
+ await release();
550
+ throw error;
551
+ }
552
+ setBuildDirectory(directory);
553
+ process.env.CARGO_TARGET_DIR = targetDirectory;
554
+ const leave = async () => {
555
+ setBuildDirectory(packageDirectory);
556
+ if (previousTarget === undefined)
557
+ delete process.env.CARGO_TARGET_DIR;
558
+ else
559
+ process.env.CARGO_TARGET_DIR = previousTarget;
560
+ if (cancellation?.unsafeCleanup) {
561
+ logger.warn(`Build processes could not be confirmed stopped; keeping workspace ${directory} and its cache lock: ${cancellation.unsafeCleanup}`);
562
+ return;
563
+ }
564
+ try {
565
+ await fsExtra.remove(directory);
566
+ }
567
+ catch (error) {
568
+ logger.warn(`Could not remove the temporary build workspace ${directory}: ${String(error)}`);
569
+ }
570
+ finally {
571
+ try {
572
+ await release();
573
+ }
574
+ catch (error) {
575
+ logger.warn(`Could not release the compilation cache lock: ${String(error)}`);
576
+ }
577
+ }
578
+ };
579
+ if (getBuildCancellationSignal()?.aborted) {
580
+ await leave();
581
+ throwIfBuildCancelled();
582
+ }
583
+ return leave;
584
+ }
585
+
586
+ async function terminateBuildTree(pid) {
587
+ if (process.platform === 'win32') {
588
+ await execa('taskkill', ['/pid', String(pid), '/T', '/F'], {
589
+ timeout: 5000,
590
+ windowsHide: true,
591
+ });
592
+ return;
593
+ }
594
+ const killGroup = (signal) => {
595
+ try {
596
+ process.kill(-pid, signal);
597
+ }
598
+ catch (error) {
599
+ if (error.code !== 'ESRCH')
600
+ throw error;
601
+ }
602
+ };
603
+ killGroup('SIGTERM');
604
+ const started = Date.now();
605
+ for (;;) {
606
+ // The package manager can exit before its compiler descendants. Only
607
+ // release the cache once no live member of their process group remains.
608
+ // Zombies have already exited and cannot write artifacts.
609
+ const { stdout } = await execa('ps', ['-axo', 'pgid=,stat='], {
610
+ timeout: 1000,
611
+ });
612
+ const alive = stdout.split('\n').some((line) => {
613
+ const [group, state] = line.trim().split(/\s+/);
614
+ return Number(group) === pid && state && !state.startsWith('Z');
615
+ });
616
+ if (!alive)
617
+ return;
618
+ if (Date.now() - started > 5000)
619
+ throw new Error('Build process group did not stop.');
620
+ if (Date.now() - started >= 250)
621
+ killGroup('SIGKILL');
622
+ await setTimeout$1(25);
623
+ }
624
+ }
377
625
  async function shellExec(command, timeout = 300000, env) {
626
+ const signal = getBuildCancellationSignal();
627
+ signal?.throwIfAborted();
378
628
  try {
379
- const { exitCode } = await execa(command, {
380
- cwd: npmDirectory,
629
+ const subprocess = execa(command.executable, command.args, {
630
+ cwd: command.cwd ?? npmDirectory,
381
631
  // Use 'inherit' to show all output directly to user in real-time.
382
632
  // This ensures linuxdeploy and other tool outputs are visible during builds.
383
633
  // In machine mode (--json) stdout is reserved for the final JSON result,
@@ -385,22 +635,53 @@ async function shellExec(command, timeout = 300000, env) {
385
635
  stdin: 'inherit',
386
636
  stdout: isMachineMode() ? process.stderr : 'inherit',
387
637
  stderr: 'inherit',
388
- shell: true,
638
+ shell: false,
639
+ detached: Boolean(signal) && process.platform !== 'win32',
389
640
  timeout,
390
641
  env: env ? { ...process.env, ...env } : process.env,
391
642
  });
392
- return exitCode;
643
+ let termination;
644
+ const cancel = () => {
645
+ if (termination || subprocess.pid === undefined)
646
+ return;
647
+ termination = terminateBuildTree(subprocess.pid).catch((error) => {
648
+ preventBuildWorkspaceCleanup(String(error));
649
+ subprocess.kill('SIGKILL');
650
+ });
651
+ };
652
+ signal?.addEventListener('abort', cancel, { once: true });
653
+ if (signal?.aborted)
654
+ cancel();
655
+ try {
656
+ const { exitCode } = await subprocess;
657
+ return exitCode;
658
+ }
659
+ catch (error) {
660
+ // A timed-out or failed package manager can leave compiler descendants.
661
+ // Use the same tree barrier before the caller releases its cache lock.
662
+ if (signal)
663
+ cancel();
664
+ throw error;
665
+ }
666
+ finally {
667
+ signal?.removeEventListener('abort', cancel);
668
+ await termination;
669
+ signal?.throwIfAborted();
670
+ }
393
671
  }
394
672
  catch (error) {
673
+ if (signal?.aborted)
674
+ throw signal.reason;
675
+ const description = JSON.stringify([command.executable, ...command.args]);
395
676
  const exitCode = error.exitCode ?? 'unknown';
396
677
  const errorMessage = error.message || 'Unknown error occurred';
397
678
  if (error.timedOut) {
398
- throw new Error(`Command timed out after ${timeout}ms: "${command}". Try increasing timeout or check network connectivity.`);
679
+ throw new Error(`Command timed out after ${timeout}ms: ${description}. Try increasing timeout or check network connectivity.`);
399
680
  }
400
681
  // AppImage/linuxdeploy guidance is added by the caller (BaseBuilder), which
401
682
  // knows the build target. We only have the command line here (the tool's
402
683
  // diagnostics stream to the terminal via stdio:inherit, not into the error).
403
- throw new Error(`Error occurred while executing command "${command}". Exit code: ${exitCode}. Details: ${errorMessage}`);
684
+ throw new Error(`Error occurred while executing command ${description}. Exit code: ${exitCode}. Details: ${errorMessage}`);
404
685
  }
405
686
  }
406
687
 
@@ -451,25 +732,54 @@ function ensureRustEnv() {
451
732
  ensureCargoBinOnPath();
452
733
  }
453
734
  async function installRust() {
454
- const rustInstallScriptForUnix = isCnMirrorEnabled()
455
- ? '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'
456
- : "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y";
457
- const rustInstallScriptForWindows = 'winget install --id Rustlang.Rustup';
458
735
  const spinner = getSpinner('Downloading Rust...');
459
736
  try {
460
- await shellExec(IS_WIN ? rustInstallScriptForWindows : rustInstallScriptForUnix, 300000, undefined);
737
+ if (IS_WIN) {
738
+ await shellExec({
739
+ executable: 'winget',
740
+ args: ['install', '--id', 'Rustlang.Rustup'],
741
+ });
742
+ }
743
+ else {
744
+ const useCnMirror = isCnMirrorEnabled();
745
+ const tempDir = await fsExtra.mkdtemp(path.join(os.tmpdir(), 'pake-rustup-'));
746
+ try {
747
+ const scriptPath = path.join(tempDir, 'rustup-init.sh');
748
+ await shellExec({
749
+ executable: 'curl',
750
+ args: [
751
+ '--proto',
752
+ '=https',
753
+ '--tlsv1.2',
754
+ '-sSf',
755
+ '-o',
756
+ scriptPath,
757
+ useCnMirror
758
+ ? 'https://rsproxy.cn/rustup-init.sh'
759
+ : 'https://sh.rustup.rs',
760
+ ],
761
+ });
762
+ await shellExec({
763
+ executable: 'sh',
764
+ args: useCnMirror ? [scriptPath] : [scriptPath, '-y'],
765
+ }, 300000, useCnMirror
766
+ ? {
767
+ RUSTUP_DIST_SERVER: 'https://rsproxy.cn',
768
+ RUSTUP_UPDATE_ROOT: 'https://rsproxy.cn/rustup',
769
+ }
770
+ : undefined);
771
+ }
772
+ finally {
773
+ await fsExtra.remove(tempDir);
774
+ }
775
+ }
461
776
  spinner.succeed(chalk.green('โœ” Rust installed successfully!'));
462
777
  ensureRustEnv();
463
778
  }
464
779
  catch (error) {
465
780
  spinner.fail(chalk.red('โœ• Rust installation failed!'));
466
- if (error instanceof Error) {
467
- console.error(error.message);
468
- }
469
- else {
470
- console.error(error);
471
- }
472
- process.exit(1);
781
+ // The CLI owns error reporting and workspace/cache cleanup.
782
+ throw error;
473
783
  }
474
784
  }
475
785
  function checkRustInstalled() {
@@ -489,9 +799,13 @@ async function combineFiles(files, output) {
489
799
  const fileContent = await fs$1.readFile(file, 'utf-8');
490
800
  return `window.addEventListener('DOMContentLoaded', (_event) => {
491
801
  const css = ${JSON.stringify(fileContent)};
492
- const style = document.createElement('style');
493
- style.textContent = css;
494
- document.head.appendChild(style);
802
+ if (typeof window.__PAKE_INJECT_STYLE__ === 'function') {
803
+ window.__PAKE_INJECT_STYLE__(css);
804
+ } else {
805
+ const style = document.createElement('style');
806
+ style.textContent = css;
807
+ document.head.appendChild(style);
808
+ }
495
809
  });`;
496
810
  }
497
811
  const fileContent = await fs$1.readFile(file);
@@ -507,24 +821,6 @@ async function combineFiles(files, output) {
507
821
  return files;
508
822
  }
509
823
 
510
- const logger = {
511
- info(...msg) {
512
- log.info(...msg.map((m) => chalk.white(m)));
513
- },
514
- debug(...msg) {
515
- log.debug(...msg);
516
- },
517
- error(...msg) {
518
- log.error(...msg.map((m) => chalk.red(m)));
519
- },
520
- warn(...msg) {
521
- log.warn(...msg.map((m) => chalk.yellow(m)));
522
- },
523
- success(...msg) {
524
- log.info(...msg.map((m) => chalk.green(m)));
525
- },
526
- };
527
-
528
824
  function generateSafeFilename(name) {
529
825
  return name
530
826
  .replace(/[<>:"/\\|?*]/g, '_')
@@ -562,31 +858,6 @@ function generateIdentifierSafeName(name) {
562
858
  return cleaned;
563
859
  }
564
860
 
565
- /**
566
- * Error class used for user-facing CLI errors.
567
- *
568
- * The top-level catch in `bin/cli.ts` prints `message` directly without a
569
- * stack trace and exits with the code mapped from `code` (see
570
- * ERROR_EXIT_CODES in utils/output.ts). Use this for predictable failures
571
- * (invalid names, missing files, etc.) so users see a clean message instead
572
- * of a Node.js stack dump. `code` and `hint` also feed the `--json` result.
573
- */
574
- class PakeError extends Error {
575
- constructor(message, options) {
576
- super(message);
577
- this.isUserError = true;
578
- this.name = 'PakeError';
579
- this.code = options?.code;
580
- this.hint = options?.hint;
581
- }
582
- }
583
- function isPakeError(error) {
584
- return (error instanceof PakeError ||
585
- (typeof error === 'object' &&
586
- error !== null &&
587
- error.isUserError === true));
588
- }
589
-
590
861
  const LINUX_TARGET_TYPES = ['deb', 'appimage', 'rpm', 'zst'];
591
862
  // Returns the valid Linux build targets from a comma-separated targets
592
863
  // string, preserving LINUX_TARGET_TYPES order. Unknown entries are dropped.
@@ -689,8 +960,11 @@ async function stageLocalTree(sourceDir) {
689
960
  const resolvedPackage = await fsExtra
690
961
  .realpath(npmDirectory)
691
962
  .catch(() => path.resolve(npmDirectory));
963
+ const installedPackage = await fsExtra.realpath(packageDirectory);
692
964
  const packageDist = path.join(resolvedPackage, 'dist');
693
- if (resolvedSource === resolvedPackage ||
965
+ if (resolvedSource === installedPackage ||
966
+ installedPackage.startsWith(resolvedSource + path.sep) ||
967
+ resolvedSource === resolvedPackage ||
694
968
  resolvedPackage.startsWith(resolvedSource + path.sep) ||
695
969
  resolvedSource === packageDist ||
696
970
  resolvedSource.startsWith(packageDist + path.sep)) {
@@ -922,7 +1196,7 @@ async function mergeIcons(options, name, tauriConf, platform, safeAppName) {
922
1196
  delete tauriConf.app.trayIcon;
923
1197
  }
924
1198
  async function injectCustomCode(options, tauriConf) {
925
- const { inject, proxyUrl, multiInstance, multiWindow, wasm } = options;
1199
+ const { inject, proxyUrl, basicAuth, multiInstance, multiWindow, wasm } = options;
926
1200
  const injectFilePath = path.join(npmDirectory, 'src-tauri/src/inject/custom.js');
927
1201
  if (inject?.length > 0) {
928
1202
  const injectArray = Array.isArray(inject) ? inject : [inject];
@@ -939,6 +1213,7 @@ async function injectCustomCode(options, tauriConf) {
939
1213
  await fsExtra.writeFile(injectFilePath, '');
940
1214
  }
941
1215
  tauriConf.pake.proxy_url = proxyUrl || '';
1216
+ tauriConf.pake.basic_auth = basicAuth;
942
1217
  tauriConf.pake.multi_instance = multiInstance;
943
1218
  tauriConf.pake.multi_window = multiWindow;
944
1219
  if (wasm) {
@@ -1041,6 +1316,34 @@ async function mergeConfig(url, options, tauriConf) {
1041
1316
  await writeAllConfigs(tauriConf, platform);
1042
1317
  }
1043
1318
 
1319
+ // Load configs from npm package directory, not from project source
1320
+ const tauriSrcDir = path.join(npmDirectory, 'src-tauri');
1321
+ const pakeConf = fsExtra.readJSONSync(path.join(tauriSrcDir, 'pake.json'));
1322
+ const CommonConf = fsExtra.readJSONSync(path.join(tauriSrcDir, 'tauri.conf.json'));
1323
+ const WinConf = fsExtra.readJSONSync(path.join(tauriSrcDir, 'tauri.windows.conf.json'));
1324
+ const MacConf = fsExtra.readJSONSync(path.join(tauriSrcDir, 'tauri.macos.conf.json'));
1325
+ const LinuxConf = fsExtra.readJSONSync(path.join(tauriSrcDir, 'tauri.linux.conf.json'));
1326
+ const platformConfigs = {
1327
+ win32: WinConf,
1328
+ darwin: MacConf,
1329
+ linux: LinuxConf,
1330
+ };
1331
+ const { platform: platform$1 } = process;
1332
+ // @ts-ignore
1333
+ const platformConfig = platformConfigs[platform$1];
1334
+ let tauriConfig = {
1335
+ ...CommonConf,
1336
+ bundle: platformConfig.bundle,
1337
+ app: {
1338
+ ...CommonConf.app,
1339
+ trayIcon: {
1340
+ ...(platformConfig?.app?.trayIcon ?? {}),
1341
+ },
1342
+ },
1343
+ build: CommonConf.build,
1344
+ pake: pakeConf,
1345
+ };
1346
+
1044
1347
  /**
1045
1348
  * Returns build environment variables overrides for macOS, where Rust crates
1046
1349
  * sometimes need explicit C/C++ flags and a deterministic SDK target. Other
@@ -1132,11 +1435,12 @@ async function detectPackageManager() {
1132
1435
  return 'pnpm';
1133
1436
  }
1134
1437
  function getInstallCommand(packageManager, useCnMirror) {
1135
- const registryOption = useCnMirror
1136
- ? ' --registry=https://registry.npmmirror.com'
1137
- : '';
1138
- const peerDepsOption = packageManager === 'npm' ? ' --legacy-peer-deps' : '';
1139
- return `cd "${npmDirectory}" && ${packageManager} install${registryOption}${peerDepsOption}`;
1438
+ const args = ['install'];
1439
+ if (useCnMirror)
1440
+ args.push('--registry=https://registry.npmmirror.com');
1441
+ if (packageManager === 'npm')
1442
+ args.push('--legacy-peer-deps');
1443
+ return { executable: packageManager, args };
1140
1444
  }
1141
1445
  async function copyFileWithSamePathGuard(sourcePath, destinationPath) {
1142
1446
  if (path.resolve(sourcePath) === path.resolve(destinationPath)) {
@@ -1268,7 +1572,7 @@ class BaseBuilder {
1268
1572
  }
1269
1573
  async prepare() {
1270
1574
  const tauriSrcPath = path.join(npmDirectory, 'src-tauri');
1271
- const tauriTargetPath = path.join(tauriSrcPath, 'target');
1575
+ const tauriTargetPath = this.getCargoTargetDir();
1272
1576
  const tauriTargetPathExists = await fsExtra.pathExists(tauriTargetPath);
1273
1577
  if (!IS_MAC && !tauriTargetPathExists) {
1274
1578
  logger.warn('โœผ The first use requires installing system dependencies.');
@@ -1297,9 +1601,21 @@ class BaseBuilder {
1297
1601
  });
1298
1602
  }
1299
1603
  }
1300
- const spinner = getSpinner('Installing package...');
1301
1604
  const useCnMirror = isCnMirrorEnabled();
1302
1605
  await configureCargoRegistry(tauriSrcPath, useCnMirror);
1606
+ // Workspaces reuse installed dependencies. Reinstalling through their
1607
+ // node_modules link would mutate the shared CLI installation.
1608
+ if (await hasReadyTauriCli(npmDirectory)) {
1609
+ return;
1610
+ }
1611
+ // Dependencies may disappear after the workspace linked them. Reinstall
1612
+ // privately even in that case, without writing through to the source tree.
1613
+ const modules = path.join(npmDirectory, 'node_modules');
1614
+ if (npmDirectory !== packageDirectory &&
1615
+ (await fsExtra.lstat(modules).catch(() => null))?.isSymbolicLink()) {
1616
+ await fsExtra.unlink(modules);
1617
+ }
1618
+ const spinner = getSpinner('Installing package...');
1303
1619
  const packageManager = await detectPackageManager();
1304
1620
  const timeout = getInstallTimeout();
1305
1621
  const buildEnv = getBuildEnvironment();
@@ -1335,23 +1651,24 @@ class BaseBuilder {
1335
1651
  }
1336
1652
  async start(url) {
1337
1653
  logger.info('Pake dev server starting...');
1338
- await mergeConfig(url, this.options, tauriConfig);
1654
+ await mergeConfig(url, this.options, structuredClone(tauriConfig));
1339
1655
  const packageManager = await detectPackageManager();
1340
1656
  const configPath = path.join(npmDirectory, 'src-tauri', '.pake', 'tauri.conf.json');
1341
1657
  const features = this.getBuildFeatures();
1342
- const featureArgs = features.length > 0 ? `--features ${features.join(',')}` : '';
1343
- const argSeparator = packageManager === 'npm' ? ' --' : '';
1344
- const command = `cd "${npmDirectory}" && ${packageManager} run tauri${argSeparator} dev --config "${configPath}" ${featureArgs}`;
1345
- await shellExec(command);
1658
+ const args = ['run', 'tauri'];
1659
+ if (packageManager === 'npm')
1660
+ args.push('--');
1661
+ args.push('dev', '--config', configPath);
1662
+ if (features.length > 0)
1663
+ args.push('--features', features.join(','));
1664
+ await shellExec({ executable: packageManager, args });
1346
1665
  }
1347
1666
  async buildAndCopy(url, target, logSuccess = true) {
1348
1667
  const { name = 'pake-app' } = this.options;
1349
- await mergeConfig(url, this.options, tauriConfig);
1668
+ await mergeConfig(url, this.options, structuredClone(tauriConfig));
1350
1669
  const packageManager = await detectPackageManager();
1351
1670
  // Build app
1352
1671
  const buildSpinner = getSpinner('Building app...');
1353
- // Let spinner run for a moment so user can see it, then stop before package manager command
1354
- await new Promise((resolve) => setTimeout(resolve, 500));
1355
1672
  buildSpinner.stop();
1356
1673
  // Show static message to keep the status visible. Info, not warn: warn
1357
1674
  // entries feed the --json warnings array and this is a status line.
@@ -1368,7 +1685,7 @@ class BaseBuilder {
1368
1685
  if (isLinuxAppImage && !buildEnv.NO_STRIP && this.options.debug) {
1369
1686
  logger.warn('โš  AppImage strip step can fail on glibc 2.38+; Pake will auto-retry with NO_STRIP=1.');
1370
1687
  }
1371
- const buildCommand = `cd "${npmDirectory}" && ${this.getBuildCommand(packageManager)}`;
1688
+ const buildCommand = this.getBuildCommand(packageManager);
1372
1689
  const buildTimeout = getBuildTimeout();
1373
1690
  try {
1374
1691
  await shellExec(buildCommand, buildTimeout, resolveExecEnv());
@@ -1476,24 +1793,23 @@ class BaseBuilder {
1476
1793
  return BaseBuilder.ARCH_DISPLAY_NAMES[arch] || arch;
1477
1794
  }
1478
1795
  buildBaseCommand(packageManager, configPath, target) {
1479
- const baseCommand = this.options.debug
1480
- ? `${packageManager} run build:debug`
1481
- : `${packageManager} run build`;
1482
- const argSeparator = packageManager === 'npm' ? ' --' : '';
1483
- let fullCommand = `${baseCommand}${argSeparator} -c "${configPath}"`;
1796
+ const args = ['run', this.options.debug ? 'build:debug' : 'build'];
1797
+ if (packageManager === 'npm')
1798
+ args.push('--');
1799
+ args.push('-c', configPath);
1484
1800
  if (target) {
1485
- fullCommand += ` --target ${target}`;
1801
+ args.push('--target', target);
1486
1802
  }
1487
1803
  // Enable verbose output in debug mode to help diagnose build issues.
1488
1804
  // This provides detailed logs from Tauri CLI and bundler tools.
1489
1805
  if (this.options.debug) {
1490
- fullCommand += ' --verbose';
1806
+ args.push('--verbose');
1491
1807
  }
1492
1808
  const features = this.getBuildFeatures();
1493
1809
  if (features.length > 0) {
1494
- fullCommand += ` --features ${features.join(',')}`;
1810
+ args.push('--features', features.join(','));
1495
1811
  }
1496
- return fullCommand;
1812
+ return { executable: packageManager, args };
1497
1813
  }
1498
1814
  getBuildFeatures() {
1499
1815
  const features = ['cli-build'];
@@ -1509,10 +1825,10 @@ class BaseBuilder {
1509
1825
  getBuildCommand(packageManager = 'pnpm') {
1510
1826
  // Use temporary config directory to avoid modifying source files
1511
1827
  const configPath = path.join(npmDirectory, 'src-tauri', '.pake', 'tauri.conf.json');
1512
- let fullCommand = this.buildBaseCommand(packageManager, configPath);
1828
+ const fullCommand = this.buildBaseCommand(packageManager, configPath);
1513
1829
  // For macOS, use app bundles by default unless DMG is explicitly requested
1514
1830
  if (IS_MAC && this.options.targets === 'app') {
1515
- fullCommand += ' --bundles app';
1831
+ fullCommand.args.push('--bundles', 'app');
1516
1832
  }
1517
1833
  return fullCommand;
1518
1834
  }
@@ -1663,7 +1979,7 @@ class MacBuilder extends BaseBuilder {
1663
1979
  else {
1664
1980
  arch = this.getArchDisplayName(this.resolveTargetArch(this.buildArch));
1665
1981
  }
1666
- return `${name}_${tauriConfig.version}_${arch}`;
1982
+ return `${name}_${this.options.appVersion}_${arch}`;
1667
1983
  }
1668
1984
  getReportArch() {
1669
1985
  return this.getActualArch();
@@ -1726,9 +2042,9 @@ class WinBuilder extends BaseBuilder {
1726
2042
  }
1727
2043
  getFileName() {
1728
2044
  const { name } = this.options;
1729
- const language = tauriConfig.bundle.windows.wix.language[0];
2045
+ const language = this.options.installerLanguage;
1730
2046
  const targetArch = this.getArchDisplayName(this.buildArch);
1731
- return `${name}_${tauriConfig.version}_${targetArch}_${language}`;
2047
+ return `${name}_${this.options.appVersion}_${targetArch}_${language}`;
1732
2048
  }
1733
2049
  getBuildCommand(packageManager = 'pnpm') {
1734
2050
  const configPath = path.join('src-tauri', '.pake', 'tauri.conf.json');
@@ -1784,7 +2100,7 @@ class LinuxBuilder extends BaseBuilder {
1784
2100
  }
1785
2101
  getFileName() {
1786
2102
  const { name = 'pake-app', targets } = this.options;
1787
- const version = tauriConfig.version;
2103
+ const version = this.options.appVersion;
1788
2104
  const buildType = this.currentBuildType || targets.split(',').map((t) => t.trim())[0];
1789
2105
  let arch;
1790
2106
  if (this.buildArch === 'arm64') {
@@ -1862,7 +2178,7 @@ class LinuxBuilder extends BaseBuilder {
1862
2178
  ];
1863
2179
  for (const { tool, pacmanPackage } of requiredTools) {
1864
2180
  try {
1865
- await shellExec(`command -v ${tool} >/dev/null 2>&1`);
2181
+ await execa(tool, ['--version'], { stdio: 'ignore' });
1866
2182
  }
1867
2183
  catch {
1868
2184
  throw new Error(`Building a zst package requires "${tool}". Install it first, e.g. "sudo pacman -S ${pacmanPackage}".`);
@@ -1872,24 +2188,30 @@ class LinuxBuilder extends BaseBuilder {
1872
2188
  async createArchPackageFromDeb({ removeSourceDeb, }) {
1873
2189
  const { name = 'pake-app' } = this.options;
1874
2190
  const packageName = generateLinuxPackageName(name);
1875
- const version = tauriConfig.version;
2191
+ const version = this.options.appVersion;
1876
2192
  const arch = this.buildArch === 'arm64' ? 'aarch64' : 'x86_64';
1877
2193
  const debPath = path.resolve(`${name}.deb`);
1878
2194
  const packagePath = path.resolve(`${name}-${version}-1-${arch}.pkg.tar.zst`);
1879
- const workDir = path.resolve('.pake-arch-package');
2195
+ await this.ensureArchPackagingTools();
2196
+ const workDir = await fsExtra.mkdtemp(path.join(os.tmpdir(), 'pake-arch-'));
1880
2197
  const dataDir = path.join(workDir, 'data');
1881
2198
  const controlDir = path.join(workDir, 'control');
1882
- await this.ensureArchPackagingTools();
1883
- await fsExtra.remove(workDir);
1884
- await fsExtra.ensureDir(dataDir);
1885
- await fsExtra.ensureDir(controlDir);
1886
2199
  try {
1887
- await shellExec(`cd "${controlDir}" && ar x "${debPath}"`);
2200
+ await fsExtra.ensureDir(dataDir);
2201
+ await fsExtra.ensureDir(controlDir);
2202
+ await shellExec({
2203
+ executable: 'ar',
2204
+ args: ['x', debPath],
2205
+ cwd: controlDir,
2206
+ });
1888
2207
  const dataArchive = (await fsExtra.readdir(controlDir)).find((file) => file.startsWith('data.tar'));
1889
2208
  if (!dataArchive) {
1890
2209
  throw new Error(`Could not find data.tar payload in ${debPath}`);
1891
2210
  }
1892
- await shellExec(`tar -xf "${path.join(controlDir, dataArchive)}" -C "${dataDir}"`);
2211
+ await shellExec({
2212
+ executable: 'tar',
2213
+ args: ['-xf', path.join(controlDir, dataArchive), '-C', dataDir],
2214
+ });
1893
2215
  // Drop the desktop entry auto-generated by the Tauri deb bundler;
1894
2216
  // the payload already ships Pake's own com.pake.<name>.desktop.
1895
2217
  await fsExtra.remove(path.join(dataDir, 'usr', 'share', 'applications', `${packageName}.desktop`));
@@ -1929,7 +2251,19 @@ post_remove() {
1929
2251
  update-desktop-database -q usr/share/applications
1930
2252
  }
1931
2253
  `);
1932
- await shellExec(`bsdtar --zstd -cf "${packagePath}" -C "${dataDir}" .PKGINFO .INSTALL usr`);
2254
+ await shellExec({
2255
+ executable: 'bsdtar',
2256
+ args: [
2257
+ '--zstd',
2258
+ '-cf',
2259
+ packagePath,
2260
+ '-C',
2261
+ dataDir,
2262
+ '.PKGINFO',
2263
+ '.INSTALL',
2264
+ 'usr',
2265
+ ],
2266
+ });
1933
2267
  await this.recordArtifact(packagePath, 'zst');
1934
2268
  logger.success('โœ” Build success!');
1935
2269
  logger.success('โœ” App installer located in', packagePath);
@@ -1967,14 +2301,15 @@ post_remove() {
1967
2301
  const buildTarget = this.buildArch === 'arm64'
1968
2302
  ? (this.getTauriTarget(this.buildArch, 'linux') ?? undefined)
1969
2303
  : undefined;
1970
- let fullCommand = this.buildBaseCommand(packageManager, configPath, buildTarget);
2304
+ const fullCommand = this.buildBaseCommand(packageManager, configPath, buildTarget);
1971
2305
  // --no-bundle: build the executable only, skipping .deb/.rpm/.appimage
1972
2306
  // packaging entirely (e.g. RPM-based distros where the bundler aborts).
1973
2307
  if (this.options.bundle === false) {
1974
- return `${fullCommand} --no-bundle`;
2308
+ fullCommand.args.push('--no-bundle');
2309
+ return fullCommand;
1975
2310
  }
1976
2311
  if (this.currentBuildType) {
1977
- fullCommand += ` --bundles ${this.currentBuildType}`;
2312
+ fullCommand.args.push('--bundles', this.currentBuildType);
1978
2313
  }
1979
2314
  // Enable verbose output for AppImage builds when debugging or PAKE_VERBOSE is set.
1980
2315
  // AppImage builds often fail with minimal error messages from linuxdeploy,
@@ -1983,7 +2318,7 @@ post_remove() {
1983
2318
  (this.options.targets.includes('appimage') ||
1984
2319
  this.options.debug ||
1985
2320
  process.env.PAKE_VERBOSE)) {
1986
- fullCommand += ' --verbose';
2321
+ fullCommand.args.push('--verbose');
1987
2322
  }
1988
2323
  return fullCommand;
1989
2324
  }
@@ -2114,6 +2449,9 @@ function getIconSourcePriority(url, appName) {
2114
2449
  : ['domain', 'dashboard'];
2115
2450
  }
2116
2451
 
2452
+ async function loadSharp$1() {
2453
+ return (await import('sharp')).default;
2454
+ }
2117
2455
  const ICO_HEADER_SIZE = 6;
2118
2456
  const ICO_DIR_ENTRY_SIZE = 16;
2119
2457
  const ICO_TYPE_ICON = 1;
@@ -2247,6 +2585,7 @@ async function pickLargestFrameAsPng(buffer, entries) {
2247
2585
  // Fallback: let sharp render directly from the ICO buffer. sharp picks the
2248
2586
  // largest embedded frame on its own.
2249
2587
  try {
2588
+ const sharp = await loadSharp$1();
2250
2589
  return await sharp(buffer).png().toBuffer();
2251
2590
  }
2252
2591
  catch {
@@ -2268,6 +2607,7 @@ async function ensureMultiResolutionIco(sourcePath, outputPath, preferredSize =
2268
2607
  if (!sourcePng) {
2269
2608
  return await writeIcoWithPreferredSize(sourcePath, outputPath, preferredSize);
2270
2609
  }
2610
+ const sharp = await loadSharp$1();
2271
2611
  const frames = await Promise.all(desiredSizes.map(async (size) => {
2272
2612
  // Reuse an existing exact-size PNG frame when possible to keep any
2273
2613
  // hand-tuned small icon (e.g. a 16x16 with deliberate pixel hinting).
@@ -2334,6 +2674,9 @@ function buildIcoFromPngBuffers(frames) {
2334
2674
  return output;
2335
2675
  }
2336
2676
 
2677
+ async function loadSharp() {
2678
+ return (await import('sharp')).default;
2679
+ }
2337
2680
  const ICON_CONFIG = {
2338
2681
  minFileSize: 100,
2339
2682
  supportedFormats: [
@@ -2354,8 +2697,20 @@ const ICON_CONFIG = {
2354
2697
  const PLATFORM_CONFIG = {
2355
2698
  win: { format: '.ico', sizes: [...WIN_STANDARD_ICO_SIZES] },
2356
2699
  linux: { format: '.png', size: 512 },
2357
- macos: { format: '.icns', sizes: [16, 32, 64, 128, 256, 512, 1024] },
2700
+ macos: { format: '.icns' },
2358
2701
  };
2702
+ const MACOS_ICONSET_FILES = [
2703
+ ['icon_16x16.png', 16],
2704
+ ['icon_16x16@2x.png', 32],
2705
+ ['icon_32x32.png', 32],
2706
+ ['icon_32x32@2x.png', 64],
2707
+ ['icon_128x128.png', 128],
2708
+ ['icon_128x128@2x.png', 256],
2709
+ ['icon_256x256.png', 256],
2710
+ ['icon_256x256@2x.png', 512],
2711
+ ['icon_512x512.png', 512],
2712
+ ['icon_512x512@2x.png', 1024],
2713
+ ];
2359
2714
  const API_KEYS = {
2360
2715
  logoDev: ['pk_JLLMUKGZRpaG5YclhXaTkg', 'pk_Ph745P8mQSeYFfW2Wk039A'],
2361
2716
  brandfetch: ['1idqvJC0CeFSeyp3Yf7', '1idej-yhU_ThggIHFyG'],
@@ -2414,6 +2769,7 @@ async function preprocessIcon(inputPath) {
2414
2769
  if (!shouldNormalize) {
2415
2770
  return inputPath;
2416
2771
  }
2772
+ const sharp = await loadSharp();
2417
2773
  const { path: tempDir } = await dir();
2418
2774
  const outputPath = path.join(tempDir, 'icon-normalized.png');
2419
2775
  await sharp(inputPath).ensureAlpha().png().toFile(outputPath);
@@ -2431,6 +2787,7 @@ async function preprocessIcon(inputPath) {
2431
2787
  */
2432
2788
  async function applyMacOSMask(inputPath) {
2433
2789
  try {
2790
+ const sharp = await loadSharp();
2434
2791
  const { path: tempDir } = await dir();
2435
2792
  const outputPath = path.join(tempDir, 'icon-macos-rounded.png');
2436
2793
  // 1. Create a 1024x1024 rounded rect mask
@@ -2474,6 +2831,32 @@ async function applyMacOSMask(inputPath) {
2474
2831
  return inputPath;
2475
2832
  }
2476
2833
  }
2834
+ async function generateMacOSIcns(inputPath, outputDir, iconName) {
2835
+ const sharp = await loadSharp();
2836
+ const iconsetPath = path.join(outputDir, `${iconName}.iconset`);
2837
+ const outputPath = path.join(outputDir, `${iconName}${PLATFORM_CONFIG.macos.format}`);
2838
+ await fsExtra.ensureDir(iconsetPath);
2839
+ const source = sharp(inputPath);
2840
+ await Promise.all(MACOS_ICONSET_FILES.map(async ([fileName, size]) => {
2841
+ await source
2842
+ .clone()
2843
+ .resize(size, size, {
2844
+ fit: 'contain',
2845
+ background: ICON_CONFIG.transparentBackground,
2846
+ })
2847
+ .ensureAlpha()
2848
+ .png()
2849
+ .toFile(path.join(iconsetPath, fileName));
2850
+ }));
2851
+ await execa('/usr/bin/iconutil', [
2852
+ '-c',
2853
+ 'icns',
2854
+ iconsetPath,
2855
+ '-o',
2856
+ outputPath,
2857
+ ]);
2858
+ return outputPath;
2859
+ }
2477
2860
  /**
2478
2861
  * Converts icon to platform-specific format
2479
2862
  */
@@ -2488,6 +2871,7 @@ async function convertIconFormat(inputPath, appName) {
2488
2871
  const iconName = getIconBaseName(appName);
2489
2872
  // Generate platform-specific format
2490
2873
  if (IS_WIN) {
2874
+ const sharp = await loadSharp();
2491
2875
  const icoPath = path.join(platformOutputDir, `${iconName}_256${PLATFORM_CONFIG.win.format}`);
2492
2876
  const sourceBuffer = await fsExtra.readFile(processedInputPath);
2493
2877
  const frames = await Promise.all(PLATFORM_CONFIG.win.sizes.map(async (size) => {
@@ -2506,6 +2890,7 @@ async function convertIconFormat(inputPath, appName) {
2506
2890
  return icoPath;
2507
2891
  }
2508
2892
  if (IS_LINUX) {
2893
+ const sharp = await loadSharp();
2509
2894
  const outputPath = path.join(platformOutputDir, `${iconName}_${PLATFORM_CONFIG.linux.size}${PLATFORM_CONFIG.linux.format}`);
2510
2895
  // Ensure we convert to proper PNG format with correct size
2511
2896
  await sharp(processedInputPath)
@@ -2520,11 +2905,7 @@ async function convertIconFormat(inputPath, appName) {
2520
2905
  }
2521
2906
  // macOS
2522
2907
  const macIconPath = await applyMacOSMask(processedInputPath);
2523
- await icongen(macIconPath, platformOutputDir, {
2524
- report: false,
2525
- icns: { name: iconName, sizes: PLATFORM_CONFIG.macos.sizes },
2526
- });
2527
- const outputPath = path.join(platformOutputDir, `${iconName}${PLATFORM_CONFIG.macos.format}`);
2908
+ const outputPath = await generateMacOSIcns(macIconPath, platformOutputDir, iconName);
2528
2909
  return (await fsExtra.pathExists(outputPath)) ? outputPath : null;
2529
2910
  }
2530
2911
  catch (error) {
@@ -2539,6 +2920,7 @@ async function isLinuxBundleIconReady(iconPath) {
2539
2920
  return false;
2540
2921
  }
2541
2922
  try {
2923
+ const sharp = await loadSharp();
2542
2924
  const { width, height } = await sharp(iconPath).metadata();
2543
2925
  return (width === PLATFORM_CONFIG.linux.size &&
2544
2926
  height === PLATFORM_CONFIG.linux.size);
@@ -2784,7 +3166,6 @@ async function downloadIcon(iconUrl, showSpinner = true, customTimeout) {
2784
3166
  const response = await fetch(iconUrl, {
2785
3167
  signal: controller.signal,
2786
3168
  });
2787
- clearTimeout(timeoutId);
2788
3169
  if (!response.ok) {
2789
3170
  if (response.status === 404 && !showSpinner) {
2790
3171
  return null;
@@ -2801,7 +3182,6 @@ async function downloadIcon(iconUrl, showSpinner = true, customTimeout) {
2801
3182
  return await saveIconFile(arrayBuffer, extension);
2802
3183
  }
2803
3184
  catch (error) {
2804
- clearTimeout(timeoutId);
2805
3185
  if (showSpinner) {
2806
3186
  if (error instanceof Error && error.name === 'AbortError') {
2807
3187
  logger.error('Icon download timed out!');
@@ -2812,6 +3192,9 @@ async function downloadIcon(iconUrl, showSpinner = true, customTimeout) {
2812
3192
  }
2813
3193
  return null;
2814
3194
  }
3195
+ finally {
3196
+ clearTimeout(timeoutId);
3197
+ }
2815
3198
  }
2816
3199
  /**
2817
3200
  * Saves icon file to temporary location
@@ -2987,6 +3370,7 @@ const DEFAULT_PAKE_OPTIONS = {
2987
3370
  useLocalFile: false,
2988
3371
  systemTrayIcon: '',
2989
3372
  proxyUrl: '',
3373
+ basicAuth: false,
2990
3374
  debug: false,
2991
3375
  json: false,
2992
3376
  inject: [],
@@ -3089,6 +3473,9 @@ ${green('|_| \\__,_|_|\\_\\___| can turn any webpage into a desktop app with
3089
3473
  .option('--debug', 'Debug build and more output', DEFAULT_PAKE_OPTIONS.debug)
3090
3474
  .option('--json', 'Machine-readable output: logs to stderr, one JSON result on stdout', DEFAULT_PAKE_OPTIONS.json)
3091
3475
  .option('--config <path>', 'Load options from a JSON config file (fields mirror CLI options, see schema/pake.schema.json)')
3476
+ .addOption(new Option('--basic-auth', 'Prompt for HTTP Basic credentials at runtime (macOS only)')
3477
+ .default(DEFAULT_PAKE_OPTIONS.basicAuth)
3478
+ .hideHelp())
3092
3479
  .addOption(new Option('--proxy-url <url>', 'Proxy URL for all network requests (http://, https://, socks5://)')
3093
3480
  .default(DEFAULT_PAKE_OPTIONS.proxyUrl)
3094
3481
  .hideHelp())
@@ -3386,6 +3773,8 @@ program.action(async (urlArg, options) => {
3386
3773
  let phase = 'input';
3387
3774
  let appName = null;
3388
3775
  let url = urlArg;
3776
+ let leaveWorkspace;
3777
+ let endCancellation;
3389
3778
  try {
3390
3779
  // Heal a dist_bak stranded by an earlier crashed local-input run before
3391
3780
  // building, or this build would embed that run's staged files.
@@ -3432,13 +3821,22 @@ program.action(async (urlArg, options) => {
3432
3821
  if (options.debug) {
3433
3822
  log.setLevel('debug');
3434
3823
  }
3824
+ endCancellation = beginBuildCancellation();
3825
+ phase = 'prepare';
3826
+ leaveWorkspace = await enterBuildWorkspace();
3827
+ phase = 'input';
3435
3828
  const appOptions = await handleOptions(options, url);
3829
+ throwIfBuildCancelled();
3436
3830
  appName = appOptions.name ?? null;
3437
3831
  const builder = BuilderProvider.create(appOptions);
3438
3832
  phase = 'prepare';
3439
3833
  await builder.prepare();
3834
+ throwIfBuildCancelled();
3440
3835
  phase = 'build';
3441
3836
  await builder.build(url);
3837
+ throwIfBuildCancelled();
3838
+ await leaveWorkspace();
3839
+ leaveWorkspace = undefined;
3442
3840
  if (jsonMode) {
3443
3841
  printJsonResult({
3444
3842
  ok: true,
@@ -3457,6 +3855,10 @@ program.action(async (urlArg, options) => {
3457
3855
  if (isCommanderExit(error) && error.exitCode === 0) {
3458
3856
  return;
3459
3857
  }
3858
+ if (leaveWorkspace) {
3859
+ await leaveWorkspace();
3860
+ leaveWorkspace = undefined;
3861
+ }
3460
3862
  const classified = classifyError(error, phase);
3461
3863
  if (jsonMode) {
3462
3864
  printJsonResult({
@@ -3489,10 +3891,14 @@ program.action(async (urlArg, options) => {
3489
3891
  process.exitCode = ERROR_EXIT_CODES[classified.code];
3490
3892
  }
3491
3893
  finally {
3492
- // A local-input run replaces the package's own dist/ during staging; put
3493
- // it back so the CLI stays intact and later builds cannot embed this
3494
- // user's files.
3495
- restoreLocalTree();
3894
+ // Failed builds discard their private inputs without touching the package.
3895
+ try {
3896
+ if (leaveWorkspace)
3897
+ await leaveWorkspace();
3898
+ }
3899
+ finally {
3900
+ endCancellation?.();
3901
+ }
3496
3902
  }
3497
3903
  });
3498
3904
  program.parseAsync().catch((error) => {