@xditya/pastr 0.2.0 → 0.4.2
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 +4 -3
- package/package.json +1 -1
- package/pastr.mjs +204 -36
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Paste from the terminal to any [pastr](https://github.com/xditya/pastr) instance
|
|
|
4
4
|
|
|
5
5
|
```sh
|
|
6
6
|
npm install -g @xditya/pastr # or: npx @xditya/pastr …
|
|
7
|
-
pastr config host https://your-pastr.example
|
|
7
|
+
pastr config host https://your-pastr.example # optional, defaults to pastr.xditya.me
|
|
8
8
|
|
|
9
9
|
ls -la | pastr # stdin → link
|
|
10
10
|
pastr main.go --expires 1d # file (language from the extension)
|
|
@@ -12,13 +12,14 @@ pastr clip -E -c # clipboard, encrypted in the terminal, link cop
|
|
|
12
12
|
pastr shot.png # images (png/jpeg/gif/webp, up to 700 KB); clip also takes a copied image
|
|
13
13
|
pastr text "hello there" -b # literal text, burn after read
|
|
14
14
|
pastr get https://host/AbCd1234#key
|
|
15
|
-
pastr ls # what you pasted
|
|
15
|
+
pastr ls # browse what you pasted here: ↑↓ or click a row, enter reveals the edit token, again copies it
|
|
16
16
|
pastr rm AbCd1234 # delete (uses the locally stored edit token)
|
|
17
|
+
pastr token AbCd1234 # show that token, for the site's Edit/Delete prompt
|
|
17
18
|
```
|
|
18
19
|
|
|
19
20
|
- Zero dependencies, Node 20+, macOS/Linux/Windows/WSL.
|
|
20
21
|
- `-E` encrypts with AES-256-GCM before upload; the key is only in the URL fragment. `-p`/`-P` uses a password instead.
|
|
21
22
|
- Edit tokens and link keys are kept in `~/.config/pastr/history.json` (`%APPDATA%\pastr` on Windows), mode 0600.
|
|
22
|
-
-
|
|
23
|
+
- Talks to pastr.xditya.me unless you point it elsewhere with `pastr config host …`, `PASTR_HOST` or `-H`.
|
|
23
24
|
|
|
24
25
|
Prefer no Node at all? Every pastr instance serves a tiny POSIX shell version: `curl -fsSL https://your-pastr.example/install.sh | sh`.
|
package/package.json
CHANGED
package/pastr.mjs
CHANGED
|
@@ -11,11 +11,49 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
11
11
|
import { homedir, platform } from "node:os";
|
|
12
12
|
import { basename, join } from "node:path";
|
|
13
13
|
import { fileURLToPath } from "node:url";
|
|
14
|
-
import { parseArgs } from "node:util";
|
|
14
|
+
import { parseArgs, styleText } from "node:util";
|
|
15
15
|
import { createInterface } from "node:readline";
|
|
16
16
|
|
|
17
|
-
export const VERSION = "0.2
|
|
17
|
+
export const VERSION = "0.4.2";
|
|
18
18
|
const NAME = "pastr";
|
|
19
|
+
const DEFAULT_HOST = "https://pastr.xditya.me";
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Terminal styling: colour only on a TTY, never when piped or under NO_COLOR,
|
|
23
|
+
// so `pastr | pbcopy` and scripts see plain text.
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
const OUT_TTY = !process.env.NO_COLOR && !!process.stdout.isTTY;
|
|
27
|
+
const ERR_TTY = !process.env.NO_COLOR && !!process.stderr.isTTY;
|
|
28
|
+
const paint = (style) => (s) => (OUT_TTY && styleText ? styleText(style, s) : s);
|
|
29
|
+
const bold = paint("bold");
|
|
30
|
+
const dim = paint("dim");
|
|
31
|
+
const cyan = paint("cyan");
|
|
32
|
+
const yellow = paint("yellow");
|
|
33
|
+
const green = paint("green");
|
|
34
|
+
const link = paint(["cyan", "underline"]);
|
|
35
|
+
|
|
36
|
+
const len = (s) => [...s].length;
|
|
37
|
+
const fit = (s, n) => (len(s) > n ? [...s].slice(0, Math.max(n - 1, 0)).join("") + "…" : s.padEnd(n));
|
|
38
|
+
|
|
39
|
+
/** Box-drawn table sized to the terminal; column `shrink` gives up characters first, then the widest. */
|
|
40
|
+
export function table(head, rows, styles = [], shrink = -1, width = process.stdout.columns || 120, selected = -1) {
|
|
41
|
+
const w = head.map((h, i) => Math.max(len(h), ...rows.map((r) => len(r[i]))));
|
|
42
|
+
let over = w.reduce((a, b) => a + b, 0) + 3 * w.length + 1 - width;
|
|
43
|
+
while (over > 0) {
|
|
44
|
+
const i = w[shrink] > 8 ? shrink : w.indexOf(Math.max(...w));
|
|
45
|
+
if (w[i] <= 8) break;
|
|
46
|
+
w[i]--;
|
|
47
|
+
over--;
|
|
48
|
+
}
|
|
49
|
+
const rule = (l, m, r) => dim(l + w.map((n) => "─".repeat(n + 2)).join(m) + r);
|
|
50
|
+
const line = (cells, f) => dim("│ ") + cells.map((c, i) => (f[i] ?? ((s) => s))(fit(c, w[i]))).join(dim(" │ ")) + dim(" │");
|
|
51
|
+
const body = rows.map((r, i) => (i === selected ? line(r, r.map(() => paint("inverse"))) : line(r, styles)));
|
|
52
|
+
return [rule("╭", "┬", "╮"), line(head, head.map(() => bold)), rule("├", "┼", "┤"), ...body, rule("╰", "┴", "╯")].join("\n") + "\n";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const ok = (msg) => out(`${OUT_TTY ? green("✓ ") : ""}${msg}\n`);
|
|
56
|
+
const note = (msg) => process.stderr.write(ERR_TTY ? styleText("dim", ` ${msg}\n`) : `${msg}\n`);
|
|
19
57
|
|
|
20
58
|
// ---------------------------------------------------------------------------
|
|
21
59
|
// Config & history
|
|
@@ -67,10 +105,7 @@ function forget(id) {
|
|
|
67
105
|
}
|
|
68
106
|
|
|
69
107
|
export function resolveHost(flag) {
|
|
70
|
-
const host = flag || process.env.PASTR_HOST || getConfig().host;
|
|
71
|
-
if (!host) {
|
|
72
|
-
throw new UsageError(`no host configured. Run \`${NAME} config host https://your-pastr.example\` or set PASTR_HOST.`);
|
|
73
|
-
}
|
|
108
|
+
const host = flag || process.env.PASTR_HOST || getConfig().host || DEFAULT_HOST;
|
|
74
109
|
return host.replace(/\/+$/, "");
|
|
75
110
|
}
|
|
76
111
|
|
|
@@ -226,10 +261,20 @@ function promptHidden(question) {
|
|
|
226
261
|
}
|
|
227
262
|
|
|
228
263
|
async function readStdin() {
|
|
229
|
-
if (process.stdin.isTTY)
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
264
|
+
if (!process.stdin.isTTY) {
|
|
265
|
+
const chunks = [];
|
|
266
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
267
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
268
|
+
}
|
|
269
|
+
// Interactive: readline handles Ctrl-D itself, so it ends the paste on Windows too (the console's
|
|
270
|
+
// Ctrl-Z convention never reaches a Node stream reliably there).
|
|
271
|
+
note("Type or paste, then press Ctrl-D on an empty line to finish:");
|
|
272
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: true, prompt: "" });
|
|
273
|
+
const lines = [];
|
|
274
|
+
rl.on("line", (l) => lines.push(l));
|
|
275
|
+
rl.on("SIGINT", () => process.exit(130));
|
|
276
|
+
await new Promise((resolve) => rl.on("close", resolve));
|
|
277
|
+
return lines.length ? lines.join("\n") + "\n" : "";
|
|
233
278
|
}
|
|
234
279
|
|
|
235
280
|
// ---------------------------------------------------------------------------
|
|
@@ -336,15 +381,16 @@ async function createPaste(host, { content, title, lang, expires, burn, encrypt,
|
|
|
336
381
|
// CLI
|
|
337
382
|
// ---------------------------------------------------------------------------
|
|
338
383
|
|
|
339
|
-
const HELP = `${NAME} ${VERSION}
|
|
384
|
+
const HELP = `${NAME} ${VERSION} · paste from the terminal
|
|
340
385
|
|
|
341
386
|
Usage
|
|
342
387
|
${NAME} [options] [file ...] paste files (text, or png/jpeg/gif/webp up to 700 KB), or stdin
|
|
343
388
|
${NAME} clip [options] paste the clipboard (text or an image)
|
|
344
389
|
${NAME} text [options] <words ...> paste literal text
|
|
345
390
|
${NAME} get <id|url> [--json] print a paste (decrypts when the URL carries a #key)
|
|
346
|
-
${NAME} ls
|
|
391
|
+
${NAME} ls [--json] browse pastes made here: arrows or click, enter reveals the edit token
|
|
347
392
|
${NAME} rm <id|url> delete a paste created from this machine
|
|
393
|
+
${NAME} token <id|url> print the edit token (paste it into the site's Edit/Delete prompt)
|
|
348
394
|
${NAME} config [host <url>] show or set the default host
|
|
349
395
|
|
|
350
396
|
Options
|
|
@@ -359,7 +405,7 @@ Options
|
|
|
359
405
|
-o, --open open the URL in a browser
|
|
360
406
|
-r, --raw print the raw URL (plain text) instead of the page URL
|
|
361
407
|
-j, --json print the full API response
|
|
362
|
-
-H, --host <url> server to use (env PASTR_HOST, or \`${NAME} config host …\`)
|
|
408
|
+
-H, --host <url> server to use (default ${DEFAULT_HOST}; env PASTR_HOST, or \`${NAME} config host …\`)
|
|
363
409
|
-h, --help show this help
|
|
364
410
|
-v, --version show the version
|
|
365
411
|
|
|
@@ -372,6 +418,13 @@ Examples
|
|
|
372
418
|
${NAME} shot.png -e 1d # image paste; "get" writes the bytes back
|
|
373
419
|
`;
|
|
374
420
|
|
|
421
|
+
/** Bold section headings, cyan command/flag column, dim trailing comments. */
|
|
422
|
+
const styleHelp = (s) =>
|
|
423
|
+
s
|
|
424
|
+
.split("\n")
|
|
425
|
+
.map((l) => (/^\S/.test(l) ? bold(l) : l.replace(/^(\s+)(\S.*?)(\s{2,}|$)/, (_, a, b, c) => a + cyan(b) + c).replace(/#.*$/, dim)))
|
|
426
|
+
.join("\n");
|
|
427
|
+
|
|
375
428
|
function fmtRel(ms) {
|
|
376
429
|
const d = ms - Date.now();
|
|
377
430
|
const abs = Math.abs(d);
|
|
@@ -380,6 +433,104 @@ function fmtRel(ms) {
|
|
|
380
433
|
return d >= 0 ? `in ${n}${u[1]}` : `${n}${u[1]} ago`;
|
|
381
434
|
}
|
|
382
435
|
|
|
436
|
+
const flags = (p) => [p.encrypted && "enc", p.burn && "burn"].filter(Boolean).join(" ");
|
|
437
|
+
|
|
438
|
+
/** Keys stay out of the table (`get <id>` finds them in history); the piped form has the full URL. */
|
|
439
|
+
function lsTable(list, selected = -1) {
|
|
440
|
+
const head = ["id", "title", "lang", "created", "expires", "", "url"];
|
|
441
|
+
const rows = list.map((p) => [p.id, p.title || "", p.lang || "", fmtRel(p.created), p.expires ? fmtRel(p.expires) : "never", flags(p), p.url.split("#")[0]]);
|
|
442
|
+
// Narrow window: drop the url column (the id is enough for `get`) rather than mangling it.
|
|
443
|
+
const need = head.reduce((sum, h, i) => sum + 3 + Math.min(i === 1 ? 20 : Infinity, Math.max(len(h), ...rows.map((r) => len(r[i])))), 1);
|
|
444
|
+
if (need > (process.stdout.columns || 120)) for (const r of [head, ...rows]) r.pop();
|
|
445
|
+
return table(head, rows, [cyan, undefined, dim, dim, undefined, yellow, dim], 1, process.stdout.columns || 120, selected);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Interactive `ls`: arrow keys or a mouse click pick a paste, Enter (or a second click) reveals its
|
|
450
|
+
* edit token, once more copies it. Runs on the alternate screen so the shell scrollback stays clean.
|
|
451
|
+
*/
|
|
452
|
+
function browse(list) {
|
|
453
|
+
const { stdin, stdout } = process;
|
|
454
|
+
let sel = 0;
|
|
455
|
+
let revealed = false;
|
|
456
|
+
let msg = "";
|
|
457
|
+
let confirmDelete = false;
|
|
458
|
+
const rowsVisible = () => Math.max(3, (stdout.rows || 24) - 10);
|
|
459
|
+
const first = () => Math.min(Math.max(0, sel - rowsVisible() + 1), Math.max(0, list.length - rowsVisible()));
|
|
460
|
+
const draw = () => {
|
|
461
|
+
const p = list[sel];
|
|
462
|
+
const start = first();
|
|
463
|
+
const detail = [
|
|
464
|
+
` ${cyan(p.id)} ${p.title || dim("untitled")}`,
|
|
465
|
+
` ${dim("url ")}${link(p.url)}`,
|
|
466
|
+
` ${dim("token ")}${p.editToken ? (revealed ? yellow(p.editToken) : dim("•".repeat(24) + " enter to reveal")) : dim("not on this machine")}`,
|
|
467
|
+
"",
|
|
468
|
+
confirmDelete ? yellow(` delete ${p.id} from the server? y/n`) : msg ? ` ${msg}` : dim(" ↑↓ move · enter reveal, again to copy token · c copy url · o open · d delete · q quit"),
|
|
469
|
+
];
|
|
470
|
+
stdout.write("\x1b[H\x1b[2J" + lsTable(list.slice(start, start + rowsVisible()), sel - start) + detail.join("\n") + "\n");
|
|
471
|
+
};
|
|
472
|
+
const enter = async () => {
|
|
473
|
+
const p = list[sel];
|
|
474
|
+
if (!p.editToken) return (msg = yellow("no token stored for this paste"));
|
|
475
|
+
if (!revealed) return (revealed = true);
|
|
476
|
+
msg = writeClipboard(p.editToken) ? green("✓ token copied") : yellow("could not copy");
|
|
477
|
+
};
|
|
478
|
+
const select = (i) => {
|
|
479
|
+
if (i === sel || i < 0 || i >= list.length) return false;
|
|
480
|
+
sel = i;
|
|
481
|
+
revealed = false;
|
|
482
|
+
msg = "";
|
|
483
|
+
return true;
|
|
484
|
+
};
|
|
485
|
+
return new Promise((resolve) => {
|
|
486
|
+
const done = () => {
|
|
487
|
+
stdout.write("\x1b[?1006l\x1b[?1000l\x1b[?25h\x1b[?1049l");
|
|
488
|
+
stdin.setRawMode(false);
|
|
489
|
+
stdin.pause();
|
|
490
|
+
resolve();
|
|
491
|
+
};
|
|
492
|
+
stdin.setRawMode(true);
|
|
493
|
+
stdin.resume();
|
|
494
|
+
stdin.setEncoding("utf8");
|
|
495
|
+
stdout.write("\x1b[?1049h\x1b[?25l\x1b[?1000h\x1b[?1006h");
|
|
496
|
+
draw();
|
|
497
|
+
stdin.on("data", async (k) => {
|
|
498
|
+
const mouse = /^\x1b\[<(\d+);(\d+);(\d+)M/.exec(k);
|
|
499
|
+
if (confirmDelete) {
|
|
500
|
+
confirmDelete = false;
|
|
501
|
+
if (k === "y") {
|
|
502
|
+
const p = list[sel];
|
|
503
|
+
try {
|
|
504
|
+
await api(new URL(p.url).origin, `/api/v1/pastes/${p.id}`, { method: "DELETE", headers: { Authorization: `Bearer ${p.editToken}` } });
|
|
505
|
+
forget(p.id);
|
|
506
|
+
list.splice(sel, 1);
|
|
507
|
+
if (!list.length) return done();
|
|
508
|
+
sel = Math.min(sel, list.length - 1);
|
|
509
|
+
msg = green(`✓ deleted ${p.id}`);
|
|
510
|
+
} catch (err) {
|
|
511
|
+
msg = yellow(err.message);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
} else if (mouse) {
|
|
515
|
+
const [, button, , y] = mouse.map(Number);
|
|
516
|
+
if (button === 64) select(sel - 1);
|
|
517
|
+
else if (button === 65) select(sel + 1);
|
|
518
|
+
else if (button === 0) {
|
|
519
|
+
const i = y - 4 + first(); // 3 header lines above the first row
|
|
520
|
+
if (i >= 0 && i < list.length && !select(i)) await enter();
|
|
521
|
+
}
|
|
522
|
+
} else if (k === "\x1b[A" || k === "k") select(sel - 1);
|
|
523
|
+
else if (k === "\x1b[B" || k === "j") select(sel + 1);
|
|
524
|
+
else if (k === "\r") await enter();
|
|
525
|
+
else if (k === "c") msg = writeClipboard(list[sel].url) ? green("✓ url copied") : yellow("could not copy");
|
|
526
|
+
else if (k === "o") openInBrowser(list[sel].url);
|
|
527
|
+
else if (k === "d") confirmDelete = !!list[sel].editToken || ((msg = yellow("no token stored for this paste")), false);
|
|
528
|
+
else if (k === "q" || k === "\x1b" || k === "\x03") return done();
|
|
529
|
+
draw();
|
|
530
|
+
});
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
|
|
383
534
|
export async function main(argv) {
|
|
384
535
|
const { values: o, positionals } = parseArgs({
|
|
385
536
|
args: argv,
|
|
@@ -402,7 +553,7 @@ export async function main(argv) {
|
|
|
402
553
|
},
|
|
403
554
|
});
|
|
404
555
|
|
|
405
|
-
if (o.help) return out(HELP);
|
|
556
|
+
if (o.help) return out(styleHelp(HELP));
|
|
406
557
|
if (o.version) return out(`${NAME} ${VERSION}\n`);
|
|
407
558
|
|
|
408
559
|
const [cmd, ...rest] = positionals;
|
|
@@ -410,20 +561,25 @@ export async function main(argv) {
|
|
|
410
561
|
if (cmd === "config") {
|
|
411
562
|
if (rest[0] === "host" && rest[1]) {
|
|
412
563
|
setConfig({ host: rest[1].replace(/\/+$/, "") });
|
|
413
|
-
return
|
|
564
|
+
return ok(`host set to ${rest[1]}`);
|
|
414
565
|
}
|
|
415
566
|
const cfg = getConfig();
|
|
416
|
-
|
|
567
|
+
const source = process.env.PASTR_HOST ? "env PASTR_HOST" : cfg.host ? "config" : "default";
|
|
568
|
+
return out(`${dim("host ")} ${link(process.env.PASTR_HOST || cfg.host || DEFAULT_HOST)} ${dim(`(${source})`)}\n${dim("config ")} ${configDir()}\n`);
|
|
417
569
|
}
|
|
418
570
|
|
|
419
571
|
if (cmd === "ls") {
|
|
420
572
|
const list = getHistory();
|
|
421
|
-
if (
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
573
|
+
if (o.json) return out(JSON.stringify(list, null, 2) + "\n");
|
|
574
|
+
if (!list.length) return out(`no pastes yet${OUT_TTY ? dim(` · try: ls -la | ${NAME}`) : ""}\n`);
|
|
575
|
+
// Piped: one tab-separated line per paste, ISO dates. TTY: a table sized to the window.
|
|
576
|
+
if (!OUT_TTY) {
|
|
577
|
+
for (const p of list) out([p.id, p.title || "", p.lang || "", new Date(p.created).toISOString(), p.expires ? new Date(p.expires).toISOString() : "never", flags(p), p.url].join("\t") + "\n");
|
|
578
|
+
return;
|
|
425
579
|
}
|
|
426
|
-
return;
|
|
580
|
+
if (process.stdin.isTTY) return browse(list);
|
|
581
|
+
out(lsTable(list));
|
|
582
|
+
return out(dim(` ${list.length} ${list.length === 1 ? "paste" : "pastes"} · ${NAME} get <id> · ${NAME} rm <id>\n`));
|
|
427
583
|
}
|
|
428
584
|
|
|
429
585
|
if (cmd === "get") {
|
|
@@ -445,6 +601,15 @@ export async function main(argv) {
|
|
|
445
601
|
return emit(env.content, env.lang);
|
|
446
602
|
}
|
|
447
603
|
|
|
604
|
+
if (cmd === "token") {
|
|
605
|
+
if (!rest[0]) throw new UsageError("usage: token <id|url>");
|
|
606
|
+
const { id } = parsePasteRef(rest[0]);
|
|
607
|
+
const entry = getHistory().find((h) => h.id === id);
|
|
608
|
+
if (!entry?.editToken) throw new CliError(`no edit token for ${id} on this machine`);
|
|
609
|
+
if (OUT_TTY) note("Anyone with this token can edit or delete the paste.");
|
|
610
|
+
return out(entry.editToken + "\n");
|
|
611
|
+
}
|
|
612
|
+
|
|
448
613
|
if (cmd === "rm") {
|
|
449
614
|
if (!rest[0]) throw new UsageError("usage: rm <id|url>");
|
|
450
615
|
const ref = parsePasteRef(rest[0]);
|
|
@@ -454,7 +619,7 @@ export async function main(argv) {
|
|
|
454
619
|
if (!token) throw new CliError(`no edit token for ${ref.id} on this machine (set PASTR_EDIT_TOKEN to use one)`);
|
|
455
620
|
await api(host, `/api/v1/pastes/${ref.id}`, { method: "DELETE", headers: { Authorization: `Bearer ${token}` } });
|
|
456
621
|
forget(ref.id);
|
|
457
|
-
return
|
|
622
|
+
return ok(`deleted ${ref.id}`);
|
|
458
623
|
}
|
|
459
624
|
|
|
460
625
|
// ---- create ----
|
|
@@ -485,21 +650,22 @@ export async function main(argv) {
|
|
|
485
650
|
|
|
486
651
|
for (const item of items) {
|
|
487
652
|
if (!item.content.trim()) throw new CliError("nothing to paste");
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
expires: o.expires,
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
});
|
|
653
|
+
// Progress on stderr while the upload (and PBKDF2 for -p) runs, cleared before the result.
|
|
654
|
+
if (ERR_TTY) process.stderr.write(styleText("dim", " uploading…"));
|
|
655
|
+
let p;
|
|
656
|
+
try {
|
|
657
|
+
p = await createPaste(host, { content: item.content, title: item.title, lang: item.lang ?? o.lang, expires: o.expires, burn: o.burn, encrypt: o.encrypt, password });
|
|
658
|
+
} finally {
|
|
659
|
+
if (ERR_TTY) process.stderr.write("\r\x1b[2K");
|
|
660
|
+
}
|
|
497
661
|
const url = o.raw ? p.rawUrl : p.url;
|
|
498
662
|
if (o.json) out(JSON.stringify(p, null, 2) + "\n");
|
|
499
|
-
else out(url + "\n");
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
663
|
+
else out(link(url) + "\n");
|
|
664
|
+
const copied = o.copy ? (writeClipboard(url) ? "copied to clipboard" : "could not copy to clipboard") : "";
|
|
665
|
+
// The URL alone goes to stdout; everything else is a dim stderr line so pipes stay clean.
|
|
666
|
+
if (!o.json && (ERR_TTY || copied)) {
|
|
667
|
+
const meta = ERR_TTY ? [item.title, p.expires ? `expires ${fmtRel(p.expires)}` : "never expires", p.burn && "burn after read", p.enc && "encrypted", copied, `${NAME} token ${p.id} to edit on the site`] : [copied];
|
|
668
|
+
note(meta.filter(Boolean).join(" · "));
|
|
503
669
|
}
|
|
504
670
|
if (o.open) openInBrowser(url);
|
|
505
671
|
}
|
|
@@ -518,7 +684,9 @@ const isMain = process.argv[1] && import.meta.url === new URL(`file://${process.
|
|
|
518
684
|
if (isMain || (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1])) {
|
|
519
685
|
main(process.argv.slice(2)).catch((err) => {
|
|
520
686
|
const usage = err instanceof UsageError || err?.code === "ERR_PARSE_ARGS_UNKNOWN_OPTION" || err?.code?.startsWith?.("ERR_PARSE_ARGS");
|
|
521
|
-
|
|
687
|
+
const prefix = ERR_TTY ? styleText("red", "✗") : `${NAME}:`;
|
|
688
|
+
const hint = usage ? `Run \`${NAME} --help\` for usage.\n` : "";
|
|
689
|
+
process.stderr.write(`${prefix} ${err.message}\n${ERR_TTY ? styleText("dim", hint) : hint}`);
|
|
522
690
|
process.exit(usage ? 2 : 1);
|
|
523
691
|
});
|
|
524
692
|
}
|