pake-cli 3.15.0 โ†’ 3.15.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -194,6 +194,12 @@ First-time packaging requires environment setup and may be slower, subsequent bu
194
194
 
195
195
  Using Pake from a script or AI agent? Pass `--json` for machine-readable results, describe apps declaratively with `--config app.json` ([schema](schema/pake.schema.json)), and package local build output directly with `pake ./dist --name MyTool`. See [llms.txt](llms.txt) for the full agent contract. Claude Code users can install the official skill with `/plugin marketplace add tw93/Pake` and `/plugin install pake@pake`.
196
196
 
197
+ Copy this to your AI agent to get started:
198
+
199
+ ```text
200
+ Use Pake (npm i -g pake-cli) to package webpages as desktop apps. Read https://raw.githubusercontent.com/tw93/Pake/main/llms.txt first; always run pake with --json and parse stdout as a single JSON object. Package <url-or-local-dist> into an app named <AppName>.
201
+ ```
202
+
197
203
  ## Development
198
204
 
199
205
  Requires Rust `>=1.85` and Node `>=22` (recommended LTS; `>=18` also works). For detailed installation guide, see [Tauri documentation](https://v2.tauri.app/start/prerequisites/). If unfamiliar with development environment, use the CLI tool instead.
package/dist/cli.js CHANGED
@@ -20,7 +20,7 @@ import * as psl from 'psl';
20
20
  import { InvalidArgumentError, program as program$1, Option } from 'commander';
21
21
 
22
22
  var name = "pake-cli";
23
- var version = "3.15.0";
23
+ var version = "3.15.2";
24
24
  var description = "๐Ÿคฑ๐Ÿป Turn any webpage into a desktop app with one command. ๐Ÿคฑ๐Ÿป ไธ€้”ฎๆ‰“ๅŒ…็ฝ‘้กต็”Ÿๆˆ่ฝป้‡ๆกŒ้ขๅบ”็”จใ€‚";
25
25
  var engines = {
26
26
  node: ">=18.0.0"
@@ -680,15 +680,63 @@ async function copyTemplateConfigs() {
680
680
  async function stageLocalTree(sourceDir) {
681
681
  const distDir = path.join(npmDirectory, 'dist');
682
682
  const distBakDir = path.join(npmDirectory, 'dist_bak');
683
- if (await fsExtra.pathExists(distBakDir)) {
683
+ // Resolve symlinked input up front: staging must produce a real copy, or
684
+ // the cli.js copy-back below would write through the link into the user's
685
+ // own directory.
686
+ const resolvedSource = await fsExtra.realpath(sourceDir);
687
+ const resolvedPackage = await fsExtra
688
+ .realpath(npmDirectory)
689
+ .catch(() => path.resolve(npmDirectory));
690
+ const packageDist = path.join(resolvedPackage, 'dist');
691
+ if (resolvedSource === resolvedPackage ||
692
+ resolvedPackage.startsWith(resolvedSource + path.sep) ||
693
+ resolvedSource === packageDist ||
694
+ resolvedSource.startsWith(packageDist + path.sep)) {
695
+ throw new PakeError(`Local input "${sourceDir}" contains the Pake CLI installation itself.`, {
696
+ code: 'INVALID_INPUT',
697
+ hint: 'Point Pake at your built output directory, not at a directory containing pake-cli.',
698
+ });
699
+ }
700
+ try {
701
+ if (await fsExtra.pathExists(distBakDir)) {
702
+ fsExtra.removeSync(distDir);
703
+ }
704
+ else {
705
+ fsExtra.moveSync(distDir, distBakDir);
706
+ }
707
+ fsExtra.copySync(resolvedSource, distDir, {
708
+ overwrite: true,
709
+ dereference: true,
710
+ });
711
+ const filesToCopyBack = ['cli.js'];
712
+ await Promise.all(filesToCopyBack.map((file) => fsExtra.copy(path.join(distBakDir, file), path.join(distDir, file))));
713
+ }
714
+ catch (error) {
715
+ // Never leave the package without its own dist/: cli.js lives there and
716
+ // every later `pake` invocation would fail until a manual reinstall.
717
+ restoreLocalTree();
718
+ throw error;
719
+ }
720
+ }
721
+ // Put the package's original dist/ back once a local-input run is over (or
722
+ // failed). Tauri bakes `frontendDist: ../dist` into every binary, so a stale
723
+ // staged tree would leak this user's files into the next app built from the
724
+ // same install. Safe to call on any run: a present dist_bak always holds the
725
+ // original package dist, including one stranded by an older crashed run.
726
+ function restoreLocalTree() {
727
+ const distDir = path.join(npmDirectory, 'dist');
728
+ const distBakDir = path.join(npmDirectory, 'dist_bak');
729
+ if (!fsExtra.pathExistsSync(distBakDir)) {
730
+ return;
731
+ }
732
+ try {
684
733
  fsExtra.removeSync(distDir);
734
+ fsExtra.moveSync(distBakDir, distDir);
685
735
  }
686
- else {
687
- fsExtra.moveSync(distDir, distBakDir);
736
+ catch (error) {
737
+ const detail = error instanceof Error ? error.message : String(error);
738
+ logger.warn(`Failed to restore the CLI's original dist/ from dist_bak: ${detail}`);
688
739
  }
689
- fsExtra.copySync(sourceDir, distDir, { overwrite: true });
690
- const filesToCopyBack = ['cli.js'];
691
- await Promise.all(filesToCopyBack.map((file) => fsExtra.copy(path.join(distBakDir, file), path.join(distDir, file))));
692
740
  }
693
741
  // Exported for unit tests (web fallback and directory entry guard).
694
742
  async function handleLocalFile(url, useLocalFile, tauriConf) {
@@ -3175,6 +3223,15 @@ ${green('|_| \\__,_|_|\\_\\___| can turn any webpage into a desktop app with
3175
3223
  const REJECTED_KEYS = new Set(['config', 'json', 'version']);
3176
3224
  // Optional CLI options that have no entry in DEFAULT_PAKE_OPTIONS.
3177
3225
  const EXTRA_STRING_KEYS = new Set(['name', 'title', 'identifier']);
3226
+ // Numeric fields share the CLI flag ranges (see cli-program.ts validators),
3227
+ // so a config file cannot smuggle a value the same flag would reject.
3228
+ const NUMBER_RANGES = {
3229
+ width: { min: 0 },
3230
+ height: { min: 0 },
3231
+ minWidth: { min: 0 },
3232
+ minHeight: { min: 0 },
3233
+ zoom: { min: 50, max: 200 },
3234
+ };
3178
3235
  function expectedTypeFor(key) {
3179
3236
  if (key === 'inject')
3180
3237
  return 'string[]';
@@ -3252,6 +3309,20 @@ async function loadConfigFile(configPath, validKeys) {
3252
3309
  hint: 'See schema/pake.schema.json for field types.',
3253
3310
  });
3254
3311
  }
3312
+ if (typeof value === 'number') {
3313
+ const range = NUMBER_RANGES[key];
3314
+ const min = range?.min ?? 0;
3315
+ const max = range?.max;
3316
+ if (!Number.isFinite(value) ||
3317
+ value < min ||
3318
+ (max !== undefined && value > max)) {
3319
+ const bounds = max !== undefined ? `${min}-${max}` : `>= ${min}`;
3320
+ throw new PakeError(`Config field "${key}" must be a finite number (${bounds}).`, {
3321
+ code: 'INVALID_INPUT',
3322
+ hint: 'See schema/pake.schema.json for field ranges.',
3323
+ });
3324
+ }
3325
+ }
3255
3326
  if (!expected && (typeof value === 'object' || value === null)) {
3256
3327
  throw new PakeError(`Config field "${key}" must be a string, number, or boolean.`, {
3257
3328
  code: 'INVALID_INPUT',
@@ -3314,6 +3385,9 @@ program.action(async (urlArg, options) => {
3314
3385
  let appName = null;
3315
3386
  let url = urlArg;
3316
3387
  try {
3388
+ // Heal a dist_bak stranded by an earlier crashed local-input run before
3389
+ // building, or this build would embed that run's staged files.
3390
+ restoreLocalTree();
3317
3391
  if (!jsonMode) {
3318
3392
  await checkUpdateTips();
3319
3393
  }
@@ -3379,7 +3453,7 @@ program.action(async (urlArg, options) => {
3379
3453
  // program.help() and --help/--version throw under exitOverride with
3380
3454
  // exitCode 0; a clean commander exit is not a failure.
3381
3455
  if (isCommanderExit(error) && error.exitCode === 0) {
3382
- process.exit(0);
3456
+ return;
3383
3457
  }
3384
3458
  const classified = classifyError(error, phase);
3385
3459
  if (jsonMode) {
@@ -3408,14 +3482,22 @@ program.action(async (urlArg, options) => {
3408
3482
  else {
3409
3483
  console.error(chalk.red(`โœ• Unexpected error: ${String(error)}`));
3410
3484
  }
3411
- process.exit(ERROR_EXIT_CODES[classified.code]);
3485
+ // exitCode + natural exit instead of process.exit: lets the finally
3486
+ // restore run and guarantees the JSON result is flushed on piped stdout.
3487
+ process.exitCode = ERROR_EXIT_CODES[classified.code];
3488
+ }
3489
+ finally {
3490
+ // A local-input run replaces the package's own dist/ during staging; put
3491
+ // it back so the CLI stays intact and later builds cannot embed this
3492
+ // user's files.
3493
+ restoreLocalTree();
3412
3494
  }
3413
3495
  });
3414
3496
  program.parseAsync().catch((error) => {
3415
3497
  if (isCommanderExit(error)) {
3416
3498
  // --help / --version and friends exit clean; commander already printed.
3417
3499
  if (error.exitCode === 0) {
3418
- process.exit(0);
3500
+ return;
3419
3501
  }
3420
3502
  // Parse errors (unknown option, invalid argument, missing value) are
3421
3503
  // invalid input. Commander already printed the message to stderr; in
@@ -3435,7 +3517,8 @@ program.parseAsync().catch((error) => {
3435
3517
  },
3436
3518
  });
3437
3519
  }
3438
- process.exit(ERROR_EXIT_CODES.INVALID_INPUT);
3520
+ process.exitCode = ERROR_EXIT_CODES.INVALID_INPUT;
3521
+ return;
3439
3522
  }
3440
3523
  if (error instanceof Error) {
3441
3524
  console.error(chalk.red(`โœ• ${error.message}`));
@@ -3443,5 +3526,5 @@ program.parseAsync().catch((error) => {
3443
3526
  else {
3444
3527
  console.error(chalk.red(`โœ• Unexpected error: ${String(error)}`));
3445
3528
  }
3446
- process.exit(1);
3529
+ process.exitCode = 1;
3447
3530
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pake-cli",
3
- "version": "3.15.0",
3
+ "version": "3.15.2",
4
4
  "description": "๐Ÿคฑ๐Ÿป Turn any webpage into a desktop app with one command. ๐Ÿคฑ๐Ÿป ไธ€้”ฎๆ‰“ๅŒ…็ฝ‘้กต็”Ÿๆˆ่ฝป้‡ๆกŒ้ขๅบ”็”จใ€‚",
5
5
  "engines": {
6
6
  "node": ">=18.0.0"
@@ -2564,7 +2564,7 @@ dependencies = [
2564
2564
 
2565
2565
  [[package]]
2566
2566
  name = "pake"
2567
- version = "3.15.0"
2567
+ version = "3.15.2"
2568
2568
  dependencies = [
2569
2569
  "objc2",
2570
2570
  "objc2-app-kit",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "pake"
3
- version = "3.15.0"
3
+ version = "3.15.2"
4
4
  description = "๐Ÿคฑ๐Ÿป Turn any webpage into a desktop app with Rust."
5
5
  authors = ["Tw93"]
6
6
  license = "GPL-3.0-or-later"
@@ -1,4 +1,4 @@
1
- use crate::app::window::open_additional_window_safe;
1
+ use crate::app::window::{open_additional_window_safe, reapply_window_icon};
2
2
  use std::str::FromStr;
3
3
  use std::sync::{Arc, Mutex};
4
4
  use std::time::{Duration, Instant};
@@ -56,6 +56,7 @@ pub fn set_system_tray(
56
56
  "show_app" => {
57
57
  if let Some(window) = app.get_webview_window("pake") {
58
58
  let _ = window.show();
59
+ reapply_window_icon(&window);
59
60
  #[cfg(target_os = "linux")]
60
61
  if _init_fullscreen && !window.is_fullscreen().unwrap_or(false) {
61
62
  let _ = window.set_fullscreen(true);
@@ -83,6 +84,7 @@ pub fn set_system_tray(
83
84
  let _ = window.hide();
84
85
  } else {
85
86
  let _ = window.show();
87
+ reapply_window_icon(&window);
86
88
  let _ = window.set_focus();
87
89
  #[cfg(target_os = "linux")]
88
90
  if _init_fullscreen && !window.is_fullscreen().unwrap_or(false) {
@@ -153,6 +155,7 @@ pub fn set_global_shortcut(
153
155
  let _ = window.hide();
154
156
  } else {
155
157
  let _ = window.show();
158
+ reapply_window_icon(&window);
156
159
  let _ = window.set_focus();
157
160
  #[cfg(target_os = "linux")]
158
161
  if _init_fullscreen && !window.is_fullscreen().unwrap_or(false) {
@@ -69,6 +69,21 @@ pub fn open_additional_window(app: &AppHandle) -> tauri::Result<WebviewWindow> {
69
69
  build_window_with_label(app, &state.pake_config, &state.tauri_config, &label)
70
70
  }
71
71
 
72
+ // Apps autostarted at Windows logon can register their window icon before
73
+ // Explorer's icon cache is ready, leaving a blank taskbar icon until the icon
74
+ // is asserted again (#1323), so re-apply it whenever a window becomes visible.
75
+ #[cfg(target_os = "windows")]
76
+ pub fn reapply_window_icon(window: &WebviewWindow) {
77
+ if let Some(icon) = window.app_handle().default_window_icon().cloned() {
78
+ if let Err(error) = window.set_icon(icon) {
79
+ eprintln!("[Pake] Failed to re-apply window icon: {error}");
80
+ }
81
+ }
82
+ }
83
+
84
+ #[cfg(not(target_os = "windows"))]
85
+ pub fn reapply_window_icon(_window: &WebviewWindow) {}
86
+
72
87
  struct WindowBuildOptions<'a> {
73
88
  label: &'a str,
74
89
  url: WebviewUrl,
@@ -207,6 +207,11 @@ function insertTextIntoEditableElement(element, text) {
207
207
  }
208
208
 
209
209
  let clipboardPasteFallbackTarget;
210
+ let clipboardPasteFallbackArmedAt = 0;
211
+ // An armed fallback older than this is a leftover from a keyup the window
212
+ // never saw (alt-tab mid-press); firing it on a later plain "v" keyup would
213
+ // paste unexpectedly.
214
+ const CLIPBOARD_PASTE_FALLBACK_TTL_MS = 5000;
210
215
 
211
216
  function pasteClipboardText(activeElement) {
212
217
  const readText = navigator.clipboard?.readText;
@@ -253,8 +258,16 @@ function handleClipboardShortcut(event) {
253
258
  if (key === "v" && canPasteIntoEditableElement(activeElement)) {
254
259
  // Let the native WebView paste event run first so images, files, and rich
255
260
  // clipboard formats remain intact. If the platform does not emit paste,
256
- // keyup applies the existing text-only fallback.
257
- clipboardPasteFallbackTarget = activeElement;
261
+ // keyup applies the existing text-only fallback. Key-repeat must not
262
+ // re-arm: after a native paste already fired and disarmed the fallback,
263
+ // a repeat keydown re-arming it would make keyup paste text a second
264
+ // time. Repeats only refresh the TTL of a still-armed target.
265
+ if (!event.repeat) {
266
+ clipboardPasteFallbackTarget = activeElement;
267
+ clipboardPasteFallbackArmedAt = Date.now();
268
+ } else if (clipboardPasteFallbackTarget === activeElement) {
269
+ clipboardPasteFallbackArmedAt = Date.now();
270
+ }
258
271
  return false;
259
272
  }
260
273
 
@@ -276,9 +289,11 @@ function handleClipboardPasteFallback(event) {
276
289
  }
277
290
 
278
291
  const activeElement = clipboardPasteFallbackTarget;
292
+ const armedAt = clipboardPasteFallbackArmedAt;
279
293
  clipboardPasteFallbackTarget = undefined;
280
294
  if (
281
295
  !activeElement ||
296
+ Date.now() - armedAt > CLIPBOARD_PASTE_FALLBACK_TTL_MS ||
282
297
  document.activeElement !== activeElement ||
283
298
  !canPasteIntoEditableElement(activeElement)
284
299
  ) {
@@ -25,7 +25,7 @@ use app::{
25
25
  set_dock_badge_label, set_zoom, update_theme_mode,
26
26
  },
27
27
  setup::{set_global_shortcut, set_system_tray},
28
- window::{open_additional_window_safe, set_window, MultiWindowState},
28
+ window::{open_additional_window_safe, reapply_window_icon, set_window, MultiWindowState},
29
29
  };
30
30
  use util::get_pake_config;
31
31
 
@@ -179,6 +179,7 @@ pub fn run_app() {
179
179
  } else if let Some(window) = app.get_webview_window("pake") {
180
180
  let _ = window.unminimize();
181
181
  let _ = window.show();
182
+ reapply_window_icon(&window);
182
183
  let _ = window.set_focus();
183
184
  }
184
185
  },
@@ -231,6 +232,7 @@ pub fn run_app() {
231
232
  tauri::async_runtime::spawn(async move {
232
233
  tokio::time::sleep(tokio::time::Duration::from_millis(WINDOW_SHOW_DELAY)).await;
233
234
  let _ = window_clone.show();
235
+ reapply_window_icon(&window_clone);
234
236
 
235
237
  // Fixed: Linux fullscreen issue with virtual keyboard
236
238
  #[cfg(target_os = "linux")]
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "productName": "Weekly",
3
3
  "identifier": "com.pake.weekly",
4
- "version": "3.15.0",
4
+ "version": "3.15.2",
5
5
  "app": {
6
6
  "withGlobalTauri": true,
7
7
  "trayIcon": {