pake-cli 3.6.0 → 3.6.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/dist/dev.js CHANGED
@@ -12,44 +12,13 @@ import prompts from 'prompts';
12
12
  import ora from 'ora';
13
13
  import { fileURLToPath } from 'url';
14
14
  import * as psl from 'psl';
15
+ import os from 'os';
15
16
  import { execa, execaSync } from 'execa';
16
17
  import dns from 'dns';
17
18
  import http from 'http';
18
19
  import { promisify } from 'util';
19
20
  import fs from 'fs';
20
-
21
- const DEFAULT_PAKE_OPTIONS = {
22
- icon: '',
23
- height: 780,
24
- width: 1200,
25
- fullscreen: false,
26
- resizable: true,
27
- hideTitleBar: false,
28
- alwaysOnTop: false,
29
- appVersion: '1.0.0',
30
- darkMode: false,
31
- disabledWebShortcuts: false,
32
- activationShortcut: '',
33
- userAgent: '',
34
- showSystemTray: false,
35
- multiArch: false,
36
- targets: 'deb',
37
- useLocalFile: false,
38
- systemTrayIcon: '',
39
- proxyUrl: '',
40
- debug: false,
41
- inject: [],
42
- installerLanguage: 'en-US',
43
- hideOnClose: true,
44
- incognito: false,
45
- };
46
- // Just for cli development
47
- const DEFAULT_DEV_PAKE_OPTIONS = {
48
- ...DEFAULT_PAKE_OPTIONS,
49
- url: 'https://weekly.tw93.fun/',
50
- name: 'Weekly',
51
- hideTitleBar: true,
52
- };
21
+ import { InvalidArgumentError, program as program$1, Option } from 'commander';
53
22
 
54
23
  const logger = {
55
24
  info(...msg) {
@@ -113,20 +82,90 @@ const IS_MAC = platform$2 === 'darwin';
113
82
  const IS_WIN = platform$2 === 'win32';
114
83
  const IS_LINUX = platform$2 === 'linux';
115
84
 
116
- // Constants
85
+ function generateSafeFilename(name) {
86
+ return name
87
+ .replace(/[<>:"/\\|?*]/g, '_')
88
+ .replace(/\s+/g, '_')
89
+ .replace(/\.+$/g, '')
90
+ .slice(0, 255);
91
+ }
92
+ function getSafeAppName(name) {
93
+ return generateSafeFilename(name).toLowerCase();
94
+ }
95
+ function generateLinuxPackageName(name) {
96
+ return name
97
+ .toLowerCase()
98
+ .replace(/[^a-z0-9\u4e00-\u9fff]+/g, '-')
99
+ .replace(/^-+|-+$/g, '')
100
+ .replace(/-+/g, '-');
101
+ }
102
+ function generateIdentifierSafeName(name) {
103
+ const cleaned = name.replace(/[^a-zA-Z0-9\u4e00-\u9fff]/g, '').toLowerCase();
104
+ if (cleaned === '') {
105
+ const fallback = Array.from(name)
106
+ .map((char) => {
107
+ const code = char.charCodeAt(0);
108
+ if ((code >= 48 && code <= 57) ||
109
+ (code >= 65 && code <= 90) ||
110
+ (code >= 97 && code <= 122)) {
111
+ return char.toLowerCase();
112
+ }
113
+ return code.toString(16);
114
+ })
115
+ .join('')
116
+ .slice(0, 50);
117
+ return fallback || 'pake-app';
118
+ }
119
+ return cleaned;
120
+ }
121
+
117
122
  const ICON_CONFIG = {
118
123
  minFileSize: 100,
119
- downloadTimeout: 10000,
120
- supportedFormats: ['png', 'ico', 'jpeg', 'jpg', 'webp'],
124
+ supportedFormats: ['png', 'ico', 'jpeg', 'jpg', 'webp', 'icns'],
121
125
  whiteBackground: { r: 255, g: 255, b: 255 },
126
+ transparentBackground: { r: 255, g: 255, b: 255, alpha: 0 },
127
+ downloadTimeout: {
128
+ ci: 5000,
129
+ default: 15000,
130
+ },
122
131
  };
123
- // API Configuration
124
- const API_TOKENS = {
125
- // cspell:disable-next-line
132
+ const PLATFORM_CONFIG = {
133
+ win: { format: '.ico', sizes: [16, 32, 48, 64, 128, 256] },
134
+ linux: { format: '.png', size: 512 },
135
+ macos: { format: '.icns', sizes: [16, 32, 64, 128, 256, 512, 1024] },
136
+ };
137
+ const API_KEYS = {
126
138
  logoDev: ['pk_JLLMUKGZRpaG5YclhXaTkg', 'pk_Ph745P8mQSeYFfW2Wk039A'],
127
- // cspell:disable-next-line
128
139
  brandfetch: ['1idqvJC0CeFSeyp3Yf7', '1idej-yhU_ThggIHFyG'],
129
140
  };
141
+ function generateIconPath(appName, isDefault = false) {
142
+ const safeName = isDefault
143
+ ? 'icon'
144
+ : generateSafeFilename(appName).toLowerCase();
145
+ const baseName = safeName;
146
+ if (IS_WIN) {
147
+ return path.join(npmDirectory, 'src-tauri', 'png', `${baseName}_256.ico`);
148
+ }
149
+ if (IS_LINUX) {
150
+ return path.join(npmDirectory, 'src-tauri', 'png', `${baseName}_512.png`);
151
+ }
152
+ return path.join(npmDirectory, 'src-tauri', 'icons', `${baseName}.icns`);
153
+ }
154
+ async function copyWindowsIconIfNeeded(convertedPath, appName) {
155
+ if (!IS_WIN || !convertedPath.endsWith('.ico')) {
156
+ return convertedPath;
157
+ }
158
+ try {
159
+ const finalIconPath = generateIconPath(appName);
160
+ await fsExtra.ensureDir(path.dirname(finalIconPath));
161
+ await fsExtra.copy(convertedPath, finalIconPath);
162
+ return finalIconPath;
163
+ }
164
+ catch (error) {
165
+ logger.warn(`Failed to copy Windows icon: ${error instanceof Error ? error.message : 'Unknown error'}`);
166
+ return convertedPath;
167
+ }
168
+ }
130
169
  /**
131
170
  * Adds white background to transparent icons only
132
171
  */
@@ -141,8 +180,8 @@ async function preprocessIcon(inputPath) {
141
180
  create: {
142
181
  width: metadata.width || 512,
143
182
  height: metadata.height || 512,
144
- channels: 3,
145
- background: ICON_CONFIG.whiteBackground,
183
+ channels: 4,
184
+ background: { ...ICON_CONFIG.whiteBackground, alpha: 1 },
146
185
  },
147
186
  })
148
187
  .composite([{ input: inputPath }])
@@ -151,7 +190,57 @@ async function preprocessIcon(inputPath) {
151
190
  return outputPath;
152
191
  }
153
192
  catch (error) {
154
- logger.warn(`Failed to add background to icon: ${error.message}`);
193
+ if (error instanceof Error) {
194
+ logger.warn(`Failed to add background to icon: ${error.message}`);
195
+ }
196
+ return inputPath;
197
+ }
198
+ }
199
+ /**
200
+ * Applies macOS squircle mask to icon
201
+ */
202
+ async function applyMacOSMask(inputPath) {
203
+ try {
204
+ const { path: tempDir } = await dir();
205
+ const outputPath = path.join(tempDir, 'icon-macos-rounded.png');
206
+ // 1. Create a 1024x1024 rounded rect mask
207
+ // rx="224" is closer to the smooth Apple squircle look for 1024px
208
+ const mask = Buffer.from('<svg width="1024" height="1024"><rect x="0" y="0" width="1024" height="1024" rx="224" ry="224" fill="white"/></svg>');
209
+ // 2. Load input, resize to 1024, apply mask
210
+ const maskedBuffer = await sharp(inputPath)
211
+ .resize(1024, 1024, {
212
+ fit: 'contain',
213
+ background: { r: 0, g: 0, b: 0, alpha: 0 },
214
+ })
215
+ .composite([
216
+ {
217
+ input: mask,
218
+ blend: 'dest-in',
219
+ },
220
+ ])
221
+ .png()
222
+ .toBuffer();
223
+ // 3. Resize to 840x840 (~18% padding) to solve "too big" visual issue
224
+ // Native MacOS icons often leave some breathing room
225
+ await sharp(maskedBuffer)
226
+ .resize(840, 840, {
227
+ fit: 'contain',
228
+ background: { r: 0, g: 0, b: 0, alpha: 0 },
229
+ })
230
+ .extend({
231
+ top: 92,
232
+ bottom: 92,
233
+ left: 92,
234
+ right: 92,
235
+ background: { r: 0, g: 0, b: 0, alpha: 0 },
236
+ })
237
+ .toFile(outputPath);
238
+ return outputPath;
239
+ }
240
+ catch (error) {
241
+ if (error instanceof Error) {
242
+ logger.warn(`Failed to apply macOS mask: ${error.message}`);
243
+ }
155
244
  return inputPath;
156
245
  }
157
246
  }
@@ -166,104 +255,155 @@ async function convertIconFormat(inputPath, appName) {
166
255
  const platformOutputDir = path.join(outputDir, 'converted-icons');
167
256
  await fsExtra.ensureDir(platformOutputDir);
168
257
  const processedInputPath = await preprocessIcon(inputPath);
169
- const iconName = appName.toLowerCase();
258
+ const iconName = generateSafeFilename(appName).toLowerCase();
170
259
  // Generate platform-specific format
171
260
  if (IS_WIN) {
261
+ // Support multiple sizes for better Windows compatibility
172
262
  await icongen(processedInputPath, platformOutputDir, {
173
263
  report: false,
174
- ico: { name: `${iconName}_256`, sizes: [256] },
264
+ ico: {
265
+ name: `${iconName}_256`,
266
+ sizes: PLATFORM_CONFIG.win.sizes,
267
+ },
175
268
  });
176
- return path.join(platformOutputDir, `${iconName}_256.ico`);
269
+ return path.join(platformOutputDir, `${iconName}_256${PLATFORM_CONFIG.win.format}`);
177
270
  }
178
271
  if (IS_LINUX) {
179
- const outputPath = path.join(platformOutputDir, `${iconName}_512.png`);
180
- await fsExtra.copy(processedInputPath, outputPath);
272
+ const outputPath = path.join(platformOutputDir, `${iconName}_${PLATFORM_CONFIG.linux.size}${PLATFORM_CONFIG.linux.format}`);
273
+ // Ensure we convert to proper PNG format with correct size
274
+ await sharp(processedInputPath)
275
+ .resize(PLATFORM_CONFIG.linux.size, PLATFORM_CONFIG.linux.size, {
276
+ fit: 'contain',
277
+ background: ICON_CONFIG.transparentBackground,
278
+ })
279
+ .ensureAlpha()
280
+ .png()
281
+ .toFile(outputPath);
181
282
  return outputPath;
182
283
  }
183
284
  // macOS
184
- await icongen(processedInputPath, platformOutputDir, {
285
+ const macIconPath = await applyMacOSMask(processedInputPath);
286
+ await icongen(macIconPath, platformOutputDir, {
185
287
  report: false,
186
- icns: { name: iconName, sizes: [16, 32, 64, 128, 256, 512, 1024] },
288
+ icns: { name: iconName, sizes: PLATFORM_CONFIG.macos.sizes },
187
289
  });
188
- const outputPath = path.join(platformOutputDir, `${iconName}.icns`);
290
+ const outputPath = path.join(platformOutputDir, `${iconName}${PLATFORM_CONFIG.macos.format}`);
189
291
  return (await fsExtra.pathExists(outputPath)) ? outputPath : null;
190
292
  }
191
293
  catch (error) {
192
- logger.warn(`Icon format conversion failed: ${error.message}`);
294
+ if (error instanceof Error) {
295
+ logger.warn(`Icon format conversion failed: ${error.message}`);
296
+ }
193
297
  return null;
194
298
  }
195
299
  }
196
- async function handleIcon(options, url) {
197
- if (options.icon) {
198
- if (options.icon.startsWith('http')) {
199
- return downloadIcon(options.icon);
200
- }
201
- return path.resolve(options.icon);
300
+ /**
301
+ * Processes downloaded or local icon for platform-specific format
302
+ */
303
+ async function processIcon(iconPath, appName) {
304
+ if (!iconPath || !appName)
305
+ return iconPath;
306
+ // Check if already in correct platform format
307
+ const ext = path.extname(iconPath).toLowerCase();
308
+ const isCorrectFormat = (IS_WIN && ext === '.ico') ||
309
+ (IS_LINUX && ext === '.png') ||
310
+ (!IS_WIN && !IS_LINUX && ext === '.icns');
311
+ if (isCorrectFormat) {
312
+ return await copyWindowsIconIfNeeded(iconPath, appName);
202
313
  }
203
- // Try to get favicon from website if URL is provided
204
- if (url && url.startsWith('http') && options.name) {
205
- const faviconPath = await tryGetFavicon(url, options.name);
206
- if (faviconPath)
207
- return faviconPath;
314
+ // Convert to platform format
315
+ const convertedPath = await convertIconFormat(iconPath, appName);
316
+ if (convertedPath) {
317
+ return await copyWindowsIconIfNeeded(convertedPath, appName);
208
318
  }
319
+ return iconPath;
320
+ }
321
+ /**
322
+ * Gets default icon with platform-specific fallback logic
323
+ */
324
+ async function getDefaultIcon() {
209
325
  logger.info('✼ No icon provided, using default icon.');
210
- // For Windows, ensure we have proper fallback handling
211
326
  if (IS_WIN) {
212
- const defaultIcoPath = path.join(npmDirectory, 'src-tauri/png/icon_256.ico');
327
+ const defaultIcoPath = generateIconPath('icon', true);
213
328
  const defaultPngPath = path.join(npmDirectory, 'src-tauri/png/icon_512.png');
214
- // First try default ico
329
+ // Try default ico first
215
330
  if (await fsExtra.pathExists(defaultIcoPath)) {
216
331
  return defaultIcoPath;
217
332
  }
218
- // If ico doesn't exist, try to convert from png
333
+ // Convert from png if ico doesn't exist
219
334
  if (await fsExtra.pathExists(defaultPngPath)) {
220
335
  logger.info('✼ Default ico not found, converting from png...');
221
336
  try {
222
337
  const convertedPath = await convertIconFormat(defaultPngPath, 'icon');
223
338
  if (convertedPath && (await fsExtra.pathExists(convertedPath))) {
224
- return convertedPath;
339
+ return await copyWindowsIconIfNeeded(convertedPath, 'icon');
225
340
  }
226
341
  }
227
342
  catch (error) {
228
- logger.warn(`Failed to convert default png to ico: ${error.message}`);
343
+ logger.warn(`Failed to convert default png to ico: ${error instanceof Error ? error.message : 'Unknown error'}`);
229
344
  }
230
345
  }
231
- // Last resort: return png path if it exists (Windows can handle png in some cases)
346
+ // Fallback to png or empty
232
347
  if (await fsExtra.pathExists(defaultPngPath)) {
233
348
  logger.warn('✼ Using png as fallback for Windows (may cause issues).');
234
349
  return defaultPngPath;
235
350
  }
236
- // If nothing exists, let the error bubble up
237
- throw new Error('No default icon found for Windows build');
351
+ logger.warn('✼ No default icon found, will use pake default.');
352
+ return '';
238
353
  }
354
+ // Linux and macOS defaults
239
355
  const iconPath = IS_LINUX
240
356
  ? 'src-tauri/png/icon_512.png'
241
357
  : 'src-tauri/icons/icon.icns';
242
358
  return path.join(npmDirectory, iconPath);
243
359
  }
360
+ /**
361
+ * Main icon handling function with simplified logic flow
362
+ */
363
+ async function handleIcon(options, url) {
364
+ // Handle custom icon (local file or remote URL)
365
+ if (options.icon) {
366
+ if (options.icon.startsWith('http')) {
367
+ const downloadedPath = await downloadIcon(options.icon);
368
+ if (downloadedPath) {
369
+ const result = await processIcon(downloadedPath, options.name || '');
370
+ if (result)
371
+ return result;
372
+ }
373
+ return '';
374
+ }
375
+ // Local file path
376
+ const resolvedPath = path.resolve(options.icon);
377
+ const result = await processIcon(resolvedPath, options.name || '');
378
+ return result || resolvedPath;
379
+ }
380
+ // Try favicon from website
381
+ if (url && options.name) {
382
+ const faviconPath = await tryGetFavicon(url, options.name);
383
+ if (faviconPath)
384
+ return faviconPath;
385
+ }
386
+ // Use default icon
387
+ return await getDefaultIcon();
388
+ }
244
389
  /**
245
390
  * Generates icon service URLs for a domain
246
391
  */
247
392
  function generateIconServiceUrls(domain) {
248
- const logoDevUrls = API_TOKENS.logoDev
393
+ const logoDevUrls = API_KEYS.logoDev
249
394
  .sort(() => Math.random() - 0.5)
250
395
  .map((token) => `https://img.logo.dev/${domain}?token=${token}&format=png&size=256`);
251
- const brandfetchUrls = API_TOKENS.brandfetch
396
+ const brandfetchUrls = API_KEYS.brandfetch
252
397
  .sort(() => Math.random() - 0.5)
253
398
  .map((key) => `https://cdn.brandfetch.io/${domain}/w/400/h/400?c=${key}`);
254
399
  return [
255
400
  ...logoDevUrls,
256
401
  ...brandfetchUrls,
257
402
  `https://logo.clearbit.com/${domain}?size=256`,
258
- `https://logo.uplead.com/${domain}`,
259
403
  `https://www.google.com/s2/favicons?domain=${domain}&sz=256`,
260
404
  `https://favicon.is/${domain}`,
261
- `https://icons.duckduckgo.com/ip3/${domain}.ico`,
262
- `https://icon.horse/icon/${domain}`,
263
405
  `https://${domain}/favicon.ico`,
264
406
  `https://www.${domain}/favicon.ico`,
265
- `https://${domain}/apple-touch-icon.png`,
266
- `https://${domain}/apple-touch-icon-precomposed.png`,
267
407
  ];
268
408
  }
269
409
  /**
@@ -274,9 +414,10 @@ async function tryGetFavicon(url, appName) {
274
414
  const domain = new URL(url).hostname;
275
415
  const spinner = getSpinner(`Fetching icon from ${domain}...`);
276
416
  const serviceUrls = generateIconServiceUrls(domain);
277
- // Use shorter timeout for CI environments
278
417
  const isCI = process.env.CI === 'true' || process.env.GITHUB_ACTIONS === 'true';
279
- const downloadTimeout = isCI ? 5000 : ICON_CONFIG.downloadTimeout;
418
+ const downloadTimeout = isCI
419
+ ? ICON_CONFIG.downloadTimeout.ci
420
+ : ICON_CONFIG.downloadTimeout.default;
280
421
  for (const serviceUrl of serviceUrls) {
281
422
  try {
282
423
  const faviconPath = await downloadIcon(serviceUrl, false, downloadTimeout);
@@ -284,23 +425,33 @@ async function tryGetFavicon(url, appName) {
284
425
  continue;
285
426
  const convertedPath = await convertIconFormat(faviconPath, appName);
286
427
  if (convertedPath) {
428
+ const finalPath = await copyWindowsIconIfNeeded(convertedPath, appName);
287
429
  spinner.succeed(chalk.green('Icon fetched and converted successfully!'));
288
- return convertedPath;
430
+ return finalPath;
289
431
  }
290
432
  }
291
433
  catch (error) {
292
- // Log specific errors in CI for debugging
293
- if (isCI) {
434
+ if (error instanceof Error) {
294
435
  logger.debug(`Icon service ${serviceUrl} failed: ${error.message}`);
295
436
  }
437
+ // Network error handling
438
+ if ((IS_LINUX || IS_WIN) && error.code === 'ENOTFOUND') {
439
+ return null;
440
+ }
441
+ // Icon generation error on Windows
442
+ if (IS_WIN && error instanceof Error && error.message.includes('icongen')) {
443
+ return null;
444
+ }
296
445
  continue;
297
446
  }
298
447
  }
299
- spinner.warn(`✼ No favicon found for ${domain}. Using default.`);
448
+ spinner.warn(`No favicon found for ${domain}. Using default.`);
300
449
  return null;
301
450
  }
302
451
  catch (error) {
303
- logger.warn(`Failed to fetch favicon: ${error.message}`);
452
+ if (error instanceof Error) {
453
+ logger.warn(`Failed to fetch favicon: ${error.message}`);
454
+ }
304
455
  return null;
305
456
  }
306
457
  }
@@ -311,7 +462,7 @@ async function downloadIcon(iconUrl, showSpinner = true, customTimeout) {
311
462
  try {
312
463
  const response = await axios.get(iconUrl, {
313
464
  responseType: 'arraybuffer',
314
- timeout: customTimeout || ICON_CONFIG.downloadTimeout,
465
+ timeout: customTimeout || 10000,
315
466
  });
316
467
  const iconData = response.data;
317
468
  if (!iconData || iconData.byteLength < ICON_CONFIG.minFileSize)
@@ -325,7 +476,7 @@ async function downloadIcon(iconUrl, showSpinner = true, customTimeout) {
325
476
  }
326
477
  catch (error) {
327
478
  if (showSpinner && !(error.response?.status === 404)) {
328
- throw error;
479
+ logger.error('Icon download failed!');
329
480
  }
330
481
  return null;
331
482
  }
@@ -335,15 +486,11 @@ async function downloadIcon(iconUrl, showSpinner = true, customTimeout) {
335
486
  */
336
487
  async function saveIconFile(iconData, extension) {
337
488
  const buffer = Buffer.from(iconData);
338
- if (IS_LINUX) {
339
- const iconPath = 'png/linux_temp.png';
340
- await fsExtra.outputFile(`${npmDirectory}/src-tauri/${iconPath}`, buffer);
341
- return iconPath;
342
- }
343
489
  const { path: tempPath } = await dir();
344
- const iconPath = `${tempPath}/icon.${extension}`;
345
- await fsExtra.outputFile(iconPath, buffer);
346
- return iconPath;
490
+ // Always save with the original extension first
491
+ const originalIconPath = path.join(tempPath, `icon.${extension}`);
492
+ await fsExtra.outputFile(originalIconPath, buffer);
493
+ return originalIconPath;
347
494
  }
348
495
 
349
496
  // Extracts the domain from a given URL.
@@ -364,6 +511,27 @@ function getDomain(inputUrl) {
364
511
  return null;
365
512
  }
366
513
  }
514
+ // Appends 'https://' protocol to the URL if not present.
515
+ function appendProtocol(inputUrl) {
516
+ try {
517
+ new URL(inputUrl);
518
+ return inputUrl;
519
+ }
520
+ catch {
521
+ return `https://${inputUrl}`;
522
+ }
523
+ }
524
+ // Normalizes the URL by ensuring it has a protocol and is valid.
525
+ function normalizeUrl(urlToNormalize) {
526
+ const urlWithProtocol = appendProtocol(urlToNormalize);
527
+ try {
528
+ new URL(urlWithProtocol);
529
+ return urlWithProtocol;
530
+ }
531
+ catch (err) {
532
+ throw new Error(`Your url "${urlWithProtocol}" is invalid: ${err.message}`);
533
+ }
534
+ }
367
535
 
368
536
  function resolveAppName(name, platform) {
369
537
  const domain = getDomain(name) || 'pake';
@@ -371,8 +539,8 @@ function resolveAppName(name, platform) {
371
539
  }
372
540
  function isValidName(name, platform) {
373
541
  const platformRegexMapping = {
374
- linux: /^[a-z0-9][a-z0-9-]*$/,
375
- default: /^[a-zA-Z0-9][a-zA-Z0-9- ]*$/,
542
+ linux: /^[a-z0-9\u4e00-\u9fff][a-z0-9\u4e00-\u9fff-]*$/,
543
+ default: /^[a-zA-Z0-9\u4e00-\u9fff][a-zA-Z0-9\u4e00-\u9fff- ]*$/,
376
544
  };
377
545
  const reg = platformRegexMapping[platform] || platformRegexMapping.default;
378
546
  return !!name && reg.test(name);
@@ -388,12 +556,10 @@ async function handleOptions(options, url) {
388
556
  const namePrompt = await promptText(promptMessage, defaultName);
389
557
  name = namePrompt || defaultName;
390
558
  }
391
- // Handle platform-specific name formatting
392
559
  if (name && platform === 'linux') {
393
- // Convert to lowercase and replace spaces with dashes for Linux
394
- name = name.toLowerCase().replace(/\s+/g, '-');
560
+ name = generateLinuxPackageName(name);
395
561
  }
396
- if (!isValidName(name, platform)) {
562
+ if (name && !isValidName(name, platform)) {
397
563
  const LINUX_NAME_ERROR = `✕ Name should only include lowercase letters, numbers, and dashes (not leading dashes). Examples: com-123-xxx, 123pan, pan123, weread, we-read, 123.`;
398
564
  const DEFAULT_NAME_ERROR = `✕ Name should only include letters, numbers, dashes, and spaces (not leading dashes and spaces). Examples: 123pan, 123Pan, Pan123, weread, WeRead, WERead, we-read, We Read, 123.`;
399
565
  const errorMsg = platform === 'linux' ? LINUX_NAME_ERROR : DEFAULT_NAME_ERROR;
@@ -411,7 +577,8 @@ async function handleOptions(options, url) {
411
577
  name,
412
578
  identifier: getIdentifier(url),
413
579
  };
414
- appOptions.icon = await handleIcon(appOptions, url);
580
+ const iconPath = await handleIcon(appOptions, url);
581
+ appOptions.icon = iconPath || '';
415
582
  return appOptions;
416
583
  }
417
584
 
@@ -447,7 +614,9 @@ async function shellExec(command, timeout = 300000, env) {
447
614
  try {
448
615
  const { exitCode } = await execa(command, {
449
616
  cwd: npmDirectory,
450
- stdio: ['inherit', 'pipe', 'inherit'], // Hide stdout verbose, keep stderr
617
+ // Use 'inherit' to show all output directly to user in real-time.
618
+ // This ensures linuxdeploy and other tool outputs are visible during builds.
619
+ stdio: 'inherit',
451
620
  shell: true,
452
621
  timeout,
453
622
  env: env ? { ...process.env, ...env } : process.env,
@@ -460,7 +629,38 @@ async function shellExec(command, timeout = 300000, env) {
460
629
  if (error.timedOut) {
461
630
  throw new Error(`Command timed out after ${timeout}ms: "${command}". Try increasing timeout or check network connectivity.`);
462
631
  }
463
- throw new Error(`Error occurred while executing command "${command}". Exit code: ${exitCode}. Details: ${errorMessage}`);
632
+ let errorMsg = `Error occurred while executing command "${command}". Exit code: ${exitCode}. Details: ${errorMessage}`;
633
+ // Provide helpful guidance for common Linux AppImage build failures
634
+ // caused by strip tool incompatibility with modern glibc (2.38+)
635
+ const lowerError = errorMessage.toLowerCase();
636
+ if (process.platform === 'linux' &&
637
+ (lowerError.includes('linuxdeploy') ||
638
+ lowerError.includes('appimage') ||
639
+ lowerError.includes('strip'))) {
640
+ errorMsg +=
641
+ '\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n' +
642
+ 'Linux AppImage Build Failed\n' +
643
+ '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n' +
644
+ 'Cause: Strip tool incompatibility with glibc 2.38+\n' +
645
+ ' (affects Debian Trixie, Arch Linux, and other modern distros)\n\n' +
646
+ 'Quick fix:\n' +
647
+ ' NO_STRIP=1 pake <url> --targets appimage --debug\n\n' +
648
+ 'Alternatives:\n' +
649
+ ' • Use DEB format: pake <url> --targets deb\n' +
650
+ ' • Update binutils: sudo apt install binutils (or pacman -S binutils)\n' +
651
+ ' • Detailed guide: https://github.com/tw93/Pake/blob/main/docs/faq.md\n' +
652
+ '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━';
653
+ if (lowerError.includes('fuse') ||
654
+ lowerError.includes('operation not permitted') ||
655
+ lowerError.includes('/dev/fuse')) {
656
+ errorMsg +=
657
+ '\n\nDocker / Container hint:\n' +
658
+ ' AppImage tooling needs access to /dev/fuse. When running inside Docker, add:\n' +
659
+ ' --privileged --device /dev/fuse --security-opt apparmor=unconfined\n' +
660
+ ' or run on the host directly.';
661
+ }
662
+ }
663
+ throw new Error(errorMsg);
464
664
  }
465
665
  }
466
666
 
@@ -509,6 +709,52 @@ async function isChinaIP(ip, domain) {
509
709
  }
510
710
  }
511
711
 
712
+ function normalizePathForComparison(targetPath) {
713
+ const normalized = path.normalize(targetPath);
714
+ return IS_WIN ? normalized.toLowerCase() : normalized;
715
+ }
716
+ function getCargoHomeCandidates() {
717
+ const candidates = new Set();
718
+ if (process.env.CARGO_HOME) {
719
+ candidates.add(process.env.CARGO_HOME);
720
+ }
721
+ const homeDir = os.homedir();
722
+ if (homeDir) {
723
+ candidates.add(path.join(homeDir, '.cargo'));
724
+ }
725
+ if (IS_WIN && process.env.USERPROFILE) {
726
+ candidates.add(path.join(process.env.USERPROFILE, '.cargo'));
727
+ }
728
+ return Array.from(candidates).filter(Boolean);
729
+ }
730
+ function ensureCargoBinOnPath() {
731
+ const currentPath = process.env.PATH || '';
732
+ const segments = currentPath.split(path.delimiter).filter(Boolean);
733
+ const normalizedSegments = new Set(segments.map((segment) => normalizePathForComparison(segment)));
734
+ const additions = [];
735
+ let cargoHomeSet = Boolean(process.env.CARGO_HOME);
736
+ for (const cargoHome of getCargoHomeCandidates()) {
737
+ const binDir = path.join(cargoHome, 'bin');
738
+ if (fsExtra.pathExistsSync(binDir) &&
739
+ !normalizedSegments.has(normalizePathForComparison(binDir))) {
740
+ additions.push(binDir);
741
+ normalizedSegments.add(normalizePathForComparison(binDir));
742
+ }
743
+ if (!cargoHomeSet && fsExtra.pathExistsSync(cargoHome)) {
744
+ process.env.CARGO_HOME = cargoHome;
745
+ cargoHomeSet = true;
746
+ }
747
+ }
748
+ if (additions.length) {
749
+ const prefix = additions.join(path.delimiter);
750
+ process.env.PATH = segments.length
751
+ ? `${prefix}${path.delimiter}${segments.join(path.delimiter)}`
752
+ : prefix;
753
+ }
754
+ }
755
+ function ensureRustEnv() {
756
+ ensureCargoBinOnPath();
757
+ }
512
758
  async function installRust() {
513
759
  const isActions = process.env.GITHUB_ACTIONS;
514
760
  const isInChina = await isChinaDomain('sh.rustup.rs');
@@ -518,16 +764,23 @@ async function installRust() {
518
764
  const rustInstallScriptForWindows = 'winget install --id Rustlang.Rustup';
519
765
  const spinner = getSpinner('Downloading Rust...');
520
766
  try {
521
- await shellExec(IS_WIN ? rustInstallScriptForWindows : rustInstallScriptForMac);
767
+ await shellExec(IS_WIN ? rustInstallScriptForWindows : rustInstallScriptForMac, 300000, undefined);
522
768
  spinner.succeed(chalk.green('✔ Rust installed successfully!'));
769
+ ensureRustEnv();
523
770
  }
524
771
  catch (error) {
525
772
  spinner.fail(chalk.red('✕ Rust installation failed!'));
526
- console.error(error.message);
773
+ if (error instanceof Error) {
774
+ console.error(error.message);
775
+ }
776
+ else {
777
+ console.error(error);
778
+ }
527
779
  process.exit(1);
528
780
  }
529
781
  }
530
782
  function checkRustInstalled() {
783
+ ensureCargoBinOnPath();
531
784
  try {
532
785
  execaSync('rustc', ['--version']);
533
786
  return true;
@@ -539,12 +792,16 @@ function checkRustInstalled() {
539
792
 
540
793
  async function combineFiles(files, output) {
541
794
  const contents = files.map((file) => {
542
- const fileContent = fs.readFileSync(file);
543
795
  if (file.endsWith('.css')) {
544
- return ("window.addEventListener('DOMContentLoaded', (_event) => { const css = `" +
545
- fileContent +
546
- "`; const style = document.createElement('style'); style.innerHTML = css; document.head.appendChild(style); });");
796
+ const fileContent = fs.readFileSync(file, 'utf-8');
797
+ return `window.addEventListener('DOMContentLoaded', (_event) => {
798
+ const css = ${JSON.stringify(fileContent)};
799
+ const style = document.createElement('style');
800
+ style.innerHTML = css;
801
+ document.head.appendChild(style);
802
+ });`;
547
803
  }
804
+ const fileContent = fs.readFileSync(file);
548
805
  return ("window.addEventListener('DOMContentLoaded', (_event) => { " +
549
806
  fileContent +
550
807
  ' });');
@@ -573,31 +830,42 @@ async function mergeConfig(url, options, tauriConf) {
573
830
  await fsExtra.copy(sourcePath, destPath);
574
831
  }
575
832
  }));
576
- const { width, height, fullscreen, hideTitleBar, alwaysOnTop, appVersion, darkMode, disabledWebShortcuts, activationShortcut, userAgent, showSystemTray, systemTrayIcon, useLocalFile, identifier, name, resizable = true, inject, proxyUrl, installerLanguage, hideOnClose, incognito, title, } = options;
833
+ const { width, height, fullscreen, maximize, hideTitleBar, alwaysOnTop, appVersion, darkMode, disabledWebShortcuts, activationShortcut, userAgent, showSystemTray, systemTrayIcon, useLocalFile, identifier, name = 'pake-app', resizable = true, inject, proxyUrl, installerLanguage, hideOnClose, incognito, title, wasm, enableDragDrop, multiInstance, startToTray, forceInternalNavigation, zoom, minWidth, minHeight, ignoreCertificateErrors, } = options;
577
834
  const { platform } = process;
578
- // Set Windows parameters.
835
+ const platformHideOnClose = hideOnClose ?? platform === 'darwin';
579
836
  const tauriConfWindowOptions = {
580
837
  width,
581
838
  height,
582
839
  fullscreen,
840
+ maximize,
583
841
  resizable,
584
842
  hide_title_bar: hideTitleBar,
585
843
  activation_shortcut: activationShortcut,
586
844
  always_on_top: alwaysOnTop,
587
845
  dark_mode: darkMode,
588
846
  disabled_web_shortcuts: disabledWebShortcuts,
589
- hide_on_close: hideOnClose,
847
+ hide_on_close: platformHideOnClose,
590
848
  incognito: incognito,
591
849
  title: title || null,
850
+ enable_wasm: wasm,
851
+ enable_drag_drop: enableDragDrop,
852
+ start_to_tray: startToTray && showSystemTray,
853
+ force_internal_navigation: forceInternalNavigation,
854
+ zoom,
855
+ min_width: minWidth,
856
+ min_height: minHeight,
857
+ ignore_certificate_errors: ignoreCertificateErrors,
592
858
  };
593
859
  Object.assign(tauriConf.pake.windows[0], { url, ...tauriConfWindowOptions });
594
860
  tauriConf.productName = name;
595
861
  tauriConf.identifier = identifier;
596
862
  tauriConf.version = appVersion;
863
+ if (platform === 'linux') {
864
+ tauriConf.mainBinaryName = `pake-${generateIdentifierSafeName(name)}`;
865
+ }
597
866
  if (platform == 'win32') {
598
867
  tauriConf.bundle.windows.wix.language[0] = installerLanguage;
599
868
  }
600
- //Judge the type of URL, whether it is a file or a website.
601
869
  const pathExists = await fsExtra.pathExists(url);
602
870
  if (pathExists) {
603
871
  logger.warn('✼ Your input might be a local file.');
@@ -639,64 +907,87 @@ async function mergeConfig(url, options, tauriConf) {
639
907
  // Remove hardcoded desktop files and regenerate with correct app name
640
908
  delete tauriConf.bundle.linux.deb.files;
641
909
  // Generate correct desktop file configuration
642
- const appNameLower = name.toLowerCase();
643
- const identifier = `com.pake.${appNameLower}`;
910
+ const appNameSafe = getSafeAppName(name);
911
+ const identifier = `com.pake.${appNameSafe}`;
644
912
  const desktopFileName = `${identifier}.desktop`;
645
913
  // Create desktop file content
914
+ // Determine if title contains Chinese characters for Name[zh_CN]
915
+ const chineseName = title && /[\u4e00-\u9fa5]/.test(title) ? title : null;
646
916
  const desktopContent = `[Desktop Entry]
647
917
  Version=1.0
648
918
  Type=Application
649
919
  Name=${name}
920
+ ${chineseName ? `Name[zh_CN]=${chineseName}` : ''}
650
921
  Comment=${name}
651
- Exec=${appNameLower}
652
- Icon=${appNameLower}
922
+ Exec=pake-${appNameSafe}
923
+ Icon=${appNameSafe}_512
653
924
  Categories=Network;WebBrowser;
654
925
  MimeType=text/html;text/xml;application/xhtml_xml;
655
926
  StartupNotify=true
656
927
  `;
657
- // Write desktop file to assets directory
658
- const assetsDir = path.join(npmDirectory, 'src-tauri/assets');
659
- const desktopFilePath = path.join(assetsDir, desktopFileName);
660
- await fsExtra.ensureDir(assetsDir);
661
- await fsExtra.writeFile(desktopFilePath, desktopContent);
928
+ // Write desktop file to src-tauri/assets directory where Tauri expects it
929
+ const srcAssetsDir = path.join(npmDirectory, 'src-tauri/assets');
930
+ const srcDesktopFilePath = path.join(srcAssetsDir, desktopFileName);
931
+ await fsExtra.ensureDir(srcAssetsDir);
932
+ await fsExtra.writeFile(srcDesktopFilePath, desktopContent);
662
933
  // Set up desktop file in bundle configuration
934
+ // Use absolute path from src-tauri directory to assets
663
935
  tauriConf.bundle.linux.deb.files = {
664
936
  [`/usr/share/applications/${desktopFileName}`]: `assets/${desktopFileName}`,
665
937
  };
666
- const validTargets = ['deb', 'appimage', 'rpm'];
938
+ const validTargets = [
939
+ 'deb',
940
+ 'appimage',
941
+ 'rpm',
942
+ 'deb-arm64',
943
+ 'appimage-arm64',
944
+ 'rpm-arm64',
945
+ ];
946
+ const baseTarget = options.targets.includes('-arm64')
947
+ ? options.targets.replace('-arm64', '')
948
+ : options.targets;
667
949
  if (validTargets.includes(options.targets)) {
668
- tauriConf.bundle.targets = [options.targets];
950
+ tauriConf.bundle.targets = [baseTarget];
669
951
  }
670
952
  else {
671
953
  logger.warn(`✼ The target must be one of ${validTargets.join(', ')}, the default 'deb' will be used.`);
672
954
  }
673
955
  }
956
+ // Set macOS bundle targets (for app vs dmg)
957
+ if (platform === 'darwin') {
958
+ const validMacTargets = ['app', 'dmg'];
959
+ if (validMacTargets.includes(options.targets)) {
960
+ tauriConf.bundle.targets = [options.targets];
961
+ }
962
+ }
674
963
  // Set icon.
964
+ const safeAppName = getSafeAppName(name);
675
965
  const platformIconMap = {
676
966
  win32: {
677
967
  fileExt: '.ico',
678
- path: `png/${name.toLowerCase()}_256.ico`,
968
+ path: `png/${safeAppName}_256.ico`,
679
969
  defaultIcon: 'png/icon_256.ico',
680
970
  message: 'Windows icon must be .ico and 256x256px.',
681
971
  },
682
972
  linux: {
683
973
  fileExt: '.png',
684
- path: `png/${name.toLowerCase()}_512.png`,
974
+ path: `png/${safeAppName}_512.png`,
685
975
  defaultIcon: 'png/icon_512.png',
686
976
  message: 'Linux icon must be .png and 512x512px.',
687
977
  },
688
978
  darwin: {
689
979
  fileExt: '.icns',
690
- path: `icons/${name.toLowerCase()}.icns`,
980
+ path: `icons/${safeAppName}.icns`,
691
981
  defaultIcon: 'icons/icon.icns',
692
982
  message: 'macOS icon must be .icns type.',
693
983
  },
694
984
  };
695
985
  const iconInfo = platformIconMap[platform];
696
- const exists = await fsExtra.pathExists(options.icon);
986
+ const resolvedIconPath = options.icon ? path.resolve(options.icon) : null;
987
+ const exists = resolvedIconPath && (await fsExtra.pathExists(resolvedIconPath));
697
988
  if (exists) {
698
989
  let updateIconPath = true;
699
- let customIconExt = path.extname(options.icon).toLowerCase();
990
+ let customIconExt = path.extname(resolvedIconPath).toLowerCase();
700
991
  if (customIconExt !== iconInfo.fileExt) {
701
992
  updateIconPath = false;
702
993
  logger.warn(`✼ ${iconInfo.message}, but you give ${customIconExt}`);
@@ -705,10 +996,14 @@ StartupNotify=true
705
996
  else {
706
997
  const iconPath = path.join(npmDirectory, 'src-tauri/', iconInfo.path);
707
998
  tauriConf.bundle.resources = [iconInfo.path];
708
- await fsExtra.copy(options.icon, iconPath);
999
+ // Avoid copying if source and destination are the same
1000
+ const absoluteDestPath = path.resolve(iconPath);
1001
+ if (resolvedIconPath !== absoluteDestPath) {
1002
+ await fsExtra.copy(resolvedIconPath, iconPath);
1003
+ }
709
1004
  }
710
1005
  if (updateIconPath) {
711
- tauriConf.bundle.icon = [options.icon];
1006
+ tauriConf.bundle.icon = [iconInfo.path];
712
1007
  }
713
1008
  else {
714
1009
  logger.warn(`✼ Icon will remain as default.`);
@@ -726,8 +1021,8 @@ StartupNotify=true
726
1021
  // 需要判断图标格式,默认只支持ico和png两种
727
1022
  let iconExt = path.extname(systemTrayIcon).toLowerCase();
728
1023
  if (iconExt == '.png' || iconExt == '.ico') {
729
- const trayIcoPath = path.join(npmDirectory, `src-tauri/png/${name.toLowerCase()}${iconExt}`);
730
- trayIconPath = `png/${name.toLowerCase()}${iconExt}`;
1024
+ const trayIcoPath = path.join(npmDirectory, `src-tauri/png/${safeAppName}${iconExt}`);
1025
+ trayIconPath = `png/${safeAppName}${iconExt}`;
731
1026
  await fsExtra.copy(systemTrayIcon, trayIcoPath);
732
1027
  }
733
1028
  else {
@@ -746,11 +1041,13 @@ StartupNotify=true
746
1041
  const injectFilePath = path.join(npmDirectory, `src-tauri/src/inject/custom.js`);
747
1042
  // inject js or css files
748
1043
  if (inject?.length > 0) {
749
- if (!inject.every((item) => item.endsWith('.css') || item.endsWith('.js'))) {
1044
+ // Ensure inject is an array before calling .every()
1045
+ const injectArray = Array.isArray(inject) ? inject : [inject];
1046
+ if (!injectArray.every((item) => item.endsWith('.css') || item.endsWith('.js'))) {
750
1047
  logger.error('The injected file must be in either CSS or JS format.');
751
1048
  return;
752
1049
  }
753
- const files = inject.map((filepath) => path.isAbsolute(filepath) ? filepath : path.join(process.cwd(), filepath));
1050
+ const files = injectArray.map((filepath) => path.isAbsolute(filepath) ? filepath : path.join(process.cwd(), filepath));
754
1051
  tauriConf.pake.inject = files;
755
1052
  await combineFiles(files, injectFilePath);
756
1053
  }
@@ -759,6 +1056,16 @@ StartupNotify=true
759
1056
  await fsExtra.writeFile(injectFilePath, '');
760
1057
  }
761
1058
  tauriConf.pake.proxy_url = proxyUrl || '';
1059
+ tauriConf.pake.multi_instance = multiInstance;
1060
+ // Configure WASM support with required HTTP headers
1061
+ if (wasm) {
1062
+ tauriConf.app.security = {
1063
+ headers: {
1064
+ 'Cross-Origin-Opener-Policy': 'same-origin',
1065
+ 'Cross-Origin-Embedder-Policy': 'require-corp',
1066
+ },
1067
+ };
1068
+ }
762
1069
  // Save config file.
763
1070
  const platformConfigPaths = {
764
1071
  win32: 'tauri.windows.conf.json',
@@ -794,10 +1101,34 @@ class BaseBuilder {
794
1101
  : undefined;
795
1102
  }
796
1103
  getInstallTimeout() {
797
- return process.platform === 'win32' ? 600000 : 300000;
1104
+ // Windows needs more time due to native compilation and antivirus scanning
1105
+ return process.platform === 'win32' ? 900000 : 600000;
798
1106
  }
799
1107
  getBuildTimeout() {
800
- return 900000; // 15 minutes for all builds
1108
+ return 900000;
1109
+ }
1110
+ async detectPackageManager() {
1111
+ if (BaseBuilder.packageManagerCache) {
1112
+ return BaseBuilder.packageManagerCache;
1113
+ }
1114
+ const { execa } = await import('execa');
1115
+ try {
1116
+ await execa('pnpm', ['--version'], { stdio: 'ignore' });
1117
+ logger.info('✺ Using pnpm for package management.');
1118
+ BaseBuilder.packageManagerCache = 'pnpm';
1119
+ return 'pnpm';
1120
+ }
1121
+ catch {
1122
+ try {
1123
+ await execa('npm', ['--version'], { stdio: 'ignore' });
1124
+ logger.info('✺ pnpm not available, using npm for package management.');
1125
+ BaseBuilder.packageManagerCache = 'npm';
1126
+ return 'npm';
1127
+ }
1128
+ catch {
1129
+ throw new Error('Neither pnpm nor npm is available. Please install a package manager.');
1130
+ }
1131
+ }
801
1132
  }
802
1133
  async prepare() {
803
1134
  const tauriSrcPath = path.join(npmDirectory, 'src-tauri');
@@ -807,6 +1138,7 @@ class BaseBuilder {
807
1138
  logger.warn('✼ The first use requires installing system dependencies.');
808
1139
  logger.warn('✼ See more in https://tauri.app/start/prerequisites/.');
809
1140
  }
1141
+ ensureRustEnv();
810
1142
  if (!checkRustInstalled()) {
811
1143
  const res = await prompts({
812
1144
  type: 'confirm',
@@ -826,24 +1158,56 @@ class BaseBuilder {
826
1158
  const rustProjectDir = path.join(tauriSrcPath, '.cargo');
827
1159
  const projectConf = path.join(rustProjectDir, 'config.toml');
828
1160
  await fsExtra.ensureDir(rustProjectDir);
829
- // 统一使用npm,简单可靠
830
- const packageManager = 'npm';
831
- const registryOption = isChina
832
- ? ' --registry=https://registry.npmmirror.com'
833
- : '';
834
- const legacyPeerDeps = ' --legacy-peer-deps'; // 解决dependency conflicts
1161
+ // Detect available package manager
1162
+ const packageManager = await this.detectPackageManager();
1163
+ const registryOption = ' --registry=https://registry.npmmirror.com';
1164
+ const peerDepsOption = packageManager === 'npm' ? ' --legacy-peer-deps' : '';
835
1165
  const timeout = this.getInstallTimeout();
836
1166
  const buildEnv = this.getBuildEnvironment();
837
- if (isChina) {
838
- logger.info('✺ Located in China, using npm/rsProxy CN mirror.');
839
- const projectCnConf = path.join(tauriSrcPath, 'rust_proxy.toml');
840
- await fsExtra.copy(projectCnConf, projectConf);
841
- await shellExec(`cd "${npmDirectory}" && ${packageManager} install${registryOption}${legacyPeerDeps} --silent`, timeout, buildEnv);
1167
+ // Show helpful message for first-time users
1168
+ if (!tauriTargetPathExists) {
1169
+ logger.info(process.platform === 'win32'
1170
+ ? '✺ First-time setup may take 10-15 minutes on Windows (compiling dependencies)...'
1171
+ : '✺ First-time setup may take 5-10 minutes (installing dependencies)...');
842
1172
  }
843
- else {
844
- await shellExec(`cd "${npmDirectory}" && ${packageManager} install${legacyPeerDeps} --silent`, timeout, buildEnv);
1173
+ let usedMirror = isChina;
1174
+ try {
1175
+ if (isChina) {
1176
+ logger.info(`✺ Located in China, using ${packageManager}/rsProxy CN mirror.`);
1177
+ const projectCnConf = path.join(tauriSrcPath, 'rust_proxy.toml');
1178
+ await fsExtra.copy(projectCnConf, projectConf);
1179
+ await shellExec(`cd "${npmDirectory}" && ${packageManager} install${registryOption}${peerDepsOption}`, timeout, buildEnv);
1180
+ }
1181
+ else {
1182
+ await shellExec(`cd "${npmDirectory}" && ${packageManager} install${peerDepsOption}`, timeout, buildEnv);
1183
+ }
1184
+ spinner.succeed(chalk.green('Package installed!'));
1185
+ }
1186
+ catch (error) {
1187
+ // If installation times out and we haven't tried the mirror yet, retry with mirror
1188
+ if (error instanceof Error &&
1189
+ error.message.includes('timed out') &&
1190
+ !usedMirror) {
1191
+ spinner.fail(chalk.yellow('Installation timed out, retrying with CN mirror...'));
1192
+ logger.info('✺ Retrying installation with CN mirror for better speed...');
1193
+ const retrySpinner = getSpinner('Retrying installation...');
1194
+ usedMirror = true;
1195
+ try {
1196
+ const projectCnConf = path.join(tauriSrcPath, 'rust_proxy.toml');
1197
+ await fsExtra.copy(projectCnConf, projectConf);
1198
+ await shellExec(`cd "${npmDirectory}" && ${packageManager} install${registryOption}${peerDepsOption}`, timeout, buildEnv);
1199
+ retrySpinner.succeed(chalk.green('Package installed with CN mirror!'));
1200
+ }
1201
+ catch (retryError) {
1202
+ retrySpinner.fail(chalk.red('Installation failed'));
1203
+ throw retryError;
1204
+ }
1205
+ }
1206
+ else {
1207
+ spinner.fail(chalk.red('Installation failed'));
1208
+ throw error;
1209
+ }
845
1210
  }
846
- spinner.succeed(chalk.green('Package installed!'));
847
1211
  if (!tauriTargetPathExists) {
848
1212
  logger.warn('✼ The first packaging may be slow, please be patient and wait, it will be faster afterwards.');
849
1213
  }
@@ -852,45 +1216,146 @@ class BaseBuilder {
852
1216
  await this.buildAndCopy(url, this.options.targets);
853
1217
  }
854
1218
  async start(url) {
1219
+ logger.info('Pake dev server starting...');
855
1220
  await mergeConfig(url, this.options, tauriConfig);
1221
+ const packageManager = await this.detectPackageManager();
1222
+ const configPath = path.join(npmDirectory, 'src-tauri', '.pake', 'tauri.conf.json');
1223
+ const features = this.getBuildFeatures();
1224
+ const featureArgs = features.length > 0 ? `--features ${features.join(',')}` : '';
1225
+ const argSeparator = packageManager === 'npm' ? ' --' : '';
1226
+ const command = `cd "${npmDirectory}" && ${packageManager} run tauri${argSeparator} dev --config "${configPath}" ${featureArgs}`;
1227
+ await shellExec(command);
856
1228
  }
857
1229
  async buildAndCopy(url, target) {
858
- const { name } = this.options;
1230
+ const { name = 'pake-app' } = this.options;
859
1231
  await mergeConfig(url, this.options, tauriConfig);
1232
+ // Detect available package manager
1233
+ const packageManager = await this.detectPackageManager();
860
1234
  // Build app
861
1235
  const buildSpinner = getSpinner('Building app...');
862
- // Let spinner run for a moment so user can see it, then stop before npm command
1236
+ // Let spinner run for a moment so user can see it, then stop before package manager command
863
1237
  await new Promise((resolve) => setTimeout(resolve, 500));
864
1238
  buildSpinner.stop();
865
1239
  // Show static message to keep the status visible
866
1240
  logger.warn('✸ Building app...');
867
- const buildEnv = this.getBuildEnvironment();
868
- await shellExec(`cd "${npmDirectory}" && ${this.getBuildCommand()}`, this.getBuildTimeout(), buildEnv);
1241
+ const baseEnv = this.getBuildEnvironment();
1242
+ let buildEnv = {
1243
+ ...(baseEnv ?? {}),
1244
+ ...(process.env.NO_STRIP ? { NO_STRIP: process.env.NO_STRIP } : {}),
1245
+ };
1246
+ const resolveExecEnv = () => Object.keys(buildEnv).length > 0 ? buildEnv : undefined;
1247
+ // Warn users about potential AppImage build failures on modern Linux systems.
1248
+ // The linuxdeploy tool bundled in Tauri uses an older strip tool that doesn't
1249
+ // recognize the .relr.dyn section introduced in glibc 2.38+.
1250
+ if (process.platform === 'linux' && this.options.targets === 'appimage') {
1251
+ if (!buildEnv.NO_STRIP) {
1252
+ logger.warn('⚠ Building AppImage on Linux may fail due to strip incompatibility with glibc 2.38+');
1253
+ logger.warn('⚠ If build fails, retry with: NO_STRIP=1 pake <url> --targets appimage');
1254
+ }
1255
+ }
1256
+ const buildCommand = `cd "${npmDirectory}" && ${this.getBuildCommand(packageManager)}`;
1257
+ const buildTimeout = this.getBuildTimeout();
1258
+ try {
1259
+ await shellExec(buildCommand, buildTimeout, resolveExecEnv());
1260
+ }
1261
+ catch (error) {
1262
+ const shouldRetryWithoutStrip = process.platform === 'linux' &&
1263
+ this.options.targets === 'appimage' &&
1264
+ !buildEnv.NO_STRIP &&
1265
+ this.isLinuxDeployStripError(error);
1266
+ if (shouldRetryWithoutStrip) {
1267
+ logger.warn('⚠ AppImage build failed during linuxdeploy strip step, retrying with NO_STRIP=1 automatically.');
1268
+ buildEnv = {
1269
+ ...buildEnv,
1270
+ NO_STRIP: '1',
1271
+ };
1272
+ await shellExec(buildCommand, buildTimeout, resolveExecEnv());
1273
+ }
1274
+ else {
1275
+ throw error;
1276
+ }
1277
+ }
869
1278
  // Copy app
870
1279
  const fileName = this.getFileName();
871
1280
  const fileType = this.getFileType(target);
872
1281
  const appPath = this.getBuildAppPath(npmDirectory, fileName, fileType);
873
1282
  const distPath = path.resolve(`${name}.${fileType}`);
874
1283
  await fsExtra.copy(appPath, distPath);
1284
+ // Copy raw binary if requested
1285
+ if (this.options.keepBinary) {
1286
+ await this.copyRawBinary(npmDirectory, name);
1287
+ }
875
1288
  await fsExtra.remove(appPath);
876
1289
  logger.success('✔ Build success!');
877
1290
  logger.success('✔ App installer located in', distPath);
1291
+ // Log binary location if preserved
1292
+ if (this.options.keepBinary) {
1293
+ const binaryPath = this.getRawBinaryPath(name);
1294
+ logger.success('✔ Raw binary located in', path.resolve(binaryPath));
1295
+ }
878
1296
  }
879
1297
  getFileType(target) {
880
1298
  return target;
881
1299
  }
882
- getBuildCommand() {
1300
+ isLinuxDeployStripError(error) {
1301
+ if (!(error instanceof Error) || !error.message) {
1302
+ return false;
1303
+ }
1304
+ const message = error.message.toLowerCase();
1305
+ return (message.includes('linuxdeploy') ||
1306
+ message.includes('failed to run linuxdeploy') ||
1307
+ message.includes('strip:') ||
1308
+ message.includes('unable to recognise the format of the input file') ||
1309
+ message.includes('appimage tool failed') ||
1310
+ message.includes('strip tool'));
1311
+ }
1312
+ /**
1313
+ * 解析目标架构
1314
+ */
1315
+ resolveTargetArch(requestedArch) {
1316
+ if (requestedArch === 'auto' || !requestedArch) {
1317
+ return process.arch;
1318
+ }
1319
+ return requestedArch;
1320
+ }
1321
+ /**
1322
+ * 获取Tauri构建目标
1323
+ */
1324
+ getTauriTarget(arch, platform = process.platform) {
1325
+ const platformMappings = BaseBuilder.ARCH_MAPPINGS[platform];
1326
+ if (!platformMappings)
1327
+ return null;
1328
+ return platformMappings[arch] || null;
1329
+ }
1330
+ /**
1331
+ * 获取架构显示名称(用于文件名)
1332
+ */
1333
+ getArchDisplayName(arch) {
1334
+ return BaseBuilder.ARCH_DISPLAY_NAMES[arch] || arch;
1335
+ }
1336
+ /**
1337
+ * 构建基础构建命令
1338
+ */
1339
+ buildBaseCommand(packageManager, configPath, target) {
883
1340
  const baseCommand = this.options.debug
884
- ? 'npm run build:debug'
885
- : 'npm run build';
886
- // Use temporary config directory to avoid modifying source files
887
- const configPath = path.join(npmDirectory, 'src-tauri', '.pake', 'tauri.conf.json');
888
- let fullCommand = `${baseCommand} -- -c "${configPath}"`;
889
- // For macOS, use app bundles by default unless DMG is explicitly requested
890
- if (IS_MAC && this.options.targets === 'app') {
891
- fullCommand += ' --bundles app';
1341
+ ? `${packageManager} run build:debug`
1342
+ : `${packageManager} run build`;
1343
+ const argSeparator = packageManager === 'npm' ? ' --' : '';
1344
+ let fullCommand = `${baseCommand}${argSeparator} -c "${configPath}"`;
1345
+ if (target) {
1346
+ fullCommand += ` --target ${target}`;
892
1347
  }
893
- // Add features
1348
+ // Enable verbose output in debug mode to help diagnose build issues.
1349
+ // This provides detailed logs from Tauri CLI and bundler tools.
1350
+ if (this.options.debug) {
1351
+ fullCommand += ' --verbose';
1352
+ }
1353
+ return fullCommand;
1354
+ }
1355
+ /**
1356
+ * 获取构建特性列表
1357
+ */
1358
+ getBuildFeatures() {
894
1359
  const features = ['cli-build'];
895
1360
  // Add macos-proxy feature for modern macOS (Darwin 23+ = macOS 14+)
896
1361
  if (IS_MAC) {
@@ -899,6 +1364,18 @@ class BaseBuilder {
899
1364
  features.push('macos-proxy');
900
1365
  }
901
1366
  }
1367
+ return features;
1368
+ }
1369
+ getBuildCommand(packageManager = 'pnpm') {
1370
+ // Use temporary config directory to avoid modifying source files
1371
+ const configPath = path.join(npmDirectory, 'src-tauri', '.pake', 'tauri.conf.json');
1372
+ let fullCommand = this.buildBaseCommand(packageManager, configPath);
1373
+ // For macOS, use app bundles by default unless DMG is explicitly requested
1374
+ if (IS_MAC && this.options.targets === 'app') {
1375
+ fullCommand += ' --bundles app';
1376
+ }
1377
+ // Add features
1378
+ const features = this.getBuildFeatures();
902
1379
  if (features.length > 0) {
903
1380
  fullCommand += ` --features ${features.join(',')}`;
904
1381
  }
@@ -924,96 +1401,250 @@ class BaseBuilder {
924
1401
  const bundleDir = fileType.toLowerCase() === 'app' ? 'macos' : fileType.toLowerCase();
925
1402
  return path.join(npmDirectory, this.getBasePath(), bundleDir, `${fileName}.${fileType}`);
926
1403
  }
1404
+ /**
1405
+ * Copy raw binary file to output directory
1406
+ */
1407
+ async copyRawBinary(npmDirectory, appName) {
1408
+ const binaryPath = this.getRawBinarySourcePath(npmDirectory, appName);
1409
+ const outputPath = this.getRawBinaryPath(appName);
1410
+ if (await fsExtra.pathExists(binaryPath)) {
1411
+ await fsExtra.copy(binaryPath, outputPath);
1412
+ // Make binary executable on Unix-like systems
1413
+ if (process.platform !== 'win32') {
1414
+ await fsExtra.chmod(outputPath, 0o755);
1415
+ }
1416
+ }
1417
+ else {
1418
+ logger.warn(`✼ Raw binary not found at ${binaryPath}, skipping...`);
1419
+ }
1420
+ }
1421
+ /**
1422
+ * Get the source path of the raw binary file in the build directory
1423
+ */
1424
+ getRawBinarySourcePath(npmDirectory, appName) {
1425
+ const basePath = this.options.debug ? 'debug' : 'release';
1426
+ const binaryName = this.getBinaryName(appName);
1427
+ // Handle cross-platform builds
1428
+ if (this.options.multiArch || this.hasArchSpecificTarget()) {
1429
+ return path.join(npmDirectory, this.getArchSpecificPath(), basePath, binaryName);
1430
+ }
1431
+ return path.join(npmDirectory, 'src-tauri/target', basePath, binaryName);
1432
+ }
1433
+ /**
1434
+ * Get the output path for the raw binary file
1435
+ */
1436
+ getRawBinaryPath(appName) {
1437
+ const extension = process.platform === 'win32' ? '.exe' : '';
1438
+ const suffix = process.platform === 'win32' ? '' : '-binary';
1439
+ return `${appName}${suffix}${extension}`;
1440
+ }
1441
+ /**
1442
+ * Get the binary name based on app name and platform
1443
+ */
1444
+ getBinaryName(appName) {
1445
+ const extension = process.platform === 'win32' ? '.exe' : '';
1446
+ // Linux uses the unique binary name we set in merge.ts
1447
+ if (process.platform === 'linux') {
1448
+ return `pake-${generateIdentifierSafeName(appName)}${extension}`;
1449
+ }
1450
+ // Windows and macOS use 'pake' as binary name
1451
+ return `pake${extension}`;
1452
+ }
1453
+ /**
1454
+ * Check if this build has architecture-specific target
1455
+ */
1456
+ hasArchSpecificTarget() {
1457
+ return false; // Override in subclasses if needed
1458
+ }
1459
+ /**
1460
+ * Get architecture-specific path for binary
1461
+ */
1462
+ getArchSpecificPath() {
1463
+ return 'src-tauri/target'; // Override in subclasses if needed
1464
+ }
927
1465
  }
1466
+ BaseBuilder.packageManagerCache = null;
1467
+ // 架构映射配置
1468
+ BaseBuilder.ARCH_MAPPINGS = {
1469
+ darwin: {
1470
+ arm64: 'aarch64-apple-darwin',
1471
+ x64: 'x86_64-apple-darwin',
1472
+ universal: 'universal-apple-darwin',
1473
+ },
1474
+ win32: {
1475
+ arm64: 'aarch64-pc-windows-msvc',
1476
+ x64: 'x86_64-pc-windows-msvc',
1477
+ },
1478
+ linux: {
1479
+ arm64: 'aarch64-unknown-linux-gnu',
1480
+ x64: 'x86_64-unknown-linux-gnu',
1481
+ },
1482
+ };
1483
+ // 架构名称映射(用于文件名生成)
1484
+ BaseBuilder.ARCH_DISPLAY_NAMES = {
1485
+ arm64: 'aarch64',
1486
+ x64: 'x64',
1487
+ universal: 'universal',
1488
+ };
928
1489
 
929
1490
  class MacBuilder extends BaseBuilder {
930
1491
  constructor(options) {
931
1492
  super(options);
932
- // Use DMG by default for distribution
933
- // Only create app bundles for testing to avoid user interaction
934
- if (process.env.PAKE_CREATE_APP === '1') {
935
- this.options.targets = 'app';
1493
+ const validArchs = ['intel', 'apple', 'universal', 'auto', 'x64', 'arm64'];
1494
+ this.buildArch = validArchs.includes(options.targets || '')
1495
+ ? options.targets
1496
+ : 'auto';
1497
+ if (options.iterativeBuild || process.env.PAKE_CREATE_APP === '1') {
1498
+ this.buildFormat = 'app';
936
1499
  }
937
1500
  else {
938
- this.options.targets = 'dmg';
1501
+ this.buildFormat = 'dmg';
939
1502
  }
1503
+ this.options.targets = this.buildFormat;
940
1504
  }
941
1505
  getFileName() {
942
- const { name } = this.options;
943
- // For app bundles, use simple name without version/arch
944
- if (this.options.targets === 'app') {
1506
+ const { name = 'pake-app' } = this.options;
1507
+ if (this.buildFormat === 'app') {
945
1508
  return name;
946
1509
  }
947
- // For DMG files, use versioned filename
948
1510
  let arch;
949
- if (this.options.multiArch) {
1511
+ if (this.buildArch === 'universal' || this.options.multiArch) {
950
1512
  arch = 'universal';
951
1513
  }
1514
+ else if (this.buildArch === 'apple') {
1515
+ arch = 'aarch64';
1516
+ }
1517
+ else if (this.buildArch === 'intel') {
1518
+ arch = 'x64';
1519
+ }
952
1520
  else {
953
- arch = process.arch === 'arm64' ? 'aarch64' : process.arch;
1521
+ arch = this.getArchDisplayName(this.resolveTargetArch(this.buildArch));
954
1522
  }
955
1523
  return `${name}_${tauriConfig.version}_${arch}`;
956
1524
  }
957
- getBuildCommand() {
958
- if (this.options.multiArch) {
959
- const baseCommand = this.options.debug
960
- ? 'npm run tauri build -- --debug'
961
- : 'npm run tauri build --';
962
- // Use temporary config directory to avoid modifying source files
963
- const configPath = path.join('src-tauri', '.pake', 'tauri.conf.json');
964
- let fullCommand = `${baseCommand} --target universal-apple-darwin -c "${configPath}"`;
965
- // Add features
966
- const features = ['cli-build'];
967
- // Add macos-proxy feature for modern macOS (Darwin 23+ = macOS 14+)
968
- const macOSVersion = this.getMacOSMajorVersion();
969
- if (macOSVersion >= 23) {
970
- features.push('macos-proxy');
971
- }
972
- if (features.length > 0) {
973
- fullCommand += ` --features ${features.join(',')}`;
974
- }
975
- return fullCommand;
1525
+ getActualArch() {
1526
+ if (this.buildArch === 'universal' || this.options.multiArch) {
1527
+ return 'universal';
976
1528
  }
977
- return super.getBuildCommand();
1529
+ else if (this.buildArch === 'apple') {
1530
+ return 'arm64';
1531
+ }
1532
+ else if (this.buildArch === 'intel') {
1533
+ return 'x64';
1534
+ }
1535
+ return this.resolveTargetArch(this.buildArch);
1536
+ }
1537
+ getBuildCommand(packageManager = 'pnpm') {
1538
+ const configPath = path.join('src-tauri', '.pake', 'tauri.conf.json');
1539
+ const actualArch = this.getActualArch();
1540
+ const buildTarget = this.getTauriTarget(actualArch, 'darwin');
1541
+ if (!buildTarget) {
1542
+ throw new Error(`Unsupported architecture: ${actualArch} for macOS`);
1543
+ }
1544
+ let fullCommand = this.buildBaseCommand(packageManager, configPath, buildTarget);
1545
+ const features = this.getBuildFeatures();
1546
+ if (features.length > 0) {
1547
+ fullCommand += ` --features ${features.join(',')}`;
1548
+ }
1549
+ return fullCommand;
978
1550
  }
979
1551
  getBasePath() {
980
- return this.options.multiArch
981
- ? 'src-tauri/target/universal-apple-darwin/release/bundle'
982
- : super.getBasePath();
1552
+ const basePath = this.options.debug ? 'debug' : 'release';
1553
+ const actualArch = this.getActualArch();
1554
+ const target = this.getTauriTarget(actualArch, 'darwin');
1555
+ return `src-tauri/target/${target}/${basePath}/bundle`;
1556
+ }
1557
+ hasArchSpecificTarget() {
1558
+ return true;
1559
+ }
1560
+ getArchSpecificPath() {
1561
+ const actualArch = this.getActualArch();
1562
+ const target = this.getTauriTarget(actualArch, 'darwin');
1563
+ return `src-tauri/target/${target}`;
983
1564
  }
984
1565
  }
985
1566
 
986
1567
  class WinBuilder extends BaseBuilder {
987
1568
  constructor(options) {
988
1569
  super(options);
989
- this.options.targets = 'msi';
1570
+ this.buildFormat = 'msi';
1571
+ const validArchs = ['x64', 'arm64', 'auto'];
1572
+ this.buildArch = validArchs.includes(options.targets || '')
1573
+ ? this.resolveTargetArch(options.targets)
1574
+ : this.resolveTargetArch('auto');
1575
+ this.options.targets = this.buildFormat;
990
1576
  }
991
1577
  getFileName() {
992
1578
  const { name } = this.options;
993
- const { arch } = process;
994
1579
  const language = tauriConfig.bundle.windows.wix.language[0];
995
- return `${name}_${tauriConfig.version}_${arch}_${language}`;
1580
+ const targetArch = this.getArchDisplayName(this.buildArch);
1581
+ return `${name}_${tauriConfig.version}_${targetArch}_${language}`;
1582
+ }
1583
+ getBuildCommand(packageManager = 'pnpm') {
1584
+ const configPath = path.join('src-tauri', '.pake', 'tauri.conf.json');
1585
+ const buildTarget = this.getTauriTarget(this.buildArch, 'win32');
1586
+ if (!buildTarget) {
1587
+ throw new Error(`Unsupported architecture: ${this.buildArch} for Windows`);
1588
+ }
1589
+ let fullCommand = this.buildBaseCommand(packageManager, configPath, buildTarget);
1590
+ const features = this.getBuildFeatures();
1591
+ if (features.length > 0) {
1592
+ fullCommand += ` --features ${features.join(',')}`;
1593
+ }
1594
+ return fullCommand;
1595
+ }
1596
+ getBasePath() {
1597
+ const basePath = this.options.debug ? 'debug' : 'release';
1598
+ const target = this.getTauriTarget(this.buildArch, 'win32');
1599
+ return `src-tauri/target/${target}/${basePath}/bundle/`;
1600
+ }
1601
+ hasArchSpecificTarget() {
1602
+ return true;
1603
+ }
1604
+ getArchSpecificPath() {
1605
+ const target = this.getTauriTarget(this.buildArch, 'win32');
1606
+ return `src-tauri/target/${target}`;
996
1607
  }
997
1608
  }
998
1609
 
999
1610
  class LinuxBuilder extends BaseBuilder {
1000
1611
  constructor(options) {
1001
1612
  super(options);
1613
+ const target = options.targets || 'deb';
1614
+ if (target.includes('-arm64')) {
1615
+ this.buildFormat = target.replace('-arm64', '');
1616
+ this.buildArch = 'arm64';
1617
+ }
1618
+ else {
1619
+ this.buildFormat = target;
1620
+ this.buildArch = this.resolveTargetArch('auto');
1621
+ }
1622
+ this.options.targets = this.buildFormat;
1002
1623
  }
1003
1624
  getFileName() {
1004
- const { name, targets } = this.options;
1625
+ const { name = 'pake-app', targets } = this.options;
1005
1626
  const version = tauriConfig.version;
1006
- let arch = process.arch === 'x64' ? 'amd64' : process.arch;
1007
- if (arch === 'arm64' && (targets === 'rpm' || targets === 'appimage')) {
1008
- arch = 'aarch64';
1627
+ let arch;
1628
+ if (this.buildArch === 'arm64') {
1629
+ arch = targets === 'rpm' || targets === 'appimage' ? 'aarch64' : 'arm64';
1630
+ }
1631
+ else {
1632
+ if (this.buildArch === 'x64') {
1633
+ arch = targets === 'rpm' ? 'x86_64' : 'amd64';
1634
+ }
1635
+ else {
1636
+ arch = this.buildArch;
1637
+ if (this.buildArch === 'arm64' &&
1638
+ (targets === 'rpm' || targets === 'appimage')) {
1639
+ arch = 'aarch64';
1640
+ }
1641
+ }
1009
1642
  }
1010
- // The RPM format uses different separators and version number formats
1011
1643
  if (targets === 'rpm') {
1012
1644
  return `${name}-${version}-1.${arch}`;
1013
1645
  }
1014
1646
  return `${name}_${version}_${arch}`;
1015
1647
  }
1016
- // Customize it, considering that there are all targets.
1017
1648
  async build(url) {
1018
1649
  const targetTypes = ['deb', 'appimage', 'rpm'];
1019
1650
  for (const target of targetTypes) {
@@ -1022,12 +1653,49 @@ class LinuxBuilder extends BaseBuilder {
1022
1653
  }
1023
1654
  }
1024
1655
  }
1656
+ getBuildCommand(packageManager = 'pnpm') {
1657
+ const configPath = path.join('src-tauri', '.pake', 'tauri.conf.json');
1658
+ const buildTarget = this.buildArch === 'arm64'
1659
+ ? (this.getTauriTarget(this.buildArch, 'linux') ?? undefined)
1660
+ : undefined;
1661
+ let fullCommand = this.buildBaseCommand(packageManager, configPath, buildTarget);
1662
+ const features = this.getBuildFeatures();
1663
+ if (features.length > 0) {
1664
+ fullCommand += ` --features ${features.join(',')}`;
1665
+ }
1666
+ // Enable verbose output for AppImage builds when debugging or PAKE_VERBOSE is set.
1667
+ // AppImage builds often fail with minimal error messages from linuxdeploy,
1668
+ // so verbose mode helps diagnose issues like strip failures and missing dependencies.
1669
+ if (this.options.targets === 'appimage' &&
1670
+ (this.options.debug || process.env.PAKE_VERBOSE)) {
1671
+ fullCommand += ' --verbose';
1672
+ }
1673
+ return fullCommand;
1674
+ }
1675
+ getBasePath() {
1676
+ const basePath = this.options.debug ? 'debug' : 'release';
1677
+ if (this.buildArch === 'arm64') {
1678
+ const target = this.getTauriTarget(this.buildArch, 'linux');
1679
+ return `src-tauri/target/${target}/${basePath}/bundle/`;
1680
+ }
1681
+ return super.getBasePath();
1682
+ }
1025
1683
  getFileType(target) {
1026
1684
  if (target === 'appimage') {
1027
1685
  return 'AppImage';
1028
1686
  }
1029
1687
  return super.getFileType(target);
1030
1688
  }
1689
+ hasArchSpecificTarget() {
1690
+ return this.buildArch === 'arm64';
1691
+ }
1692
+ getArchSpecificPath() {
1693
+ if (this.buildArch === 'arm64') {
1694
+ const target = this.getTauriTarget(this.buildArch, 'linux');
1695
+ return `src-tauri/target/${target}`;
1696
+ }
1697
+ return super.getArchSpecificPath();
1698
+ }
1031
1699
  }
1032
1700
 
1033
1701
  const { platform } = process;
@@ -1046,13 +1714,230 @@ class BuilderProvider {
1046
1714
  }
1047
1715
  }
1048
1716
 
1049
- async function startBuild() {
1717
+ var version = "3.6.1";
1718
+ var packageJson = {
1719
+ version: version};
1720
+
1721
+ const DEFAULT_PAKE_OPTIONS = {
1722
+ icon: '',
1723
+ height: 780,
1724
+ width: 1200,
1725
+ fullscreen: false,
1726
+ maximize: false,
1727
+ hideTitleBar: false,
1728
+ alwaysOnTop: false,
1729
+ appVersion: '1.0.0',
1730
+ darkMode: false,
1731
+ disabledWebShortcuts: false,
1732
+ activationShortcut: '',
1733
+ userAgent: '',
1734
+ showSystemTray: false,
1735
+ multiArch: false,
1736
+ targets: (() => {
1737
+ switch (process.platform) {
1738
+ case 'linux':
1739
+ return 'deb';
1740
+ case 'darwin':
1741
+ return 'dmg';
1742
+ case 'win32':
1743
+ return 'msi';
1744
+ default:
1745
+ return 'deb';
1746
+ }
1747
+ })(),
1748
+ useLocalFile: false,
1749
+ systemTrayIcon: '',
1750
+ proxyUrl: '',
1751
+ debug: false,
1752
+ inject: [],
1753
+ installerLanguage: 'en-US',
1754
+ hideOnClose: undefined, // Platform-specific: true for macOS, false for others
1755
+ incognito: false,
1756
+ wasm: false,
1757
+ enableDragDrop: false,
1758
+ keepBinary: false,
1759
+ multiInstance: false,
1760
+ startToTray: false,
1761
+ forceInternalNavigation: false,
1762
+ iterativeBuild: false,
1763
+ zoom: 100,
1764
+ minWidth: 0,
1765
+ minHeight: 0,
1766
+ ignoreCertificateErrors: false,
1767
+ };
1768
+
1769
+ function validateNumberInput(value) {
1770
+ const parsedValue = Number(value);
1771
+ if (isNaN(parsedValue)) {
1772
+ throw new InvalidArgumentError('Not a number.');
1773
+ }
1774
+ return parsedValue;
1775
+ }
1776
+ function validateUrlInput(url) {
1777
+ const isFile = fs.existsSync(url);
1778
+ if (!isFile) {
1779
+ try {
1780
+ return normalizeUrl(url);
1781
+ }
1782
+ catch (error) {
1783
+ if (error instanceof Error) {
1784
+ throw new InvalidArgumentError(error.message);
1785
+ }
1786
+ throw error;
1787
+ }
1788
+ }
1789
+ return url;
1790
+ }
1791
+
1792
+ function getCliProgram() {
1793
+ const { green, yellow } = chalk;
1794
+ const logo = `${chalk.green(' ____ _')}
1795
+ ${green('| _ \\ __ _| | _____')}
1796
+ ${green('| |_) / _` | |/ / _ \\')}
1797
+ ${green('| __/ (_| | < __/')} ${yellow('https://github.com/tw93/pake')}
1798
+ ${green('|_| \\__,_|_|\\_\\___| can turn any webpage into a desktop app with Rust.')}
1799
+ `;
1800
+ return program$1
1801
+ .addHelpText('beforeAll', logo)
1802
+ .usage(`[url] [options]`)
1803
+ .showHelpAfterError()
1804
+ .argument('[url]', 'The web URL you want to package', validateUrlInput)
1805
+ .option('--name <string>', 'Application name')
1806
+ .option('--icon <string>', 'Application icon', DEFAULT_PAKE_OPTIONS.icon)
1807
+ .option('--width <number>', 'Window width', validateNumberInput, DEFAULT_PAKE_OPTIONS.width)
1808
+ .option('--height <number>', 'Window height', validateNumberInput, DEFAULT_PAKE_OPTIONS.height)
1809
+ .option('--use-local-file', 'Use local file packaging', DEFAULT_PAKE_OPTIONS.useLocalFile)
1810
+ .option('--fullscreen', 'Start in full screen', DEFAULT_PAKE_OPTIONS.fullscreen)
1811
+ .option('--hide-title-bar', 'For Mac, hide title bar', DEFAULT_PAKE_OPTIONS.hideTitleBar)
1812
+ .option('--multi-arch', 'For Mac, both Intel and M1', DEFAULT_PAKE_OPTIONS.multiArch)
1813
+ .option('--inject <files>', 'Inject local CSS/JS files into the page', (val, previous) => {
1814
+ if (!val)
1815
+ return DEFAULT_PAKE_OPTIONS.inject;
1816
+ // Split by comma and trim whitespace, filter out empty strings
1817
+ const files = val
1818
+ .split(',')
1819
+ .map((item) => item.trim())
1820
+ .filter((item) => item.length > 0);
1821
+ // If previous values exist (from multiple --inject options), merge them
1822
+ return previous ? [...previous, ...files] : files;
1823
+ }, DEFAULT_PAKE_OPTIONS.inject)
1824
+ .option('--debug', 'Debug build and more output', DEFAULT_PAKE_OPTIONS.debug)
1825
+ .addOption(new Option('--proxy-url <url>', 'Proxy URL for all network requests (http://, https://, socks5://)')
1826
+ .default(DEFAULT_PAKE_OPTIONS.proxyUrl)
1827
+ .hideHelp())
1828
+ .addOption(new Option('--user-agent <string>', 'Custom user agent')
1829
+ .default(DEFAULT_PAKE_OPTIONS.userAgent)
1830
+ .hideHelp())
1831
+ .addOption(new Option('--targets <string>', 'Build target format for your system').default(DEFAULT_PAKE_OPTIONS.targets))
1832
+ .addOption(new Option('--app-version <string>', 'App version, the same as package.json version')
1833
+ .default(DEFAULT_PAKE_OPTIONS.appVersion)
1834
+ .hideHelp())
1835
+ .addOption(new Option('--always-on-top', 'Always on the top level')
1836
+ .default(DEFAULT_PAKE_OPTIONS.alwaysOnTop)
1837
+ .hideHelp())
1838
+ .addOption(new Option('--maximize', 'Start window maximized')
1839
+ .default(DEFAULT_PAKE_OPTIONS.maximize)
1840
+ .hideHelp())
1841
+ .addOption(new Option('--dark-mode', 'Force Mac app to use dark mode')
1842
+ .default(DEFAULT_PAKE_OPTIONS.darkMode)
1843
+ .hideHelp())
1844
+ .addOption(new Option('--disabled-web-shortcuts', 'Disabled webPage shortcuts')
1845
+ .default(DEFAULT_PAKE_OPTIONS.disabledWebShortcuts)
1846
+ .hideHelp())
1847
+ .addOption(new Option('--activation-shortcut <string>', 'Shortcut key to active App')
1848
+ .default(DEFAULT_PAKE_OPTIONS.activationShortcut)
1849
+ .hideHelp())
1850
+ .addOption(new Option('--show-system-tray', 'Show system tray in app')
1851
+ .default(DEFAULT_PAKE_OPTIONS.showSystemTray)
1852
+ .hideHelp())
1853
+ .addOption(new Option('--system-tray-icon <string>', 'Custom system tray icon')
1854
+ .default(DEFAULT_PAKE_OPTIONS.systemTrayIcon)
1855
+ .hideHelp())
1856
+ .addOption(new Option('--hide-on-close [boolean]', 'Hide window on close instead of exiting (default: true for macOS, false for others)')
1857
+ .default(DEFAULT_PAKE_OPTIONS.hideOnClose)
1858
+ .argParser((value) => {
1859
+ if (value === undefined)
1860
+ return true; // --hide-on-close without value
1861
+ if (value === 'true')
1862
+ return true;
1863
+ if (value === 'false')
1864
+ return false;
1865
+ throw new Error('--hide-on-close must be true or false');
1866
+ })
1867
+ .hideHelp())
1868
+ .addOption(new Option('--title <string>', 'Window title').hideHelp())
1869
+ .addOption(new Option('--incognito', 'Launch app in incognito/private mode')
1870
+ .default(DEFAULT_PAKE_OPTIONS.incognito)
1871
+ .hideHelp())
1872
+ .addOption(new Option('--wasm', 'Enable WebAssembly support (Flutter Web, etc.)')
1873
+ .default(DEFAULT_PAKE_OPTIONS.wasm)
1874
+ .hideHelp())
1875
+ .addOption(new Option('--enable-drag-drop', 'Enable drag and drop functionality')
1876
+ .default(DEFAULT_PAKE_OPTIONS.enableDragDrop)
1877
+ .hideHelp())
1878
+ .addOption(new Option('--keep-binary', 'Keep raw binary file alongside installer')
1879
+ .default(DEFAULT_PAKE_OPTIONS.keepBinary)
1880
+ .hideHelp())
1881
+ .addOption(new Option('--multi-instance', 'Allow multiple app instances')
1882
+ .default(DEFAULT_PAKE_OPTIONS.multiInstance)
1883
+ .hideHelp())
1884
+ .addOption(new Option('--start-to-tray', 'Start app minimized to tray')
1885
+ .default(DEFAULT_PAKE_OPTIONS.startToTray)
1886
+ .hideHelp())
1887
+ .addOption(new Option('--force-internal-navigation', 'Keep every link inside the Pake window instead of opening external handlers')
1888
+ .default(DEFAULT_PAKE_OPTIONS.forceInternalNavigation)
1889
+ .hideHelp())
1890
+ .addOption(new Option('--installer-language <string>', 'Installer language')
1891
+ .default(DEFAULT_PAKE_OPTIONS.installerLanguage)
1892
+ .hideHelp())
1893
+ .addOption(new Option('--zoom <number>', 'Initial page zoom level (50-200)')
1894
+ .default(DEFAULT_PAKE_OPTIONS.zoom)
1895
+ .argParser((value) => {
1896
+ const zoom = parseInt(value);
1897
+ if (isNaN(zoom) || zoom < 50 || zoom > 200) {
1898
+ throw new Error('--zoom must be a number between 50 and 200');
1899
+ }
1900
+ return zoom;
1901
+ })
1902
+ .hideHelp())
1903
+ .addOption(new Option('--min-width <number>', 'Minimum window width')
1904
+ .default(DEFAULT_PAKE_OPTIONS.minWidth)
1905
+ .argParser(validateNumberInput)
1906
+ .hideHelp())
1907
+ .addOption(new Option('--min-height <number>', 'Minimum window height')
1908
+ .default(DEFAULT_PAKE_OPTIONS.minHeight)
1909
+ .argParser(validateNumberInput)
1910
+ .hideHelp())
1911
+ .addOption(new Option('--ignore-certificate-errors', 'Ignore certificate errors (for self-signed certificates)')
1912
+ .default(DEFAULT_PAKE_OPTIONS.ignoreCertificateErrors)
1913
+ .hideHelp())
1914
+ .addOption(new Option('--iterative-build', 'Turn on rapid build mode (app only, no dmg/deb/msi), good for debugging')
1915
+ .default(DEFAULT_PAKE_OPTIONS.iterativeBuild)
1916
+ .hideHelp())
1917
+ .version(packageJson.version, '-v, --version')
1918
+ .configureHelp({
1919
+ sortSubcommands: true,
1920
+ optionTerm: (option) => {
1921
+ if (option.flags === '-v, --version' || option.flags === '-h, --help')
1922
+ return '';
1923
+ return option.flags;
1924
+ },
1925
+ optionDescription: (option) => {
1926
+ if (option.flags === '-v, --version' || option.flags === '-h, --help')
1927
+ return '';
1928
+ return option.description;
1929
+ },
1930
+ });
1931
+ }
1932
+
1933
+ const program = getCliProgram();
1934
+ program.action(async (url, options) => {
1050
1935
  log.setDefaultLevel('debug');
1051
- const appOptions = await handleOptions(DEFAULT_DEV_PAKE_OPTIONS, DEFAULT_DEV_PAKE_OPTIONS.url);
1936
+ const appOptions = await handleOptions(options, url);
1052
1937
  log.debug('PakeAppOptions', appOptions);
1053
1938
  const builder = BuilderProvider.create(appOptions);
1054
1939
  await builder.prepare();
1055
- await builder.start(DEFAULT_DEV_PAKE_OPTIONS.url);
1056
- }
1057
- startBuild();
1940
+ await builder.start(url);
1941
+ });
1942
+ program.parse();
1058
1943
  //# sourceMappingURL=dev.js.map