pake-cli 3.13.1 → 3.15.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/README.md +20 -1
- package/dist/cli.js +476 -55
- package/package.json +1 -1
- package/src-tauri/Cargo.lock +1 -1
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/icons/notion.icns +0 -0
- package/src-tauri/icons/youtubemusic.icns +0 -0
- package/src-tauri/pake.json +1 -0
- package/src-tauri/png/notion_256.ico +0 -0
- package/src-tauri/png/notion_32.ico +0 -0
- package/src-tauri/png/notion_512.png +0 -0
- package/src-tauri/png/youtubemusic_256.ico +0 -0
- package/src-tauri/png/youtubemusic_32.ico +0 -0
- package/src-tauri/png/youtubemusic_512.png +0 -0
- package/src-tauri/src/app/config.rs +2 -0
- package/src-tauri/src/app/setup.rs +5 -2
- package/src-tauri/src/app/window.rs +4 -0
- package/src-tauri/src/inject/event.js +115 -50
- package/src-tauri/src/inject/style.js +27 -2
- package/src-tauri/tauri.conf.json +1 -1
package/README.md
CHANGED
|
@@ -130,6 +130,22 @@
|
|
|
130
130
|
<td><img src=https://raw.githubusercontent.com/tw93/static/main/pake/Excalidraw.png width=600/></td>
|
|
131
131
|
<td><img src=https://raw.githubusercontent.com/tw93/static/main/pake/XiaoHongShu.png width=600/></td>
|
|
132
132
|
</tr>
|
|
133
|
+
<tr>
|
|
134
|
+
<td>Notion
|
|
135
|
+
<a href="https://github.com/tw93/Pake/releases/latest/download/Notion.dmg">Mac</a>
|
|
136
|
+
<a href="https://github.com/tw93/Pake/releases/latest/download/Notion_x64.msi">Windows</a>
|
|
137
|
+
<a href="https://github.com/tw93/Pake/releases/latest/download/Notion_x86_64.deb">Linux</a>
|
|
138
|
+
</td>
|
|
139
|
+
<td>Flomo
|
|
140
|
+
<a href="https://github.com/tw93/Pake/releases/latest/download/Flomo.dmg">Mac</a>
|
|
141
|
+
<a href="https://github.com/tw93/Pake/releases/latest/download/Flomo_x64.msi">Windows</a>
|
|
142
|
+
<a href="https://github.com/tw93/Pake/releases/latest/download/Flomo_x86_64.deb">Linux</a>
|
|
143
|
+
</td>
|
|
144
|
+
</tr>
|
|
145
|
+
<tr>
|
|
146
|
+
<td><img src=https://raw.githubusercontent.com/tw93/static/main/pake/Notion.png width=600/></td>
|
|
147
|
+
<td><img src=https://raw.githubusercontent.com/tw93/static/main/pake/Flomo.png width=600/></td>
|
|
148
|
+
</tr>
|
|
133
149
|
</table>
|
|
134
150
|
|
|
135
151
|
<details>
|
|
@@ -153,8 +169,9 @@
|
|
|
153
169
|
| <kbd>⌘</kbd> + <kbd>⇧</kbd> + <kbd>H</kbd> | <kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>H</kbd> | Go to Home Page |
|
|
154
170
|
| <kbd>⌘</kbd> + <kbd>⌥</kbd> + <kbd>I</kbd> | <kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>I</kbd> | Toggle Developer Tools (Debug Only) |
|
|
155
171
|
| <kbd>⌘</kbd> + <kbd>⇧</kbd> + <kbd>⌫</kbd> | <kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>Del</kbd> | Clear Cache & Restart |
|
|
172
|
+
| <kbd>⌃</kbd> + <kbd>⌘</kbd> + <kbd>F</kbd> | <kbd>F11</kbd> | Toggle native window fullscreen |
|
|
156
173
|
|
|
157
|
-
In addition, double-click the title bar to switch to full-screen mode. For Mac users, you can also use the gesture to go to the previous or next page and drag the title bar to move the window. The new menu also offers options for navigation, zoom, and window controls.
|
|
174
|
+
In addition, double-click the title bar to switch to full-screen mode. On Windows and Linux, use `--hide-window-decorations` for a frameless window with a top drag region. For Mac users, you can also use the gesture to go to the previous or next page and drag the title bar to move the window. The new menu also offers options for navigation, zoom, and window controls.
|
|
158
175
|
|
|
159
176
|
</details>
|
|
160
177
|
|
|
@@ -175,6 +192,8 @@ pake https://weekly.tw93.fun --name Weekly --icon https://cdn.tw93.fun/pake/week
|
|
|
175
192
|
|
|
176
193
|
First-time packaging requires environment setup and may be slower, subsequent builds are fast. For complete parameter documentation, see [CLI Usage Guide](docs/cli-usage.md). Don't want to use CLI? Try [GitHub Actions Online Building](docs/github-actions-usage.md).
|
|
177
194
|
|
|
195
|
+
Using Pake from a script or AI agent? Pass `--json` for machine-readable results, describe apps declaratively with `--config app.json` ([schema](schema/pake.schema.json)), and package local build output directly with `pake ./dist --name MyTool`. See [llms.txt](llms.txt) for the full agent contract. Claude Code users can install the official skill with `/plugin marketplace add tw93/Pake` and `/plugin install pake@pake`.
|
|
196
|
+
|
|
178
197
|
## Development
|
|
179
198
|
|
|
180
199
|
Requires Rust `>=1.85` and Node `>=22` (recommended LTS; `>=18` also works). For detailed installation guide, see [Tauri documentation](https://v2.tauri.app/start/prerequisites/). If unfamiliar with development environment, use the CLI tool instead.
|
package/dist/cli.js
CHANGED
|
@@ -20,7 +20,7 @@ import * as psl from 'psl';
|
|
|
20
20
|
import { InvalidArgumentError, program as program$1, Option } from 'commander';
|
|
21
21
|
|
|
22
22
|
var name = "pake-cli";
|
|
23
|
-
var version = "3.
|
|
23
|
+
var version = "3.15.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"
|
|
@@ -172,6 +172,58 @@ let tauriConfig = {
|
|
|
172
172
|
pake: pakeConf,
|
|
173
173
|
};
|
|
174
174
|
|
|
175
|
+
// Stable exit-code contract: 0 success, 2 invalid input, 3 build/network
|
|
176
|
+
// failure, 4 missing environment, 1 unexpected. Documented in cli-usage docs.
|
|
177
|
+
const ERROR_EXIT_CODES = {
|
|
178
|
+
INVALID_INPUT: 2,
|
|
179
|
+
BUILD_FAILED: 3,
|
|
180
|
+
NETWORK: 3,
|
|
181
|
+
ENV_MISSING: 4,
|
|
182
|
+
UNEXPECTED: 1,
|
|
183
|
+
};
|
|
184
|
+
let machineMode = false;
|
|
185
|
+
const capturedWarnings = [];
|
|
186
|
+
/**
|
|
187
|
+
* Route all loglevel output to stderr, capture warnings for the final JSON
|
|
188
|
+
* result, and strip ANSI colors. Must be called before any logging happens.
|
|
189
|
+
*/
|
|
190
|
+
function enableMachineMode() {
|
|
191
|
+
if (machineMode)
|
|
192
|
+
return;
|
|
193
|
+
machineMode = true;
|
|
194
|
+
chalk.level = 0;
|
|
195
|
+
log.methodFactory = (methodName) => {
|
|
196
|
+
return (...args) => {
|
|
197
|
+
if (methodName === 'warn') {
|
|
198
|
+
capturedWarnings.push(args.map(String).join(' '));
|
|
199
|
+
}
|
|
200
|
+
console.error(...args);
|
|
201
|
+
};
|
|
202
|
+
};
|
|
203
|
+
// Rebuild logging methods with the new factory.
|
|
204
|
+
log.setLevel(log.getLevel());
|
|
205
|
+
}
|
|
206
|
+
function isMachineMode() {
|
|
207
|
+
return machineMode;
|
|
208
|
+
}
|
|
209
|
+
function getCapturedWarnings() {
|
|
210
|
+
return [...capturedWarnings];
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Whether Pake may prompt the user. False in machine mode, without a TTY,
|
|
214
|
+
* or inside CI, where prompts would hang or produce garbage.
|
|
215
|
+
*/
|
|
216
|
+
function isInteractive() {
|
|
217
|
+
return (!machineMode &&
|
|
218
|
+
Boolean(process.stdin.isTTY) &&
|
|
219
|
+
Boolean(process.stdout.isTTY) &&
|
|
220
|
+
!process.env.CI &&
|
|
221
|
+
!process.env.GITHUB_ACTIONS);
|
|
222
|
+
}
|
|
223
|
+
function printJsonResult(result) {
|
|
224
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
225
|
+
}
|
|
226
|
+
|
|
175
227
|
// Generates a stable identifier based on the app URL (and optionally name).
|
|
176
228
|
// When name is provided it is included in the hash so two apps wrapping
|
|
177
229
|
// the same URL can coexist. Omitting name preserves backward compatibility
|
|
@@ -217,6 +269,8 @@ function getSpinner(text) {
|
|
|
217
269
|
text: `${chalk.cyan(text)}\n`,
|
|
218
270
|
spinner: loadingType,
|
|
219
271
|
color: 'cyan',
|
|
272
|
+
// In machine mode stdout must stay parseable and stderr low-noise.
|
|
273
|
+
isSilent: isMachineMode(),
|
|
220
274
|
}).start();
|
|
221
275
|
}
|
|
222
276
|
|
|
@@ -324,7 +378,11 @@ async function shellExec(command, timeout = 300000, env) {
|
|
|
324
378
|
cwd: npmDirectory,
|
|
325
379
|
// Use 'inherit' to show all output directly to user in real-time.
|
|
326
380
|
// This ensures linuxdeploy and other tool outputs are visible during builds.
|
|
327
|
-
|
|
381
|
+
// In machine mode (--json) stdout is reserved for the final JSON result,
|
|
382
|
+
// so subprocess stdout is rerouted to stderr instead.
|
|
383
|
+
stdin: 'inherit',
|
|
384
|
+
stdout: isMachineMode() ? process.stderr : 'inherit',
|
|
385
|
+
stderr: 'inherit',
|
|
328
386
|
shell: true,
|
|
329
387
|
timeout,
|
|
330
388
|
env: env ? { ...process.env, ...env } : process.env,
|
|
@@ -502,6 +560,31 @@ function generateIdentifierSafeName(name) {
|
|
|
502
560
|
return cleaned;
|
|
503
561
|
}
|
|
504
562
|
|
|
563
|
+
/**
|
|
564
|
+
* Error class used for user-facing CLI errors.
|
|
565
|
+
*
|
|
566
|
+
* The top-level catch in `bin/cli.ts` prints `message` directly without a
|
|
567
|
+
* stack trace and exits with the code mapped from `code` (see
|
|
568
|
+
* ERROR_EXIT_CODES in utils/output.ts). Use this for predictable failures
|
|
569
|
+
* (invalid names, missing files, etc.) so users see a clean message instead
|
|
570
|
+
* of a Node.js stack dump. `code` and `hint` also feed the `--json` result.
|
|
571
|
+
*/
|
|
572
|
+
class PakeError extends Error {
|
|
573
|
+
constructor(message, options) {
|
|
574
|
+
super(message);
|
|
575
|
+
this.isUserError = true;
|
|
576
|
+
this.name = 'PakeError';
|
|
577
|
+
this.code = options?.code;
|
|
578
|
+
this.hint = options?.hint;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
function isPakeError(error) {
|
|
582
|
+
return (error instanceof PakeError ||
|
|
583
|
+
(typeof error === 'object' &&
|
|
584
|
+
error !== null &&
|
|
585
|
+
error.isUserError === true));
|
|
586
|
+
}
|
|
587
|
+
|
|
505
588
|
const LINUX_TARGET_TYPES = ['deb', 'appimage', 'rpm', 'zst'];
|
|
506
589
|
// Returns the valid Linux build targets from a comma-separated targets
|
|
507
590
|
// string, preserving LINUX_TARGET_TYPES order. Unknown entries are dropped.
|
|
@@ -535,6 +618,7 @@ function resolveLinuxBundleTargets(targets) {
|
|
|
535
618
|
function buildWindowConfigOverrides(options, platform = asSupportedPlatform(process.platform)) {
|
|
536
619
|
const platformHideOnClose = options.hideOnClose ?? platform === 'darwin';
|
|
537
620
|
const platformHideTitleBar = platform === 'darwin' ? options.hideTitleBar : false;
|
|
621
|
+
const platformHideWindowDecorations = platform !== 'darwin' ? options.hideWindowDecorations : false;
|
|
538
622
|
return {
|
|
539
623
|
width: options.width,
|
|
540
624
|
height: options.height,
|
|
@@ -542,6 +626,7 @@ function buildWindowConfigOverrides(options, platform = asSupportedPlatform(proc
|
|
|
542
626
|
maximize: options.maximize,
|
|
543
627
|
resizable: options.resizable ?? true,
|
|
544
628
|
hide_title_bar: platformHideTitleBar,
|
|
629
|
+
hide_window_decorations: platformHideWindowDecorations,
|
|
545
630
|
activation_shortcut: options.activationShortcut,
|
|
546
631
|
always_on_top: options.alwaysOnTop,
|
|
547
632
|
dark_mode: options.darkMode,
|
|
@@ -587,30 +672,60 @@ async function copyTemplateConfigs() {
|
|
|
587
672
|
}
|
|
588
673
|
}));
|
|
589
674
|
}
|
|
675
|
+
// Replace the CLI's own dist/ with the user's static files while keeping the
|
|
676
|
+
// build artifacts (cli.js) the packaged app does not need but the CLI does.
|
|
677
|
+
// dist_bak always holds the ORIGINAL package dist: once it exists, later
|
|
678
|
+
// stagings must not overwrite it with a previous user tree, or the original
|
|
679
|
+
// files would be unrecoverable across repeated local builds.
|
|
680
|
+
async function stageLocalTree(sourceDir) {
|
|
681
|
+
const distDir = path.join(npmDirectory, 'dist');
|
|
682
|
+
const distBakDir = path.join(npmDirectory, 'dist_bak');
|
|
683
|
+
if (await fsExtra.pathExists(distBakDir)) {
|
|
684
|
+
fsExtra.removeSync(distDir);
|
|
685
|
+
}
|
|
686
|
+
else {
|
|
687
|
+
fsExtra.moveSync(distDir, distBakDir);
|
|
688
|
+
}
|
|
689
|
+
fsExtra.copySync(sourceDir, distDir, { overwrite: true });
|
|
690
|
+
const filesToCopyBack = ['cli.js'];
|
|
691
|
+
await Promise.all(filesToCopyBack.map((file) => fsExtra.copy(path.join(distBakDir, file), path.join(distDir, file))));
|
|
692
|
+
}
|
|
693
|
+
// Exported for unit tests (web fallback and directory entry guard).
|
|
590
694
|
async function handleLocalFile(url, useLocalFile, tauriConf) {
|
|
591
695
|
const pathExists = await fsExtra.pathExists(url);
|
|
592
|
-
if (pathExists) {
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
await Promise.all(filesToCopyBack.map((file) => fsExtra.copy(path.join(distBakDir, file), path.join(distDir, file))));
|
|
696
|
+
if (!pathExists) {
|
|
697
|
+
tauriConf.pake.windows[0].url_type = 'web';
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
const stat = await fsExtra.stat(url);
|
|
701
|
+
if (stat.isDirectory()) {
|
|
702
|
+
// A directory of static web assets (e.g. a generated dist/): the whole
|
|
703
|
+
// tree is packaged and the app entry is its root index.html.
|
|
704
|
+
const entryFile = 'index.html';
|
|
705
|
+
if (!(await fsExtra.pathExists(path.join(url, entryFile)))) {
|
|
706
|
+
throw new PakeError(`Local directory "${url}" has no ${entryFile} at its root.`, {
|
|
707
|
+
code: 'INVALID_INPUT',
|
|
708
|
+
hint: 'Point Pake at the built output directory that contains index.html.',
|
|
709
|
+
});
|
|
607
710
|
}
|
|
608
|
-
|
|
711
|
+
logger.info(`✺ Packaging local directory: ${url}`);
|
|
712
|
+
await stageLocalTree(url);
|
|
713
|
+
tauriConf.pake.windows[0].url = entryFile;
|
|
609
714
|
tauriConf.pake.windows[0].url_type = 'local';
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
logger.info(`✺ Packaging local file: ${url}`);
|
|
718
|
+
const fileName = path.basename(url);
|
|
719
|
+
const distDir = path.join(npmDirectory, 'dist');
|
|
720
|
+
if (!useLocalFile) {
|
|
721
|
+
const urlPath = path.join(distDir, fileName);
|
|
722
|
+
await fsExtra.copy(url, urlPath);
|
|
610
723
|
}
|
|
611
724
|
else {
|
|
612
|
-
|
|
725
|
+
await stageLocalTree(path.dirname(url));
|
|
613
726
|
}
|
|
727
|
+
tauriConf.pake.windows[0].url = fileName;
|
|
728
|
+
tauriConf.pake.windows[0].url_type = 'local';
|
|
614
729
|
}
|
|
615
730
|
function buildLinuxDesktopContent(name, title, linuxBinaryName) {
|
|
616
731
|
const chineseName = title && /[\u4e00-\u9fa5]/.test(title) ? title : null;
|
|
@@ -827,6 +942,9 @@ async function mergeConfig(url, options, tauriConf) {
|
|
|
827
942
|
if (options.hideTitleBar && platform !== 'darwin') {
|
|
828
943
|
logger.warn('✼ --hide-title-bar is only supported on macOS and will be ignored on this platform.');
|
|
829
944
|
}
|
|
945
|
+
if (options.hideWindowDecorations && platform === 'darwin') {
|
|
946
|
+
logger.warn('✼ --hide-window-decorations is only supported on Windows and Linux and will be ignored on this platform.');
|
|
947
|
+
}
|
|
830
948
|
const tauriConfWindowOptions = buildWindowConfigOverrides(options, platform);
|
|
831
949
|
Object.assign(tauriConf.pake.windows[0], { url, ...tauriConfWindowOptions });
|
|
832
950
|
tauriConf.productName = name;
|
|
@@ -1043,8 +1161,61 @@ const APPIMAGE_FAILURE_GUIDANCE = `\n\n${APPIMAGE_BAR}\n` +
|
|
|
1043
1161
|
APPIMAGE_BAR;
|
|
1044
1162
|
class BaseBuilder {
|
|
1045
1163
|
constructor(options) {
|
|
1164
|
+
this.artifacts = [];
|
|
1046
1165
|
this.options = options;
|
|
1047
1166
|
}
|
|
1167
|
+
/** Final artifacts produced by this build, for the `--json` result. */
|
|
1168
|
+
getArtifacts() {
|
|
1169
|
+
return [...this.artifacts];
|
|
1170
|
+
}
|
|
1171
|
+
/** Architecture reported in the `--json` result. */
|
|
1172
|
+
getReportArch() {
|
|
1173
|
+
return this.options.multiArch ? 'universal' : process.arch;
|
|
1174
|
+
}
|
|
1175
|
+
// Drop a recorded artifact whose file was later removed (e.g. the
|
|
1176
|
+
// temporary .deb consumed by zst repacking), so --json never lists a
|
|
1177
|
+
// path that no longer exists.
|
|
1178
|
+
removeArtifact(artifactPath) {
|
|
1179
|
+
const resolved = path.resolve(artifactPath);
|
|
1180
|
+
this.artifacts = this.artifacts.filter((artifact) => artifact.path !== resolved);
|
|
1181
|
+
}
|
|
1182
|
+
async recordArtifact(artifactPath, format) {
|
|
1183
|
+
try {
|
|
1184
|
+
const stat = await fsExtra.stat(artifactPath);
|
|
1185
|
+
let sizeBytes = stat.size;
|
|
1186
|
+
if (stat.isDirectory()) {
|
|
1187
|
+
sizeBytes = await BaseBuilder.getPathSize(artifactPath);
|
|
1188
|
+
}
|
|
1189
|
+
this.artifacts.push({
|
|
1190
|
+
path: path.resolve(artifactPath),
|
|
1191
|
+
sizeBytes,
|
|
1192
|
+
format,
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
catch {
|
|
1196
|
+
// Never fail a finished build over size bookkeeping.
|
|
1197
|
+
this.artifacts.push({
|
|
1198
|
+
path: path.resolve(artifactPath),
|
|
1199
|
+
sizeBytes: 0,
|
|
1200
|
+
format,
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
static async getPathSize(directory) {
|
|
1205
|
+
let size = 0;
|
|
1206
|
+
for (const entry of await fsExtra.readdir(directory, {
|
|
1207
|
+
withFileTypes: true,
|
|
1208
|
+
})) {
|
|
1209
|
+
const entryPath = path.join(directory, entry.name);
|
|
1210
|
+
if (entry.isDirectory()) {
|
|
1211
|
+
size += await BaseBuilder.getPathSize(entryPath);
|
|
1212
|
+
}
|
|
1213
|
+
else if (entry.isFile()) {
|
|
1214
|
+
size += (await fsExtra.stat(entryPath)).size;
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
return size;
|
|
1218
|
+
}
|
|
1048
1219
|
async prepare() {
|
|
1049
1220
|
const tauriSrcPath = path.join(npmDirectory, 'src-tauri');
|
|
1050
1221
|
const tauriTargetPath = path.join(tauriSrcPath, 'target');
|
|
@@ -1055,6 +1226,12 @@ class BaseBuilder {
|
|
|
1055
1226
|
}
|
|
1056
1227
|
ensureRustEnv();
|
|
1057
1228
|
if (!checkRustInstalled()) {
|
|
1229
|
+
if (!isInteractive()) {
|
|
1230
|
+
throw new PakeError('Rust required to package your webapp.', {
|
|
1231
|
+
code: 'ENV_MISSING',
|
|
1232
|
+
hint: 'Install Rust via https://rustup.rs, then rerun the same command.',
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
1058
1235
|
const res = await prompts({
|
|
1059
1236
|
type: 'confirm',
|
|
1060
1237
|
message: 'Rust not detected. Install now?',
|
|
@@ -1064,8 +1241,10 @@ class BaseBuilder {
|
|
|
1064
1241
|
await installRust();
|
|
1065
1242
|
}
|
|
1066
1243
|
else {
|
|
1067
|
-
|
|
1068
|
-
|
|
1244
|
+
throw new PakeError('Rust required to package your webapp.', {
|
|
1245
|
+
code: 'ENV_MISSING',
|
|
1246
|
+
hint: 'Install Rust via https://rustup.rs, then rerun the same command.',
|
|
1247
|
+
});
|
|
1069
1248
|
}
|
|
1070
1249
|
}
|
|
1071
1250
|
const spinner = getSpinner('Installing package...');
|
|
@@ -1124,8 +1303,9 @@ class BaseBuilder {
|
|
|
1124
1303
|
// Let spinner run for a moment so user can see it, then stop before package manager command
|
|
1125
1304
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1126
1305
|
buildSpinner.stop();
|
|
1127
|
-
// Show static message to keep the status visible
|
|
1128
|
-
|
|
1306
|
+
// Show static message to keep the status visible. Info, not warn: warn
|
|
1307
|
+
// entries feed the --json warnings array and this is a status line.
|
|
1308
|
+
logger.info('✸ Building app...');
|
|
1129
1309
|
const baseEnv = getBuildEnvironment();
|
|
1130
1310
|
let buildEnv = {
|
|
1131
1311
|
...(baseEnv ?? {}),
|
|
@@ -1169,6 +1349,7 @@ class BaseBuilder {
|
|
|
1169
1349
|
// executable the build produced instead.
|
|
1170
1350
|
if (this.options.bundle === false) {
|
|
1171
1351
|
await this.copyRawBinary(npmDirectory, name);
|
|
1352
|
+
await this.recordArtifact(this.getRawBinaryPath(name), 'binary');
|
|
1172
1353
|
if (logSuccess) {
|
|
1173
1354
|
logger.success('✔ Build success!');
|
|
1174
1355
|
logger.success('✔ Raw binary located in', path.resolve(this.getRawBinaryPath(name)));
|
|
@@ -1181,9 +1362,11 @@ class BaseBuilder {
|
|
|
1181
1362
|
const appPath = this.getBuildAppPath(npmDirectory, fileName, fileType);
|
|
1182
1363
|
const distPath = path.resolve(`${name}.${fileType}`);
|
|
1183
1364
|
await fsExtra.copy(appPath, distPath);
|
|
1365
|
+
await this.recordArtifact(distPath, fileType);
|
|
1184
1366
|
// Copy raw binary if requested
|
|
1185
1367
|
if (this.options.keepBinary) {
|
|
1186
1368
|
await this.copyRawBinary(npmDirectory, name);
|
|
1369
|
+
await this.recordArtifact(this.getRawBinaryPath(name), 'binary');
|
|
1187
1370
|
}
|
|
1188
1371
|
await fsExtra.remove(appPath);
|
|
1189
1372
|
if (logSuccess) {
|
|
@@ -1210,6 +1393,13 @@ class BaseBuilder {
|
|
|
1210
1393
|
// fsExtra.move uses fs.rename (atomic on same filesystem) and falls back
|
|
1211
1394
|
// to copy+remove only when moving across volumes.
|
|
1212
1395
|
await fsExtra.move(appBundlePath, appDest, { overwrite: true });
|
|
1396
|
+
// Keep the JSON result pointing at where the artifact actually lives.
|
|
1397
|
+
const movedFrom = path.resolve(appBundlePath);
|
|
1398
|
+
for (const artifact of this.artifacts) {
|
|
1399
|
+
if (artifact.path === movedFrom) {
|
|
1400
|
+
artifact.path = appDest;
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1213
1403
|
logger.success(`✔ ${appBundleName.replace(/\.app$/, '')} installed to /Applications`);
|
|
1214
1404
|
}
|
|
1215
1405
|
catch (error) {
|
|
@@ -1425,6 +1615,9 @@ class MacBuilder extends BaseBuilder {
|
|
|
1425
1615
|
}
|
|
1426
1616
|
return `${name}_${tauriConfig.version}_${arch}`;
|
|
1427
1617
|
}
|
|
1618
|
+
getReportArch() {
|
|
1619
|
+
return this.getActualArch();
|
|
1620
|
+
}
|
|
1428
1621
|
getActualArch() {
|
|
1429
1622
|
if (this.buildArch === 'universal' || this.options.multiArch) {
|
|
1430
1623
|
return 'universal';
|
|
@@ -1478,6 +1671,9 @@ class WinBuilder extends BaseBuilder {
|
|
|
1478
1671
|
: this.resolveTargetArch('auto');
|
|
1479
1672
|
this.options.targets = this.buildFormat;
|
|
1480
1673
|
}
|
|
1674
|
+
getReportArch() {
|
|
1675
|
+
return this.buildArch;
|
|
1676
|
+
}
|
|
1481
1677
|
getFileName() {
|
|
1482
1678
|
const { name } = this.options;
|
|
1483
1679
|
const language = tauriConfig.bundle.windows.wix.language[0];
|
|
@@ -1533,6 +1729,9 @@ class LinuxBuilder extends BaseBuilder {
|
|
|
1533
1729
|
}
|
|
1534
1730
|
this.options.targets = this.buildFormat;
|
|
1535
1731
|
}
|
|
1732
|
+
getReportArch() {
|
|
1733
|
+
return this.buildArch;
|
|
1734
|
+
}
|
|
1536
1735
|
getFileName() {
|
|
1537
1736
|
const { name = 'pake-app', targets } = this.options;
|
|
1538
1737
|
const version = tauriConfig.version;
|
|
@@ -1681,12 +1880,14 @@ post_remove() {
|
|
|
1681
1880
|
}
|
|
1682
1881
|
`);
|
|
1683
1882
|
await shellExec(`bsdtar --zstd -cf "${packagePath}" -C "${dataDir}" .PKGINFO .INSTALL usr`);
|
|
1883
|
+
await this.recordArtifact(packagePath, 'zst');
|
|
1684
1884
|
logger.success('✔ Build success!');
|
|
1685
1885
|
logger.success('✔ App installer located in', packagePath);
|
|
1686
1886
|
}
|
|
1687
1887
|
finally {
|
|
1688
1888
|
if (removeSourceDeb) {
|
|
1689
1889
|
await fsExtra.remove(debPath);
|
|
1890
|
+
this.removeArtifact(debPath);
|
|
1690
1891
|
}
|
|
1691
1892
|
await fsExtra.remove(workDir);
|
|
1692
1893
|
}
|
|
@@ -2384,8 +2585,8 @@ async function handleIcon(options, url) {
|
|
|
2384
2585
|
return localIconPath;
|
|
2385
2586
|
}
|
|
2386
2587
|
}
|
|
2387
|
-
// Try favicon from website
|
|
2388
|
-
if (url && options.name) {
|
|
2588
|
+
// Try favicon from website; local file/directory input has no favicon.
|
|
2589
|
+
if (url && options.name && /^https?:\/\//i.test(url)) {
|
|
2389
2590
|
const faviconPath = await tryGetFavicon(url, options.name);
|
|
2390
2591
|
if (faviconPath)
|
|
2391
2592
|
return faviconPath;
|
|
@@ -2628,28 +2829,6 @@ function safeDomainsToRegex(domains) {
|
|
|
2628
2829
|
: '';
|
|
2629
2830
|
}
|
|
2630
2831
|
|
|
2631
|
-
/**
|
|
2632
|
-
* Error class used for user-facing CLI errors.
|
|
2633
|
-
*
|
|
2634
|
-
* The top-level catch in `bin/cli.ts` prints `message` directly without a
|
|
2635
|
-
* stack trace and exits with code 1. Use this for predictable failures
|
|
2636
|
-
* (invalid names, missing files, etc.) so users see a clean message instead
|
|
2637
|
-
* of a Node.js stack dump.
|
|
2638
|
-
*/
|
|
2639
|
-
class PakeError extends Error {
|
|
2640
|
-
constructor(message) {
|
|
2641
|
-
super(message);
|
|
2642
|
-
this.isUserError = true;
|
|
2643
|
-
this.name = 'PakeError';
|
|
2644
|
-
}
|
|
2645
|
-
}
|
|
2646
|
-
function isPakeError(error) {
|
|
2647
|
-
return (error instanceof PakeError ||
|
|
2648
|
-
(typeof error === 'object' &&
|
|
2649
|
-
error !== null &&
|
|
2650
|
-
error.isUserError === true));
|
|
2651
|
-
}
|
|
2652
|
-
|
|
2653
2832
|
function resolveAppName(name, platform) {
|
|
2654
2833
|
const domain = getDomain(name) || 'pake';
|
|
2655
2834
|
return platform !== 'linux' ? capitalizeFirstLetter(domain) : domain;
|
|
@@ -2681,9 +2860,14 @@ async function handleOptions(options, url) {
|
|
|
2681
2860
|
const defaultName = pathExists
|
|
2682
2861
|
? resolveLocalAppName(url, platform)
|
|
2683
2862
|
: resolveAppName(url, platform);
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2863
|
+
if (isInteractive()) {
|
|
2864
|
+
const promptMessage = 'Enter your application name';
|
|
2865
|
+
const namePrompt = await promptText(promptMessage, defaultName);
|
|
2866
|
+
name = namePrompt?.trim() || defaultName;
|
|
2867
|
+
}
|
|
2868
|
+
else {
|
|
2869
|
+
name = defaultName;
|
|
2870
|
+
}
|
|
2687
2871
|
}
|
|
2688
2872
|
if (name && platform === 'linux') {
|
|
2689
2873
|
name = generateLinuxPackageName(name);
|
|
@@ -2727,7 +2911,9 @@ const DEFAULT_PAKE_OPTIONS = {
|
|
|
2727
2911
|
width: 1200,
|
|
2728
2912
|
fullscreen: false,
|
|
2729
2913
|
maximize: false,
|
|
2914
|
+
resizable: true,
|
|
2730
2915
|
hideTitleBar: false,
|
|
2916
|
+
hideWindowDecorations: false,
|
|
2731
2917
|
alwaysOnTop: false,
|
|
2732
2918
|
appVersion: '1.0.0',
|
|
2733
2919
|
darkMode: false,
|
|
@@ -2752,6 +2938,7 @@ const DEFAULT_PAKE_OPTIONS = {
|
|
|
2752
2938
|
systemTrayIcon: '',
|
|
2753
2939
|
proxyUrl: '',
|
|
2754
2940
|
debug: false,
|
|
2941
|
+
json: false,
|
|
2755
2942
|
inject: [],
|
|
2756
2943
|
installerLanguage: 'en-US',
|
|
2757
2944
|
hideOnClose: undefined, // Platform-specific: true for macOS, false for others
|
|
@@ -2791,9 +2978,16 @@ function validateNumberInput(value) {
|
|
|
2791
2978
|
}
|
|
2792
2979
|
return parsedValue;
|
|
2793
2980
|
}
|
|
2981
|
+
// Path-shaped input (./x, ../x, /x, ~/x, C:\x). A missing path must fail
|
|
2982
|
+
// loudly: appending https:// to "./typo" would otherwise produce a valid URL
|
|
2983
|
+
// like https://./typo and a silently broken app (worst case for agents).
|
|
2984
|
+
const PATH_LIKE_PATTERN = /^(\.{1,2}[\\/]|[\\/]|~[\\/]|[a-zA-Z]:[\\/])/;
|
|
2794
2985
|
function validateUrlInput(url) {
|
|
2795
2986
|
const isFile = fs.existsSync(url);
|
|
2796
2987
|
if (!isFile) {
|
|
2988
|
+
if (PATH_LIKE_PATTERN.test(url)) {
|
|
2989
|
+
throw new InvalidArgumentError(`Local path "${url}" does not exist. Check the path, or pass a web URL instead.`);
|
|
2990
|
+
}
|
|
2797
2991
|
try {
|
|
2798
2992
|
return normalizeUrl(url);
|
|
2799
2993
|
}
|
|
@@ -2829,6 +3023,7 @@ ${green('|_| \\__,_|_|\\_\\___| can turn any webpage into a desktop app with
|
|
|
2829
3023
|
.option('--use-local-file', 'Use local file packaging', DEFAULT_PAKE_OPTIONS.useLocalFile)
|
|
2830
3024
|
.option('--fullscreen', 'Start in full screen', DEFAULT_PAKE_OPTIONS.fullscreen)
|
|
2831
3025
|
.option('--hide-title-bar', 'For Mac, hide title bar', DEFAULT_PAKE_OPTIONS.hideTitleBar)
|
|
3026
|
+
.option('--hide-window-decorations', 'Hide native window decorations on Windows and Linux', DEFAULT_PAKE_OPTIONS.hideWindowDecorations)
|
|
2832
3027
|
.option('--multi-arch', 'For Mac, both Intel and M1', DEFAULT_PAKE_OPTIONS.multiArch)
|
|
2833
3028
|
.option('--inject <files>', 'Inject local CSS/JS files into the page', (val, previous) => {
|
|
2834
3029
|
if (!val)
|
|
@@ -2842,6 +3037,8 @@ ${green('|_| \\__,_|_|\\_\\___| can turn any webpage into a desktop app with
|
|
|
2842
3037
|
return previous ? [...previous, ...files] : files;
|
|
2843
3038
|
}, DEFAULT_PAKE_OPTIONS.inject)
|
|
2844
3039
|
.option('--debug', 'Debug build and more output', DEFAULT_PAKE_OPTIONS.debug)
|
|
3040
|
+
.option('--json', 'Machine-readable output: logs to stderr, one JSON result on stdout', DEFAULT_PAKE_OPTIONS.json)
|
|
3041
|
+
.option('--config <path>', 'Load options from a JSON config file (fields mirror CLI options, see schema/pake.schema.json)')
|
|
2845
3042
|
.addOption(new Option('--proxy-url <url>', 'Proxy URL for all network requests (http://, https://, socks5://)')
|
|
2846
3043
|
.default(DEFAULT_PAKE_OPTIONS.proxyUrl)
|
|
2847
3044
|
.hideHelp())
|
|
@@ -2974,16 +3171,181 @@ ${green('|_| \\__,_|_|\\_\\___| can turn any webpage into a desktop app with
|
|
|
2974
3171
|
});
|
|
2975
3172
|
}
|
|
2976
3173
|
|
|
3174
|
+
// Invocation concerns, not app manifest fields; pass these as CLI flags.
|
|
3175
|
+
const REJECTED_KEYS = new Set(['config', 'json', 'version']);
|
|
3176
|
+
// Optional CLI options that have no entry in DEFAULT_PAKE_OPTIONS.
|
|
3177
|
+
const EXTRA_STRING_KEYS = new Set(['name', 'title', 'identifier']);
|
|
3178
|
+
function expectedTypeFor(key) {
|
|
3179
|
+
if (key === 'inject')
|
|
3180
|
+
return 'string[]';
|
|
3181
|
+
if (key === 'hideOnClose')
|
|
3182
|
+
return 'boolean';
|
|
3183
|
+
if (EXTRA_STRING_KEYS.has(key))
|
|
3184
|
+
return 'string';
|
|
3185
|
+
const defaultValue = DEFAULT_PAKE_OPTIONS[key];
|
|
3186
|
+
const type = typeof defaultValue;
|
|
3187
|
+
if (type === 'string' || type === 'number' || type === 'boolean') {
|
|
3188
|
+
return type;
|
|
3189
|
+
}
|
|
3190
|
+
return null;
|
|
3191
|
+
}
|
|
3192
|
+
function matchesType(value, type) {
|
|
3193
|
+
if (type === 'string[]') {
|
|
3194
|
+
return Array.isArray(value) && value.every((v) => typeof v === 'string');
|
|
3195
|
+
}
|
|
3196
|
+
return typeof value === type;
|
|
3197
|
+
}
|
|
3198
|
+
async function loadConfigFile(configPath, validKeys) {
|
|
3199
|
+
if (!(await fsExtra.pathExists(configPath))) {
|
|
3200
|
+
throw new PakeError(`Config file not found: ${configPath}`, {
|
|
3201
|
+
code: 'INVALID_INPUT',
|
|
3202
|
+
hint: 'Pass a path to a JSON file matching schema/pake.schema.json.',
|
|
3203
|
+
});
|
|
3204
|
+
}
|
|
3205
|
+
let parsed;
|
|
3206
|
+
try {
|
|
3207
|
+
parsed = JSON.parse(await fsExtra.readFile(configPath, 'utf8'));
|
|
3208
|
+
}
|
|
3209
|
+
catch (error) {
|
|
3210
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
3211
|
+
throw new PakeError(`Config file is not valid JSON: ${detail}`, {
|
|
3212
|
+
code: 'INVALID_INPUT',
|
|
3213
|
+
hint: `Fix the JSON syntax in ${configPath}.`,
|
|
3214
|
+
});
|
|
3215
|
+
}
|
|
3216
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
3217
|
+
throw new PakeError('Config file must contain a JSON object.', {
|
|
3218
|
+
code: 'INVALID_INPUT',
|
|
3219
|
+
hint: 'See schema/pake.schema.json for the expected shape.',
|
|
3220
|
+
});
|
|
3221
|
+
}
|
|
3222
|
+
const result = { options: {} };
|
|
3223
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
3224
|
+
if (key === '$schema')
|
|
3225
|
+
continue;
|
|
3226
|
+
if (key === 'url') {
|
|
3227
|
+
if (typeof value !== 'string') {
|
|
3228
|
+
throw new PakeError('Config field "url" must be a string.', {
|
|
3229
|
+
code: 'INVALID_INPUT',
|
|
3230
|
+
hint: 'Use a web URL or a local file/directory path.',
|
|
3231
|
+
});
|
|
3232
|
+
}
|
|
3233
|
+
result.url = value;
|
|
3234
|
+
continue;
|
|
3235
|
+
}
|
|
3236
|
+
if (REJECTED_KEYS.has(key)) {
|
|
3237
|
+
throw new PakeError(`Config field "${key}" is not allowed in a config file.`, {
|
|
3238
|
+
code: 'INVALID_INPUT',
|
|
3239
|
+
hint: `Pass --${key} on the command line instead.`,
|
|
3240
|
+
});
|
|
3241
|
+
}
|
|
3242
|
+
if (!validKeys.has(key)) {
|
|
3243
|
+
throw new PakeError(`Unknown config field "${key}".`, {
|
|
3244
|
+
code: 'INVALID_INPUT',
|
|
3245
|
+
hint: 'Field names are camelCase CLI option names; see schema/pake.schema.json.',
|
|
3246
|
+
});
|
|
3247
|
+
}
|
|
3248
|
+
const expected = expectedTypeFor(key);
|
|
3249
|
+
if (expected && !matchesType(value, expected)) {
|
|
3250
|
+
throw new PakeError(`Config field "${key}" must be of type ${expected}.`, {
|
|
3251
|
+
code: 'INVALID_INPUT',
|
|
3252
|
+
hint: 'See schema/pake.schema.json for field types.',
|
|
3253
|
+
});
|
|
3254
|
+
}
|
|
3255
|
+
if (!expected && (typeof value === 'object' || value === null)) {
|
|
3256
|
+
throw new PakeError(`Config field "${key}" must be a string, number, or boolean.`, {
|
|
3257
|
+
code: 'INVALID_INPUT',
|
|
3258
|
+
hint: 'See schema/pake.schema.json for field types.',
|
|
3259
|
+
});
|
|
3260
|
+
}
|
|
3261
|
+
result.options[key] = value;
|
|
3262
|
+
}
|
|
3263
|
+
return result;
|
|
3264
|
+
}
|
|
3265
|
+
|
|
2977
3266
|
const program = getCliProgram();
|
|
3267
|
+
// Make commander throw instead of exiting so option/argument parse errors
|
|
3268
|
+
// honor the exit-code contract (2 = invalid input) and still emit the JSON
|
|
3269
|
+
// result object when --json was requested.
|
|
3270
|
+
program.exitOverride();
|
|
3271
|
+
function isCommanderExit(error) {
|
|
3272
|
+
return (typeof error === 'object' &&
|
|
3273
|
+
error !== null &&
|
|
3274
|
+
typeof error.code === 'string' &&
|
|
3275
|
+
error.code.startsWith('commander.'));
|
|
3276
|
+
}
|
|
3277
|
+
const PHASE_ERROR_CODES = {
|
|
3278
|
+
input: 'INVALID_INPUT',
|
|
3279
|
+
prepare: 'ENV_MISSING',
|
|
3280
|
+
build: 'BUILD_FAILED',
|
|
3281
|
+
};
|
|
3282
|
+
function classifyError(error, phase) {
|
|
3283
|
+
if (isPakeError(error)) {
|
|
3284
|
+
return {
|
|
3285
|
+
code: error.code ?? PHASE_ERROR_CODES[phase],
|
|
3286
|
+
message: error.message,
|
|
3287
|
+
hint: error.hint ?? null,
|
|
3288
|
+
};
|
|
3289
|
+
}
|
|
3290
|
+
if (error instanceof Error) {
|
|
3291
|
+
return {
|
|
3292
|
+
code: PHASE_ERROR_CODES[phase],
|
|
3293
|
+
message: error.message,
|
|
3294
|
+
hint: null,
|
|
3295
|
+
};
|
|
3296
|
+
}
|
|
3297
|
+
return {
|
|
3298
|
+
code: 'UNEXPECTED',
|
|
3299
|
+
message: `Unexpected error: ${String(error)}`,
|
|
3300
|
+
hint: null,
|
|
3301
|
+
};
|
|
3302
|
+
}
|
|
2978
3303
|
async function checkUpdateTips() {
|
|
2979
3304
|
updateNotifier({ pkg: packageJson, updateCheckInterval: 1000 * 60 }).notify({
|
|
2980
3305
|
isGlobal: true,
|
|
2981
3306
|
});
|
|
2982
3307
|
}
|
|
2983
|
-
program.action(async (
|
|
3308
|
+
program.action(async (urlArg, options) => {
|
|
3309
|
+
const jsonMode = Boolean(options.json);
|
|
3310
|
+
if (jsonMode) {
|
|
3311
|
+
enableMachineMode();
|
|
3312
|
+
}
|
|
3313
|
+
let phase = 'input';
|
|
3314
|
+
let appName = null;
|
|
3315
|
+
let url = urlArg;
|
|
2984
3316
|
try {
|
|
2985
|
-
|
|
3317
|
+
if (!jsonMode) {
|
|
3318
|
+
await checkUpdateTips();
|
|
3319
|
+
}
|
|
3320
|
+
// Config file fills in whatever the command line did not set explicitly:
|
|
3321
|
+
// CLI flag > config field > built-in default.
|
|
3322
|
+
if (options.config) {
|
|
3323
|
+
const validKeys = new Set(program.options.map((option) => option.attributeName()));
|
|
3324
|
+
const loaded = await loadConfigFile(options.config, validKeys);
|
|
3325
|
+
for (const [key, value] of Object.entries(loaded.options)) {
|
|
3326
|
+
if (program.getOptionValueSource(key) !== 'cli') {
|
|
3327
|
+
options[key] = value;
|
|
3328
|
+
}
|
|
3329
|
+
}
|
|
3330
|
+
if (!url && loaded.url) {
|
|
3331
|
+
try {
|
|
3332
|
+
url = validateUrlInput(loaded.url);
|
|
3333
|
+
}
|
|
3334
|
+
catch (error) {
|
|
3335
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
3336
|
+
throw new PakeError(`Invalid "url" in config file: ${detail}`, {
|
|
3337
|
+
code: 'INVALID_INPUT',
|
|
3338
|
+
});
|
|
3339
|
+
}
|
|
3340
|
+
}
|
|
3341
|
+
}
|
|
2986
3342
|
if (!url) {
|
|
3343
|
+
if (jsonMode) {
|
|
3344
|
+
throw new PakeError('No URL or local path to package.', {
|
|
3345
|
+
code: 'INVALID_INPUT',
|
|
3346
|
+
hint: 'Pass a URL/path argument or a config file with a "url" field.',
|
|
3347
|
+
});
|
|
3348
|
+
}
|
|
2987
3349
|
program.help({
|
|
2988
3350
|
error: false,
|
|
2989
3351
|
});
|
|
@@ -2995,13 +3357,47 @@ program.action(async (url, options) => {
|
|
|
2995
3357
|
log.setLevel('debug');
|
|
2996
3358
|
}
|
|
2997
3359
|
const appOptions = await handleOptions(options, url);
|
|
3360
|
+
appName = appOptions.name ?? null;
|
|
2998
3361
|
const builder = BuilderProvider.create(appOptions);
|
|
3362
|
+
phase = 'prepare';
|
|
2999
3363
|
await builder.prepare();
|
|
3364
|
+
phase = 'build';
|
|
3000
3365
|
await builder.build(url);
|
|
3366
|
+
if (jsonMode) {
|
|
3367
|
+
printJsonResult({
|
|
3368
|
+
ok: true,
|
|
3369
|
+
name: appName,
|
|
3370
|
+
platform: process.platform,
|
|
3371
|
+
arch: builder.getReportArch(),
|
|
3372
|
+
outputs: builder.getArtifacts(),
|
|
3373
|
+
warnings: getCapturedWarnings(),
|
|
3374
|
+
error: null,
|
|
3375
|
+
});
|
|
3376
|
+
}
|
|
3001
3377
|
}
|
|
3002
3378
|
catch (error) {
|
|
3003
|
-
|
|
3004
|
-
|
|
3379
|
+
// program.help() and --help/--version throw under exitOverride with
|
|
3380
|
+
// exitCode 0; a clean commander exit is not a failure.
|
|
3381
|
+
if (isCommanderExit(error) && error.exitCode === 0) {
|
|
3382
|
+
process.exit(0);
|
|
3383
|
+
}
|
|
3384
|
+
const classified = classifyError(error, phase);
|
|
3385
|
+
if (jsonMode) {
|
|
3386
|
+
printJsonResult({
|
|
3387
|
+
ok: false,
|
|
3388
|
+
name: appName,
|
|
3389
|
+
platform: process.platform,
|
|
3390
|
+
arch: null,
|
|
3391
|
+
outputs: [],
|
|
3392
|
+
warnings: getCapturedWarnings(),
|
|
3393
|
+
error: classified,
|
|
3394
|
+
});
|
|
3395
|
+
}
|
|
3396
|
+
else if (isPakeError(error)) {
|
|
3397
|
+
console.error(chalk.red(classified.message));
|
|
3398
|
+
if (classified.hint) {
|
|
3399
|
+
console.error(chalk.yellow(`✼ ${classified.hint}`));
|
|
3400
|
+
}
|
|
3005
3401
|
}
|
|
3006
3402
|
else if (error instanceof Error) {
|
|
3007
3403
|
console.error(chalk.red(`✕ ${error.message}`));
|
|
@@ -3012,10 +3408,35 @@ program.action(async (url, options) => {
|
|
|
3012
3408
|
else {
|
|
3013
3409
|
console.error(chalk.red(`✕ Unexpected error: ${String(error)}`));
|
|
3014
3410
|
}
|
|
3015
|
-
process.exit(
|
|
3411
|
+
process.exit(ERROR_EXIT_CODES[classified.code]);
|
|
3016
3412
|
}
|
|
3017
3413
|
});
|
|
3018
3414
|
program.parseAsync().catch((error) => {
|
|
3415
|
+
if (isCommanderExit(error)) {
|
|
3416
|
+
// --help / --version and friends exit clean; commander already printed.
|
|
3417
|
+
if (error.exitCode === 0) {
|
|
3418
|
+
process.exit(0);
|
|
3419
|
+
}
|
|
3420
|
+
// Parse errors (unknown option, invalid argument, missing value) are
|
|
3421
|
+
// invalid input. Commander already printed the message to stderr; in
|
|
3422
|
+
// json mode also emit the machine-readable result on stdout.
|
|
3423
|
+
if (process.argv.includes('--json')) {
|
|
3424
|
+
printJsonResult({
|
|
3425
|
+
ok: false,
|
|
3426
|
+
name: null,
|
|
3427
|
+
platform: process.platform,
|
|
3428
|
+
arch: null,
|
|
3429
|
+
outputs: [],
|
|
3430
|
+
warnings: [],
|
|
3431
|
+
error: {
|
|
3432
|
+
code: 'INVALID_INPUT',
|
|
3433
|
+
message: error.message.trim(),
|
|
3434
|
+
hint: 'Run pake --help for the accepted options.',
|
|
3435
|
+
},
|
|
3436
|
+
});
|
|
3437
|
+
}
|
|
3438
|
+
process.exit(ERROR_EXIT_CODES.INVALID_INPUT);
|
|
3439
|
+
}
|
|
3019
3440
|
if (error instanceof Error) {
|
|
3020
3441
|
console.error(chalk.red(`✕ ${error.message}`));
|
|
3021
3442
|
}
|
package/package.json
CHANGED
package/src-tauri/Cargo.lock
CHANGED
package/src-tauri/Cargo.toml
CHANGED
|
Binary file
|
|
Binary file
|
package/src-tauri/pake.json
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -22,7 +22,10 @@ pub fn set_system_tray(
|
|
|
22
22
|
return Ok(());
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
// Menu events are broadcast to every handler in Tauri v2, so the tray item
|
|
26
|
+
// must not share the "new_window" id with the app menu accelerator
|
|
27
|
+
// (Cmd/Ctrl+N), or one click opens two windows.
|
|
28
|
+
let new_window = MenuItemBuilder::with_id("tray_new_window", "New Window").build(app)?;
|
|
26
29
|
let hide_app = MenuItemBuilder::with_id("hide_app", "Hide").build(app)?;
|
|
27
30
|
let show_app = MenuItemBuilder::with_id("show_app", "Show").build(app)?;
|
|
28
31
|
let quit = MenuItemBuilder::with_id("quit", "Quit").build(app)?;
|
|
@@ -42,7 +45,7 @@ pub fn set_system_tray(
|
|
|
42
45
|
let mut tray_builder = TrayIconBuilder::new()
|
|
43
46
|
.menu(&menu)
|
|
44
47
|
.on_menu_event(move |app, event| match event.id().as_ref() {
|
|
45
|
-
"
|
|
48
|
+
"tray_new_window" => {
|
|
46
49
|
open_additional_window_safe(app);
|
|
47
50
|
}
|
|
48
51
|
"hide_app" => {
|
|
@@ -380,6 +380,10 @@ fn build_window(
|
|
|
380
380
|
{
|
|
381
381
|
window_builder = window_builder.data_directory(_data_dir).theme(theme);
|
|
382
382
|
|
|
383
|
+
if window_config.hide_window_decorations {
|
|
384
|
+
window_builder = window_builder.decorations(false);
|
|
385
|
+
}
|
|
386
|
+
|
|
383
387
|
if !config.proxy_url.is_empty() {
|
|
384
388
|
if let Ok(proxy_url) = Url::from_str(&config.proxy_url) {
|
|
385
389
|
parsed_proxy_url = Some(proxy_url.clone());
|
|
@@ -15,25 +15,32 @@ function setZoom(zoom) {
|
|
|
15
15
|
// CSS hacks. `transform: scale` and `html.style.zoom` break complex SPAs like
|
|
16
16
|
// ChatGPT: the page shifts right on Windows and parts of the UI stop repainting
|
|
17
17
|
// on macOS. Native zoom recalculates layout exactly like a browser does.
|
|
18
|
+
const zoomPercent = normalizeZoomPercent(zoom);
|
|
19
|
+
const normalizedZoom = `${zoomPercent}%`;
|
|
18
20
|
const invoke = window.__TAURI__?.core?.invoke;
|
|
19
21
|
if (invoke) {
|
|
20
|
-
invoke("set_zoom", { percent:
|
|
22
|
+
invoke("set_zoom", { percent: zoomPercent }).catch(() => {});
|
|
21
23
|
}
|
|
22
24
|
|
|
23
|
-
window.localStorage.setItem("htmlZoom",
|
|
25
|
+
window.localStorage.setItem("htmlZoom", normalizedZoom);
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
function zoomCommon(zoomChange) {
|
|
27
29
|
const currentZoom = window.localStorage.getItem("htmlZoom") || "100%";
|
|
28
|
-
setZoom(zoomChange(currentZoom));
|
|
30
|
+
setZoom(zoomChange(normalizeZoomPercent(currentZoom)));
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
function zoomIn() {
|
|
32
|
-
zoomCommon((currentZoom) => `${Math.min(
|
|
34
|
+
zoomCommon((currentZoom) => `${Math.min(currentZoom + 10, 200)}%`);
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
function zoomOut() {
|
|
36
|
-
zoomCommon((currentZoom) => `${Math.max(
|
|
38
|
+
zoomCommon((currentZoom) => `${Math.max(currentZoom - 10, 30)}%`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function normalizeZoomPercent(zoom) {
|
|
42
|
+
const parsed = parseFloat(zoom);
|
|
43
|
+
return Number.isFinite(parsed) ? parsed : 100;
|
|
37
44
|
}
|
|
38
45
|
|
|
39
46
|
let pasteAsPlainTextPending = false;
|
|
@@ -53,8 +60,51 @@ function handleShortcut(event) {
|
|
|
53
60
|
}
|
|
54
61
|
}
|
|
55
62
|
|
|
63
|
+
function toggleNativeFullscreen(appWindow) {
|
|
64
|
+
appWindow
|
|
65
|
+
.isFullscreen()
|
|
66
|
+
.then((fullscreen) => appWindow.setFullscreen(!fullscreen))
|
|
67
|
+
.catch((error) => {
|
|
68
|
+
console.warn("[Pake] Failed to toggle native fullscreen:", error);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function handleWindowFullscreenShortcut(event) {
|
|
73
|
+
if (
|
|
74
|
+
!event.isTrusted ||
|
|
75
|
+
event.repeat ||
|
|
76
|
+
event.key !== "F11" ||
|
|
77
|
+
!isNonMacDesktop()
|
|
78
|
+
) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const appWindow = window.__TAURI__?.window?.getCurrentWindow?.();
|
|
83
|
+
if (!appWindow) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
event.preventDefault();
|
|
88
|
+
event.stopImmediatePropagation();
|
|
89
|
+
toggleNativeFullscreen(appWindow);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function getDesktopPlatform() {
|
|
93
|
+
return (
|
|
94
|
+
navigator.userAgentData?.platform ||
|
|
95
|
+
navigator.platform ||
|
|
96
|
+
navigator.userAgent
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
56
100
|
function isNonMacDesktop() {
|
|
57
|
-
return /
|
|
101
|
+
return /win|linux/i.test(getDesktopPlatform());
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function hasImmersiveHeader(config = window["pakeConfig"] || {}) {
|
|
105
|
+
return /mac/i.test(getDesktopPlatform())
|
|
106
|
+
? config.hide_title_bar === true
|
|
107
|
+
: config.hide_window_decorations === true;
|
|
58
108
|
}
|
|
59
109
|
|
|
60
110
|
function isEditableElement(element) {
|
|
@@ -156,19 +206,9 @@ function insertTextIntoEditableElement(element, text) {
|
|
|
156
206
|
return false;
|
|
157
207
|
}
|
|
158
208
|
|
|
159
|
-
|
|
160
|
-
try {
|
|
161
|
-
return document.execCommand("paste") === true;
|
|
162
|
-
} catch (error) {
|
|
163
|
-
return false;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
209
|
+
let clipboardPasteFallbackTarget;
|
|
166
210
|
|
|
167
211
|
function pasteClipboardText(activeElement) {
|
|
168
|
-
if (runBrowserPasteCommand()) {
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
212
|
const readText = navigator.clipboard?.readText;
|
|
173
213
|
if (typeof readText !== "function") {
|
|
174
214
|
return;
|
|
@@ -211,9 +251,11 @@ function handleClipboardShortcut(event) {
|
|
|
211
251
|
}
|
|
212
252
|
|
|
213
253
|
if (key === "v" && canPasteIntoEditableElement(activeElement)) {
|
|
214
|
-
event
|
|
215
|
-
|
|
216
|
-
|
|
254
|
+
// Let the native WebView paste event run first so images, files, and rich
|
|
255
|
+
// clipboard formats remain intact. If the platform does not emit paste,
|
|
256
|
+
// keyup applies the existing text-only fallback.
|
|
257
|
+
clipboardPasteFallbackTarget = activeElement;
|
|
258
|
+
return false;
|
|
217
259
|
}
|
|
218
260
|
|
|
219
261
|
if (key === "a" && isEditable && selectEditableElement(activeElement)) {
|
|
@@ -224,6 +266,42 @@ function handleClipboardShortcut(event) {
|
|
|
224
266
|
return false;
|
|
225
267
|
}
|
|
226
268
|
|
|
269
|
+
function handleClipboardPasteFallback(event) {
|
|
270
|
+
if (
|
|
271
|
+
event.isTrusted !== true ||
|
|
272
|
+
!isNonMacDesktop() ||
|
|
273
|
+
event.key?.toLowerCase() !== "v"
|
|
274
|
+
) {
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const activeElement = clipboardPasteFallbackTarget;
|
|
279
|
+
clipboardPasteFallbackTarget = undefined;
|
|
280
|
+
if (
|
|
281
|
+
!activeElement ||
|
|
282
|
+
document.activeElement !== activeElement ||
|
|
283
|
+
!canPasteIntoEditableElement(activeElement)
|
|
284
|
+
) {
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
pasteClipboardText(activeElement);
|
|
289
|
+
return true;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function handlePaste(event) {
|
|
293
|
+
clipboardPasteFallbackTarget = undefined;
|
|
294
|
+
if (!pasteAsPlainTextPending) return;
|
|
295
|
+
|
|
296
|
+
event.preventDefault();
|
|
297
|
+
event.stopImmediatePropagation();
|
|
298
|
+
|
|
299
|
+
const text = event.clipboardData?.getData("text/plain") || "";
|
|
300
|
+
if (text) {
|
|
301
|
+
document.execCommand("insertText", false, text);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
227
305
|
const DOWNLOADABLE_FILE_EXTENSIONS = {
|
|
228
306
|
documents: [
|
|
229
307
|
"pdf",
|
|
@@ -502,7 +580,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
502
580
|
}
|
|
503
581
|
}
|
|
504
582
|
|
|
505
|
-
if (!document.getElementById("pake-top-dom")) {
|
|
583
|
+
if (!document.getElementById("pake-top-dom") && hasImmersiveHeader()) {
|
|
506
584
|
const topDom = document.createElement("div");
|
|
507
585
|
topDom.id = "pake-top-dom";
|
|
508
586
|
document.body.appendChild(topDom);
|
|
@@ -510,24 +588,25 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
510
588
|
|
|
511
589
|
const domEl = document.getElementById("pake-top-dom");
|
|
512
590
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
});
|
|
516
|
-
|
|
517
|
-
domEl.addEventListener("mousedown", (e) => {
|
|
518
|
-
e.preventDefault();
|
|
519
|
-
if (e.buttons === 1 && e.detail !== 2) {
|
|
591
|
+
if (domEl) {
|
|
592
|
+
domEl.addEventListener("touchstart", () => {
|
|
520
593
|
appWindow.startDragging();
|
|
521
|
-
}
|
|
522
|
-
});
|
|
594
|
+
});
|
|
523
595
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
596
|
+
domEl.addEventListener("mousedown", (e) => {
|
|
597
|
+
e.preventDefault();
|
|
598
|
+
if (e.buttons === 1 && e.detail !== 2) {
|
|
599
|
+
appWindow.startDragging();
|
|
600
|
+
}
|
|
527
601
|
});
|
|
528
|
-
|
|
602
|
+
|
|
603
|
+
domEl.addEventListener("dblclick", () => {
|
|
604
|
+
toggleNativeFullscreen(appWindow);
|
|
605
|
+
});
|
|
606
|
+
}
|
|
529
607
|
|
|
530
608
|
if (window["pakeConfig"]?.disabled_web_shortcuts !== true) {
|
|
609
|
+
document.addEventListener("keydown", handleWindowFullscreenShortcut, true);
|
|
531
610
|
document.addEventListener("keyup", (event) => {
|
|
532
611
|
if (/windows|linux/i.test(navigator.userAgent) && event.ctrlKey) {
|
|
533
612
|
handleShortcut(event);
|
|
@@ -539,22 +618,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
539
618
|
}
|
|
540
619
|
|
|
541
620
|
document.addEventListener("keydown", handleClipboardShortcut, true);
|
|
542
|
-
|
|
543
|
-
document.addEventListener(
|
|
544
|
-
"paste",
|
|
545
|
-
(event) => {
|
|
546
|
-
if (pasteAsPlainTextPending) {
|
|
547
|
-
event.preventDefault();
|
|
548
|
-
event.stopImmediatePropagation();
|
|
549
|
-
|
|
550
|
-
const text = event.clipboardData?.getData("text/plain") || "";
|
|
551
|
-
if (text) {
|
|
552
|
-
document.execCommand("insertText", false, text);
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
},
|
|
556
|
-
true,
|
|
557
|
-
);
|
|
621
|
+
document.addEventListener("keyup", handleClipboardPasteFallback, true);
|
|
622
|
+
document.addEventListener("paste", handlePaste, true);
|
|
558
623
|
|
|
559
624
|
// Trigger a native browser download via a transient anchor click. The Rust
|
|
560
625
|
// on_download handler then writes the file to the Downloads folder. This is
|
|
@@ -403,6 +403,11 @@ window.addEventListener("DOMContentLoaded", (_event) => {
|
|
|
403
403
|
padding-top: 0px;
|
|
404
404
|
}
|
|
405
405
|
|
|
406
|
+
#notion-app .notion-sidebar,#notion-app .notion-topbar{
|
|
407
|
+
padding-top: 20px;
|
|
408
|
+
box-sizing: content-box;
|
|
409
|
+
}
|
|
410
|
+
|
|
406
411
|
#header-area > div > .css-gtiexd > div:nth-child(1) > div, #header-area .logoIcon .user-info{
|
|
407
412
|
padding-top: 20px;
|
|
408
413
|
}
|
|
@@ -497,9 +502,29 @@ window.addEventListener("DOMContentLoaded", (_event) => {
|
|
|
497
502
|
}
|
|
498
503
|
`;
|
|
499
504
|
const isMac = /Mac/i.test(navigator.userAgent);
|
|
500
|
-
if (window["pakeConfig"]
|
|
505
|
+
if (hasImmersiveHeader(window["pakeConfig"])) {
|
|
501
506
|
const topPaddingStyleElement = document.createElement("style");
|
|
502
|
-
topPaddingStyleElement.textContent =
|
|
507
|
+
topPaddingStyleElement.textContent = isMac
|
|
508
|
+
? topPaddingCSS
|
|
509
|
+
: `
|
|
510
|
+
#pake-top-dom:active {
|
|
511
|
+
cursor: grabbing;
|
|
512
|
+
cursor: -webkit-grabbing;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
#pake-top-dom {
|
|
516
|
+
position: fixed;
|
|
517
|
+
background: transparent;
|
|
518
|
+
top: 0;
|
|
519
|
+
width: 100%;
|
|
520
|
+
height: 20px;
|
|
521
|
+
cursor: grab;
|
|
522
|
+
-webkit-app-region: drag;
|
|
523
|
+
user-select: none;
|
|
524
|
+
-webkit-user-select: none;
|
|
525
|
+
z-index: 99999;
|
|
526
|
+
}
|
|
527
|
+
`;
|
|
503
528
|
document.head.appendChild(topPaddingStyleElement);
|
|
504
529
|
}
|
|
505
530
|
});
|