pake-cli 3.6.2 → 3.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/dev.js DELETED
@@ -1,1943 +0,0 @@
1
- import log from 'loglevel';
2
- import fsExtra from 'fs-extra';
3
- import chalk from 'chalk';
4
- import path from 'path';
5
- import axios from 'axios';
6
- import { dir } from 'tmp-promise';
7
- import { fileTypeFromBuffer } from 'file-type';
8
- import icongen from 'icon-gen';
9
- import sharp from 'sharp';
10
- import crypto from 'crypto';
11
- import prompts from 'prompts';
12
- import ora from 'ora';
13
- import { fileURLToPath } from 'url';
14
- import * as psl from 'psl';
15
- import os from 'os';
16
- import { execa, execaSync } from 'execa';
17
- import dns from 'dns';
18
- import http from 'http';
19
- import { promisify } from 'util';
20
- import fs from 'fs';
21
- import { InvalidArgumentError, program as program$1, Option } from 'commander';
22
-
23
- const logger = {
24
- info(...msg) {
25
- log.info(...msg.map((m) => chalk.white(m)));
26
- },
27
- debug(...msg) {
28
- log.debug(...msg);
29
- },
30
- error(...msg) {
31
- log.error(...msg.map((m) => chalk.red(m)));
32
- },
33
- warn(...msg) {
34
- log.info(...msg.map((m) => chalk.yellow(m)));
35
- },
36
- success(...msg) {
37
- log.info(...msg.map((m) => chalk.green(m)));
38
- },
39
- };
40
-
41
- // Generates an identifier based on the given URL.
42
- function getIdentifier(url) {
43
- const postFixHash = crypto
44
- .createHash('md5')
45
- .update(url)
46
- .digest('hex')
47
- .substring(0, 6);
48
- return `com.pake.${postFixHash}`;
49
- }
50
- async function promptText(message, initial) {
51
- const response = await prompts({
52
- type: 'text',
53
- name: 'content',
54
- message,
55
- initial,
56
- });
57
- return response.content;
58
- }
59
- function capitalizeFirstLetter(string) {
60
- return string.charAt(0).toUpperCase() + string.slice(1);
61
- }
62
- function getSpinner(text) {
63
- const loadingType = {
64
- interval: 80,
65
- frames: ['✦', '✶', '✺', '✵', '✸', '✹', '✺'],
66
- };
67
- return ora({
68
- text: `${chalk.cyan(text)}\n`,
69
- spinner: loadingType,
70
- color: 'cyan',
71
- }).start();
72
- }
73
-
74
- // Convert the current module URL to a file path
75
- const currentModulePath = fileURLToPath(import.meta.url);
76
- // Resolve the parent directory of the current module
77
- const npmDirectory = path.join(path.dirname(currentModulePath), '..');
78
- const tauriConfigDirectory = path.join(npmDirectory, 'src-tauri', '.pake');
79
-
80
- const { platform: platform$2 } = process;
81
- const IS_MAC = platform$2 === 'darwin';
82
- const IS_WIN = platform$2 === 'win32';
83
- const IS_LINUX = platform$2 === 'linux';
84
-
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
-
122
- const ICON_CONFIG = {
123
- minFileSize: 100,
124
- supportedFormats: ['png', 'ico', 'jpeg', 'jpg', 'webp', 'icns'],
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
- },
131
- };
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 = {
138
- logoDev: ['pk_JLLMUKGZRpaG5YclhXaTkg', 'pk_Ph745P8mQSeYFfW2Wk039A'],
139
- brandfetch: ['1idqvJC0CeFSeyp3Yf7', '1idej-yhU_ThggIHFyG'],
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
- }
169
- /**
170
- * Adds white background to transparent icons only
171
- */
172
- async function preprocessIcon(inputPath) {
173
- try {
174
- const metadata = await sharp(inputPath).metadata();
175
- if (metadata.channels !== 4)
176
- return inputPath; // No transparency
177
- const { path: tempDir } = await dir();
178
- const outputPath = path.join(tempDir, 'icon-with-background.png');
179
- await sharp({
180
- create: {
181
- width: metadata.width || 512,
182
- height: metadata.height || 512,
183
- channels: 4,
184
- background: { ...ICON_CONFIG.whiteBackground, alpha: 1 },
185
- },
186
- })
187
- .composite([{ input: inputPath }])
188
- .png()
189
- .toFile(outputPath);
190
- return outputPath;
191
- }
192
- catch (error) {
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
- }
244
- return inputPath;
245
- }
246
- }
247
- /**
248
- * Converts icon to platform-specific format
249
- */
250
- async function convertIconFormat(inputPath, appName) {
251
- try {
252
- if (!(await fsExtra.pathExists(inputPath)))
253
- return null;
254
- const { path: outputDir } = await dir();
255
- const platformOutputDir = path.join(outputDir, 'converted-icons');
256
- await fsExtra.ensureDir(platformOutputDir);
257
- const processedInputPath = await preprocessIcon(inputPath);
258
- const iconName = generateSafeFilename(appName).toLowerCase();
259
- // Generate platform-specific format
260
- if (IS_WIN) {
261
- // Support multiple sizes for better Windows compatibility
262
- await icongen(processedInputPath, platformOutputDir, {
263
- report: false,
264
- ico: {
265
- name: `${iconName}_256`,
266
- sizes: PLATFORM_CONFIG.win.sizes,
267
- },
268
- });
269
- return path.join(platformOutputDir, `${iconName}_256${PLATFORM_CONFIG.win.format}`);
270
- }
271
- if (IS_LINUX) {
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);
282
- return outputPath;
283
- }
284
- // macOS
285
- const macIconPath = await applyMacOSMask(processedInputPath);
286
- await icongen(macIconPath, platformOutputDir, {
287
- report: false,
288
- icns: { name: iconName, sizes: PLATFORM_CONFIG.macos.sizes },
289
- });
290
- const outputPath = path.join(platformOutputDir, `${iconName}${PLATFORM_CONFIG.macos.format}`);
291
- return (await fsExtra.pathExists(outputPath)) ? outputPath : null;
292
- }
293
- catch (error) {
294
- if (error instanceof Error) {
295
- logger.warn(`Icon format conversion failed: ${error.message}`);
296
- }
297
- return null;
298
- }
299
- }
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);
313
- }
314
- // Convert to platform format
315
- const convertedPath = await convertIconFormat(iconPath, appName);
316
- if (convertedPath) {
317
- return await copyWindowsIconIfNeeded(convertedPath, appName);
318
- }
319
- return iconPath;
320
- }
321
- /**
322
- * Gets default icon with platform-specific fallback logic
323
- */
324
- async function getDefaultIcon() {
325
- logger.info('✼ No icon provided, using default icon.');
326
- if (IS_WIN) {
327
- const defaultIcoPath = generateIconPath('icon', true);
328
- const defaultPngPath = path.join(npmDirectory, 'src-tauri/png/icon_512.png');
329
- // Try default ico first
330
- if (await fsExtra.pathExists(defaultIcoPath)) {
331
- return defaultIcoPath;
332
- }
333
- // Convert from png if ico doesn't exist
334
- if (await fsExtra.pathExists(defaultPngPath)) {
335
- logger.info('✼ Default ico not found, converting from png...');
336
- try {
337
- const convertedPath = await convertIconFormat(defaultPngPath, 'icon');
338
- if (convertedPath && (await fsExtra.pathExists(convertedPath))) {
339
- return await copyWindowsIconIfNeeded(convertedPath, 'icon');
340
- }
341
- }
342
- catch (error) {
343
- logger.warn(`Failed to convert default png to ico: ${error instanceof Error ? error.message : 'Unknown error'}`);
344
- }
345
- }
346
- // Fallback to png or empty
347
- if (await fsExtra.pathExists(defaultPngPath)) {
348
- logger.warn('✼ Using png as fallback for Windows (may cause issues).');
349
- return defaultPngPath;
350
- }
351
- logger.warn('✼ No default icon found, will use pake default.');
352
- return '';
353
- }
354
- // Linux and macOS defaults
355
- const iconPath = IS_LINUX
356
- ? 'src-tauri/png/icon_512.png'
357
- : 'src-tauri/icons/icon.icns';
358
- return path.join(npmDirectory, iconPath);
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
- }
389
- /**
390
- * Generates icon service URLs for a domain
391
- */
392
- function generateIconServiceUrls(domain) {
393
- const logoDevUrls = API_KEYS.logoDev
394
- .sort(() => Math.random() - 0.5)
395
- .map((token) => `https://img.logo.dev/${domain}?token=${token}&format=png&size=256`);
396
- const brandfetchUrls = API_KEYS.brandfetch
397
- .sort(() => Math.random() - 0.5)
398
- .map((key) => `https://cdn.brandfetch.io/${domain}/w/400/h/400?c=${key}`);
399
- return [
400
- ...logoDevUrls,
401
- ...brandfetchUrls,
402
- `https://logo.clearbit.com/${domain}?size=256`,
403
- `https://www.google.com/s2/favicons?domain=${domain}&sz=256`,
404
- `https://favicon.is/${domain}`,
405
- `https://${domain}/favicon.ico`,
406
- `https://www.${domain}/favicon.ico`,
407
- ];
408
- }
409
- /**
410
- * Attempts to fetch favicon from website
411
- */
412
- async function tryGetFavicon(url, appName) {
413
- try {
414
- const domain = new URL(url).hostname;
415
- const spinner = getSpinner(`Fetching icon from ${domain}...`);
416
- const serviceUrls = generateIconServiceUrls(domain);
417
- const isCI = process.env.CI === 'true' || process.env.GITHUB_ACTIONS === 'true';
418
- const downloadTimeout = isCI
419
- ? ICON_CONFIG.downloadTimeout.ci
420
- : ICON_CONFIG.downloadTimeout.default;
421
- for (const serviceUrl of serviceUrls) {
422
- try {
423
- const faviconPath = await downloadIcon(serviceUrl, false, downloadTimeout);
424
- if (!faviconPath)
425
- continue;
426
- const convertedPath = await convertIconFormat(faviconPath, appName);
427
- if (convertedPath) {
428
- const finalPath = await copyWindowsIconIfNeeded(convertedPath, appName);
429
- spinner.succeed(chalk.green('Icon fetched and converted successfully!'));
430
- return finalPath;
431
- }
432
- }
433
- catch (error) {
434
- if (error instanceof Error) {
435
- logger.debug(`Icon service ${serviceUrl} failed: ${error.message}`);
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
- }
445
- continue;
446
- }
447
- }
448
- spinner.warn(`No favicon found for ${domain}. Using default.`);
449
- return null;
450
- }
451
- catch (error) {
452
- if (error instanceof Error) {
453
- logger.warn(`Failed to fetch favicon: ${error.message}`);
454
- }
455
- return null;
456
- }
457
- }
458
- /**
459
- * Downloads icon from URL
460
- */
461
- async function downloadIcon(iconUrl, showSpinner = true, customTimeout) {
462
- try {
463
- const response = await axios.get(iconUrl, {
464
- responseType: 'arraybuffer',
465
- timeout: customTimeout || 10000,
466
- });
467
- const iconData = response.data;
468
- if (!iconData || iconData.byteLength < ICON_CONFIG.minFileSize)
469
- return null;
470
- const fileDetails = await fileTypeFromBuffer(iconData);
471
- if (!fileDetails ||
472
- !ICON_CONFIG.supportedFormats.includes(fileDetails.ext)) {
473
- return null;
474
- }
475
- return await saveIconFile(iconData, fileDetails.ext);
476
- }
477
- catch (error) {
478
- if (showSpinner && !(error.response?.status === 404)) {
479
- logger.error('Icon download failed!');
480
- }
481
- return null;
482
- }
483
- }
484
- /**
485
- * Saves icon file to temporary location
486
- */
487
- async function saveIconFile(iconData, extension) {
488
- const buffer = Buffer.from(iconData);
489
- const { path: tempPath } = await dir();
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;
494
- }
495
-
496
- // Extracts the domain from a given URL.
497
- function getDomain(inputUrl) {
498
- try {
499
- const url = new URL(inputUrl);
500
- // Use PSL to parse domain names.
501
- const parsed = psl.parse(url.hostname);
502
- // If domain is available, split it and return the SLD.
503
- if ('domain' in parsed && parsed.domain) {
504
- return parsed.domain.split('.')[0];
505
- }
506
- else {
507
- return null;
508
- }
509
- }
510
- catch (error) {
511
- return null;
512
- }
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
- }
535
-
536
- function resolveAppName(name, platform) {
537
- const domain = getDomain(name) || 'pake';
538
- return platform !== 'linux' ? capitalizeFirstLetter(domain) : domain;
539
- }
540
- function isValidName(name, platform) {
541
- const platformRegexMapping = {
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- ]*$/,
544
- };
545
- const reg = platformRegexMapping[platform] || platformRegexMapping.default;
546
- return !!name && reg.test(name);
547
- }
548
- async function handleOptions(options, url) {
549
- const { platform } = process;
550
- const isActions = process.env.GITHUB_ACTIONS;
551
- let name = options.name;
552
- const pathExists = await fsExtra.pathExists(url);
553
- if (!options.name) {
554
- const defaultName = pathExists ? '' : resolveAppName(url, platform);
555
- const promptMessage = 'Enter your application name';
556
- const namePrompt = await promptText(promptMessage, defaultName);
557
- name = namePrompt || defaultName;
558
- }
559
- if (name && platform === 'linux') {
560
- name = generateLinuxPackageName(name);
561
- }
562
- if (name && !isValidName(name, platform)) {
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.`;
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.`;
565
- const errorMsg = platform === 'linux' ? LINUX_NAME_ERROR : DEFAULT_NAME_ERROR;
566
- logger.error(errorMsg);
567
- if (isActions) {
568
- name = resolveAppName(url, platform);
569
- logger.warn(`✼ Inside github actions, use the default name: ${name}`);
570
- }
571
- else {
572
- process.exit(1);
573
- }
574
- }
575
- const appOptions = {
576
- ...options,
577
- name,
578
- identifier: getIdentifier(url),
579
- };
580
- const iconPath = await handleIcon(appOptions, url);
581
- appOptions.icon = iconPath || '';
582
- return appOptions;
583
- }
584
-
585
- // Load configs from npm package directory, not from project source
586
- const tauriSrcDir = path.join(npmDirectory, 'src-tauri');
587
- const pakeConf = fsExtra.readJSONSync(path.join(tauriSrcDir, 'pake.json'));
588
- const CommonConf = fsExtra.readJSONSync(path.join(tauriSrcDir, 'tauri.conf.json'));
589
- const WinConf = fsExtra.readJSONSync(path.join(tauriSrcDir, 'tauri.windows.conf.json'));
590
- const MacConf = fsExtra.readJSONSync(path.join(tauriSrcDir, 'tauri.macos.conf.json'));
591
- const LinuxConf = fsExtra.readJSONSync(path.join(tauriSrcDir, 'tauri.linux.conf.json'));
592
- const platformConfigs = {
593
- win32: WinConf,
594
- darwin: MacConf,
595
- linux: LinuxConf,
596
- };
597
- const { platform: platform$1 } = process;
598
- // @ts-ignore
599
- const platformConfig = platformConfigs[platform$1];
600
- let tauriConfig = {
601
- ...CommonConf,
602
- bundle: platformConfig.bundle,
603
- app: {
604
- ...CommonConf.app,
605
- trayIcon: {
606
- ...(platformConfig?.app?.trayIcon ?? {}),
607
- },
608
- },
609
- build: CommonConf.build,
610
- pake: pakeConf,
611
- };
612
-
613
- async function shellExec(command, timeout = 300000, env) {
614
- try {
615
- const { exitCode } = await execa(command, {
616
- cwd: npmDirectory,
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',
620
- shell: true,
621
- timeout,
622
- env: env ? { ...process.env, ...env } : process.env,
623
- });
624
- return exitCode;
625
- }
626
- catch (error) {
627
- const exitCode = error.exitCode ?? 'unknown';
628
- const errorMessage = error.message || 'Unknown error occurred';
629
- if (error.timedOut) {
630
- throw new Error(`Command timed out after ${timeout}ms: "${command}". Try increasing timeout or check network connectivity.`);
631
- }
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);
664
- }
665
- }
666
-
667
- const resolve = promisify(dns.resolve);
668
- const ping = async (host) => {
669
- const lookup = promisify(dns.lookup);
670
- const ip = await lookup(host);
671
- const start = new Date();
672
- // Prevent timeouts from affecting user experience.
673
- const requestPromise = new Promise((resolve, reject) => {
674
- const req = http.get(`http://${ip.address}`, (res) => {
675
- const delay = new Date().getTime() - start.getTime();
676
- res.resume();
677
- resolve(delay);
678
- });
679
- req.on('error', (err) => {
680
- reject(err);
681
- });
682
- });
683
- const timeoutPromise = new Promise((_, reject) => {
684
- setTimeout(() => {
685
- reject(new Error('Request timed out after 3 seconds'));
686
- }, 1000);
687
- });
688
- return Promise.race([requestPromise, timeoutPromise]);
689
- };
690
- async function isChinaDomain(domain) {
691
- try {
692
- const [ip] = await resolve(domain);
693
- return await isChinaIP(ip, domain);
694
- }
695
- catch (error) {
696
- logger.debug(`${domain} can't be parse!`);
697
- return true;
698
- }
699
- }
700
- async function isChinaIP(ip, domain) {
701
- try {
702
- const delay = await ping(ip);
703
- logger.debug(`${domain} latency is ${delay} ms`);
704
- return delay > 1000;
705
- }
706
- catch (error) {
707
- logger.debug(`ping ${domain} failed!`);
708
- return true;
709
- }
710
- }
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
- }
758
- async function installRust() {
759
- const isActions = process.env.GITHUB_ACTIONS;
760
- const isInChina = await isChinaDomain('sh.rustup.rs');
761
- const rustInstallScriptForMac = isInChina && !isActions
762
- ? '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'
763
- : "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y";
764
- const rustInstallScriptForWindows = 'winget install --id Rustlang.Rustup';
765
- const spinner = getSpinner('Downloading Rust...');
766
- try {
767
- await shellExec(IS_WIN ? rustInstallScriptForWindows : rustInstallScriptForMac, 300000, undefined);
768
- spinner.succeed(chalk.green('✔ Rust installed successfully!'));
769
- ensureRustEnv();
770
- }
771
- catch (error) {
772
- spinner.fail(chalk.red('✕ Rust installation failed!'));
773
- if (error instanceof Error) {
774
- console.error(error.message);
775
- }
776
- else {
777
- console.error(error);
778
- }
779
- process.exit(1);
780
- }
781
- }
782
- function checkRustInstalled() {
783
- ensureCargoBinOnPath();
784
- try {
785
- execaSync('rustc', ['--version']);
786
- return true;
787
- }
788
- catch {
789
- return false;
790
- }
791
- }
792
-
793
- async function combineFiles(files, output) {
794
- const contents = files.map((file) => {
795
- if (file.endsWith('.css')) {
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
- });`;
803
- }
804
- const fileContent = fs.readFileSync(file);
805
- return ("window.addEventListener('DOMContentLoaded', (_event) => { " +
806
- fileContent +
807
- ' });');
808
- });
809
- fs.writeFileSync(output, contents.join('\n'));
810
- return files;
811
- }
812
-
813
- async function mergeConfig(url, options, tauriConf) {
814
- // Ensure .pake directory exists and copy source templates if needed
815
- const srcTauriDir = path.join(npmDirectory, 'src-tauri');
816
- await fsExtra.ensureDir(tauriConfigDirectory);
817
- // Copy source config files to .pake directory (as templates)
818
- const sourceFiles = [
819
- 'tauri.conf.json',
820
- 'tauri.macos.conf.json',
821
- 'tauri.windows.conf.json',
822
- 'tauri.linux.conf.json',
823
- 'pake.json',
824
- ];
825
- await Promise.all(sourceFiles.map(async (file) => {
826
- const sourcePath = path.join(srcTauriDir, file);
827
- const destPath = path.join(tauriConfigDirectory, file);
828
- if ((await fsExtra.pathExists(sourcePath)) &&
829
- !(await fsExtra.pathExists(destPath))) {
830
- await fsExtra.copy(sourcePath, destPath);
831
- }
832
- }));
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;
834
- const { platform } = process;
835
- const platformHideOnClose = hideOnClose ?? platform === 'darwin';
836
- const tauriConfWindowOptions = {
837
- width,
838
- height,
839
- fullscreen,
840
- maximize,
841
- resizable,
842
- hide_title_bar: hideTitleBar,
843
- activation_shortcut: activationShortcut,
844
- always_on_top: alwaysOnTop,
845
- dark_mode: darkMode,
846
- disabled_web_shortcuts: disabledWebShortcuts,
847
- hide_on_close: platformHideOnClose,
848
- incognito: incognito,
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,
858
- };
859
- Object.assign(tauriConf.pake.windows[0], { url, ...tauriConfWindowOptions });
860
- tauriConf.productName = name;
861
- tauriConf.identifier = identifier;
862
- tauriConf.version = appVersion;
863
- if (platform === 'linux') {
864
- tauriConf.mainBinaryName = `pake-${generateIdentifierSafeName(name)}`;
865
- }
866
- if (platform == 'win32') {
867
- tauriConf.bundle.windows.wix.language[0] = installerLanguage;
868
- }
869
- const pathExists = await fsExtra.pathExists(url);
870
- if (pathExists) {
871
- logger.warn('✼ Your input might be a local file.');
872
- tauriConf.pake.windows[0].url_type = 'local';
873
- const fileName = path.basename(url);
874
- const dirName = path.dirname(url);
875
- const distDir = path.join(npmDirectory, 'dist');
876
- const distBakDir = path.join(npmDirectory, 'dist_bak');
877
- if (!useLocalFile) {
878
- const urlPath = path.join(distDir, fileName);
879
- await fsExtra.copy(url, urlPath);
880
- }
881
- else {
882
- fsExtra.moveSync(distDir, distBakDir, { overwrite: true });
883
- fsExtra.copySync(dirName, distDir, { overwrite: true });
884
- // ignore it, because about_pake.html have be erased.
885
- // const filesToCopyBack = ['cli.js', 'about_pake.html'];
886
- const filesToCopyBack = ['cli.js'];
887
- await Promise.all(filesToCopyBack.map((file) => fsExtra.copy(path.join(distBakDir, file), path.join(distDir, file))));
888
- }
889
- tauriConf.pake.windows[0].url = fileName;
890
- tauriConf.pake.windows[0].url_type = 'local';
891
- }
892
- else {
893
- tauriConf.pake.windows[0].url_type = 'web';
894
- }
895
- const platformMap = {
896
- win32: 'windows',
897
- linux: 'linux',
898
- darwin: 'macos',
899
- };
900
- const currentPlatform = platformMap[platform];
901
- if (userAgent.length > 0) {
902
- tauriConf.pake.user_agent[currentPlatform] = userAgent;
903
- }
904
- tauriConf.pake.system_tray[currentPlatform] = showSystemTray;
905
- // Processing targets are currently only open to Linux.
906
- if (platform === 'linux') {
907
- // Remove hardcoded desktop files and regenerate with correct app name
908
- delete tauriConf.bundle.linux.deb.files;
909
- // Generate correct desktop file configuration
910
- const appNameSafe = getSafeAppName(name);
911
- const identifier = `com.pake.${appNameSafe}`;
912
- const desktopFileName = `${identifier}.desktop`;
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;
916
- const desktopContent = `[Desktop Entry]
917
- Version=1.0
918
- Type=Application
919
- Name=${name}
920
- ${chineseName ? `Name[zh_CN]=${chineseName}` : ''}
921
- Comment=${name}
922
- Exec=pake-${appNameSafe}
923
- Icon=${appNameSafe}_512
924
- Categories=Network;WebBrowser;
925
- MimeType=text/html;text/xml;application/xhtml_xml;
926
- StartupNotify=true
927
- `;
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);
933
- // Set up desktop file in bundle configuration
934
- // Use absolute path from src-tauri directory to assets
935
- tauriConf.bundle.linux.deb.files = {
936
- [`/usr/share/applications/${desktopFileName}`]: `assets/${desktopFileName}`,
937
- };
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;
949
- if (validTargets.includes(options.targets)) {
950
- tauriConf.bundle.targets = [baseTarget];
951
- }
952
- else {
953
- logger.warn(`✼ The target must be one of ${validTargets.join(', ')}, the default 'deb' will be used.`);
954
- }
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
- }
963
- // Set icon.
964
- const safeAppName = getSafeAppName(name);
965
- const platformIconMap = {
966
- win32: {
967
- fileExt: '.ico',
968
- path: `png/${safeAppName}_256.ico`,
969
- defaultIcon: 'png/icon_256.ico',
970
- message: 'Windows icon must be .ico and 256x256px.',
971
- },
972
- linux: {
973
- fileExt: '.png',
974
- path: `png/${safeAppName}_512.png`,
975
- defaultIcon: 'png/icon_512.png',
976
- message: 'Linux icon must be .png and 512x512px.',
977
- },
978
- darwin: {
979
- fileExt: '.icns',
980
- path: `icons/${safeAppName}.icns`,
981
- defaultIcon: 'icons/icon.icns',
982
- message: 'macOS icon must be .icns type.',
983
- },
984
- };
985
- const iconInfo = platformIconMap[platform];
986
- const resolvedIconPath = options.icon ? path.resolve(options.icon) : null;
987
- const exists = resolvedIconPath && (await fsExtra.pathExists(resolvedIconPath));
988
- if (exists) {
989
- let updateIconPath = true;
990
- let customIconExt = path.extname(resolvedIconPath).toLowerCase();
991
- if (customIconExt !== iconInfo.fileExt) {
992
- updateIconPath = false;
993
- logger.warn(`✼ ${iconInfo.message}, but you give ${customIconExt}`);
994
- tauriConf.bundle.icon = [iconInfo.defaultIcon];
995
- }
996
- else {
997
- const iconPath = path.join(npmDirectory, 'src-tauri/', iconInfo.path);
998
- tauriConf.bundle.resources = [iconInfo.path];
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
- }
1004
- }
1005
- if (updateIconPath) {
1006
- tauriConf.bundle.icon = [iconInfo.path];
1007
- }
1008
- else {
1009
- logger.warn(`✼ Icon will remain as default.`);
1010
- }
1011
- }
1012
- else {
1013
- logger.warn('✼ Custom icon path may be invalid, default icon will be used instead.');
1014
- tauriConf.bundle.icon = [iconInfo.defaultIcon];
1015
- }
1016
- // Set tray icon path.
1017
- let trayIconPath = platform === 'darwin' ? 'png/icon_512.png' : tauriConf.bundle.icon[0];
1018
- if (systemTrayIcon.length > 0) {
1019
- try {
1020
- await fsExtra.pathExists(systemTrayIcon);
1021
- // 需要判断图标格式,默认只支持ico和png两种
1022
- let iconExt = path.extname(systemTrayIcon).toLowerCase();
1023
- if (iconExt == '.png' || iconExt == '.ico') {
1024
- const trayIcoPath = path.join(npmDirectory, `src-tauri/png/${safeAppName}${iconExt}`);
1025
- trayIconPath = `png/${safeAppName}${iconExt}`;
1026
- await fsExtra.copy(systemTrayIcon, trayIcoPath);
1027
- }
1028
- else {
1029
- logger.warn(`✼ System tray icon must be .ico or .png, but you provided ${iconExt}.`);
1030
- logger.warn(`✼ Default system tray icon will be used.`);
1031
- }
1032
- }
1033
- catch {
1034
- logger.warn(`✼ ${systemTrayIcon} not exists!`);
1035
- logger.warn(`✼ Default system tray icon will remain unchanged.`);
1036
- }
1037
- }
1038
- tauriConf.app.trayIcon.iconPath = trayIconPath;
1039
- tauriConf.pake.system_tray_path = trayIconPath;
1040
- delete tauriConf.app.trayIcon;
1041
- const injectFilePath = path.join(npmDirectory, `src-tauri/src/inject/custom.js`);
1042
- // inject js or css files
1043
- if (inject?.length > 0) {
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'))) {
1047
- logger.error('The injected file must be in either CSS or JS format.');
1048
- return;
1049
- }
1050
- const files = injectArray.map((filepath) => path.isAbsolute(filepath) ? filepath : path.join(process.cwd(), filepath));
1051
- tauriConf.pake.inject = files;
1052
- await combineFiles(files, injectFilePath);
1053
- }
1054
- else {
1055
- tauriConf.pake.inject = [];
1056
- await fsExtra.writeFile(injectFilePath, '');
1057
- }
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
- }
1069
- // Save config file.
1070
- const platformConfigPaths = {
1071
- win32: 'tauri.windows.conf.json',
1072
- darwin: 'tauri.macos.conf.json',
1073
- linux: 'tauri.linux.conf.json',
1074
- };
1075
- const configPath = path.join(tauriConfigDirectory, platformConfigPaths[platform]);
1076
- const bundleConf = { bundle: tauriConf.bundle };
1077
- await fsExtra.outputJSON(configPath, bundleConf, { spaces: 4 });
1078
- const pakeConfigPath = path.join(tauriConfigDirectory, 'pake.json');
1079
- await fsExtra.outputJSON(pakeConfigPath, tauriConf.pake, { spaces: 4 });
1080
- let tauriConf2 = JSON.parse(JSON.stringify(tauriConf));
1081
- delete tauriConf2.pake;
1082
- // delete tauriConf2.bundle;
1083
- {
1084
- tauriConf2.bundle = bundleConf.bundle;
1085
- }
1086
- const configJsonPath = path.join(tauriConfigDirectory, 'tauri.conf.json');
1087
- await fsExtra.outputJSON(configJsonPath, tauriConf2, { spaces: 4 });
1088
- }
1089
-
1090
- class BaseBuilder {
1091
- constructor(options) {
1092
- this.options = options;
1093
- }
1094
- getBuildEnvironment() {
1095
- return IS_MAC
1096
- ? {
1097
- CFLAGS: '-fno-modules',
1098
- CXXFLAGS: '-fno-modules',
1099
- MACOSX_DEPLOYMENT_TARGET: '14.0',
1100
- }
1101
- : undefined;
1102
- }
1103
- getInstallTimeout() {
1104
- // Windows needs more time due to native compilation and antivirus scanning
1105
- return process.platform === 'win32' ? 900000 : 600000;
1106
- }
1107
- getBuildTimeout() {
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
- }
1132
- }
1133
- async prepare() {
1134
- const tauriSrcPath = path.join(npmDirectory, 'src-tauri');
1135
- const tauriTargetPath = path.join(tauriSrcPath, 'target');
1136
- const tauriTargetPathExists = await fsExtra.pathExists(tauriTargetPath);
1137
- if (!IS_MAC && !tauriTargetPathExists) {
1138
- logger.warn('✼ The first use requires installing system dependencies.');
1139
- logger.warn('✼ See more in https://tauri.app/start/prerequisites/.');
1140
- }
1141
- ensureRustEnv();
1142
- if (!checkRustInstalled()) {
1143
- const res = await prompts({
1144
- type: 'confirm',
1145
- message: 'Rust not detected. Install now?',
1146
- name: 'value',
1147
- });
1148
- if (res.value) {
1149
- await installRust();
1150
- }
1151
- else {
1152
- logger.error('✕ Rust required to package your webapp.');
1153
- process.exit(0);
1154
- }
1155
- }
1156
- const isChina = await isChinaDomain('www.npmjs.com');
1157
- const spinner = getSpinner('Installing package...');
1158
- const rustProjectDir = path.join(tauriSrcPath, '.cargo');
1159
- const projectConf = path.join(rustProjectDir, 'config.toml');
1160
- await fsExtra.ensureDir(rustProjectDir);
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' : '';
1165
- const timeout = this.getInstallTimeout();
1166
- const buildEnv = this.getBuildEnvironment();
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)...');
1172
- }
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
- }
1210
- }
1211
- if (!tauriTargetPathExists) {
1212
- logger.warn('✼ The first packaging may be slow, please be patient and wait, it will be faster afterwards.');
1213
- }
1214
- }
1215
- async build(url) {
1216
- await this.buildAndCopy(url, this.options.targets);
1217
- }
1218
- async start(url) {
1219
- logger.info('Pake dev server starting...');
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);
1228
- }
1229
- async buildAndCopy(url, target) {
1230
- const { name = 'pake-app' } = this.options;
1231
- await mergeConfig(url, this.options, tauriConfig);
1232
- // Detect available package manager
1233
- const packageManager = await this.detectPackageManager();
1234
- // Build app
1235
- const buildSpinner = getSpinner('Building app...');
1236
- // Let spinner run for a moment so user can see it, then stop before package manager command
1237
- await new Promise((resolve) => setTimeout(resolve, 500));
1238
- buildSpinner.stop();
1239
- // Show static message to keep the status visible
1240
- logger.warn('✸ Building app...');
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
- }
1278
- // Copy app
1279
- const fileName = this.getFileName();
1280
- const fileType = this.getFileType(target);
1281
- const appPath = this.getBuildAppPath(npmDirectory, fileName, fileType);
1282
- const distPath = path.resolve(`${name}.${fileType}`);
1283
- await fsExtra.copy(appPath, distPath);
1284
- // Copy raw binary if requested
1285
- if (this.options.keepBinary) {
1286
- await this.copyRawBinary(npmDirectory, name);
1287
- }
1288
- await fsExtra.remove(appPath);
1289
- logger.success('✔ Build success!');
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
- }
1296
- }
1297
- getFileType(target) {
1298
- return target;
1299
- }
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) {
1340
- const baseCommand = this.options.debug
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}`;
1347
- }
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() {
1359
- const features = ['cli-build'];
1360
- // Add macos-proxy feature for modern macOS (Darwin 23+ = macOS 14+)
1361
- if (IS_MAC) {
1362
- const macOSVersion = this.getMacOSMajorVersion();
1363
- if (macOSVersion >= 23) {
1364
- features.push('macos-proxy');
1365
- }
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();
1379
- if (features.length > 0) {
1380
- fullCommand += ` --features ${features.join(',')}`;
1381
- }
1382
- return fullCommand;
1383
- }
1384
- getMacOSMajorVersion() {
1385
- try {
1386
- const os = require('os');
1387
- const release = os.release();
1388
- const majorVersion = parseInt(release.split('.')[0], 10);
1389
- return majorVersion;
1390
- }
1391
- catch (error) {
1392
- return 0; // Disable proxy feature if version detection fails
1393
- }
1394
- }
1395
- getBasePath() {
1396
- const basePath = this.options.debug ? 'debug' : 'release';
1397
- return `src-tauri/target/${basePath}/bundle/`;
1398
- }
1399
- getBuildAppPath(npmDirectory, fileName, fileType) {
1400
- // For app bundles on macOS, the directory is 'macos', not 'app'
1401
- const bundleDir = fileType.toLowerCase() === 'app' ? 'macos' : fileType.toLowerCase();
1402
- return path.join(npmDirectory, this.getBasePath(), bundleDir, `${fileName}.${fileType}`);
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
- }
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
- };
1489
-
1490
- class MacBuilder extends BaseBuilder {
1491
- constructor(options) {
1492
- super(options);
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';
1499
- }
1500
- else {
1501
- this.buildFormat = 'dmg';
1502
- }
1503
- this.options.targets = this.buildFormat;
1504
- }
1505
- getFileName() {
1506
- const { name = 'pake-app' } = this.options;
1507
- if (this.buildFormat === 'app') {
1508
- return name;
1509
- }
1510
- let arch;
1511
- if (this.buildArch === 'universal' || this.options.multiArch) {
1512
- arch = 'universal';
1513
- }
1514
- else if (this.buildArch === 'apple') {
1515
- arch = 'aarch64';
1516
- }
1517
- else if (this.buildArch === 'intel') {
1518
- arch = 'x64';
1519
- }
1520
- else {
1521
- arch = this.getArchDisplayName(this.resolveTargetArch(this.buildArch));
1522
- }
1523
- return `${name}_${tauriConfig.version}_${arch}`;
1524
- }
1525
- getActualArch() {
1526
- if (this.buildArch === 'universal' || this.options.multiArch) {
1527
- return 'universal';
1528
- }
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;
1550
- }
1551
- 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}`;
1564
- }
1565
- }
1566
-
1567
- class WinBuilder extends BaseBuilder {
1568
- constructor(options) {
1569
- super(options);
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;
1576
- }
1577
- getFileName() {
1578
- const { name } = this.options;
1579
- const language = tauriConfig.bundle.windows.wix.language[0];
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}`;
1607
- }
1608
- }
1609
-
1610
- class LinuxBuilder extends BaseBuilder {
1611
- constructor(options) {
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;
1623
- }
1624
- getFileName() {
1625
- const { name = 'pake-app', targets } = this.options;
1626
- const version = tauriConfig.version;
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
- }
1642
- }
1643
- if (targets === 'rpm') {
1644
- return `${name}-${version}-1.${arch}`;
1645
- }
1646
- return `${name}_${version}_${arch}`;
1647
- }
1648
- async build(url) {
1649
- const targetTypes = ['deb', 'appimage', 'rpm'];
1650
- for (const target of targetTypes) {
1651
- if (this.options.targets === target) {
1652
- await this.buildAndCopy(url, target);
1653
- }
1654
- }
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
- }
1683
- getFileType(target) {
1684
- if (target === 'appimage') {
1685
- return 'AppImage';
1686
- }
1687
- return super.getFileType(target);
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
- }
1699
- }
1700
-
1701
- const { platform } = process;
1702
- const buildersMap = {
1703
- darwin: MacBuilder,
1704
- win32: WinBuilder,
1705
- linux: LinuxBuilder,
1706
- };
1707
- class BuilderProvider {
1708
- static create(options) {
1709
- const Builder = buildersMap[platform];
1710
- if (!Builder) {
1711
- throw new Error('The current system is not supported!');
1712
- }
1713
- return new Builder(options);
1714
- }
1715
- }
1716
-
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) => {
1935
- log.setDefaultLevel('debug');
1936
- const appOptions = await handleOptions(options, url);
1937
- log.debug('PakeAppOptions', appOptions);
1938
- const builder = BuilderProvider.create(appOptions);
1939
- await builder.prepare();
1940
- await builder.start(url);
1941
- });
1942
- program.parse();
1943
- //# sourceMappingURL=dev.js.map