pake-cli 3.14.0 โ 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 +2 -0
- package/dist/cli.js +469 -55
- 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 +44 -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.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.
|
|
@@ -589,30 +672,60 @@ async function copyTemplateConfigs() {
|
|
|
589
672
|
}
|
|
590
673
|
}));
|
|
591
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).
|
|
592
694
|
async function handleLocalFile(url, useLocalFile, tauriConf) {
|
|
593
695
|
const pathExists = await fsExtra.pathExists(url);
|
|
594
|
-
if (pathExists) {
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
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
|
+
});
|
|
609
710
|
}
|
|
610
|
-
|
|
711
|
+
logger.info(`โบ Packaging local directory: ${url}`);
|
|
712
|
+
await stageLocalTree(url);
|
|
713
|
+
tauriConf.pake.windows[0].url = entryFile;
|
|
611
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);
|
|
612
723
|
}
|
|
613
724
|
else {
|
|
614
|
-
|
|
725
|
+
await stageLocalTree(path.dirname(url));
|
|
615
726
|
}
|
|
727
|
+
tauriConf.pake.windows[0].url = fileName;
|
|
728
|
+
tauriConf.pake.windows[0].url_type = 'local';
|
|
616
729
|
}
|
|
617
730
|
function buildLinuxDesktopContent(name, title, linuxBinaryName) {
|
|
618
731
|
const chineseName = title && /[\u4e00-\u9fa5]/.test(title) ? title : null;
|
|
@@ -1048,8 +1161,61 @@ const APPIMAGE_FAILURE_GUIDANCE = `\n\n${APPIMAGE_BAR}\n` +
|
|
|
1048
1161
|
APPIMAGE_BAR;
|
|
1049
1162
|
class BaseBuilder {
|
|
1050
1163
|
constructor(options) {
|
|
1164
|
+
this.artifacts = [];
|
|
1051
1165
|
this.options = options;
|
|
1052
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
|
+
}
|
|
1053
1219
|
async prepare() {
|
|
1054
1220
|
const tauriSrcPath = path.join(npmDirectory, 'src-tauri');
|
|
1055
1221
|
const tauriTargetPath = path.join(tauriSrcPath, 'target');
|
|
@@ -1060,6 +1226,12 @@ class BaseBuilder {
|
|
|
1060
1226
|
}
|
|
1061
1227
|
ensureRustEnv();
|
|
1062
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
|
+
}
|
|
1063
1235
|
const res = await prompts({
|
|
1064
1236
|
type: 'confirm',
|
|
1065
1237
|
message: 'Rust not detected. Install now?',
|
|
@@ -1069,8 +1241,10 @@ class BaseBuilder {
|
|
|
1069
1241
|
await installRust();
|
|
1070
1242
|
}
|
|
1071
1243
|
else {
|
|
1072
|
-
|
|
1073
|
-
|
|
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
|
+
});
|
|
1074
1248
|
}
|
|
1075
1249
|
}
|
|
1076
1250
|
const spinner = getSpinner('Installing package...');
|
|
@@ -1129,8 +1303,9 @@ class BaseBuilder {
|
|
|
1129
1303
|
// Let spinner run for a moment so user can see it, then stop before package manager command
|
|
1130
1304
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1131
1305
|
buildSpinner.stop();
|
|
1132
|
-
// Show static message to keep the status visible
|
|
1133
|
-
|
|
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...');
|
|
1134
1309
|
const baseEnv = getBuildEnvironment();
|
|
1135
1310
|
let buildEnv = {
|
|
1136
1311
|
...(baseEnv ?? {}),
|
|
@@ -1174,6 +1349,7 @@ class BaseBuilder {
|
|
|
1174
1349
|
// executable the build produced instead.
|
|
1175
1350
|
if (this.options.bundle === false) {
|
|
1176
1351
|
await this.copyRawBinary(npmDirectory, name);
|
|
1352
|
+
await this.recordArtifact(this.getRawBinaryPath(name), 'binary');
|
|
1177
1353
|
if (logSuccess) {
|
|
1178
1354
|
logger.success('โ Build success!');
|
|
1179
1355
|
logger.success('โ Raw binary located in', path.resolve(this.getRawBinaryPath(name)));
|
|
@@ -1186,9 +1362,11 @@ class BaseBuilder {
|
|
|
1186
1362
|
const appPath = this.getBuildAppPath(npmDirectory, fileName, fileType);
|
|
1187
1363
|
const distPath = path.resolve(`${name}.${fileType}`);
|
|
1188
1364
|
await fsExtra.copy(appPath, distPath);
|
|
1365
|
+
await this.recordArtifact(distPath, fileType);
|
|
1189
1366
|
// Copy raw binary if requested
|
|
1190
1367
|
if (this.options.keepBinary) {
|
|
1191
1368
|
await this.copyRawBinary(npmDirectory, name);
|
|
1369
|
+
await this.recordArtifact(this.getRawBinaryPath(name), 'binary');
|
|
1192
1370
|
}
|
|
1193
1371
|
await fsExtra.remove(appPath);
|
|
1194
1372
|
if (logSuccess) {
|
|
@@ -1215,6 +1393,13 @@ class BaseBuilder {
|
|
|
1215
1393
|
// fsExtra.move uses fs.rename (atomic on same filesystem) and falls back
|
|
1216
1394
|
// to copy+remove only when moving across volumes.
|
|
1217
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
|
+
}
|
|
1218
1403
|
logger.success(`โ ${appBundleName.replace(/\.app$/, '')} installed to /Applications`);
|
|
1219
1404
|
}
|
|
1220
1405
|
catch (error) {
|
|
@@ -1430,6 +1615,9 @@ class MacBuilder extends BaseBuilder {
|
|
|
1430
1615
|
}
|
|
1431
1616
|
return `${name}_${tauriConfig.version}_${arch}`;
|
|
1432
1617
|
}
|
|
1618
|
+
getReportArch() {
|
|
1619
|
+
return this.getActualArch();
|
|
1620
|
+
}
|
|
1433
1621
|
getActualArch() {
|
|
1434
1622
|
if (this.buildArch === 'universal' || this.options.multiArch) {
|
|
1435
1623
|
return 'universal';
|
|
@@ -1483,6 +1671,9 @@ class WinBuilder extends BaseBuilder {
|
|
|
1483
1671
|
: this.resolveTargetArch('auto');
|
|
1484
1672
|
this.options.targets = this.buildFormat;
|
|
1485
1673
|
}
|
|
1674
|
+
getReportArch() {
|
|
1675
|
+
return this.buildArch;
|
|
1676
|
+
}
|
|
1486
1677
|
getFileName() {
|
|
1487
1678
|
const { name } = this.options;
|
|
1488
1679
|
const language = tauriConfig.bundle.windows.wix.language[0];
|
|
@@ -1538,6 +1729,9 @@ class LinuxBuilder extends BaseBuilder {
|
|
|
1538
1729
|
}
|
|
1539
1730
|
this.options.targets = this.buildFormat;
|
|
1540
1731
|
}
|
|
1732
|
+
getReportArch() {
|
|
1733
|
+
return this.buildArch;
|
|
1734
|
+
}
|
|
1541
1735
|
getFileName() {
|
|
1542
1736
|
const { name = 'pake-app', targets } = this.options;
|
|
1543
1737
|
const version = tauriConfig.version;
|
|
@@ -1686,12 +1880,14 @@ post_remove() {
|
|
|
1686
1880
|
}
|
|
1687
1881
|
`);
|
|
1688
1882
|
await shellExec(`bsdtar --zstd -cf "${packagePath}" -C "${dataDir}" .PKGINFO .INSTALL usr`);
|
|
1883
|
+
await this.recordArtifact(packagePath, 'zst');
|
|
1689
1884
|
logger.success('โ Build success!');
|
|
1690
1885
|
logger.success('โ App installer located in', packagePath);
|
|
1691
1886
|
}
|
|
1692
1887
|
finally {
|
|
1693
1888
|
if (removeSourceDeb) {
|
|
1694
1889
|
await fsExtra.remove(debPath);
|
|
1890
|
+
this.removeArtifact(debPath);
|
|
1695
1891
|
}
|
|
1696
1892
|
await fsExtra.remove(workDir);
|
|
1697
1893
|
}
|
|
@@ -2389,8 +2585,8 @@ async function handleIcon(options, url) {
|
|
|
2389
2585
|
return localIconPath;
|
|
2390
2586
|
}
|
|
2391
2587
|
}
|
|
2392
|
-
// Try favicon from website
|
|
2393
|
-
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)) {
|
|
2394
2590
|
const faviconPath = await tryGetFavicon(url, options.name);
|
|
2395
2591
|
if (faviconPath)
|
|
2396
2592
|
return faviconPath;
|
|
@@ -2633,28 +2829,6 @@ function safeDomainsToRegex(domains) {
|
|
|
2633
2829
|
: '';
|
|
2634
2830
|
}
|
|
2635
2831
|
|
|
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
2832
|
function resolveAppName(name, platform) {
|
|
2659
2833
|
const domain = getDomain(name) || 'pake';
|
|
2660
2834
|
return platform !== 'linux' ? capitalizeFirstLetter(domain) : domain;
|
|
@@ -2686,9 +2860,14 @@ async function handleOptions(options, url) {
|
|
|
2686
2860
|
const defaultName = pathExists
|
|
2687
2861
|
? resolveLocalAppName(url, platform)
|
|
2688
2862
|
: resolveAppName(url, platform);
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
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
|
+
}
|
|
2692
2871
|
}
|
|
2693
2872
|
if (name && platform === 'linux') {
|
|
2694
2873
|
name = generateLinuxPackageName(name);
|
|
@@ -2732,6 +2911,7 @@ const DEFAULT_PAKE_OPTIONS = {
|
|
|
2732
2911
|
width: 1200,
|
|
2733
2912
|
fullscreen: false,
|
|
2734
2913
|
maximize: false,
|
|
2914
|
+
resizable: true,
|
|
2735
2915
|
hideTitleBar: false,
|
|
2736
2916
|
hideWindowDecorations: false,
|
|
2737
2917
|
alwaysOnTop: false,
|
|
@@ -2758,6 +2938,7 @@ const DEFAULT_PAKE_OPTIONS = {
|
|
|
2758
2938
|
systemTrayIcon: '',
|
|
2759
2939
|
proxyUrl: '',
|
|
2760
2940
|
debug: false,
|
|
2941
|
+
json: false,
|
|
2761
2942
|
inject: [],
|
|
2762
2943
|
installerLanguage: 'en-US',
|
|
2763
2944
|
hideOnClose: undefined, // Platform-specific: true for macOS, false for others
|
|
@@ -2797,9 +2978,16 @@ function validateNumberInput(value) {
|
|
|
2797
2978
|
}
|
|
2798
2979
|
return parsedValue;
|
|
2799
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]:[\\/])/;
|
|
2800
2985
|
function validateUrlInput(url) {
|
|
2801
2986
|
const isFile = fs.existsSync(url);
|
|
2802
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
|
+
}
|
|
2803
2991
|
try {
|
|
2804
2992
|
return normalizeUrl(url);
|
|
2805
2993
|
}
|
|
@@ -2849,6 +3037,8 @@ ${green('|_| \\__,_|_|\\_\\___| can turn any webpage into a desktop app with
|
|
|
2849
3037
|
return previous ? [...previous, ...files] : files;
|
|
2850
3038
|
}, DEFAULT_PAKE_OPTIONS.inject)
|
|
2851
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)')
|
|
2852
3042
|
.addOption(new Option('--proxy-url <url>', 'Proxy URL for all network requests (http://, https://, socks5://)')
|
|
2853
3043
|
.default(DEFAULT_PAKE_OPTIONS.proxyUrl)
|
|
2854
3044
|
.hideHelp())
|
|
@@ -2981,16 +3171,181 @@ ${green('|_| \\__,_|_|\\_\\___| can turn any webpage into a desktop app with
|
|
|
2981
3171
|
});
|
|
2982
3172
|
}
|
|
2983
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
|
+
|
|
2984
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
|
+
}
|
|
2985
3303
|
async function checkUpdateTips() {
|
|
2986
3304
|
updateNotifier({ pkg: packageJson, updateCheckInterval: 1000 * 60 }).notify({
|
|
2987
3305
|
isGlobal: true,
|
|
2988
3306
|
});
|
|
2989
3307
|
}
|
|
2990
|
-
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;
|
|
2991
3316
|
try {
|
|
2992
|
-
|
|
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
|
+
}
|
|
2993
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
|
+
}
|
|
2994
3349
|
program.help({
|
|
2995
3350
|
error: false,
|
|
2996
3351
|
});
|
|
@@ -3002,13 +3357,47 @@ program.action(async (url, options) => {
|
|
|
3002
3357
|
log.setLevel('debug');
|
|
3003
3358
|
}
|
|
3004
3359
|
const appOptions = await handleOptions(options, url);
|
|
3360
|
+
appName = appOptions.name ?? null;
|
|
3005
3361
|
const builder = BuilderProvider.create(appOptions);
|
|
3362
|
+
phase = 'prepare';
|
|
3006
3363
|
await builder.prepare();
|
|
3364
|
+
phase = 'build';
|
|
3007
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
|
+
}
|
|
3008
3377
|
}
|
|
3009
3378
|
catch (error) {
|
|
3010
|
-
|
|
3011
|
-
|
|
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
|
+
}
|
|
3012
3401
|
}
|
|
3013
3402
|
else if (error instanceof Error) {
|
|
3014
3403
|
console.error(chalk.red(`โ ${error.message}`));
|
|
@@ -3019,10 +3408,35 @@ program.action(async (url, options) => {
|
|
|
3019
3408
|
else {
|
|
3020
3409
|
console.error(chalk.red(`โ Unexpected error: ${String(error)}`));
|
|
3021
3410
|
}
|
|
3022
|
-
process.exit(
|
|
3411
|
+
process.exit(ERROR_EXIT_CODES[classified.code]);
|
|
3023
3412
|
}
|
|
3024
3413
|
});
|
|
3025
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
|
+
}
|
|
3026
3440
|
if (error instanceof Error) {
|
|
3027
3441
|
console.error(chalk.red(`โ ${error.message}`));
|
|
3028
3442
|
}
|
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,9 @@ function insertTextIntoEditableElement(element, text) {
|
|
|
206
206
|
return false;
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
-
|
|
210
|
-
try {
|
|
211
|
-
return document.execCommand("paste") === true;
|
|
212
|
-
} catch (error) {
|
|
213
|
-
return false;
|
|
214
|
-
}
|
|
215
|
-
}
|
|
209
|
+
let clipboardPasteFallbackTarget;
|
|
216
210
|
|
|
217
211
|
function pasteClipboardText(activeElement) {
|
|
218
|
-
if (runBrowserPasteCommand()) {
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
212
|
const readText = navigator.clipboard?.readText;
|
|
223
213
|
if (typeof readText !== "function") {
|
|
224
214
|
return;
|
|
@@ -261,9 +251,11 @@ function handleClipboardShortcut(event) {
|
|
|
261
251
|
}
|
|
262
252
|
|
|
263
253
|
if (key === "v" && canPasteIntoEditableElement(activeElement)) {
|
|
264
|
-
event
|
|
265
|
-
|
|
266
|
-
|
|
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;
|
|
267
259
|
}
|
|
268
260
|
|
|
269
261
|
if (key === "a" && isEditable && selectEditableElement(activeElement)) {
|
|
@@ -274,6 +266,42 @@ function handleClipboardShortcut(event) {
|
|
|
274
266
|
return false;
|
|
275
267
|
}
|
|
276
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
|
+
|
|
277
305
|
const DOWNLOADABLE_FILE_EXTENSIONS = {
|
|
278
306
|
documents: [
|
|
279
307
|
"pdf",
|
|
@@ -590,22 +618,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
590
618
|
}
|
|
591
619
|
|
|
592
620
|
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
|
-
);
|
|
621
|
+
document.addEventListener("keyup", handleClipboardPasteFallback, true);
|
|
622
|
+
document.addEventListener("paste", handlePaste, true);
|
|
609
623
|
|
|
610
624
|
// Trigger a native browser download via a transient anchor click. The Rust
|
|
611
625
|
// on_download handler then writes the file to the Downloads folder. This is
|