pake-cli 3.15.7 โ†’ 3.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -4,13 +4,14 @@ 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';
@@ -18,8 +19,9 @@ import * as psl from 'psl';
18
19
  import { InvalidArgumentError, program as program$1, Option } from 'commander';
19
20
 
20
21
  var name = "pake-cli";
21
- var version = "3.15.7";
22
+ var version = "3.16.1";
22
23
  var description = "๐Ÿคฑ๐Ÿป Turn any webpage into a desktop app with one command. ๐Ÿคฑ๐Ÿป ไธ€้”ฎๆ‰“ๅŒ…็ฝ‘้กต็”Ÿๆˆ่ฝป้‡ๆกŒ้ขๅบ”็”จใ€‚";
24
+ var homepage = "https://faberon.io/projects/pake";
23
25
  var engines = {
24
26
  node: ">=20.9.0"
25
27
  };
@@ -97,7 +99,6 @@ var devDependencies = {
97
99
  "@types/prompts": "^2.4.9",
98
100
  "@types/tmp": "^0.2.6",
99
101
  "@types/update-notifier": "^6.0.8",
100
- "app-root-path": "^3.1.0",
101
102
  "cross-env": "^10.1.0",
102
103
  prettier: "^3.8.1",
103
104
  rollup: "^4.59.0",
@@ -121,6 +122,7 @@ var packageJson = {
121
122
  name: name,
122
123
  version: version,
123
124
  description: description,
125
+ homepage: homepage,
124
126
  engines: engines,
125
127
  packageManager: packageManager,
126
128
  bin: bin,
@@ -137,40 +139,6 @@ var packageJson = {
137
139
  pnpm: pnpm
138
140
  };
139
141
 
140
- // Convert the current module URL to a file path
141
- const currentModulePath = fileURLToPath(import.meta.url);
142
- // Resolve the parent directory of the current module
143
- const npmDirectory = path.join(path.dirname(currentModulePath), '..');
144
- const tauriConfigDirectory = path.join(npmDirectory, 'src-tauri', '.pake');
145
-
146
- // Load configs from npm package directory, not from project source
147
- const tauriSrcDir = path.join(npmDirectory, 'src-tauri');
148
- const pakeConf = fsExtra.readJSONSync(path.join(tauriSrcDir, 'pake.json'));
149
- const CommonConf = fsExtra.readJSONSync(path.join(tauriSrcDir, 'tauri.conf.json'));
150
- const WinConf = fsExtra.readJSONSync(path.join(tauriSrcDir, 'tauri.windows.conf.json'));
151
- const MacConf = fsExtra.readJSONSync(path.join(tauriSrcDir, 'tauri.macos.conf.json'));
152
- const LinuxConf = fsExtra.readJSONSync(path.join(tauriSrcDir, 'tauri.linux.conf.json'));
153
- const platformConfigs = {
154
- win32: WinConf,
155
- darwin: MacConf,
156
- linux: LinuxConf,
157
- };
158
- const { platform: platform$2 } = process;
159
- // @ts-ignore
160
- const platformConfig = platformConfigs[platform$2];
161
- let tauriConfig = {
162
- ...CommonConf,
163
- bundle: platformConfig.bundle,
164
- app: {
165
- ...CommonConf.app,
166
- trayIcon: {
167
- ...(platformConfig?.app?.trayIcon ?? {}),
168
- },
169
- },
170
- build: CommonConf.build,
171
- pake: pakeConf,
172
- };
173
-
174
142
  // Stable exit-code contract: 0 success, 2 invalid input, 3 build/network
175
143
  // failure, 4 missing environment, 1 unexpected. Documented in cli-usage docs.
176
144
  const ERROR_EXIT_CODES = {
@@ -279,10 +247,10 @@ function isCnMirrorEnabled(value = process.env[CN_MIRROR_ENV]) {
279
247
  return TRUE_VALUES.has((value ?? '').trim().toLowerCase());
280
248
  }
281
249
 
282
- const { platform: platform$1 } = process;
283
- const IS_MAC = platform$1 === 'darwin';
284
- const IS_WIN = platform$1 === 'win32';
285
- 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';
286
254
  // Distro IDs / ID_LIKE families that ship an RPM-based package manager.
287
255
  const RPM_FAMILY_IDS = new Set([
288
256
  'rhel',
@@ -371,10 +339,295 @@ function getDefaultLinuxTargets() {
371
339
  return detectLinuxPackageFamily() === 'rpm' ? 'rpm,appimage' : 'deb,appimage';
372
340
  }
373
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
+ }
374
625
  async function shellExec(command, timeout = 300000, env) {
626
+ const signal = getBuildCancellationSignal();
627
+ signal?.throwIfAborted();
375
628
  try {
376
- const { exitCode } = await execa(command, {
377
- cwd: npmDirectory,
629
+ const subprocess = execa(command.executable, command.args, {
630
+ cwd: command.cwd ?? npmDirectory,
378
631
  // Use 'inherit' to show all output directly to user in real-time.
379
632
  // This ensures linuxdeploy and other tool outputs are visible during builds.
380
633
  // In machine mode (--json) stdout is reserved for the final JSON result,
@@ -382,22 +635,53 @@ async function shellExec(command, timeout = 300000, env) {
382
635
  stdin: 'inherit',
383
636
  stdout: isMachineMode() ? process.stderr : 'inherit',
384
637
  stderr: 'inherit',
385
- shell: true,
638
+ shell: false,
639
+ detached: Boolean(signal) && process.platform !== 'win32',
386
640
  timeout,
387
641
  env: env ? { ...process.env, ...env } : process.env,
388
642
  });
389
- 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
+ }
390
671
  }
391
672
  catch (error) {
673
+ if (signal?.aborted)
674
+ throw signal.reason;
675
+ const description = JSON.stringify([command.executable, ...command.args]);
392
676
  const exitCode = error.exitCode ?? 'unknown';
393
677
  const errorMessage = error.message || 'Unknown error occurred';
394
678
  if (error.timedOut) {
395
- 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.`);
396
680
  }
397
681
  // AppImage/linuxdeploy guidance is added by the caller (BaseBuilder), which
398
682
  // knows the build target. We only have the command line here (the tool's
399
683
  // diagnostics stream to the terminal via stdio:inherit, not into the error).
400
- 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}`);
401
685
  }
402
686
  }
403
687
 
@@ -448,25 +732,54 @@ function ensureRustEnv() {
448
732
  ensureCargoBinOnPath();
449
733
  }
450
734
  async function installRust() {
451
- const rustInstallScriptForUnix = isCnMirrorEnabled()
452
- ? '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'
453
- : "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y";
454
- const rustInstallScriptForWindows = 'winget install --id Rustlang.Rustup';
455
735
  const spinner = getSpinner('Downloading Rust...');
456
736
  try {
457
- 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
+ }
458
776
  spinner.succeed(chalk.green('โœ” Rust installed successfully!'));
459
777
  ensureRustEnv();
460
778
  }
461
779
  catch (error) {
462
780
  spinner.fail(chalk.red('โœ• Rust installation failed!'));
463
- if (error instanceof Error) {
464
- console.error(error.message);
465
- }
466
- else {
467
- console.error(error);
468
- }
469
- process.exit(1);
781
+ // The CLI owns error reporting and workspace/cache cleanup.
782
+ throw error;
470
783
  }
471
784
  }
472
785
  function checkRustInstalled() {
@@ -486,9 +799,13 @@ async function combineFiles(files, output) {
486
799
  const fileContent = await fs$1.readFile(file, 'utf-8');
487
800
  return `window.addEventListener('DOMContentLoaded', (_event) => {
488
801
  const css = ${JSON.stringify(fileContent)};
489
- const style = document.createElement('style');
490
- style.textContent = css;
491
- 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
+ }
492
809
  });`;
493
810
  }
494
811
  const fileContent = await fs$1.readFile(file);
@@ -504,24 +821,6 @@ async function combineFiles(files, output) {
504
821
  return files;
505
822
  }
506
823
 
507
- const logger = {
508
- info(...msg) {
509
- log.info(...msg.map((m) => chalk.white(m)));
510
- },
511
- debug(...msg) {
512
- log.debug(...msg);
513
- },
514
- error(...msg) {
515
- log.error(...msg.map((m) => chalk.red(m)));
516
- },
517
- warn(...msg) {
518
- log.warn(...msg.map((m) => chalk.yellow(m)));
519
- },
520
- success(...msg) {
521
- log.info(...msg.map((m) => chalk.green(m)));
522
- },
523
- };
524
-
525
824
  function generateSafeFilename(name) {
526
825
  return name
527
826
  .replace(/[<>:"/\\|?*]/g, '_')
@@ -559,31 +858,6 @@ function generateIdentifierSafeName(name) {
559
858
  return cleaned;
560
859
  }
561
860
 
562
- /**
563
- * Error class used for user-facing CLI errors.
564
- *
565
- * The top-level catch in `bin/cli.ts` prints `message` directly without a
566
- * stack trace and exits with the code mapped from `code` (see
567
- * ERROR_EXIT_CODES in utils/output.ts). Use this for predictable failures
568
- * (invalid names, missing files, etc.) so users see a clean message instead
569
- * of a Node.js stack dump. `code` and `hint` also feed the `--json` result.
570
- */
571
- class PakeError extends Error {
572
- constructor(message, options) {
573
- super(message);
574
- this.isUserError = true;
575
- this.name = 'PakeError';
576
- this.code = options?.code;
577
- this.hint = options?.hint;
578
- }
579
- }
580
- function isPakeError(error) {
581
- return (error instanceof PakeError ||
582
- (typeof error === 'object' &&
583
- error !== null &&
584
- error.isUserError === true));
585
- }
586
-
587
861
  const LINUX_TARGET_TYPES = ['deb', 'appimage', 'rpm', 'zst'];
588
862
  // Returns the valid Linux build targets from a comma-separated targets
589
863
  // string, preserving LINUX_TARGET_TYPES order. Unknown entries are dropped.
@@ -686,8 +960,11 @@ async function stageLocalTree(sourceDir) {
686
960
  const resolvedPackage = await fsExtra
687
961
  .realpath(npmDirectory)
688
962
  .catch(() => path.resolve(npmDirectory));
963
+ const installedPackage = await fsExtra.realpath(packageDirectory);
689
964
  const packageDist = path.join(resolvedPackage, 'dist');
690
- if (resolvedSource === resolvedPackage ||
965
+ if (resolvedSource === installedPackage ||
966
+ installedPackage.startsWith(resolvedSource + path.sep) ||
967
+ resolvedSource === resolvedPackage ||
691
968
  resolvedPackage.startsWith(resolvedSource + path.sep) ||
692
969
  resolvedSource === packageDist ||
693
970
  resolvedSource.startsWith(packageDist + path.sep)) {
@@ -919,7 +1196,7 @@ async function mergeIcons(options, name, tauriConf, platform, safeAppName) {
919
1196
  delete tauriConf.app.trayIcon;
920
1197
  }
921
1198
  async function injectCustomCode(options, tauriConf) {
922
- const { inject, proxyUrl, multiInstance, multiWindow, wasm } = options;
1199
+ const { inject, proxyUrl, basicAuth, multiInstance, multiWindow, wasm } = options;
923
1200
  const injectFilePath = path.join(npmDirectory, 'src-tauri/src/inject/custom.js');
924
1201
  if (inject?.length > 0) {
925
1202
  const injectArray = Array.isArray(inject) ? inject : [inject];
@@ -936,6 +1213,7 @@ async function injectCustomCode(options, tauriConf) {
936
1213
  await fsExtra.writeFile(injectFilePath, '');
937
1214
  }
938
1215
  tauriConf.pake.proxy_url = proxyUrl || '';
1216
+ tauriConf.pake.basic_auth = basicAuth;
939
1217
  tauriConf.pake.multi_instance = multiInstance;
940
1218
  tauriConf.pake.multi_window = multiWindow;
941
1219
  if (wasm) {
@@ -1038,6 +1316,34 @@ async function mergeConfig(url, options, tauriConf) {
1038
1316
  await writeAllConfigs(tauriConf, platform);
1039
1317
  }
1040
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
+
1041
1347
  /**
1042
1348
  * Returns build environment variables overrides for macOS, where Rust crates
1043
1349
  * sometimes need explicit C/C++ flags and a deterministic SDK target. Other
@@ -1129,11 +1435,12 @@ async function detectPackageManager() {
1129
1435
  return 'pnpm';
1130
1436
  }
1131
1437
  function getInstallCommand(packageManager, useCnMirror) {
1132
- const registryOption = useCnMirror
1133
- ? ' --registry=https://registry.npmmirror.com'
1134
- : '';
1135
- const peerDepsOption = packageManager === 'npm' ? ' --legacy-peer-deps' : '';
1136
- 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 };
1137
1444
  }
1138
1445
  async function copyFileWithSamePathGuard(sourcePath, destinationPath) {
1139
1446
  if (path.resolve(sourcePath) === path.resolve(destinationPath)) {
@@ -1265,7 +1572,7 @@ class BaseBuilder {
1265
1572
  }
1266
1573
  async prepare() {
1267
1574
  const tauriSrcPath = path.join(npmDirectory, 'src-tauri');
1268
- const tauriTargetPath = path.join(tauriSrcPath, 'target');
1575
+ const tauriTargetPath = this.getCargoTargetDir();
1269
1576
  const tauriTargetPathExists = await fsExtra.pathExists(tauriTargetPath);
1270
1577
  if (!IS_MAC && !tauriTargetPathExists) {
1271
1578
  logger.warn('โœผ The first use requires installing system dependencies.');
@@ -1294,9 +1601,21 @@ class BaseBuilder {
1294
1601
  });
1295
1602
  }
1296
1603
  }
1297
- const spinner = getSpinner('Installing package...');
1298
1604
  const useCnMirror = isCnMirrorEnabled();
1299
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...');
1300
1619
  const packageManager = await detectPackageManager();
1301
1620
  const timeout = getInstallTimeout();
1302
1621
  const buildEnv = getBuildEnvironment();
@@ -1332,23 +1651,24 @@ class BaseBuilder {
1332
1651
  }
1333
1652
  async start(url) {
1334
1653
  logger.info('Pake dev server starting...');
1335
- await mergeConfig(url, this.options, tauriConfig);
1654
+ await mergeConfig(url, this.options, structuredClone(tauriConfig));
1336
1655
  const packageManager = await detectPackageManager();
1337
1656
  const configPath = path.join(npmDirectory, 'src-tauri', '.pake', 'tauri.conf.json');
1338
1657
  const features = this.getBuildFeatures();
1339
- const featureArgs = features.length > 0 ? `--features ${features.join(',')}` : '';
1340
- const argSeparator = packageManager === 'npm' ? ' --' : '';
1341
- const command = `cd "${npmDirectory}" && ${packageManager} run tauri${argSeparator} dev --config "${configPath}" ${featureArgs}`;
1342
- 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 });
1343
1665
  }
1344
1666
  async buildAndCopy(url, target, logSuccess = true) {
1345
1667
  const { name = 'pake-app' } = this.options;
1346
- await mergeConfig(url, this.options, tauriConfig);
1668
+ await mergeConfig(url, this.options, structuredClone(tauriConfig));
1347
1669
  const packageManager = await detectPackageManager();
1348
1670
  // Build app
1349
1671
  const buildSpinner = getSpinner('Building app...');
1350
- // Let spinner run for a moment so user can see it, then stop before package manager command
1351
- await new Promise((resolve) => setTimeout(resolve, 500));
1352
1672
  buildSpinner.stop();
1353
1673
  // Show static message to keep the status visible. Info, not warn: warn
1354
1674
  // entries feed the --json warnings array and this is a status line.
@@ -1365,7 +1685,7 @@ class BaseBuilder {
1365
1685
  if (isLinuxAppImage && !buildEnv.NO_STRIP && this.options.debug) {
1366
1686
  logger.warn('โš  AppImage strip step can fail on glibc 2.38+; Pake will auto-retry with NO_STRIP=1.');
1367
1687
  }
1368
- const buildCommand = `cd "${npmDirectory}" && ${this.getBuildCommand(packageManager)}`;
1688
+ const buildCommand = this.getBuildCommand(packageManager);
1369
1689
  const buildTimeout = getBuildTimeout();
1370
1690
  try {
1371
1691
  await shellExec(buildCommand, buildTimeout, resolveExecEnv());
@@ -1473,24 +1793,23 @@ class BaseBuilder {
1473
1793
  return BaseBuilder.ARCH_DISPLAY_NAMES[arch] || arch;
1474
1794
  }
1475
1795
  buildBaseCommand(packageManager, configPath, target) {
1476
- const baseCommand = this.options.debug
1477
- ? `${packageManager} run build:debug`
1478
- : `${packageManager} run build`;
1479
- const argSeparator = packageManager === 'npm' ? ' --' : '';
1480
- 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);
1481
1800
  if (target) {
1482
- fullCommand += ` --target ${target}`;
1801
+ args.push('--target', target);
1483
1802
  }
1484
1803
  // Enable verbose output in debug mode to help diagnose build issues.
1485
1804
  // This provides detailed logs from Tauri CLI and bundler tools.
1486
1805
  if (this.options.debug) {
1487
- fullCommand += ' --verbose';
1806
+ args.push('--verbose');
1488
1807
  }
1489
1808
  const features = this.getBuildFeatures();
1490
1809
  if (features.length > 0) {
1491
- fullCommand += ` --features ${features.join(',')}`;
1810
+ args.push('--features', features.join(','));
1492
1811
  }
1493
- return fullCommand;
1812
+ return { executable: packageManager, args };
1494
1813
  }
1495
1814
  getBuildFeatures() {
1496
1815
  const features = ['cli-build'];
@@ -1506,10 +1825,10 @@ class BaseBuilder {
1506
1825
  getBuildCommand(packageManager = 'pnpm') {
1507
1826
  // Use temporary config directory to avoid modifying source files
1508
1827
  const configPath = path.join(npmDirectory, 'src-tauri', '.pake', 'tauri.conf.json');
1509
- let fullCommand = this.buildBaseCommand(packageManager, configPath);
1828
+ const fullCommand = this.buildBaseCommand(packageManager, configPath);
1510
1829
  // For macOS, use app bundles by default unless DMG is explicitly requested
1511
1830
  if (IS_MAC && this.options.targets === 'app') {
1512
- fullCommand += ' --bundles app';
1831
+ fullCommand.args.push('--bundles', 'app');
1513
1832
  }
1514
1833
  return fullCommand;
1515
1834
  }
@@ -1660,7 +1979,7 @@ class MacBuilder extends BaseBuilder {
1660
1979
  else {
1661
1980
  arch = this.getArchDisplayName(this.resolveTargetArch(this.buildArch));
1662
1981
  }
1663
- return `${name}_${tauriConfig.version}_${arch}`;
1982
+ return `${name}_${this.options.appVersion}_${arch}`;
1664
1983
  }
1665
1984
  getReportArch() {
1666
1985
  return this.getActualArch();
@@ -1723,9 +2042,9 @@ class WinBuilder extends BaseBuilder {
1723
2042
  }
1724
2043
  getFileName() {
1725
2044
  const { name } = this.options;
1726
- const language = tauriConfig.bundle.windows.wix.language[0];
2045
+ const language = this.options.installerLanguage;
1727
2046
  const targetArch = this.getArchDisplayName(this.buildArch);
1728
- return `${name}_${tauriConfig.version}_${targetArch}_${language}`;
2047
+ return `${name}_${this.options.appVersion}_${targetArch}_${language}`;
1729
2048
  }
1730
2049
  getBuildCommand(packageManager = 'pnpm') {
1731
2050
  const configPath = path.join('src-tauri', '.pake', 'tauri.conf.json');
@@ -1781,7 +2100,7 @@ class LinuxBuilder extends BaseBuilder {
1781
2100
  }
1782
2101
  getFileName() {
1783
2102
  const { name = 'pake-app', targets } = this.options;
1784
- const version = tauriConfig.version;
2103
+ const version = this.options.appVersion;
1785
2104
  const buildType = this.currentBuildType || targets.split(',').map((t) => t.trim())[0];
1786
2105
  let arch;
1787
2106
  if (this.buildArch === 'arm64') {
@@ -1859,7 +2178,7 @@ class LinuxBuilder extends BaseBuilder {
1859
2178
  ];
1860
2179
  for (const { tool, pacmanPackage } of requiredTools) {
1861
2180
  try {
1862
- await shellExec(`command -v ${tool} >/dev/null 2>&1`);
2181
+ await execa(tool, ['--version'], { stdio: 'ignore' });
1863
2182
  }
1864
2183
  catch {
1865
2184
  throw new Error(`Building a zst package requires "${tool}". Install it first, e.g. "sudo pacman -S ${pacmanPackage}".`);
@@ -1869,24 +2188,30 @@ class LinuxBuilder extends BaseBuilder {
1869
2188
  async createArchPackageFromDeb({ removeSourceDeb, }) {
1870
2189
  const { name = 'pake-app' } = this.options;
1871
2190
  const packageName = generateLinuxPackageName(name);
1872
- const version = tauriConfig.version;
2191
+ const version = this.options.appVersion;
1873
2192
  const arch = this.buildArch === 'arm64' ? 'aarch64' : 'x86_64';
1874
2193
  const debPath = path.resolve(`${name}.deb`);
1875
2194
  const packagePath = path.resolve(`${name}-${version}-1-${arch}.pkg.tar.zst`);
1876
- const workDir = path.resolve('.pake-arch-package');
2195
+ await this.ensureArchPackagingTools();
2196
+ const workDir = await fsExtra.mkdtemp(path.join(os.tmpdir(), 'pake-arch-'));
1877
2197
  const dataDir = path.join(workDir, 'data');
1878
2198
  const controlDir = path.join(workDir, 'control');
1879
- await this.ensureArchPackagingTools();
1880
- await fsExtra.remove(workDir);
1881
- await fsExtra.ensureDir(dataDir);
1882
- await fsExtra.ensureDir(controlDir);
1883
2199
  try {
1884
- 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
+ });
1885
2207
  const dataArchive = (await fsExtra.readdir(controlDir)).find((file) => file.startsWith('data.tar'));
1886
2208
  if (!dataArchive) {
1887
2209
  throw new Error(`Could not find data.tar payload in ${debPath}`);
1888
2210
  }
1889
- 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
+ });
1890
2215
  // Drop the desktop entry auto-generated by the Tauri deb bundler;
1891
2216
  // the payload already ships Pake's own com.pake.<name>.desktop.
1892
2217
  await fsExtra.remove(path.join(dataDir, 'usr', 'share', 'applications', `${packageName}.desktop`));
@@ -1926,7 +2251,19 @@ post_remove() {
1926
2251
  update-desktop-database -q usr/share/applications
1927
2252
  }
1928
2253
  `);
1929
- 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
+ });
1930
2267
  await this.recordArtifact(packagePath, 'zst');
1931
2268
  logger.success('โœ” Build success!');
1932
2269
  logger.success('โœ” App installer located in', packagePath);
@@ -1964,14 +2301,15 @@ post_remove() {
1964
2301
  const buildTarget = this.buildArch === 'arm64'
1965
2302
  ? (this.getTauriTarget(this.buildArch, 'linux') ?? undefined)
1966
2303
  : undefined;
1967
- let fullCommand = this.buildBaseCommand(packageManager, configPath, buildTarget);
2304
+ const fullCommand = this.buildBaseCommand(packageManager, configPath, buildTarget);
1968
2305
  // --no-bundle: build the executable only, skipping .deb/.rpm/.appimage
1969
2306
  // packaging entirely (e.g. RPM-based distros where the bundler aborts).
1970
2307
  if (this.options.bundle === false) {
1971
- return `${fullCommand} --no-bundle`;
2308
+ fullCommand.args.push('--no-bundle');
2309
+ return fullCommand;
1972
2310
  }
1973
2311
  if (this.currentBuildType) {
1974
- fullCommand += ` --bundles ${this.currentBuildType}`;
2312
+ fullCommand.args.push('--bundles', this.currentBuildType);
1975
2313
  }
1976
2314
  // Enable verbose output for AppImage builds when debugging or PAKE_VERBOSE is set.
1977
2315
  // AppImage builds often fail with minimal error messages from linuxdeploy,
@@ -1980,7 +2318,7 @@ post_remove() {
1980
2318
  (this.options.targets.includes('appimage') ||
1981
2319
  this.options.debug ||
1982
2320
  process.env.PAKE_VERBOSE)) {
1983
- fullCommand += ' --verbose';
2321
+ fullCommand.args.push('--verbose');
1984
2322
  }
1985
2323
  return fullCommand;
1986
2324
  }
@@ -2828,7 +3166,6 @@ async function downloadIcon(iconUrl, showSpinner = true, customTimeout) {
2828
3166
  const response = await fetch(iconUrl, {
2829
3167
  signal: controller.signal,
2830
3168
  });
2831
- clearTimeout(timeoutId);
2832
3169
  if (!response.ok) {
2833
3170
  if (response.status === 404 && !showSpinner) {
2834
3171
  return null;
@@ -2845,7 +3182,6 @@ async function downloadIcon(iconUrl, showSpinner = true, customTimeout) {
2845
3182
  return await saveIconFile(arrayBuffer, extension);
2846
3183
  }
2847
3184
  catch (error) {
2848
- clearTimeout(timeoutId);
2849
3185
  if (showSpinner) {
2850
3186
  if (error instanceof Error && error.name === 'AbortError') {
2851
3187
  logger.error('Icon download timed out!');
@@ -2856,6 +3192,9 @@ async function downloadIcon(iconUrl, showSpinner = true, customTimeout) {
2856
3192
  }
2857
3193
  return null;
2858
3194
  }
3195
+ finally {
3196
+ clearTimeout(timeoutId);
3197
+ }
2859
3198
  }
2860
3199
  /**
2861
3200
  * Saves icon file to temporary location
@@ -3031,6 +3370,7 @@ const DEFAULT_PAKE_OPTIONS = {
3031
3370
  useLocalFile: false,
3032
3371
  systemTrayIcon: '',
3033
3372
  proxyUrl: '',
3373
+ basicAuth: false,
3034
3374
  debug: false,
3035
3375
  json: false,
3036
3376
  inject: [],
@@ -3133,6 +3473,9 @@ ${green('|_| \\__,_|_|\\_\\___| can turn any webpage into a desktop app with
3133
3473
  .option('--debug', 'Debug build and more output', DEFAULT_PAKE_OPTIONS.debug)
3134
3474
  .option('--json', 'Machine-readable output: logs to stderr, one JSON result on stdout', DEFAULT_PAKE_OPTIONS.json)
3135
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())
3136
3479
  .addOption(new Option('--proxy-url <url>', 'Proxy URL for all network requests (http://, https://, socks5://)')
3137
3480
  .default(DEFAULT_PAKE_OPTIONS.proxyUrl)
3138
3481
  .hideHelp())
@@ -3430,6 +3773,8 @@ program.action(async (urlArg, options) => {
3430
3773
  let phase = 'input';
3431
3774
  let appName = null;
3432
3775
  let url = urlArg;
3776
+ let leaveWorkspace;
3777
+ let endCancellation;
3433
3778
  try {
3434
3779
  // Heal a dist_bak stranded by an earlier crashed local-input run before
3435
3780
  // building, or this build would embed that run's staged files.
@@ -3476,13 +3821,22 @@ program.action(async (urlArg, options) => {
3476
3821
  if (options.debug) {
3477
3822
  log.setLevel('debug');
3478
3823
  }
3824
+ endCancellation = beginBuildCancellation();
3825
+ phase = 'prepare';
3826
+ leaveWorkspace = await enterBuildWorkspace();
3827
+ phase = 'input';
3479
3828
  const appOptions = await handleOptions(options, url);
3829
+ throwIfBuildCancelled();
3480
3830
  appName = appOptions.name ?? null;
3481
3831
  const builder = BuilderProvider.create(appOptions);
3482
3832
  phase = 'prepare';
3483
3833
  await builder.prepare();
3834
+ throwIfBuildCancelled();
3484
3835
  phase = 'build';
3485
3836
  await builder.build(url);
3837
+ throwIfBuildCancelled();
3838
+ await leaveWorkspace();
3839
+ leaveWorkspace = undefined;
3486
3840
  if (jsonMode) {
3487
3841
  printJsonResult({
3488
3842
  ok: true,
@@ -3501,6 +3855,10 @@ program.action(async (urlArg, options) => {
3501
3855
  if (isCommanderExit(error) && error.exitCode === 0) {
3502
3856
  return;
3503
3857
  }
3858
+ if (leaveWorkspace) {
3859
+ await leaveWorkspace();
3860
+ leaveWorkspace = undefined;
3861
+ }
3504
3862
  const classified = classifyError(error, phase);
3505
3863
  if (jsonMode) {
3506
3864
  printJsonResult({
@@ -3533,10 +3891,14 @@ program.action(async (urlArg, options) => {
3533
3891
  process.exitCode = ERROR_EXIT_CODES[classified.code];
3534
3892
  }
3535
3893
  finally {
3536
- // A local-input run replaces the package's own dist/ during staging; put
3537
- // it back so the CLI stays intact and later builds cannot embed this
3538
- // user's files.
3539
- 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
+ }
3540
3902
  }
3541
3903
  });
3542
3904
  program.parseAsync().catch((error) => {