pake-cli 3.11.9 โ 3.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE-EXCEPTION +20 -0
- package/README.md +6 -1
- package/dist/cli.js +100 -30
- package/package.json +2 -1
- package/src-tauri/Cargo.lock +1 -1
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/src/inject/auth.js +20 -6
- package/src-tauri/src/inject/event.js +61 -13
- package/src-tauri/src/lib.rs +136 -8
- package/src-tauri/tauri.conf.json +1 -1
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Pake Output Exception
|
|
2
|
+
|
|
3
|
+
This is an exception to the GNU General Public License version 3 (GPLv3),
|
|
4
|
+
under which Pake is licensed (see the LICENSE file).
|
|
5
|
+
|
|
6
|
+
As an additional permission under section 7 of the GPLv3:
|
|
7
|
+
|
|
8
|
+
When you use Pake, or a build of Pake produced by the standard Pake build
|
|
9
|
+
and packaging process, to generate a target application ("Pake Output"),
|
|
10
|
+
the portions of Pake incorporated into that Pake Output do not by
|
|
11
|
+
themselves cause the Pake Output as a whole to be or become subject to the
|
|
12
|
+
GPLv3. You may distribute such Pake Output under license terms of your own
|
|
13
|
+
choosing, including proprietary terms.
|
|
14
|
+
|
|
15
|
+
This permission does NOT apply to Pake itself or to any modified version of
|
|
16
|
+
Pake's own source code: anyone who distributes Pake, or a work derived from
|
|
17
|
+
Pake's source code beyond the configuration and packaging performed by the
|
|
18
|
+
standard build process, remains fully bound by the GPLv3.
|
|
19
|
+
|
|
20
|
+
Copyright (c) 2024 Tw93 and the Pake contributors.
|
package/README.md
CHANGED
|
@@ -202,12 +202,17 @@ Pake's development can not be without these Hackers. They contributed a lot of c
|
|
|
202
202
|
|
|
203
203
|
## Support
|
|
204
204
|
|
|
205
|
+
- The most direct way to support me is getting [Mole for Mac](https://mole.fit), my paid Mac cleanup app.
|
|
205
206
|
- If Pake helped you, [share it](https://twitter.com/intent/tweet?url=https://github.com/tw93/Pake&text=Pake%20-%20Turn%20any%20webpage%20into%20a%20desktop%20app%20with%20one%20command.%20Nearly%2020x%20smaller%20than%20Electron%20packages,%20supports%20macOS%20Windows%20Linux) with friends or give it a star.
|
|
206
207
|
- Got ideas or bugs? Open an issue or PR, feel free to contribute your best AI model.
|
|
207
208
|
- I have two cats, TangYuan and Coke. If you think Pake delights your life, you can feed them <a href="https://cats.tw93.fun?name=Pake" target="_blank">canned food ๐ฅฉ</a>.
|
|
208
209
|
|
|
210
|
+
<details>
|
|
211
|
+
<summary>These lovely people already did ๐ฑ</summary>
|
|
212
|
+
<br/>
|
|
209
213
|
<a href="https://cats.tw93.fun?name=Pake"><img src="https://cdn.jsdelivr.net/gh/tw93/sponsors@main/assets/sponsors.svg" width="1000" loading="lazy" /></a>
|
|
214
|
+
</details>
|
|
210
215
|
|
|
211
216
|
## License
|
|
212
217
|
|
|
213
|
-
Pake is open source under GPL-3.0, see [LICENSE](./LICENSE); apps you build with Pake are entirely yours to use and distribute. If you fork Pake into your own product, to avoid confusion please give it a different name and credit Pake as the source.
|
|
218
|
+
Pake is open source under GPL-3.0, see [LICENSE](./LICENSE) and [Pake Output Exception](./LICENSE-EXCEPTION); apps you build with Pake are entirely yours to use and distribute. If you fork Pake into your own product, to avoid confusion please give it a different name and credit Pake as the source.
|
package/dist/cli.js
CHANGED
|
@@ -20,7 +20,7 @@ import { InvalidArgumentError, program as program$1, Option } from 'commander';
|
|
|
20
20
|
import fs$1 from 'fs';
|
|
21
21
|
|
|
22
22
|
var name = "pake-cli";
|
|
23
|
-
var version = "3.
|
|
23
|
+
var version = "3.12.0";
|
|
24
24
|
var description = "๐คฑ๐ป Turn any webpage into a desktop app with one command. ๐คฑ๐ป ไธ้ฎๆๅ
็ฝ้กต็ๆ่ฝป้ๆก้ขๅบ็จใ";
|
|
25
25
|
var engines = {
|
|
26
26
|
node: ">=18.0.0"
|
|
@@ -46,6 +46,7 @@ var keywords = [
|
|
|
46
46
|
"productivity"
|
|
47
47
|
];
|
|
48
48
|
var files = [
|
|
49
|
+
"LICENSE-EXCEPTION",
|
|
49
50
|
"dist",
|
|
50
51
|
"src-tauri"
|
|
51
52
|
];
|
|
@@ -417,6 +418,9 @@ function filterLinuxTargets(targets) {
|
|
|
417
418
|
const requested = targets.split(',').map((target) => target.trim());
|
|
418
419
|
return LINUX_TARGET_TYPES.filter((target) => requested.includes(target));
|
|
419
420
|
}
|
|
421
|
+
function needsTemporaryDebForZst(targets) {
|
|
422
|
+
return targets.includes('zst') && !targets.includes('deb');
|
|
423
|
+
}
|
|
420
424
|
|
|
421
425
|
/**
|
|
422
426
|
* Pure transform from CLI options to the window-config slice that gets
|
|
@@ -427,13 +431,14 @@ function filterLinuxTargets(targets) {
|
|
|
427
431
|
*/
|
|
428
432
|
function buildWindowConfigOverrides(options, platform = asSupportedPlatform(process.platform)) {
|
|
429
433
|
const platformHideOnClose = options.hideOnClose ?? platform === 'darwin';
|
|
434
|
+
const platformHideTitleBar = platform === 'darwin' ? options.hideTitleBar : false;
|
|
430
435
|
return {
|
|
431
436
|
width: options.width,
|
|
432
437
|
height: options.height,
|
|
433
438
|
fullscreen: options.fullscreen,
|
|
434
439
|
maximize: options.maximize,
|
|
435
440
|
resizable: options.resizable ?? true,
|
|
436
|
-
hide_title_bar:
|
|
441
|
+
hide_title_bar: platformHideTitleBar,
|
|
437
442
|
activation_shortcut: options.activationShortcut,
|
|
438
443
|
always_on_top: options.alwaysOnTop,
|
|
439
444
|
dark_mode: options.darkMode,
|
|
@@ -708,6 +713,9 @@ async function mergeConfig(url, options, tauriConf) {
|
|
|
708
713
|
await copyTemplateConfigs();
|
|
709
714
|
const { appVersion, userAgent, showSystemTray, useLocalFile, identifier, name = 'pake-app', installerLanguage, wasm, camera, microphone, } = options;
|
|
710
715
|
const platform = asSupportedPlatform(process.platform);
|
|
716
|
+
if (options.hideTitleBar && platform !== 'darwin') {
|
|
717
|
+
logger.warn('โผ --hide-title-bar is only supported on macOS and will be ignored on this platform.');
|
|
718
|
+
}
|
|
711
719
|
const tauriConfWindowOptions = buildWindowConfigOverrides(options, platform);
|
|
712
720
|
Object.assign(tauriConf.pake.windows[0], { url, ...tauriConfWindowOptions });
|
|
713
721
|
tauriConf.productName = name;
|
|
@@ -1158,14 +1166,22 @@ class BaseBuilder {
|
|
|
1158
1166
|
return 0; // Disable proxy feature if version detection fails
|
|
1159
1167
|
}
|
|
1160
1168
|
}
|
|
1169
|
+
getCargoTargetDir() {
|
|
1170
|
+
return process.env.CARGO_TARGET_DIR || path.join('src-tauri', 'target');
|
|
1171
|
+
}
|
|
1172
|
+
resolveBuildPath(npmDirectory, buildPath) {
|
|
1173
|
+
return path.isAbsolute(buildPath)
|
|
1174
|
+
? buildPath
|
|
1175
|
+
: path.join(npmDirectory, buildPath);
|
|
1176
|
+
}
|
|
1161
1177
|
getBasePath() {
|
|
1162
1178
|
const basePath = this.options.debug ? 'debug' : 'release';
|
|
1163
|
-
return
|
|
1179
|
+
return path.join(this.getCargoTargetDir(), basePath, 'bundle');
|
|
1164
1180
|
}
|
|
1165
1181
|
getBuildAppPath(npmDirectory, fileName, fileType) {
|
|
1166
1182
|
// For app bundles on macOS, the directory is 'macos', not 'app'
|
|
1167
1183
|
const bundleDir = fileType.toLowerCase() === 'app' ? 'macos' : fileType.toLowerCase();
|
|
1168
|
-
return path.join(npmDirectory, this.getBasePath(), bundleDir, `${fileName}.${fileType}`);
|
|
1184
|
+
return path.join(this.resolveBuildPath(npmDirectory, this.getBasePath()), bundleDir, `${fileName}.${fileType}`);
|
|
1169
1185
|
}
|
|
1170
1186
|
/**
|
|
1171
1187
|
* Copy raw binary file to output directory
|
|
@@ -1192,9 +1208,9 @@ class BaseBuilder {
|
|
|
1192
1208
|
const binaryName = this.getBinaryName(appName);
|
|
1193
1209
|
// Handle cross-platform builds
|
|
1194
1210
|
if (this.options.multiArch || this.hasArchSpecificTarget()) {
|
|
1195
|
-
return path.join(npmDirectory, this.getArchSpecificPath(), basePath, binaryName);
|
|
1211
|
+
return path.join(this.resolveBuildPath(npmDirectory, this.getArchSpecificPath()), basePath, binaryName);
|
|
1196
1212
|
}
|
|
1197
|
-
return path.join(npmDirectory,
|
|
1213
|
+
return path.join(this.resolveBuildPath(npmDirectory, this.getCargoTargetDir()), basePath, binaryName);
|
|
1198
1214
|
}
|
|
1199
1215
|
/**
|
|
1200
1216
|
* Get the output path for the raw binary file
|
|
@@ -1225,7 +1241,7 @@ class BaseBuilder {
|
|
|
1225
1241
|
* Get architecture-specific path for binary
|
|
1226
1242
|
*/
|
|
1227
1243
|
getArchSpecificPath() {
|
|
1228
|
-
return
|
|
1244
|
+
return this.getCargoTargetDir(); // Override in subclasses if needed
|
|
1229
1245
|
}
|
|
1230
1246
|
}
|
|
1231
1247
|
BaseBuilder.ARCH_MAPPINGS = {
|
|
@@ -1311,7 +1327,10 @@ class MacBuilder extends BaseBuilder {
|
|
|
1311
1327
|
const basePath = this.options.debug ? 'debug' : 'release';
|
|
1312
1328
|
const actualArch = this.getActualArch();
|
|
1313
1329
|
const target = this.getTauriTarget(actualArch, 'darwin');
|
|
1314
|
-
|
|
1330
|
+
if (!target) {
|
|
1331
|
+
throw new Error(`Unsupported architecture: ${actualArch} for macOS`);
|
|
1332
|
+
}
|
|
1333
|
+
return path.join(this.getCargoTargetDir(), target, basePath, 'bundle');
|
|
1315
1334
|
}
|
|
1316
1335
|
hasArchSpecificTarget() {
|
|
1317
1336
|
return true;
|
|
@@ -1319,7 +1338,10 @@ class MacBuilder extends BaseBuilder {
|
|
|
1319
1338
|
getArchSpecificPath() {
|
|
1320
1339
|
const actualArch = this.getActualArch();
|
|
1321
1340
|
const target = this.getTauriTarget(actualArch, 'darwin');
|
|
1322
|
-
|
|
1341
|
+
if (!target) {
|
|
1342
|
+
throw new Error(`Unsupported architecture: ${actualArch} for macOS`);
|
|
1343
|
+
}
|
|
1344
|
+
return path.join(this.getCargoTargetDir(), target);
|
|
1323
1345
|
}
|
|
1324
1346
|
}
|
|
1325
1347
|
|
|
@@ -1350,14 +1372,26 @@ class WinBuilder extends BaseBuilder {
|
|
|
1350
1372
|
getBasePath() {
|
|
1351
1373
|
const basePath = this.options.debug ? 'debug' : 'release';
|
|
1352
1374
|
const target = this.getTauriTarget(this.buildArch, 'win32');
|
|
1353
|
-
|
|
1375
|
+
if (!target) {
|
|
1376
|
+
throw new Error(`Unsupported architecture: ${this.buildArch} for Windows`);
|
|
1377
|
+
}
|
|
1378
|
+
return path.join(this.getCargoTargetDir(), target, basePath, 'bundle');
|
|
1354
1379
|
}
|
|
1355
1380
|
hasArchSpecificTarget() {
|
|
1356
1381
|
return true;
|
|
1357
1382
|
}
|
|
1358
1383
|
getArchSpecificPath() {
|
|
1359
1384
|
const target = this.getTauriTarget(this.buildArch, 'win32');
|
|
1360
|
-
|
|
1385
|
+
if (!target) {
|
|
1386
|
+
throw new Error(`Unsupported architecture: ${this.buildArch} for Windows`);
|
|
1387
|
+
}
|
|
1388
|
+
return path.join(this.getCargoTargetDir(), target);
|
|
1389
|
+
}
|
|
1390
|
+
getRawBinaryPath(appName) {
|
|
1391
|
+
return `${appName}.exe`;
|
|
1392
|
+
}
|
|
1393
|
+
getBinaryName(appName) {
|
|
1394
|
+
return `pake-${generateIdentifierSafeName(appName)}.exe`;
|
|
1361
1395
|
}
|
|
1362
1396
|
}
|
|
1363
1397
|
|
|
@@ -1407,11 +1441,16 @@ class LinuxBuilder extends BaseBuilder {
|
|
|
1407
1441
|
if (targets.length === 0) {
|
|
1408
1442
|
throw new Error(`No valid Linux target in "${this.options.targets}". Valid targets: ${LINUX_TARGET_TYPES.join(', ')}.`);
|
|
1409
1443
|
}
|
|
1444
|
+
const useTemporaryDebForZst = needsTemporaryDebForZst(targets);
|
|
1410
1445
|
for (const target of targets) {
|
|
1411
1446
|
this.currentBuildType = target;
|
|
1412
1447
|
if (target === 'zst') {
|
|
1413
|
-
|
|
1414
|
-
|
|
1448
|
+
if (useTemporaryDebForZst) {
|
|
1449
|
+
await this.buildAndCopy(url, 'deb', false);
|
|
1450
|
+
}
|
|
1451
|
+
await this.createArchPackageFromDeb({
|
|
1452
|
+
removeSourceDeb: useTemporaryDebForZst,
|
|
1453
|
+
});
|
|
1415
1454
|
}
|
|
1416
1455
|
else {
|
|
1417
1456
|
await this.buildAndCopy(url, target);
|
|
@@ -1432,7 +1471,7 @@ class LinuxBuilder extends BaseBuilder {
|
|
|
1432
1471
|
}
|
|
1433
1472
|
}
|
|
1434
1473
|
}
|
|
1435
|
-
async createArchPackageFromDeb() {
|
|
1474
|
+
async createArchPackageFromDeb({ removeSourceDeb, }) {
|
|
1436
1475
|
const { name = 'pake-app' } = this.options;
|
|
1437
1476
|
const packageName = generateLinuxPackageName(name);
|
|
1438
1477
|
const version = tauriConfig.version;
|
|
@@ -1493,11 +1532,13 @@ post_remove() {
|
|
|
1493
1532
|
}
|
|
1494
1533
|
`);
|
|
1495
1534
|
await shellExec(`bsdtar --zstd -cf "${packagePath}" -C "${dataDir}" .PKGINFO .INSTALL usr`);
|
|
1496
|
-
await fsExtra.remove(debPath);
|
|
1497
1535
|
logger.success('โ Build success!');
|
|
1498
1536
|
logger.success('โ App installer located in', packagePath);
|
|
1499
1537
|
}
|
|
1500
1538
|
finally {
|
|
1539
|
+
if (removeSourceDeb) {
|
|
1540
|
+
await fsExtra.remove(debPath);
|
|
1541
|
+
}
|
|
1501
1542
|
await fsExtra.remove(workDir);
|
|
1502
1543
|
}
|
|
1503
1544
|
}
|
|
@@ -1545,7 +1586,10 @@ post_remove() {
|
|
|
1545
1586
|
const basePath = this.options.debug ? 'debug' : 'release';
|
|
1546
1587
|
if (this.buildArch === 'arm64') {
|
|
1547
1588
|
const target = this.getTauriTarget(this.buildArch, 'linux');
|
|
1548
|
-
|
|
1589
|
+
if (!target) {
|
|
1590
|
+
throw new Error(`Unsupported architecture: ${this.buildArch} for Linux`);
|
|
1591
|
+
}
|
|
1592
|
+
return path.join(this.getCargoTargetDir(), target, basePath, 'bundle');
|
|
1549
1593
|
}
|
|
1550
1594
|
return super.getBasePath();
|
|
1551
1595
|
}
|
|
@@ -1561,7 +1605,10 @@ post_remove() {
|
|
|
1561
1605
|
getArchSpecificPath() {
|
|
1562
1606
|
if (this.buildArch === 'arm64') {
|
|
1563
1607
|
const target = this.getTauriTarget(this.buildArch, 'linux');
|
|
1564
|
-
|
|
1608
|
+
if (!target) {
|
|
1609
|
+
throw new Error(`Unsupported architecture: ${this.buildArch} for Linux`);
|
|
1610
|
+
}
|
|
1611
|
+
return path.join(this.getCargoTargetDir(), target);
|
|
1565
1612
|
}
|
|
1566
1613
|
return super.getArchSpecificPath();
|
|
1567
1614
|
}
|
|
@@ -2399,6 +2446,20 @@ function normalizeUrl(urlToNormalize) {
|
|
|
2399
2446
|
throw new Error(`Your url "${urlWithProtocol}" is invalid: ${err.message}`);
|
|
2400
2447
|
}
|
|
2401
2448
|
}
|
|
2449
|
+
// Compiles a comma-separated domain list into a regex source for
|
|
2450
|
+
// internal_url_regex. Each domain is escaped and matched against the URL host
|
|
2451
|
+
// and its subdomains so path or query text cannot accidentally opt a link in.
|
|
2452
|
+
// Returns '' for empty input.
|
|
2453
|
+
function safeDomainsToRegex(domains) {
|
|
2454
|
+
const escaped = domains
|
|
2455
|
+
.split(',')
|
|
2456
|
+
.map((domain) => domain.trim().toLowerCase())
|
|
2457
|
+
.filter(Boolean)
|
|
2458
|
+
.map((domain) => domain.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
|
2459
|
+
return escaped.length
|
|
2460
|
+
? `^https?:\\/\\/(?:[^/?#@]+\\.)*(?:${escaped.join('|')})(?::\\d+)?(?:[/?#]|$)`
|
|
2461
|
+
: '';
|
|
2462
|
+
}
|
|
2402
2463
|
|
|
2403
2464
|
/**
|
|
2404
2465
|
* Error class used for user-facing CLI errors.
|
|
@@ -2479,6 +2540,10 @@ async function handleOptions(options, url) {
|
|
|
2479
2540
|
name: resolvedName,
|
|
2480
2541
|
identifier: resolveIdentifier(url, options.name, options.identifier),
|
|
2481
2542
|
};
|
|
2543
|
+
// --safe-domain is sugar over --internal-url-regex; an explicit regex wins.
|
|
2544
|
+
if (!options.internalUrlRegex && options.safeDomain) {
|
|
2545
|
+
appOptions.internalUrlRegex = safeDomainsToRegex(options.safeDomain);
|
|
2546
|
+
}
|
|
2482
2547
|
const iconPath = await handleIcon(appOptions, url);
|
|
2483
2548
|
appOptions.icon = iconPath || '';
|
|
2484
2549
|
return appOptions;
|
|
@@ -2527,6 +2592,7 @@ const DEFAULT_PAKE_OPTIONS = {
|
|
|
2527
2592
|
startToTray: false,
|
|
2528
2593
|
forceInternalNavigation: false,
|
|
2529
2594
|
internalUrlRegex: '',
|
|
2595
|
+
safeDomain: '',
|
|
2530
2596
|
enableFind: false,
|
|
2531
2597
|
iterativeBuild: false,
|
|
2532
2598
|
zoom: 100,
|
|
@@ -2540,6 +2606,9 @@ const DEFAULT_PAKE_OPTIONS = {
|
|
|
2540
2606
|
};
|
|
2541
2607
|
|
|
2542
2608
|
function validateNumberInput(value) {
|
|
2609
|
+
if (value.trim() === '') {
|
|
2610
|
+
throw new InvalidArgumentError('Not a number.');
|
|
2611
|
+
}
|
|
2543
2612
|
const parsedValue = Number(value);
|
|
2544
2613
|
if (!Number.isFinite(parsedValue)) {
|
|
2545
2614
|
throw new InvalidArgumentError('Not a number.');
|
|
@@ -2576,6 +2645,7 @@ ${green('|_| \\__,_|_|\\_\\___| can turn any webpage into a desktop app with
|
|
|
2576
2645
|
return program$1
|
|
2577
2646
|
.addHelpText('beforeAll', logo)
|
|
2578
2647
|
.usage(`[url] [options]`)
|
|
2648
|
+
.helpOption('-h, --help', 'Show all CLI options')
|
|
2579
2649
|
.showHelpAfterError()
|
|
2580
2650
|
.argument('[url]', 'The web URL you want to package', validateUrlInput)
|
|
2581
2651
|
.option('--name <string>', 'Application name')
|
|
@@ -2664,12 +2734,9 @@ ${green('|_| \\__,_|_|\\_\\___| can turn any webpage into a desktop app with
|
|
|
2664
2734
|
.addOption(new Option('--start-to-tray', 'Start app minimized to tray')
|
|
2665
2735
|
.default(DEFAULT_PAKE_OPTIONS.startToTray)
|
|
2666
2736
|
.hideHelp())
|
|
2667
|
-
.addOption(new Option('--force-internal-navigation', 'Keep every link inside the Pake window instead of opening external handlers')
|
|
2668
|
-
.default(DEFAULT_PAKE_OPTIONS.
|
|
2669
|
-
.
|
|
2670
|
-
.addOption(new Option('--internal-url-regex <string>', 'Regex pattern to match URLs that should be considered internal')
|
|
2671
|
-
.default(DEFAULT_PAKE_OPTIONS.internalUrlRegex)
|
|
2672
|
-
.hideHelp())
|
|
2737
|
+
.addOption(new Option('--force-internal-navigation', 'Keep every link inside the Pake window instead of opening external handlers').default(DEFAULT_PAKE_OPTIONS.forceInternalNavigation))
|
|
2738
|
+
.addOption(new Option('--internal-url-regex <string>', 'Regex pattern to match URLs that should be considered internal').default(DEFAULT_PAKE_OPTIONS.internalUrlRegex))
|
|
2739
|
+
.addOption(new Option('--safe-domain <domains>', 'Comma-separated domains kept inside the app (e.g. SSO/workspace callbacks)').default(DEFAULT_PAKE_OPTIONS.safeDomain))
|
|
2673
2740
|
.addOption(new Option('--enable-find', 'Enable in-page Find UI with Cmd/Ctrl+F/G shortcuts')
|
|
2674
2741
|
.default(DEFAULT_PAKE_OPTIONS.enableFind)
|
|
2675
2742
|
.hideHelp())
|
|
@@ -2700,9 +2767,7 @@ ${green('|_| \\__,_|_|\\_\\___| can turn any webpage into a desktop app with
|
|
|
2700
2767
|
.addOption(new Option('--iterative-build', 'Turn on rapid build mode (app only, no dmg/deb/msi), good for debugging')
|
|
2701
2768
|
.default(DEFAULT_PAKE_OPTIONS.iterativeBuild)
|
|
2702
2769
|
.hideHelp())
|
|
2703
|
-
.addOption(new Option('--new-window', 'Allow sites to open new windows (for auth flows, tabs, branches)')
|
|
2704
|
-
.default(DEFAULT_PAKE_OPTIONS.newWindow)
|
|
2705
|
-
.hideHelp())
|
|
2770
|
+
.addOption(new Option('--new-window', 'Allow sites to open new windows (for auth flows, tabs, branches)').default(DEFAULT_PAKE_OPTIONS.newWindow))
|
|
2706
2771
|
.addOption(new Option('--install', 'Auto-install app to /Applications (macOS) after build and remove local bundle')
|
|
2707
2772
|
.default(DEFAULT_PAKE_OPTIONS.install)
|
|
2708
2773
|
.hideHelp())
|
|
@@ -2715,14 +2780,19 @@ ${green('|_| \\__,_|_|\\_\\___| can turn any webpage into a desktop app with
|
|
|
2715
2780
|
.version(packageJson.version, '-v, --version')
|
|
2716
2781
|
.configureHelp({
|
|
2717
2782
|
sortSubcommands: true,
|
|
2783
|
+
visibleOptions: (command) => {
|
|
2784
|
+
const options = [...command.options];
|
|
2785
|
+
const helpOption = command
|
|
2786
|
+
._helpOption;
|
|
2787
|
+
if (helpOption) {
|
|
2788
|
+
options.push(helpOption);
|
|
2789
|
+
}
|
|
2790
|
+
return options;
|
|
2791
|
+
},
|
|
2718
2792
|
optionTerm: (option) => {
|
|
2719
|
-
if (option.flags === '-v, --version' || option.flags === '-h, --help')
|
|
2720
|
-
return '';
|
|
2721
2793
|
return option.flags;
|
|
2722
2794
|
},
|
|
2723
2795
|
optionDescription: (option) => {
|
|
2724
|
-
if (option.flags === '-v, --version' || option.flags === '-h, --help')
|
|
2725
|
-
return '';
|
|
2726
2796
|
return option.description;
|
|
2727
2797
|
},
|
|
2728
2798
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pake-cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.12.0",
|
|
4
4
|
"description": "๐คฑ๐ป Turn any webpage into a desktop app with one command. ๐คฑ๐ป ไธ้ฎๆๅ
็ฝ้กต็ๆ่ฝป้ๆก้ขๅบ็จใ",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=18.0.0"
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"productivity"
|
|
27
27
|
],
|
|
28
28
|
"files": [
|
|
29
|
+
"LICENSE-EXCEPTION",
|
|
29
30
|
"dist",
|
|
30
31
|
"src-tauri"
|
|
31
32
|
],
|
package/src-tauri/Cargo.lock
CHANGED
package/src-tauri/Cargo.toml
CHANGED
|
@@ -27,12 +27,26 @@ function matchesAuthUrl(url, baseUrl = window.location.href) {
|
|
|
27
27
|
/\/o\/oauth2/,
|
|
28
28
|
];
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
30
|
+
// Enterprise SSO. Match identity providers on the host, and SAML/SSO/ADFS on
|
|
31
|
+
// the pathname with endpoint-shaped patterns only, so ordinary pages such as
|
|
32
|
+
// /settings/sso/providers (or a query string carrying an SSO URL) are not
|
|
33
|
+
// misread as authentication.
|
|
34
|
+
const enterpriseHostPatterns = [/(^|\.)okta\.com$/, /(^|\.)onelogin\.com$/];
|
|
35
|
+
const enterprisePathPatterns = [
|
|
36
|
+
/\/saml2?\/(sso|acs|login|metadata|consume|redirect|callback|continue)/,
|
|
37
|
+
/\/sso\/(saml|oidc|oauth|login|authorize|redirect|callback|acs|start|continue|metadata)/,
|
|
38
|
+
/\/adfs\/ls\b/,
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
const isMatch =
|
|
42
|
+
oauthPatterns.some(
|
|
43
|
+
(pattern) =>
|
|
44
|
+
pattern.test(hostname) ||
|
|
45
|
+
pattern.test(pathname) ||
|
|
46
|
+
pattern.test(fullUrl),
|
|
47
|
+
) ||
|
|
48
|
+
enterpriseHostPatterns.some((pattern) => pattern.test(hostname)) ||
|
|
49
|
+
enterprisePathPatterns.some((pattern) => pattern.test(pathname));
|
|
36
50
|
|
|
37
51
|
if (isMatch) {
|
|
38
52
|
console.log("[Pake] OAuth URL detected:", url);
|
|
@@ -272,6 +272,33 @@ function shouldBypassPakeLinkHandling(rawHref) {
|
|
|
272
272
|
);
|
|
273
273
|
}
|
|
274
274
|
|
|
275
|
+
function shouldNavigateAuthInCurrentWindow() {
|
|
276
|
+
return /macintosh|mac os x/i.test(navigator.userAgent);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function canNavigateAuthUrl(url) {
|
|
280
|
+
const normalizedUrl = normalizeAnchorHref(url).toLowerCase();
|
|
281
|
+
return normalizedUrl !== "" && normalizedUrl !== "about:blank";
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function navigateInCurrentWindow(url) {
|
|
285
|
+
window.location.href = url;
|
|
286
|
+
return window;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function openAuthNavigation(originalWindowOpen, url, name, specs) {
|
|
290
|
+
if (shouldNavigateAuthInCurrentWindow() && canNavigateAuthUrl(url)) {
|
|
291
|
+
return navigateInCurrentWindow(url);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const authWindow = originalWindowOpen.call(window, url, name, specs);
|
|
295
|
+
if (!authWindow) {
|
|
296
|
+
return navigateInCurrentWindow(url);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return authWindow;
|
|
300
|
+
}
|
|
301
|
+
|
|
275
302
|
document.addEventListener("DOMContentLoaded", () => {
|
|
276
303
|
const tauri = window.__TAURI__;
|
|
277
304
|
const appWindow = tauri.window.getCurrentWindow();
|
|
@@ -432,24 +459,23 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
432
459
|
const absoluteUrl = hrefUrl.href;
|
|
433
460
|
let filename = anchorElement.download || getFilenameFromUrl(absoluteUrl);
|
|
434
461
|
|
|
435
|
-
// Keep OAuth/authentication flows inside the app
|
|
462
|
+
// Keep OAuth/authentication flows inside the app. Without --new-window,
|
|
463
|
+
// navigate in place so the SSO redirect chain and callback stay in the
|
|
464
|
+
// webview instead of falling through to the system browser.
|
|
436
465
|
if (window.isAuthLink(absoluteUrl)) {
|
|
437
466
|
console.log("[Pake] Handling OAuth navigation in-app:", absoluteUrl);
|
|
467
|
+
e.preventDefault();
|
|
468
|
+
e.stopImmediatePropagation();
|
|
438
469
|
|
|
439
470
|
if (window.pakeConfig?.new_window) {
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
const authWindow = originalWindowOpen.call(
|
|
444
|
-
window,
|
|
471
|
+
openAuthNavigation(
|
|
472
|
+
originalWindowOpen,
|
|
445
473
|
absoluteUrl,
|
|
446
474
|
"_blank",
|
|
447
475
|
"width=1200,height=800,scrollbars=yes,resizable=yes",
|
|
448
476
|
);
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
window.location.href = absoluteUrl;
|
|
452
|
-
}
|
|
477
|
+
} else {
|
|
478
|
+
window.location.href = absoluteUrl;
|
|
453
479
|
}
|
|
454
480
|
|
|
455
481
|
return;
|
|
@@ -465,7 +491,15 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
465
491
|
}
|
|
466
492
|
|
|
467
493
|
if (isInternalUrl(absoluteUrl)) {
|
|
468
|
-
//
|
|
494
|
+
// With --new-window the Rust on_new_window handler opens an in-app
|
|
495
|
+
// window; without it, deferring to the native handler sends the
|
|
496
|
+
// _blank target to the system browser and strands SSO callbacks.
|
|
497
|
+
// Navigate in place so internal links stay inside the webview.
|
|
498
|
+
if (!window.pakeConfig?.new_window) {
|
|
499
|
+
e.preventDefault();
|
|
500
|
+
e.stopImmediatePropagation();
|
|
501
|
+
window.location.href = absoluteUrl;
|
|
502
|
+
}
|
|
469
503
|
return;
|
|
470
504
|
}
|
|
471
505
|
|
|
@@ -544,9 +578,15 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
544
578
|
return originalWindowOpen.call(window, url, name, specs);
|
|
545
579
|
}
|
|
546
580
|
|
|
547
|
-
//
|
|
581
|
+
// Avoid macOS WebKit auth-popup crashes by navigating auth URLs in-place.
|
|
548
582
|
if (window.isAuthPopup(url, name)) {
|
|
549
|
-
|
|
583
|
+
try {
|
|
584
|
+
const baseUrl = window.location.origin + window.location.pathname;
|
|
585
|
+
const absoluteUrl = new URL(url, baseUrl).href;
|
|
586
|
+
return openAuthNavigation(originalWindowOpen, absoluteUrl, name, specs);
|
|
587
|
+
} catch (error) {
|
|
588
|
+
return openAuthNavigation(originalWindowOpen, url, name, specs);
|
|
589
|
+
}
|
|
550
590
|
}
|
|
551
591
|
|
|
552
592
|
try {
|
|
@@ -563,6 +603,14 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
563
603
|
return null;
|
|
564
604
|
}
|
|
565
605
|
|
|
606
|
+
// With --new-window the native handler opens an in-app window; without it,
|
|
607
|
+
// originalWindowOpen would route the internal target to the system browser
|
|
608
|
+
// and strand SSO callbacks, so navigate in place instead.
|
|
609
|
+
if (!window.pakeConfig?.new_window) {
|
|
610
|
+
window.location.href = absoluteUrl;
|
|
611
|
+
return window;
|
|
612
|
+
}
|
|
613
|
+
|
|
566
614
|
return originalWindowOpen.call(window, absoluteUrl, name, specs);
|
|
567
615
|
} catch (error) {
|
|
568
616
|
return originalWindowOpen.call(window, url, name, specs);
|
package/src-tauri/src/lib.rs
CHANGED
|
@@ -10,6 +10,12 @@ use tauri_plugin_window_state::StateFlags;
|
|
|
10
10
|
use std::time::Duration;
|
|
11
11
|
|
|
12
12
|
const WINDOW_SHOW_DELAY: u64 = 50;
|
|
13
|
+
#[cfg(target_os = "linux")]
|
|
14
|
+
const PAKE_LINUX_WEBKIT_SAFE_MODE: &str = "PAKE_LINUX_WEBKIT_SAFE_MODE";
|
|
15
|
+
#[cfg(target_os = "linux")]
|
|
16
|
+
const WEBKIT_DISABLE_DMABUF_RENDERER: &str = "WEBKIT_DISABLE_DMABUF_RENDERER";
|
|
17
|
+
#[cfg(target_os = "linux")]
|
|
18
|
+
const WEBKIT_DISABLE_COMPOSITING_MODE: &str = "WEBKIT_DISABLE_COMPOSITING_MODE";
|
|
13
19
|
|
|
14
20
|
use app::{
|
|
15
21
|
invoke::{
|
|
@@ -21,16 +27,83 @@ use app::{
|
|
|
21
27
|
};
|
|
22
28
|
use util::get_pake_config;
|
|
23
29
|
|
|
30
|
+
#[cfg(any(target_os = "linux", test))]
|
|
31
|
+
fn is_disabled_env_value(value: &str) -> bool {
|
|
32
|
+
matches!(
|
|
33
|
+
value.trim().to_ascii_lowercase().as_str(),
|
|
34
|
+
"0" | "false" | "off" | "no" | "native" | "disabled"
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
#[cfg(any(target_os = "linux", test))]
|
|
39
|
+
fn is_non_empty_env_value(value: Option<&str>) -> bool {
|
|
40
|
+
value.map(|value| !value.trim().is_empty()).unwrap_or(false)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
#[cfg(any(target_os = "linux", test))]
|
|
44
|
+
fn contains_niri(value: &str) -> bool {
|
|
45
|
+
value
|
|
46
|
+
.split([':', ';', ',', ' '])
|
|
47
|
+
.any(|part| part.eq_ignore_ascii_case("niri"))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
#[cfg(any(target_os = "linux", test))]
|
|
51
|
+
fn should_enable_linux_webkit_safe_mode_from_values(
|
|
52
|
+
safe_mode: Option<&str>,
|
|
53
|
+
niri_socket: Option<&str>,
|
|
54
|
+
desktop_values: &[Option<&str>],
|
|
55
|
+
) -> bool {
|
|
56
|
+
if let Some(value) = safe_mode.filter(|value| !value.trim().is_empty()) {
|
|
57
|
+
return !is_disabled_env_value(value);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let is_niri_session = is_non_empty_env_value(niri_socket)
|
|
61
|
+
|| desktop_values
|
|
62
|
+
.iter()
|
|
63
|
+
.flatten()
|
|
64
|
+
.any(|value| contains_niri(value));
|
|
65
|
+
|
|
66
|
+
!is_niri_session
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
#[cfg(target_os = "linux")]
|
|
70
|
+
fn apply_linux_webkit_runtime_flags() {
|
|
71
|
+
let safe_mode = std::env::var(PAKE_LINUX_WEBKIT_SAFE_MODE).ok();
|
|
72
|
+
if safe_mode.as_deref().is_some_and(is_disabled_env_value) {
|
|
73
|
+
std::env::remove_var(WEBKIT_DISABLE_DMABUF_RENDERER);
|
|
74
|
+
std::env::remove_var(WEBKIT_DISABLE_COMPOSITING_MODE);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let desktop_values = [
|
|
79
|
+
std::env::var("XDG_CURRENT_DESKTOP").ok(),
|
|
80
|
+
std::env::var("XDG_SESSION_DESKTOP").ok(),
|
|
81
|
+
std::env::var("DESKTOP_SESSION").ok(),
|
|
82
|
+
];
|
|
83
|
+
let desktop_refs = desktop_values
|
|
84
|
+
.iter()
|
|
85
|
+
.map(|value| value.as_deref())
|
|
86
|
+
.collect::<Vec<_>>();
|
|
87
|
+
|
|
88
|
+
if !should_enable_linux_webkit_safe_mode_from_values(
|
|
89
|
+
safe_mode.as_deref(),
|
|
90
|
+
std::env::var("NIRI_SOCKET").ok().as_deref(),
|
|
91
|
+
&desktop_refs,
|
|
92
|
+
) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if std::env::var(WEBKIT_DISABLE_DMABUF_RENDERER).is_err() {
|
|
97
|
+
std::env::set_var(WEBKIT_DISABLE_DMABUF_RENDERER, "1");
|
|
98
|
+
}
|
|
99
|
+
if std::env::var(WEBKIT_DISABLE_COMPOSITING_MODE).is_err() {
|
|
100
|
+
std::env::set_var(WEBKIT_DISABLE_COMPOSITING_MODE, "1");
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
24
104
|
pub fn run_app() {
|
|
25
105
|
#[cfg(target_os = "linux")]
|
|
26
|
-
|
|
27
|
-
if std::env::var("WEBKIT_DISABLE_DMABUF_RENDERER").is_err() {
|
|
28
|
-
std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1");
|
|
29
|
-
}
|
|
30
|
-
if std::env::var("WEBKIT_DISABLE_COMPOSITING_MODE").is_err() {
|
|
31
|
-
std::env::set_var("WEBKIT_DISABLE_COMPOSITING_MODE", "1");
|
|
32
|
-
}
|
|
33
|
-
}
|
|
106
|
+
apply_linux_webkit_runtime_flags();
|
|
34
107
|
|
|
35
108
|
let (pake_config, tauri_config) = get_pake_config();
|
|
36
109
|
let tauri_app = tauri::Builder::default();
|
|
@@ -202,3 +275,58 @@ pub fn run_app() {
|
|
|
202
275
|
pub fn run() {
|
|
203
276
|
run_app()
|
|
204
277
|
}
|
|
278
|
+
|
|
279
|
+
#[cfg(test)]
|
|
280
|
+
mod tests {
|
|
281
|
+
use super::*;
|
|
282
|
+
|
|
283
|
+
#[test]
|
|
284
|
+
fn linux_webkit_safe_mode_stays_on_by_default() {
|
|
285
|
+
assert!(should_enable_linux_webkit_safe_mode_from_values(
|
|
286
|
+
None,
|
|
287
|
+
None,
|
|
288
|
+
&[None, None, None]
|
|
289
|
+
));
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
#[test]
|
|
293
|
+
fn linux_webkit_safe_mode_is_disabled_for_niri_socket() {
|
|
294
|
+
assert!(!should_enable_linux_webkit_safe_mode_from_values(
|
|
295
|
+
None,
|
|
296
|
+
Some("/run/user/501/niri.sock"),
|
|
297
|
+
&[None, None, None]
|
|
298
|
+
));
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
#[test]
|
|
302
|
+
fn linux_webkit_safe_mode_is_disabled_for_niri_desktop() {
|
|
303
|
+
assert!(!should_enable_linux_webkit_safe_mode_from_values(
|
|
304
|
+
None,
|
|
305
|
+
None,
|
|
306
|
+
&[Some("niri"), None, None]
|
|
307
|
+
));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
#[test]
|
|
311
|
+
fn linux_webkit_safe_mode_can_be_forced_on_for_niri() {
|
|
312
|
+
assert!(should_enable_linux_webkit_safe_mode_from_values(
|
|
313
|
+
Some("1"),
|
|
314
|
+
Some("/run/user/501/niri.sock"),
|
|
315
|
+
&[Some("niri"), None, None]
|
|
316
|
+
));
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
#[test]
|
|
320
|
+
fn linux_webkit_safe_mode_can_be_disabled_explicitly() {
|
|
321
|
+
for value in ["0", "false", "off", "no", "native", "disabled"] {
|
|
322
|
+
assert!(
|
|
323
|
+
!should_enable_linux_webkit_safe_mode_from_values(
|
|
324
|
+
Some(value),
|
|
325
|
+
None,
|
|
326
|
+
&[None, None, None]
|
|
327
|
+
),
|
|
328
|
+
"expected {value} to disable safe mode"
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|