pake-cli 3.14.0 โ 3.15.1
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 +2 -0
- package/dist/cli.js +553 -56
- package/package.json +1 -1
- package/src-tauri/Cargo.lock +1 -1
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/src/app/setup.rs +5 -2
- package/src-tauri/src/inject/event.js +59 -30
- package/src-tauri/tauri.conf.json +1 -1
package/README.md
CHANGED
|
@@ -192,6 +192,8 @@ pake https://weekly.tw93.fun --name Weekly --icon https://cdn.tw93.fun/pake/week
|
|
|
192
192
|
|
|
193
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).
|
|
194
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
|
+
|
|
195
197
|
## Development
|
|
196
198
|
|
|
197
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.1";
|
|
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.
|
|
@@ -589,30 +672,108 @@ async function copyTemplateConfigs() {
|
|
|
589
672
|
}
|
|
590
673
|
}));
|
|
591
674
|
}
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
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
|
+
// Resolve symlinked input up front: staging must produce a real copy, or
|
|
684
|
+
// the cli.js copy-back below would write through the link into the user's
|
|
685
|
+
// own directory.
|
|
686
|
+
const resolvedSource = await fsExtra.realpath(sourceDir);
|
|
687
|
+
const resolvedPackage = await fsExtra
|
|
688
|
+
.realpath(npmDirectory)
|
|
689
|
+
.catch(() => path.resolve(npmDirectory));
|
|
690
|
+
const packageDist = path.join(resolvedPackage, 'dist');
|
|
691
|
+
if (resolvedSource === resolvedPackage ||
|
|
692
|
+
resolvedPackage.startsWith(resolvedSource + path.sep) ||
|
|
693
|
+
resolvedSource === packageDist ||
|
|
694
|
+
resolvedSource.startsWith(packageDist + path.sep)) {
|
|
695
|
+
throw new PakeError(`Local input "${sourceDir}" contains the Pake CLI installation itself.`, {
|
|
696
|
+
code: 'INVALID_INPUT',
|
|
697
|
+
hint: 'Point Pake at your built output directory, not at a directory containing pake-cli.',
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
try {
|
|
701
|
+
if (await fsExtra.pathExists(distBakDir)) {
|
|
702
|
+
fsExtra.removeSync(distDir);
|
|
603
703
|
}
|
|
604
704
|
else {
|
|
605
|
-
fsExtra.moveSync(distDir, distBakDir
|
|
606
|
-
fsExtra.copySync(dirName, distDir, { overwrite: true });
|
|
607
|
-
const filesToCopyBack = ['cli.js'];
|
|
608
|
-
await Promise.all(filesToCopyBack.map((file) => fsExtra.copy(path.join(distBakDir, file), path.join(distDir, file))));
|
|
705
|
+
fsExtra.moveSync(distDir, distBakDir);
|
|
609
706
|
}
|
|
610
|
-
|
|
707
|
+
fsExtra.copySync(resolvedSource, distDir, {
|
|
708
|
+
overwrite: true,
|
|
709
|
+
dereference: true,
|
|
710
|
+
});
|
|
711
|
+
const filesToCopyBack = ['cli.js'];
|
|
712
|
+
await Promise.all(filesToCopyBack.map((file) => fsExtra.copy(path.join(distBakDir, file), path.join(distDir, file))));
|
|
713
|
+
}
|
|
714
|
+
catch (error) {
|
|
715
|
+
// Never leave the package without its own dist/: cli.js lives there and
|
|
716
|
+
// every later `pake` invocation would fail until a manual reinstall.
|
|
717
|
+
restoreLocalTree();
|
|
718
|
+
throw error;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
// Put the package's original dist/ back once a local-input run is over (or
|
|
722
|
+
// failed). Tauri bakes `frontendDist: ../dist` into every binary, so a stale
|
|
723
|
+
// staged tree would leak this user's files into the next app built from the
|
|
724
|
+
// same install. Safe to call on any run: a present dist_bak always holds the
|
|
725
|
+
// original package dist, including one stranded by an older crashed run.
|
|
726
|
+
function restoreLocalTree() {
|
|
727
|
+
const distDir = path.join(npmDirectory, 'dist');
|
|
728
|
+
const distBakDir = path.join(npmDirectory, 'dist_bak');
|
|
729
|
+
if (!fsExtra.pathExistsSync(distBakDir)) {
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
try {
|
|
733
|
+
fsExtra.removeSync(distDir);
|
|
734
|
+
fsExtra.moveSync(distBakDir, distDir);
|
|
735
|
+
}
|
|
736
|
+
catch (error) {
|
|
737
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
738
|
+
logger.warn(`Failed to restore the CLI's original dist/ from dist_bak: ${detail}`);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
// Exported for unit tests (web fallback and directory entry guard).
|
|
742
|
+
async function handleLocalFile(url, useLocalFile, tauriConf) {
|
|
743
|
+
const pathExists = await fsExtra.pathExists(url);
|
|
744
|
+
if (!pathExists) {
|
|
745
|
+
tauriConf.pake.windows[0].url_type = 'web';
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
const stat = await fsExtra.stat(url);
|
|
749
|
+
if (stat.isDirectory()) {
|
|
750
|
+
// A directory of static web assets (e.g. a generated dist/): the whole
|
|
751
|
+
// tree is packaged and the app entry is its root index.html.
|
|
752
|
+
const entryFile = 'index.html';
|
|
753
|
+
if (!(await fsExtra.pathExists(path.join(url, entryFile)))) {
|
|
754
|
+
throw new PakeError(`Local directory "${url}" has no ${entryFile} at its root.`, {
|
|
755
|
+
code: 'INVALID_INPUT',
|
|
756
|
+
hint: 'Point Pake at the built output directory that contains index.html.',
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
logger.info(`โบ Packaging local directory: ${url}`);
|
|
760
|
+
await stageLocalTree(url);
|
|
761
|
+
tauriConf.pake.windows[0].url = entryFile;
|
|
611
762
|
tauriConf.pake.windows[0].url_type = 'local';
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
logger.info(`โบ Packaging local file: ${url}`);
|
|
766
|
+
const fileName = path.basename(url);
|
|
767
|
+
const distDir = path.join(npmDirectory, 'dist');
|
|
768
|
+
if (!useLocalFile) {
|
|
769
|
+
const urlPath = path.join(distDir, fileName);
|
|
770
|
+
await fsExtra.copy(url, urlPath);
|
|
612
771
|
}
|
|
613
772
|
else {
|
|
614
|
-
|
|
773
|
+
await stageLocalTree(path.dirname(url));
|
|
615
774
|
}
|
|
775
|
+
tauriConf.pake.windows[0].url = fileName;
|
|
776
|
+
tauriConf.pake.windows[0].url_type = 'local';
|
|
616
777
|
}
|
|
617
778
|
function buildLinuxDesktopContent(name, title, linuxBinaryName) {
|
|
618
779
|
const chineseName = title && /[\u4e00-\u9fa5]/.test(title) ? title : null;
|
|
@@ -1048,8 +1209,61 @@ const APPIMAGE_FAILURE_GUIDANCE = `\n\n${APPIMAGE_BAR}\n` +
|
|
|
1048
1209
|
APPIMAGE_BAR;
|
|
1049
1210
|
class BaseBuilder {
|
|
1050
1211
|
constructor(options) {
|
|
1212
|
+
this.artifacts = [];
|
|
1051
1213
|
this.options = options;
|
|
1052
1214
|
}
|
|
1215
|
+
/** Final artifacts produced by this build, for the `--json` result. */
|
|
1216
|
+
getArtifacts() {
|
|
1217
|
+
return [...this.artifacts];
|
|
1218
|
+
}
|
|
1219
|
+
/** Architecture reported in the `--json` result. */
|
|
1220
|
+
getReportArch() {
|
|
1221
|
+
return this.options.multiArch ? 'universal' : process.arch;
|
|
1222
|
+
}
|
|
1223
|
+
// Drop a recorded artifact whose file was later removed (e.g. the
|
|
1224
|
+
// temporary .deb consumed by zst repacking), so --json never lists a
|
|
1225
|
+
// path that no longer exists.
|
|
1226
|
+
removeArtifact(artifactPath) {
|
|
1227
|
+
const resolved = path.resolve(artifactPath);
|
|
1228
|
+
this.artifacts = this.artifacts.filter((artifact) => artifact.path !== resolved);
|
|
1229
|
+
}
|
|
1230
|
+
async recordArtifact(artifactPath, format) {
|
|
1231
|
+
try {
|
|
1232
|
+
const stat = await fsExtra.stat(artifactPath);
|
|
1233
|
+
let sizeBytes = stat.size;
|
|
1234
|
+
if (stat.isDirectory()) {
|
|
1235
|
+
sizeBytes = await BaseBuilder.getPathSize(artifactPath);
|
|
1236
|
+
}
|
|
1237
|
+
this.artifacts.push({
|
|
1238
|
+
path: path.resolve(artifactPath),
|
|
1239
|
+
sizeBytes,
|
|
1240
|
+
format,
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
1243
|
+
catch {
|
|
1244
|
+
// Never fail a finished build over size bookkeeping.
|
|
1245
|
+
this.artifacts.push({
|
|
1246
|
+
path: path.resolve(artifactPath),
|
|
1247
|
+
sizeBytes: 0,
|
|
1248
|
+
format,
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
static async getPathSize(directory) {
|
|
1253
|
+
let size = 0;
|
|
1254
|
+
for (const entry of await fsExtra.readdir(directory, {
|
|
1255
|
+
withFileTypes: true,
|
|
1256
|
+
})) {
|
|
1257
|
+
const entryPath = path.join(directory, entry.name);
|
|
1258
|
+
if (entry.isDirectory()) {
|
|
1259
|
+
size += await BaseBuilder.getPathSize(entryPath);
|
|
1260
|
+
}
|
|
1261
|
+
else if (entry.isFile()) {
|
|
1262
|
+
size += (await fsExtra.stat(entryPath)).size;
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
return size;
|
|
1266
|
+
}
|
|
1053
1267
|
async prepare() {
|
|
1054
1268
|
const tauriSrcPath = path.join(npmDirectory, 'src-tauri');
|
|
1055
1269
|
const tauriTargetPath = path.join(tauriSrcPath, 'target');
|
|
@@ -1060,6 +1274,12 @@ class BaseBuilder {
|
|
|
1060
1274
|
}
|
|
1061
1275
|
ensureRustEnv();
|
|
1062
1276
|
if (!checkRustInstalled()) {
|
|
1277
|
+
if (!isInteractive()) {
|
|
1278
|
+
throw new PakeError('Rust required to package your webapp.', {
|
|
1279
|
+
code: 'ENV_MISSING',
|
|
1280
|
+
hint: 'Install Rust via https://rustup.rs, then rerun the same command.',
|
|
1281
|
+
});
|
|
1282
|
+
}
|
|
1063
1283
|
const res = await prompts({
|
|
1064
1284
|
type: 'confirm',
|
|
1065
1285
|
message: 'Rust not detected. Install now?',
|
|
@@ -1069,8 +1289,10 @@ class BaseBuilder {
|
|
|
1069
1289
|
await installRust();
|
|
1070
1290
|
}
|
|
1071
1291
|
else {
|
|
1072
|
-
|
|
1073
|
-
|
|
1292
|
+
throw new PakeError('Rust required to package your webapp.', {
|
|
1293
|
+
code: 'ENV_MISSING',
|
|
1294
|
+
hint: 'Install Rust via https://rustup.rs, then rerun the same command.',
|
|
1295
|
+
});
|
|
1074
1296
|
}
|
|
1075
1297
|
}
|
|
1076
1298
|
const spinner = getSpinner('Installing package...');
|
|
@@ -1129,8 +1351,9 @@ class BaseBuilder {
|
|
|
1129
1351
|
// Let spinner run for a moment so user can see it, then stop before package manager command
|
|
1130
1352
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1131
1353
|
buildSpinner.stop();
|
|
1132
|
-
// Show static message to keep the status visible
|
|
1133
|
-
|
|
1354
|
+
// Show static message to keep the status visible. Info, not warn: warn
|
|
1355
|
+
// entries feed the --json warnings array and this is a status line.
|
|
1356
|
+
logger.info('โธ Building app...');
|
|
1134
1357
|
const baseEnv = getBuildEnvironment();
|
|
1135
1358
|
let buildEnv = {
|
|
1136
1359
|
...(baseEnv ?? {}),
|
|
@@ -1174,6 +1397,7 @@ class BaseBuilder {
|
|
|
1174
1397
|
// executable the build produced instead.
|
|
1175
1398
|
if (this.options.bundle === false) {
|
|
1176
1399
|
await this.copyRawBinary(npmDirectory, name);
|
|
1400
|
+
await this.recordArtifact(this.getRawBinaryPath(name), 'binary');
|
|
1177
1401
|
if (logSuccess) {
|
|
1178
1402
|
logger.success('โ Build success!');
|
|
1179
1403
|
logger.success('โ Raw binary located in', path.resolve(this.getRawBinaryPath(name)));
|
|
@@ -1186,9 +1410,11 @@ class BaseBuilder {
|
|
|
1186
1410
|
const appPath = this.getBuildAppPath(npmDirectory, fileName, fileType);
|
|
1187
1411
|
const distPath = path.resolve(`${name}.${fileType}`);
|
|
1188
1412
|
await fsExtra.copy(appPath, distPath);
|
|
1413
|
+
await this.recordArtifact(distPath, fileType);
|
|
1189
1414
|
// Copy raw binary if requested
|
|
1190
1415
|
if (this.options.keepBinary) {
|
|
1191
1416
|
await this.copyRawBinary(npmDirectory, name);
|
|
1417
|
+
await this.recordArtifact(this.getRawBinaryPath(name), 'binary');
|
|
1192
1418
|
}
|
|
1193
1419
|
await fsExtra.remove(appPath);
|
|
1194
1420
|
if (logSuccess) {
|
|
@@ -1215,6 +1441,13 @@ class BaseBuilder {
|
|
|
1215
1441
|
// fsExtra.move uses fs.rename (atomic on same filesystem) and falls back
|
|
1216
1442
|
// to copy+remove only when moving across volumes.
|
|
1217
1443
|
await fsExtra.move(appBundlePath, appDest, { overwrite: true });
|
|
1444
|
+
// Keep the JSON result pointing at where the artifact actually lives.
|
|
1445
|
+
const movedFrom = path.resolve(appBundlePath);
|
|
1446
|
+
for (const artifact of this.artifacts) {
|
|
1447
|
+
if (artifact.path === movedFrom) {
|
|
1448
|
+
artifact.path = appDest;
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1218
1451
|
logger.success(`โ ${appBundleName.replace(/\.app$/, '')} installed to /Applications`);
|
|
1219
1452
|
}
|
|
1220
1453
|
catch (error) {
|
|
@@ -1430,6 +1663,9 @@ class MacBuilder extends BaseBuilder {
|
|
|
1430
1663
|
}
|
|
1431
1664
|
return `${name}_${tauriConfig.version}_${arch}`;
|
|
1432
1665
|
}
|
|
1666
|
+
getReportArch() {
|
|
1667
|
+
return this.getActualArch();
|
|
1668
|
+
}
|
|
1433
1669
|
getActualArch() {
|
|
1434
1670
|
if (this.buildArch === 'universal' || this.options.multiArch) {
|
|
1435
1671
|
return 'universal';
|
|
@@ -1483,6 +1719,9 @@ class WinBuilder extends BaseBuilder {
|
|
|
1483
1719
|
: this.resolveTargetArch('auto');
|
|
1484
1720
|
this.options.targets = this.buildFormat;
|
|
1485
1721
|
}
|
|
1722
|
+
getReportArch() {
|
|
1723
|
+
return this.buildArch;
|
|
1724
|
+
}
|
|
1486
1725
|
getFileName() {
|
|
1487
1726
|
const { name } = this.options;
|
|
1488
1727
|
const language = tauriConfig.bundle.windows.wix.language[0];
|
|
@@ -1538,6 +1777,9 @@ class LinuxBuilder extends BaseBuilder {
|
|
|
1538
1777
|
}
|
|
1539
1778
|
this.options.targets = this.buildFormat;
|
|
1540
1779
|
}
|
|
1780
|
+
getReportArch() {
|
|
1781
|
+
return this.buildArch;
|
|
1782
|
+
}
|
|
1541
1783
|
getFileName() {
|
|
1542
1784
|
const { name = 'pake-app', targets } = this.options;
|
|
1543
1785
|
const version = tauriConfig.version;
|
|
@@ -1686,12 +1928,14 @@ post_remove() {
|
|
|
1686
1928
|
}
|
|
1687
1929
|
`);
|
|
1688
1930
|
await shellExec(`bsdtar --zstd -cf "${packagePath}" -C "${dataDir}" .PKGINFO .INSTALL usr`);
|
|
1931
|
+
await this.recordArtifact(packagePath, 'zst');
|
|
1689
1932
|
logger.success('โ Build success!');
|
|
1690
1933
|
logger.success('โ App installer located in', packagePath);
|
|
1691
1934
|
}
|
|
1692
1935
|
finally {
|
|
1693
1936
|
if (removeSourceDeb) {
|
|
1694
1937
|
await fsExtra.remove(debPath);
|
|
1938
|
+
this.removeArtifact(debPath);
|
|
1695
1939
|
}
|
|
1696
1940
|
await fsExtra.remove(workDir);
|
|
1697
1941
|
}
|
|
@@ -2389,8 +2633,8 @@ async function handleIcon(options, url) {
|
|
|
2389
2633
|
return localIconPath;
|
|
2390
2634
|
}
|
|
2391
2635
|
}
|
|
2392
|
-
// Try favicon from website
|
|
2393
|
-
if (url && options.name) {
|
|
2636
|
+
// Try favicon from website; local file/directory input has no favicon.
|
|
2637
|
+
if (url && options.name && /^https?:\/\//i.test(url)) {
|
|
2394
2638
|
const faviconPath = await tryGetFavicon(url, options.name);
|
|
2395
2639
|
if (faviconPath)
|
|
2396
2640
|
return faviconPath;
|
|
@@ -2633,28 +2877,6 @@ function safeDomainsToRegex(domains) {
|
|
|
2633
2877
|
: '';
|
|
2634
2878
|
}
|
|
2635
2879
|
|
|
2636
|
-
/**
|
|
2637
|
-
* Error class used for user-facing CLI errors.
|
|
2638
|
-
*
|
|
2639
|
-
* The top-level catch in `bin/cli.ts` prints `message` directly without a
|
|
2640
|
-
* stack trace and exits with code 1. Use this for predictable failures
|
|
2641
|
-
* (invalid names, missing files, etc.) so users see a clean message instead
|
|
2642
|
-
* of a Node.js stack dump.
|
|
2643
|
-
*/
|
|
2644
|
-
class PakeError extends Error {
|
|
2645
|
-
constructor(message) {
|
|
2646
|
-
super(message);
|
|
2647
|
-
this.isUserError = true;
|
|
2648
|
-
this.name = 'PakeError';
|
|
2649
|
-
}
|
|
2650
|
-
}
|
|
2651
|
-
function isPakeError(error) {
|
|
2652
|
-
return (error instanceof PakeError ||
|
|
2653
|
-
(typeof error === 'object' &&
|
|
2654
|
-
error !== null &&
|
|
2655
|
-
error.isUserError === true));
|
|
2656
|
-
}
|
|
2657
|
-
|
|
2658
2880
|
function resolveAppName(name, platform) {
|
|
2659
2881
|
const domain = getDomain(name) || 'pake';
|
|
2660
2882
|
return platform !== 'linux' ? capitalizeFirstLetter(domain) : domain;
|
|
@@ -2686,9 +2908,14 @@ async function handleOptions(options, url) {
|
|
|
2686
2908
|
const defaultName = pathExists
|
|
2687
2909
|
? resolveLocalAppName(url, platform)
|
|
2688
2910
|
: resolveAppName(url, platform);
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2911
|
+
if (isInteractive()) {
|
|
2912
|
+
const promptMessage = 'Enter your application name';
|
|
2913
|
+
const namePrompt = await promptText(promptMessage, defaultName);
|
|
2914
|
+
name = namePrompt?.trim() || defaultName;
|
|
2915
|
+
}
|
|
2916
|
+
else {
|
|
2917
|
+
name = defaultName;
|
|
2918
|
+
}
|
|
2692
2919
|
}
|
|
2693
2920
|
if (name && platform === 'linux') {
|
|
2694
2921
|
name = generateLinuxPackageName(name);
|
|
@@ -2732,6 +2959,7 @@ const DEFAULT_PAKE_OPTIONS = {
|
|
|
2732
2959
|
width: 1200,
|
|
2733
2960
|
fullscreen: false,
|
|
2734
2961
|
maximize: false,
|
|
2962
|
+
resizable: true,
|
|
2735
2963
|
hideTitleBar: false,
|
|
2736
2964
|
hideWindowDecorations: false,
|
|
2737
2965
|
alwaysOnTop: false,
|
|
@@ -2758,6 +2986,7 @@ const DEFAULT_PAKE_OPTIONS = {
|
|
|
2758
2986
|
systemTrayIcon: '',
|
|
2759
2987
|
proxyUrl: '',
|
|
2760
2988
|
debug: false,
|
|
2989
|
+
json: false,
|
|
2761
2990
|
inject: [],
|
|
2762
2991
|
installerLanguage: 'en-US',
|
|
2763
2992
|
hideOnClose: undefined, // Platform-specific: true for macOS, false for others
|
|
@@ -2797,9 +3026,16 @@ function validateNumberInput(value) {
|
|
|
2797
3026
|
}
|
|
2798
3027
|
return parsedValue;
|
|
2799
3028
|
}
|
|
3029
|
+
// Path-shaped input (./x, ../x, /x, ~/x, C:\x). A missing path must fail
|
|
3030
|
+
// loudly: appending https:// to "./typo" would otherwise produce a valid URL
|
|
3031
|
+
// like https://./typo and a silently broken app (worst case for agents).
|
|
3032
|
+
const PATH_LIKE_PATTERN = /^(\.{1,2}[\\/]|[\\/]|~[\\/]|[a-zA-Z]:[\\/])/;
|
|
2800
3033
|
function validateUrlInput(url) {
|
|
2801
3034
|
const isFile = fs.existsSync(url);
|
|
2802
3035
|
if (!isFile) {
|
|
3036
|
+
if (PATH_LIKE_PATTERN.test(url)) {
|
|
3037
|
+
throw new InvalidArgumentError(`Local path "${url}" does not exist. Check the path, or pass a web URL instead.`);
|
|
3038
|
+
}
|
|
2803
3039
|
try {
|
|
2804
3040
|
return normalizeUrl(url);
|
|
2805
3041
|
}
|
|
@@ -2849,6 +3085,8 @@ ${green('|_| \\__,_|_|\\_\\___| can turn any webpage into a desktop app with
|
|
|
2849
3085
|
return previous ? [...previous, ...files] : files;
|
|
2850
3086
|
}, DEFAULT_PAKE_OPTIONS.inject)
|
|
2851
3087
|
.option('--debug', 'Debug build and more output', DEFAULT_PAKE_OPTIONS.debug)
|
|
3088
|
+
.option('--json', 'Machine-readable output: logs to stderr, one JSON result on stdout', DEFAULT_PAKE_OPTIONS.json)
|
|
3089
|
+
.option('--config <path>', 'Load options from a JSON config file (fields mirror CLI options, see schema/pake.schema.json)')
|
|
2852
3090
|
.addOption(new Option('--proxy-url <url>', 'Proxy URL for all network requests (http://, https://, socks5://)')
|
|
2853
3091
|
.default(DEFAULT_PAKE_OPTIONS.proxyUrl)
|
|
2854
3092
|
.hideHelp())
|
|
@@ -2981,16 +3219,207 @@ ${green('|_| \\__,_|_|\\_\\___| can turn any webpage into a desktop app with
|
|
|
2981
3219
|
});
|
|
2982
3220
|
}
|
|
2983
3221
|
|
|
3222
|
+
// Invocation concerns, not app manifest fields; pass these as CLI flags.
|
|
3223
|
+
const REJECTED_KEYS = new Set(['config', 'json', 'version']);
|
|
3224
|
+
// Optional CLI options that have no entry in DEFAULT_PAKE_OPTIONS.
|
|
3225
|
+
const EXTRA_STRING_KEYS = new Set(['name', 'title', 'identifier']);
|
|
3226
|
+
// Numeric fields share the CLI flag ranges (see cli-program.ts validators),
|
|
3227
|
+
// so a config file cannot smuggle a value the same flag would reject.
|
|
3228
|
+
const NUMBER_RANGES = {
|
|
3229
|
+
width: { min: 0 },
|
|
3230
|
+
height: { min: 0 },
|
|
3231
|
+
minWidth: { min: 0 },
|
|
3232
|
+
minHeight: { min: 0 },
|
|
3233
|
+
zoom: { min: 50, max: 200 },
|
|
3234
|
+
};
|
|
3235
|
+
function expectedTypeFor(key) {
|
|
3236
|
+
if (key === 'inject')
|
|
3237
|
+
return 'string[]';
|
|
3238
|
+
if (key === 'hideOnClose')
|
|
3239
|
+
return 'boolean';
|
|
3240
|
+
if (EXTRA_STRING_KEYS.has(key))
|
|
3241
|
+
return 'string';
|
|
3242
|
+
const defaultValue = DEFAULT_PAKE_OPTIONS[key];
|
|
3243
|
+
const type = typeof defaultValue;
|
|
3244
|
+
if (type === 'string' || type === 'number' || type === 'boolean') {
|
|
3245
|
+
return type;
|
|
3246
|
+
}
|
|
3247
|
+
return null;
|
|
3248
|
+
}
|
|
3249
|
+
function matchesType(value, type) {
|
|
3250
|
+
if (type === 'string[]') {
|
|
3251
|
+
return Array.isArray(value) && value.every((v) => typeof v === 'string');
|
|
3252
|
+
}
|
|
3253
|
+
return typeof value === type;
|
|
3254
|
+
}
|
|
3255
|
+
async function loadConfigFile(configPath, validKeys) {
|
|
3256
|
+
if (!(await fsExtra.pathExists(configPath))) {
|
|
3257
|
+
throw new PakeError(`Config file not found: ${configPath}`, {
|
|
3258
|
+
code: 'INVALID_INPUT',
|
|
3259
|
+
hint: 'Pass a path to a JSON file matching schema/pake.schema.json.',
|
|
3260
|
+
});
|
|
3261
|
+
}
|
|
3262
|
+
let parsed;
|
|
3263
|
+
try {
|
|
3264
|
+
parsed = JSON.parse(await fsExtra.readFile(configPath, 'utf8'));
|
|
3265
|
+
}
|
|
3266
|
+
catch (error) {
|
|
3267
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
3268
|
+
throw new PakeError(`Config file is not valid JSON: ${detail}`, {
|
|
3269
|
+
code: 'INVALID_INPUT',
|
|
3270
|
+
hint: `Fix the JSON syntax in ${configPath}.`,
|
|
3271
|
+
});
|
|
3272
|
+
}
|
|
3273
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
3274
|
+
throw new PakeError('Config file must contain a JSON object.', {
|
|
3275
|
+
code: 'INVALID_INPUT',
|
|
3276
|
+
hint: 'See schema/pake.schema.json for the expected shape.',
|
|
3277
|
+
});
|
|
3278
|
+
}
|
|
3279
|
+
const result = { options: {} };
|
|
3280
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
3281
|
+
if (key === '$schema')
|
|
3282
|
+
continue;
|
|
3283
|
+
if (key === 'url') {
|
|
3284
|
+
if (typeof value !== 'string') {
|
|
3285
|
+
throw new PakeError('Config field "url" must be a string.', {
|
|
3286
|
+
code: 'INVALID_INPUT',
|
|
3287
|
+
hint: 'Use a web URL or a local file/directory path.',
|
|
3288
|
+
});
|
|
3289
|
+
}
|
|
3290
|
+
result.url = value;
|
|
3291
|
+
continue;
|
|
3292
|
+
}
|
|
3293
|
+
if (REJECTED_KEYS.has(key)) {
|
|
3294
|
+
throw new PakeError(`Config field "${key}" is not allowed in a config file.`, {
|
|
3295
|
+
code: 'INVALID_INPUT',
|
|
3296
|
+
hint: `Pass --${key} on the command line instead.`,
|
|
3297
|
+
});
|
|
3298
|
+
}
|
|
3299
|
+
if (!validKeys.has(key)) {
|
|
3300
|
+
throw new PakeError(`Unknown config field "${key}".`, {
|
|
3301
|
+
code: 'INVALID_INPUT',
|
|
3302
|
+
hint: 'Field names are camelCase CLI option names; see schema/pake.schema.json.',
|
|
3303
|
+
});
|
|
3304
|
+
}
|
|
3305
|
+
const expected = expectedTypeFor(key);
|
|
3306
|
+
if (expected && !matchesType(value, expected)) {
|
|
3307
|
+
throw new PakeError(`Config field "${key}" must be of type ${expected}.`, {
|
|
3308
|
+
code: 'INVALID_INPUT',
|
|
3309
|
+
hint: 'See schema/pake.schema.json for field types.',
|
|
3310
|
+
});
|
|
3311
|
+
}
|
|
3312
|
+
if (typeof value === 'number') {
|
|
3313
|
+
const range = NUMBER_RANGES[key];
|
|
3314
|
+
const min = range?.min ?? 0;
|
|
3315
|
+
const max = range?.max;
|
|
3316
|
+
if (!Number.isFinite(value) ||
|
|
3317
|
+
value < min ||
|
|
3318
|
+
(max !== undefined && value > max)) {
|
|
3319
|
+
const bounds = max !== undefined ? `${min}-${max}` : `>= ${min}`;
|
|
3320
|
+
throw new PakeError(`Config field "${key}" must be a finite number (${bounds}).`, {
|
|
3321
|
+
code: 'INVALID_INPUT',
|
|
3322
|
+
hint: 'See schema/pake.schema.json for field ranges.',
|
|
3323
|
+
});
|
|
3324
|
+
}
|
|
3325
|
+
}
|
|
3326
|
+
if (!expected && (typeof value === 'object' || value === null)) {
|
|
3327
|
+
throw new PakeError(`Config field "${key}" must be a string, number, or boolean.`, {
|
|
3328
|
+
code: 'INVALID_INPUT',
|
|
3329
|
+
hint: 'See schema/pake.schema.json for field types.',
|
|
3330
|
+
});
|
|
3331
|
+
}
|
|
3332
|
+
result.options[key] = value;
|
|
3333
|
+
}
|
|
3334
|
+
return result;
|
|
3335
|
+
}
|
|
3336
|
+
|
|
2984
3337
|
const program = getCliProgram();
|
|
3338
|
+
// Make commander throw instead of exiting so option/argument parse errors
|
|
3339
|
+
// honor the exit-code contract (2 = invalid input) and still emit the JSON
|
|
3340
|
+
// result object when --json was requested.
|
|
3341
|
+
program.exitOverride();
|
|
3342
|
+
function isCommanderExit(error) {
|
|
3343
|
+
return (typeof error === 'object' &&
|
|
3344
|
+
error !== null &&
|
|
3345
|
+
typeof error.code === 'string' &&
|
|
3346
|
+
error.code.startsWith('commander.'));
|
|
3347
|
+
}
|
|
3348
|
+
const PHASE_ERROR_CODES = {
|
|
3349
|
+
input: 'INVALID_INPUT',
|
|
3350
|
+
prepare: 'ENV_MISSING',
|
|
3351
|
+
build: 'BUILD_FAILED',
|
|
3352
|
+
};
|
|
3353
|
+
function classifyError(error, phase) {
|
|
3354
|
+
if (isPakeError(error)) {
|
|
3355
|
+
return {
|
|
3356
|
+
code: error.code ?? PHASE_ERROR_CODES[phase],
|
|
3357
|
+
message: error.message,
|
|
3358
|
+
hint: error.hint ?? null,
|
|
3359
|
+
};
|
|
3360
|
+
}
|
|
3361
|
+
if (error instanceof Error) {
|
|
3362
|
+
return {
|
|
3363
|
+
code: PHASE_ERROR_CODES[phase],
|
|
3364
|
+
message: error.message,
|
|
3365
|
+
hint: null,
|
|
3366
|
+
};
|
|
3367
|
+
}
|
|
3368
|
+
return {
|
|
3369
|
+
code: 'UNEXPECTED',
|
|
3370
|
+
message: `Unexpected error: ${String(error)}`,
|
|
3371
|
+
hint: null,
|
|
3372
|
+
};
|
|
3373
|
+
}
|
|
2985
3374
|
async function checkUpdateTips() {
|
|
2986
3375
|
updateNotifier({ pkg: packageJson, updateCheckInterval: 1000 * 60 }).notify({
|
|
2987
3376
|
isGlobal: true,
|
|
2988
3377
|
});
|
|
2989
3378
|
}
|
|
2990
|
-
program.action(async (
|
|
3379
|
+
program.action(async (urlArg, options) => {
|
|
3380
|
+
const jsonMode = Boolean(options.json);
|
|
3381
|
+
if (jsonMode) {
|
|
3382
|
+
enableMachineMode();
|
|
3383
|
+
}
|
|
3384
|
+
let phase = 'input';
|
|
3385
|
+
let appName = null;
|
|
3386
|
+
let url = urlArg;
|
|
2991
3387
|
try {
|
|
2992
|
-
|
|
3388
|
+
// Heal a dist_bak stranded by an earlier crashed local-input run before
|
|
3389
|
+
// building, or this build would embed that run's staged files.
|
|
3390
|
+
restoreLocalTree();
|
|
3391
|
+
if (!jsonMode) {
|
|
3392
|
+
await checkUpdateTips();
|
|
3393
|
+
}
|
|
3394
|
+
// Config file fills in whatever the command line did not set explicitly:
|
|
3395
|
+
// CLI flag > config field > built-in default.
|
|
3396
|
+
if (options.config) {
|
|
3397
|
+
const validKeys = new Set(program.options.map((option) => option.attributeName()));
|
|
3398
|
+
const loaded = await loadConfigFile(options.config, validKeys);
|
|
3399
|
+
for (const [key, value] of Object.entries(loaded.options)) {
|
|
3400
|
+
if (program.getOptionValueSource(key) !== 'cli') {
|
|
3401
|
+
options[key] = value;
|
|
3402
|
+
}
|
|
3403
|
+
}
|
|
3404
|
+
if (!url && loaded.url) {
|
|
3405
|
+
try {
|
|
3406
|
+
url = validateUrlInput(loaded.url);
|
|
3407
|
+
}
|
|
3408
|
+
catch (error) {
|
|
3409
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
3410
|
+
throw new PakeError(`Invalid "url" in config file: ${detail}`, {
|
|
3411
|
+
code: 'INVALID_INPUT',
|
|
3412
|
+
});
|
|
3413
|
+
}
|
|
3414
|
+
}
|
|
3415
|
+
}
|
|
2993
3416
|
if (!url) {
|
|
3417
|
+
if (jsonMode) {
|
|
3418
|
+
throw new PakeError('No URL or local path to package.', {
|
|
3419
|
+
code: 'INVALID_INPUT',
|
|
3420
|
+
hint: 'Pass a URL/path argument or a config file with a "url" field.',
|
|
3421
|
+
});
|
|
3422
|
+
}
|
|
2994
3423
|
program.help({
|
|
2995
3424
|
error: false,
|
|
2996
3425
|
});
|
|
@@ -3002,13 +3431,47 @@ program.action(async (url, options) => {
|
|
|
3002
3431
|
log.setLevel('debug');
|
|
3003
3432
|
}
|
|
3004
3433
|
const appOptions = await handleOptions(options, url);
|
|
3434
|
+
appName = appOptions.name ?? null;
|
|
3005
3435
|
const builder = BuilderProvider.create(appOptions);
|
|
3436
|
+
phase = 'prepare';
|
|
3006
3437
|
await builder.prepare();
|
|
3438
|
+
phase = 'build';
|
|
3007
3439
|
await builder.build(url);
|
|
3440
|
+
if (jsonMode) {
|
|
3441
|
+
printJsonResult({
|
|
3442
|
+
ok: true,
|
|
3443
|
+
name: appName,
|
|
3444
|
+
platform: process.platform,
|
|
3445
|
+
arch: builder.getReportArch(),
|
|
3446
|
+
outputs: builder.getArtifacts(),
|
|
3447
|
+
warnings: getCapturedWarnings(),
|
|
3448
|
+
error: null,
|
|
3449
|
+
});
|
|
3450
|
+
}
|
|
3008
3451
|
}
|
|
3009
3452
|
catch (error) {
|
|
3010
|
-
|
|
3011
|
-
|
|
3453
|
+
// program.help() and --help/--version throw under exitOverride with
|
|
3454
|
+
// exitCode 0; a clean commander exit is not a failure.
|
|
3455
|
+
if (isCommanderExit(error) && error.exitCode === 0) {
|
|
3456
|
+
return;
|
|
3457
|
+
}
|
|
3458
|
+
const classified = classifyError(error, phase);
|
|
3459
|
+
if (jsonMode) {
|
|
3460
|
+
printJsonResult({
|
|
3461
|
+
ok: false,
|
|
3462
|
+
name: appName,
|
|
3463
|
+
platform: process.platform,
|
|
3464
|
+
arch: null,
|
|
3465
|
+
outputs: [],
|
|
3466
|
+
warnings: getCapturedWarnings(),
|
|
3467
|
+
error: classified,
|
|
3468
|
+
});
|
|
3469
|
+
}
|
|
3470
|
+
else if (isPakeError(error)) {
|
|
3471
|
+
console.error(chalk.red(classified.message));
|
|
3472
|
+
if (classified.hint) {
|
|
3473
|
+
console.error(chalk.yellow(`โผ ${classified.hint}`));
|
|
3474
|
+
}
|
|
3012
3475
|
}
|
|
3013
3476
|
else if (error instanceof Error) {
|
|
3014
3477
|
console.error(chalk.red(`โ ${error.message}`));
|
|
@@ -3019,15 +3482,49 @@ program.action(async (url, options) => {
|
|
|
3019
3482
|
else {
|
|
3020
3483
|
console.error(chalk.red(`โ Unexpected error: ${String(error)}`));
|
|
3021
3484
|
}
|
|
3022
|
-
process.exit
|
|
3485
|
+
// exitCode + natural exit instead of process.exit: lets the finally
|
|
3486
|
+
// restore run and guarantees the JSON result is flushed on piped stdout.
|
|
3487
|
+
process.exitCode = ERROR_EXIT_CODES[classified.code];
|
|
3488
|
+
}
|
|
3489
|
+
finally {
|
|
3490
|
+
// A local-input run replaces the package's own dist/ during staging; put
|
|
3491
|
+
// it back so the CLI stays intact and later builds cannot embed this
|
|
3492
|
+
// user's files.
|
|
3493
|
+
restoreLocalTree();
|
|
3023
3494
|
}
|
|
3024
3495
|
});
|
|
3025
3496
|
program.parseAsync().catch((error) => {
|
|
3497
|
+
if (isCommanderExit(error)) {
|
|
3498
|
+
// --help / --version and friends exit clean; commander already printed.
|
|
3499
|
+
if (error.exitCode === 0) {
|
|
3500
|
+
return;
|
|
3501
|
+
}
|
|
3502
|
+
// Parse errors (unknown option, invalid argument, missing value) are
|
|
3503
|
+
// invalid input. Commander already printed the message to stderr; in
|
|
3504
|
+
// json mode also emit the machine-readable result on stdout.
|
|
3505
|
+
if (process.argv.includes('--json')) {
|
|
3506
|
+
printJsonResult({
|
|
3507
|
+
ok: false,
|
|
3508
|
+
name: null,
|
|
3509
|
+
platform: process.platform,
|
|
3510
|
+
arch: null,
|
|
3511
|
+
outputs: [],
|
|
3512
|
+
warnings: [],
|
|
3513
|
+
error: {
|
|
3514
|
+
code: 'INVALID_INPUT',
|
|
3515
|
+
message: error.message.trim(),
|
|
3516
|
+
hint: 'Run pake --help for the accepted options.',
|
|
3517
|
+
},
|
|
3518
|
+
});
|
|
3519
|
+
}
|
|
3520
|
+
process.exitCode = ERROR_EXIT_CODES.INVALID_INPUT;
|
|
3521
|
+
return;
|
|
3522
|
+
}
|
|
3026
3523
|
if (error instanceof Error) {
|
|
3027
3524
|
console.error(chalk.red(`โ ${error.message}`));
|
|
3028
3525
|
}
|
|
3029
3526
|
else {
|
|
3030
3527
|
console.error(chalk.red(`โ Unexpected error: ${String(error)}`));
|
|
3031
3528
|
}
|
|
3032
|
-
process.
|
|
3529
|
+
process.exitCode = 1;
|
|
3033
3530
|
});
|
package/package.json
CHANGED
package/src-tauri/Cargo.lock
CHANGED
package/src-tauri/Cargo.toml
CHANGED
|
@@ -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" => {
|
|
@@ -206,19 +206,14 @@ function insertTextIntoEditableElement(element, text) {
|
|
|
206
206
|
return false;
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
}
|
|
209
|
+
let clipboardPasteFallbackTarget;
|
|
210
|
+
let clipboardPasteFallbackArmedAt = 0;
|
|
211
|
+
// An armed fallback older than this is a leftover from a keyup the window
|
|
212
|
+
// never saw (alt-tab mid-press); firing it on a later plain "v" keyup would
|
|
213
|
+
// paste unexpectedly.
|
|
214
|
+
const CLIPBOARD_PASTE_FALLBACK_TTL_MS = 5000;
|
|
216
215
|
|
|
217
216
|
function pasteClipboardText(activeElement) {
|
|
218
|
-
if (runBrowserPasteCommand()) {
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
217
|
const readText = navigator.clipboard?.readText;
|
|
223
218
|
if (typeof readText !== "function") {
|
|
224
219
|
return;
|
|
@@ -261,9 +256,19 @@ function handleClipboardShortcut(event) {
|
|
|
261
256
|
}
|
|
262
257
|
|
|
263
258
|
if (key === "v" && canPasteIntoEditableElement(activeElement)) {
|
|
264
|
-
event
|
|
265
|
-
|
|
266
|
-
|
|
259
|
+
// Let the native WebView paste event run first so images, files, and rich
|
|
260
|
+
// clipboard formats remain intact. If the platform does not emit paste,
|
|
261
|
+
// keyup applies the existing text-only fallback. Key-repeat must not
|
|
262
|
+
// re-arm: after a native paste already fired and disarmed the fallback,
|
|
263
|
+
// a repeat keydown re-arming it would make keyup paste text a second
|
|
264
|
+
// time. Repeats only refresh the TTL of a still-armed target.
|
|
265
|
+
if (!event.repeat) {
|
|
266
|
+
clipboardPasteFallbackTarget = activeElement;
|
|
267
|
+
clipboardPasteFallbackArmedAt = Date.now();
|
|
268
|
+
} else if (clipboardPasteFallbackTarget === activeElement) {
|
|
269
|
+
clipboardPasteFallbackArmedAt = Date.now();
|
|
270
|
+
}
|
|
271
|
+
return false;
|
|
267
272
|
}
|
|
268
273
|
|
|
269
274
|
if (key === "a" && isEditable && selectEditableElement(activeElement)) {
|
|
@@ -274,6 +279,44 @@ function handleClipboardShortcut(event) {
|
|
|
274
279
|
return false;
|
|
275
280
|
}
|
|
276
281
|
|
|
282
|
+
function handleClipboardPasteFallback(event) {
|
|
283
|
+
if (
|
|
284
|
+
event.isTrusted !== true ||
|
|
285
|
+
!isNonMacDesktop() ||
|
|
286
|
+
event.key?.toLowerCase() !== "v"
|
|
287
|
+
) {
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const activeElement = clipboardPasteFallbackTarget;
|
|
292
|
+
const armedAt = clipboardPasteFallbackArmedAt;
|
|
293
|
+
clipboardPasteFallbackTarget = undefined;
|
|
294
|
+
if (
|
|
295
|
+
!activeElement ||
|
|
296
|
+
Date.now() - armedAt > CLIPBOARD_PASTE_FALLBACK_TTL_MS ||
|
|
297
|
+
document.activeElement !== activeElement ||
|
|
298
|
+
!canPasteIntoEditableElement(activeElement)
|
|
299
|
+
) {
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
pasteClipboardText(activeElement);
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function handlePaste(event) {
|
|
308
|
+
clipboardPasteFallbackTarget = undefined;
|
|
309
|
+
if (!pasteAsPlainTextPending) return;
|
|
310
|
+
|
|
311
|
+
event.preventDefault();
|
|
312
|
+
event.stopImmediatePropagation();
|
|
313
|
+
|
|
314
|
+
const text = event.clipboardData?.getData("text/plain") || "";
|
|
315
|
+
if (text) {
|
|
316
|
+
document.execCommand("insertText", false, text);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
277
320
|
const DOWNLOADABLE_FILE_EXTENSIONS = {
|
|
278
321
|
documents: [
|
|
279
322
|
"pdf",
|
|
@@ -590,22 +633,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
590
633
|
}
|
|
591
634
|
|
|
592
635
|
document.addEventListener("keydown", handleClipboardShortcut, true);
|
|
593
|
-
|
|
594
|
-
document.addEventListener(
|
|
595
|
-
"paste",
|
|
596
|
-
(event) => {
|
|
597
|
-
if (pasteAsPlainTextPending) {
|
|
598
|
-
event.preventDefault();
|
|
599
|
-
event.stopImmediatePropagation();
|
|
600
|
-
|
|
601
|
-
const text = event.clipboardData?.getData("text/plain") || "";
|
|
602
|
-
if (text) {
|
|
603
|
-
document.execCommand("insertText", false, text);
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
},
|
|
607
|
-
true,
|
|
608
|
-
);
|
|
636
|
+
document.addEventListener("keyup", handleClipboardPasteFallback, true);
|
|
637
|
+
document.addEventListener("paste", handlePaste, true);
|
|
609
638
|
|
|
610
639
|
// Trigger a native browser download via a transient anchor click. The Rust
|
|
611
640
|
// on_download handler then writes the file to the Downloads folder. This is
|