@vercel/board 0.24.0 → 0.25.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/dist/{a2-BxfKZFvJ.mjs → a2-CQ3WcqMv.mjs} +6 -6
- package/dist/access.mjs +3 -3
- package/dist/{actors-rPnEQLZu.mjs → actors-CDDhhFNY.mjs} +2 -2
- package/dist/{auth-5nLsx9qB.mjs → auth-BwWH3Twu.mjs} +1 -1
- package/dist/{backend-D0GDYiFr.d.mts → backend-DmXjVnil.d.mts} +6 -2
- package/dist/backend-a2.d.mts +2 -2
- package/dist/backend-a2.mjs +1 -1
- package/dist/{backend-http-rfiG-AKP.mjs → backend-http-BJ7AdK0c.mjs} +6 -6
- package/dist/{backend-http-Dvq2p__1.d.mts → backend-http-BsP0Xq9e.d.mts} +2 -2
- package/dist/{backend-http-tGUo0e8j.mjs → backend-http-Cs-UKpYp.mjs} +2 -2
- package/dist/backend-http.d.mts +1 -1
- package/dist/backend-http.mjs +1 -1
- package/dist/backend-http.node.d.mts +1 -1
- package/dist/backend-http.node.mjs +1 -1
- package/dist/{backend-vercel-C3zuxmcD.mjs → backend-vercel-B0Q0K0O5.mjs} +3 -3
- package/dist/{backend-vercel-01QKDobk.d.mts → backend-vercel-CED673bx.d.mts} +2 -2
- package/dist/{backend-vercel-DM5PyMc2.mjs → backend-vercel-CG2SYa03.mjs} +3 -3
- package/dist/backend-vercel.d.mts +1 -1
- package/dist/backend-vercel.mjs +1 -1
- package/dist/backend-vercel.node.d.mts +1 -1
- package/dist/backend-vercel.node.mjs +1 -1
- package/dist/{chat-provider-D0k3y8zY.d.mts → chat-provider-6eYkL2ef.d.mts} +1 -1
- package/dist/cli-embedded.d.mts +55 -4
- package/dist/cli-embedded.mjs +38 -18
- package/dist/cli.mjs +4 -4
- package/dist/{client-ClznIBMg.mjs → client-BY-BCggB.mjs} +13 -7
- package/dist/{conformance-DQ9qVzWP.mjs → conformance-BZ53bxrG.mjs} +1 -1
- package/dist/conformance.d.mts +1 -1
- package/dist/conformance.mjs +1 -1
- package/dist/{engine-kHfSOXnQ.mjs → engine-SeaJ_CIY.mjs} +2 -2
- package/dist/{errors-CQ-fwxM3.mjs → errors-Bven0k29.mjs} +7 -3
- package/dist/{exports-Bw81oDDl.mjs → exports-ByvYtxFT.mjs} +6 -6
- package/dist/exports-CPrIkpz3.d.mts +4 -0
- package/dist/{index-B_hWanKF.d.mts → index-k2LfZxl-.d.mts} +2 -2
- package/dist/index.d.mts +3 -3
- package/dist/{index.default-B6oPlvEa.mjs → index.default-DmfhBZZi.mjs} +1 -1
- package/dist/index.mjs +6 -6
- package/dist/index.node.d.mts +3 -3
- package/dist/index.node.mjs +4 -4
- package/dist/inspect/app.js +1 -1
- package/dist/{main-CzWvRy91.mjs → main-BNkywL2P.mjs} +706 -141
- package/dist/{middleware-DUjyQP_a.mjs → middleware-B0J7QBq7.mjs} +1 -1
- package/dist/{node-CirFYqw0.mjs → node-C51_xzCb.mjs} +23 -23
- package/dist/{provider-D11Dfda0.d.mts → provider-C_sj4-GL.d.mts} +10 -1
- package/dist/{serve-DHyA-6FR.mjs → serve-DYQ64Q5R.mjs} +6 -5
- package/dist/{server-Cp2oIQOj.mjs → server-BgK1sgj7.mjs} +4 -4
- package/dist/{serving-B307gppu.mjs → serving-rXQcEgTO.mjs} +1 -1
- package/dist/sync-github.d.mts +2 -2
- package/dist/sync-kapso.d.mts +2 -2
- package/dist/sync-linear.d.mts +2 -2
- package/dist/sync-slack.d.mts +2 -2
- package/dist/sync.chat.d.mts +1 -1
- package/dist/sync.d.mts +1 -1
- package/dist/{tokens-DUXaXwOj.mjs → tokens-U-XRlEfq.mjs} +2 -2
- package/dist/{watchers-hash-C_1jGA53.mjs → watchers-hash-C4CJg7K1.mjs} +2 -2
- package/docs/01-board/03-identity.md +16 -7
- package/docs/01-board/04-messages.md +2 -2
- package/docs/01-board/05-reading.md +16 -8
- package/docs/01-board/07-schedules.md +2 -2
- package/docs/01-board/09-access.md +5 -4
- package/docs/03-guides/01-coding-agents.md +4 -2
- package/docs/03-guides/03-ci.md +2 -2
- package/docs/04-reference/01-cli.md +78 -19
- package/docs/04-reference/02-board.md +2 -1
- package/docs/04-reference/03-http.md +4 -0
- package/package.json +1 -1
- package/dist/exports-87_yESpp.d.mts +0 -4
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { i as fileSize, n as DIRECT_UPLOAD_BYTES_MAX } from "./files-SniSmeEr.mjs";
|
|
2
2
|
import { a as AttachmentId } from "./schemas-Be08nyfW.mjs";
|
|
3
|
-
import { d as VERSION, i as withMentions } from "./watchers-hash-
|
|
3
|
+
import { d as VERSION, i as withMentions } from "./watchers-hash-C4CJg7K1.mjs";
|
|
4
4
|
import { n as displayEmoji, r as normalizeEmoji } from "./emoji-CTLpvH-A.mjs";
|
|
5
|
-
import { t as BoardError } from "./client-
|
|
6
|
-
import { i as groups, n as filters$1, o as options, r as globalOptions, s as usage, t as commands } from "./commands-BtL4jFq8.mjs";
|
|
7
|
-
import { n as connect, s as vercelProjectFile, t as boardUrl } from "./node-
|
|
8
|
-
import { t as http } from "./backend-http-
|
|
9
|
-
import { t as board } from "./server-
|
|
10
|
-
import { t as conformance } from "./conformance-
|
|
11
|
-
import { n as vercel, t as credentialSource } from "./backend-vercel-
|
|
5
|
+
import { t as BoardError } from "./client-BY-BCggB.mjs";
|
|
6
|
+
import { i as groups, n as filters$1, o as options, r as globalOptions, s as usage$1, t as commands } from "./commands-BtL4jFq8.mjs";
|
|
7
|
+
import { n as connect, s as vercelProjectFile, t as boardUrl } from "./node-C51_xzCb.mjs";
|
|
8
|
+
import { t as http } from "./backend-http-Cs-UKpYp.mjs";
|
|
9
|
+
import { t as board } from "./server-BgK1sgj7.mjs";
|
|
10
|
+
import { t as conformance } from "./conformance-BZ53bxrG.mjs";
|
|
11
|
+
import { n as vercel, t as credentialSource } from "./backend-vercel-B0Q0K0O5.mjs";
|
|
12
12
|
import { t as displayText } from "./bots-CGkO76qH.mjs";
|
|
13
13
|
import { spawn } from "node:child_process";
|
|
14
14
|
import { readFile, stat, writeFile } from "node:fs/promises";
|
|
@@ -450,7 +450,7 @@ const flag = (option) => `${option.short ? `-${option.short}, ` : ""}--${option.
|
|
|
450
450
|
const shown = (list) => list.filter((option) => !option.hidden);
|
|
451
451
|
/** A command's rows in the list: itself, when it runs alone, and each of its subcommands. */
|
|
452
452
|
function listed(command) {
|
|
453
|
-
return [...!command.subcommands || command.runsWithoutSubcommand ? [[usage([command]), command.description]] : [], ...(command.subcommands ?? []).filter((sub) => !sub.hidden).map((sub) => [usage([command, sub]), sub.description])];
|
|
453
|
+
return [...!command.subcommands || command.runsWithoutSubcommand ? [[usage$1([command]), command.description]] : [], ...(command.subcommands ?? []).filter((sub) => !sub.hidden).map((sub) => [usage$1([command, sub]), sub.description])];
|
|
454
454
|
}
|
|
455
455
|
/** Every command, by group, then what the app adds (`serve`, `inspect`), and the options they share. */
|
|
456
456
|
function rootHelp(app, version) {
|
|
@@ -473,7 +473,7 @@ function commandHelp(name, path) {
|
|
|
473
473
|
const options = shown(command.options);
|
|
474
474
|
const subcommands = (command.subcommands ?? []).filter((sub) => !sub.hidden);
|
|
475
475
|
const lines = [
|
|
476
|
-
`usage: ${name} ${usage(path)}${options.length || command.filters ? " [options]" : ""}`,
|
|
476
|
+
`usage: ${name} ${usage$1(path)}${options.length || command.filters ? " [options]" : ""}`,
|
|
477
477
|
"",
|
|
478
478
|
...wrap(command.description, COLUMNS)
|
|
479
479
|
];
|
|
@@ -482,7 +482,7 @@ function commandHelp(name, path) {
|
|
|
482
482
|
if (entries.length) lines.push("", `${title}:`, ...rows(entries));
|
|
483
483
|
};
|
|
484
484
|
section("arguments", command.arguments.filter((argument) => argument.description).map((argument) => [argument.name, argument.description]));
|
|
485
|
-
section("commands", subcommands.map((sub) => [usage([...path, sub]), sub.description]));
|
|
485
|
+
section("commands", subcommands.map((sub) => [usage$1([...path, sub]), sub.description]));
|
|
486
486
|
section("options", options.map((option) => [flag(option), option.description]));
|
|
487
487
|
if (command.filters) {
|
|
488
488
|
section("filters", shown(filters$1).map((option) => [flag(option), option.description]));
|
|
@@ -508,8 +508,120 @@ function commandPath(words, all) {
|
|
|
508
508
|
return sub ? [command, sub] : [command];
|
|
509
509
|
}
|
|
510
510
|
//#endregion
|
|
511
|
+
//#region src/cli/failure.ts
|
|
512
|
+
function exitCode(failure) {
|
|
513
|
+
if (failure.class === "usage" || failure.class === "missing_input") return 2;
|
|
514
|
+
if (failure.class === "timeout") return 124;
|
|
515
|
+
if (failure.class === "canceled") return 130;
|
|
516
|
+
return 1;
|
|
517
|
+
}
|
|
518
|
+
/** An argument as a shell reads it back: bare when it's safe, quoted otherwise. A placeholder (`<TEAM>`) stays as it is. */
|
|
519
|
+
const shellWord = (arg) => /^<[A-Z_]+>$/.test(arg) || /^[\w@%+=:,./-]+$/.test(arg) ? arg : `'${arg.replace(/'/g, `'\\''`)}'`;
|
|
520
|
+
/** A recovery's command, as a person types it. */
|
|
521
|
+
const commandLine = (argv) => argv.map(shellWord).join(" ");
|
|
522
|
+
/**
|
|
523
|
+
* The failure as lines, the headline first and the rest as they sit under it, before the indent a
|
|
524
|
+
* CLI adds under its own mark (`error: `, `✗ `): details, context rows, then each way out after a
|
|
525
|
+
* blank line, its command two spaces in.
|
|
526
|
+
*/
|
|
527
|
+
function failureLines(failure) {
|
|
528
|
+
const lines = [failure.message, ...failure.details ?? []];
|
|
529
|
+
for (const [label, value] of failure.context ?? []) lines.push(`${label.padEnd(14)} ${value}`);
|
|
530
|
+
if (failure.next?.length) {
|
|
531
|
+
lines.push("");
|
|
532
|
+
for (const { instruction, argv } of failure.next) if (!argv) lines.push(instruction);
|
|
533
|
+
else lines.push(`${instruction}:`, ` ${commandLine(argv)}`);
|
|
534
|
+
}
|
|
535
|
+
return lines;
|
|
536
|
+
}
|
|
537
|
+
/** How board's own CLI prints it: `error: ` and the headline, the rest two spaces in. */
|
|
538
|
+
function formatFailure(failure) {
|
|
539
|
+
return failureLines(failure).map((line, index) => index === 0 ? `error: ${line}` : line ? ` ${line}` : "").join("\n");
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* The JSON error document `--json` prints on stderr, one line: each way out with a command in
|
|
543
|
+
* `next`, and the ones that are only words in `hint`.
|
|
544
|
+
*/
|
|
545
|
+
function failureDocument(failure) {
|
|
546
|
+
const next = (failure.next ?? []).flatMap(({ instruction, argv }) => argv ? [{
|
|
547
|
+
command: commandLine(argv),
|
|
548
|
+
argv,
|
|
549
|
+
when: instruction
|
|
550
|
+
}] : []);
|
|
551
|
+
const hint = (failure.next ?? []).flatMap(({ instruction, argv }) => argv ? [] : [instruction]);
|
|
552
|
+
return {
|
|
553
|
+
status: failure.actionRequired ? "action_required" : "error",
|
|
554
|
+
reason: failure.reason,
|
|
555
|
+
message: failure.message,
|
|
556
|
+
...next.length ? { next } : {},
|
|
557
|
+
...hint.length ? { hint: hint.join(" ") } : {},
|
|
558
|
+
...failure.data ? { data: failure.data } : {}
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* The closest of `known` to `word`, when it's a likely typo: one edit away (a letter added, dropped,
|
|
563
|
+
* changed, or two swapped), or two for a word of six letters or more.
|
|
564
|
+
*/
|
|
565
|
+
function closest(word, known) {
|
|
566
|
+
let best;
|
|
567
|
+
let distance = Infinity;
|
|
568
|
+
for (const candidate of known) {
|
|
569
|
+
const d = edits(word.toLowerCase(), candidate.toLowerCase());
|
|
570
|
+
if (d < distance) [best, distance] = [candidate, d];
|
|
571
|
+
}
|
|
572
|
+
return best !== void 0 && distance > 0 && distance <= (word.length >= 6 ? 2 : 1) ? best : void 0;
|
|
573
|
+
}
|
|
574
|
+
/** How many edits turn `a` into `b`, a swap of neighbors counting as one. */
|
|
575
|
+
function edits(a, b) {
|
|
576
|
+
const d = Array.from({ length: a.length + 1 }, (_, i) => Array.from({ length: b.length + 1 }, (_, j) => i === 0 ? j : j === 0 ? i : 0));
|
|
577
|
+
for (let i = 1; i <= a.length; i++) for (let j = 1; j <= b.length; j++) {
|
|
578
|
+
d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
579
|
+
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) d[i][j] = Math.min(d[i][j], d[i - 2][j - 2] + 1);
|
|
580
|
+
}
|
|
581
|
+
return d[a.length][b.length];
|
|
582
|
+
}
|
|
583
|
+
//#endregion
|
|
511
584
|
//#region src/cli/main.ts
|
|
512
|
-
|
|
585
|
+
/** A failure this run says in its own words: what a command throws to stop with it. */
|
|
586
|
+
var Failed = class extends Error {
|
|
587
|
+
failure;
|
|
588
|
+
constructor(failure) {
|
|
589
|
+
super(failure.message);
|
|
590
|
+
this.failure = failure;
|
|
591
|
+
}
|
|
592
|
+
};
|
|
593
|
+
/**
|
|
594
|
+
* Misuse: `message` says what's wrong with the command as typed. It exits 2, and points at the
|
|
595
|
+
* command's help unless `next` says something better.
|
|
596
|
+
*/
|
|
597
|
+
function usage(message, options = {}) {
|
|
598
|
+
return new Failed({
|
|
599
|
+
class: "usage",
|
|
600
|
+
reason: options.reason ?? "invalid_arguments",
|
|
601
|
+
message,
|
|
602
|
+
next: options.next ?? [helpRecovery()]
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
/** A command's help page, which says how to use it. */
|
|
606
|
+
const helpRecovery = (command = current.command) => ({
|
|
607
|
+
instruction: "See how to use it",
|
|
608
|
+
argv: [
|
|
609
|
+
...cli(),
|
|
610
|
+
...command ? [command] : [],
|
|
611
|
+
"--help"
|
|
612
|
+
]
|
|
613
|
+
});
|
|
614
|
+
/** This CLI's name as argv, to build a command a person runs: `['vb']`, `['vercel', 'board']`. */
|
|
615
|
+
const cli = () => named().split(" ");
|
|
616
|
+
/** This run's command as typed, with `extra` where options go: before any `--`, after which everything is text. */
|
|
617
|
+
function rerun(...extra) {
|
|
618
|
+
const at = typed.indexOf("--");
|
|
619
|
+
return [...cli(), ...at === -1 ? [...typed, ...extra] : [
|
|
620
|
+
...typed.slice(0, at),
|
|
621
|
+
...extra,
|
|
622
|
+
...typed.slice(at)
|
|
623
|
+
]];
|
|
624
|
+
}
|
|
513
625
|
/** What the parser accepts: every option, by name (`commands.ts`). */
|
|
514
626
|
const OPTIONS = options;
|
|
515
627
|
/** Options by their short letter: `n` is `limit`. */
|
|
@@ -550,14 +662,43 @@ function markAttachmentId(args) {
|
|
|
550
662
|
}
|
|
551
663
|
/** Commands that are gone, and what to run instead. Their flags don't matter: an old hook's `vb update --skip` gets this too. */
|
|
552
664
|
function removedCommand(word) {
|
|
553
|
-
const
|
|
665
|
+
const gone = (next, why = "") => usage(`\`${named()} ${word}\` is gone.${why}`, {
|
|
666
|
+
reason: "unknown_command",
|
|
667
|
+
next
|
|
668
|
+
});
|
|
669
|
+
const url = [{
|
|
670
|
+
instruction: "Pass the board's URL",
|
|
671
|
+
argv: [
|
|
672
|
+
...cli(),
|
|
673
|
+
"read",
|
|
674
|
+
"dev",
|
|
675
|
+
"--url",
|
|
676
|
+
"<URL>"
|
|
677
|
+
]
|
|
678
|
+
}, { instruction: "Or, in a Vercel project, its team's board needs no URL" }];
|
|
554
679
|
const removed = {
|
|
555
|
-
update:
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
680
|
+
update: () => gone([{
|
|
681
|
+
instruction: "Reply in the thread you're working in",
|
|
682
|
+
argv: [
|
|
683
|
+
...cli(),
|
|
684
|
+
"reply",
|
|
685
|
+
"<ID>",
|
|
686
|
+
"<TEXT>"
|
|
687
|
+
]
|
|
688
|
+
}, {
|
|
689
|
+
instruction: "Or post where it fits",
|
|
690
|
+
argv: [
|
|
691
|
+
...cli(),
|
|
692
|
+
"post",
|
|
693
|
+
"<CHANNEL>",
|
|
694
|
+
"<TEXT>"
|
|
695
|
+
]
|
|
696
|
+
}]),
|
|
697
|
+
stuck: () => gone([], " A board on a2() keeps its dead letters in A2, on the backend's `servers`."),
|
|
698
|
+
link: () => gone(url),
|
|
699
|
+
use: () => gone(url)
|
|
559
700
|
};
|
|
560
|
-
return Object.hasOwn(removed, word) ? removed[word] : void 0;
|
|
701
|
+
return Object.hasOwn(removed, word) ? removed[word]() : void 0;
|
|
561
702
|
}
|
|
562
703
|
/**
|
|
563
704
|
* The command `argv` runs, by its own name (`delete` for `rm`), as the parser reads it: the
|
|
@@ -584,6 +725,17 @@ let app = {
|
|
|
584
725
|
};
|
|
585
726
|
let flags = {};
|
|
586
727
|
let positionals = [];
|
|
728
|
+
/** The command this run runs, by its own name, and what follows it: what a failure says it was doing. */
|
|
729
|
+
let current = {
|
|
730
|
+
command: void 0,
|
|
731
|
+
args: []
|
|
732
|
+
};
|
|
733
|
+
/** What this run was given, as typed: what a recovery runs again, with what it lacked. */
|
|
734
|
+
let typed = [];
|
|
735
|
+
/** What this run's writes had to say, for its JSON result. */
|
|
736
|
+
let warnings = [];
|
|
737
|
+
/** The board this run reached by its URL, for a failure to name. */
|
|
738
|
+
let reached = null;
|
|
587
739
|
/** The help `words` ask for: a command's page when they name one (`read`, `identity link`), or else every command. */
|
|
588
740
|
function help(words = []) {
|
|
589
741
|
const own = Object.values(app.commands).map((command) => command.spec);
|
|
@@ -595,43 +747,115 @@ function help(words = []) {
|
|
|
595
747
|
}
|
|
596
748
|
/** How messages name this CLI: `vb` for the standalone one, as they always have, or the app's own name. */
|
|
597
749
|
const named = () => app.name === "board" ? "vb" : app.name;
|
|
598
|
-
/**
|
|
750
|
+
/** The commands that take their text as an argument, where text that starts with `-` goes after `--`. */
|
|
751
|
+
const TEXT = /* @__PURE__ */ new Set([
|
|
752
|
+
"post",
|
|
753
|
+
"reply",
|
|
754
|
+
"edit",
|
|
755
|
+
"schedule"
|
|
756
|
+
]);
|
|
757
|
+
/** Parses `argv` for this run, or throws what's wrong with it. */
|
|
599
758
|
function parse(argv) {
|
|
600
759
|
const args = withMentions(argv);
|
|
601
|
-
const
|
|
760
|
+
const word = commandWord(args);
|
|
761
|
+
current = {
|
|
762
|
+
command: word,
|
|
763
|
+
args: []
|
|
764
|
+
};
|
|
765
|
+
const removed = removedCommand(word ?? "");
|
|
766
|
+
if (removed) throw removed;
|
|
602
767
|
try {
|
|
603
|
-
if (removed) throw new UsageError(removed);
|
|
604
768
|
const parsed = parseAll(markAttachmentId(args));
|
|
605
769
|
flags = parsed.values;
|
|
606
770
|
positionals = parsed.positionals.map((arg) => arg.startsWith(MARK) ? arg.slice(1) : arg);
|
|
607
|
-
return true;
|
|
608
771
|
} catch (error) {
|
|
609
|
-
|
|
610
|
-
io.error(terminalText(`error: ${unknown ? `unknown option: ${unknown} (run \`${app.name} help\`; text starting with - goes after --, as in -- "${unknown}")` : error.message}`) + "\n");
|
|
611
|
-
return false;
|
|
772
|
+
throw parseFailure(error, argv, word);
|
|
612
773
|
}
|
|
613
774
|
}
|
|
775
|
+
/** What the parser rejected, as misuse: an unknown option (with the likeliest one meant), or an option without its value. */
|
|
776
|
+
function parseFailure(error, argv, word) {
|
|
777
|
+
const option = error.message.match(/--[a-z][\w-]*/)?.[0] ?? /'(-[^' ,]+)/.exec(error.message)?.[1];
|
|
778
|
+
if (error.code === "ERR_PARSE_ARGS_UNKNOWN_OPTION" && option) {
|
|
779
|
+
const guess = option.startsWith("--") ? closest(option.slice(2), optionsOf(word)) : void 0;
|
|
780
|
+
const next = [{
|
|
781
|
+
instruction: "See all options",
|
|
782
|
+
argv: [
|
|
783
|
+
...cli(),
|
|
784
|
+
...word ? [word] : [],
|
|
785
|
+
"--help"
|
|
786
|
+
]
|
|
787
|
+
}];
|
|
788
|
+
const at = argv.indexOf(option);
|
|
789
|
+
if (word && TEXT.has(word) && at !== -1) {
|
|
790
|
+
const rest = [...argv.slice(0, at), ...argv.slice(at + 1)];
|
|
791
|
+
const dash = rest.indexOf("--");
|
|
792
|
+
const text = dash === -1 ? [
|
|
793
|
+
...rest,
|
|
794
|
+
"--",
|
|
795
|
+
option
|
|
796
|
+
] : [
|
|
797
|
+
...rest.slice(0, dash + 1),
|
|
798
|
+
option,
|
|
799
|
+
...rest.slice(dash + 1)
|
|
800
|
+
];
|
|
801
|
+
next.push({
|
|
802
|
+
instruction: "Or, for text that starts with -, put it after --",
|
|
803
|
+
argv: [...cli(), ...text]
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
return usage(`Unknown option ${option} for ${`${named()}${word ? ` ${word}` : ""}`}.${guess ? ` Did you mean --${guess}?` : ""}`, {
|
|
807
|
+
reason: "unknown_option",
|
|
808
|
+
next
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
if (error.code === "ERR_PARSE_ARGS_INVALID_OPTION_VALUE" && option) return usage(`${option} needs a value.`, { reason: "missing_value" });
|
|
812
|
+
return usage(sentence(error.message));
|
|
813
|
+
}
|
|
814
|
+
/** The options `word`'s command takes, its subcommands' and the filters included, and the ones every command takes. */
|
|
815
|
+
function optionsOf(word) {
|
|
816
|
+
const command = [...commands, ...Object.values(app.commands).map((own) => own.spec)].find((known) => known.name === word);
|
|
817
|
+
return [...command ? [command, ...command.subcommands ?? []].flatMap((c) => [...c.options, ...c.filters ? filters$1 : []]) : [], ...globalOptions].map((option) => option.name);
|
|
818
|
+
}
|
|
819
|
+
/** `text` as a sentence: a capital first, and a period last. */
|
|
820
|
+
const sentence = (text) => {
|
|
821
|
+
const trimmed = text.trim();
|
|
822
|
+
return `${trimmed.charAt(0).toUpperCase()}${trimmed.slice(1)}${/[.!?]$/.test(trimmed) ? "" : "."}`;
|
|
823
|
+
};
|
|
614
824
|
const TERMINAL = {
|
|
615
825
|
write: (chunk) => void process.stdout.write(chunk),
|
|
616
826
|
error: (chunk) => void process.stderr.write(chunk),
|
|
617
827
|
async readFile(path) {
|
|
618
828
|
const info = await stat(path).catch(() => null);
|
|
619
829
|
if (!info?.isFile()) return null;
|
|
620
|
-
if (info.size > 104857600) throw
|
|
830
|
+
if (info.size > 104857600) throw tooLarge(path);
|
|
621
831
|
return new Uint8Array(await readFile(path));
|
|
622
832
|
},
|
|
623
833
|
writeFile: (path, data) => writeFile(path, data),
|
|
624
|
-
stdin: readStdin
|
|
834
|
+
stdin: readStdin,
|
|
835
|
+
tty: process.stdout.isTTY === true
|
|
625
836
|
};
|
|
626
837
|
let io = TERMINAL;
|
|
627
838
|
const out = (text) => io.write(`${terminalText(text)}\n`);
|
|
628
|
-
|
|
839
|
+
/** `--json`'s result: one document, with what the run's writes warned about, indented at a terminal. */
|
|
840
|
+
const result = (value) => io.write(`${JSON.stringify(warnings.length ? {
|
|
841
|
+
...value,
|
|
842
|
+
warnings
|
|
843
|
+
} : value, null, io.tty ? 2 : void 0)}\n`);
|
|
844
|
+
/** One line of a JSON stream (`tail`, `identity link`): compact, wherever it goes. */
|
|
845
|
+
const line = (value) => io.write(`${JSON.stringify(value)}\n`);
|
|
629
846
|
/** A message's ID as you'd write it: `#812` or `812`. It's passed to the board as it is. */
|
|
630
|
-
function messageId(value, what = "message
|
|
847
|
+
function messageId(value, what = "message ID") {
|
|
631
848
|
const id = value?.replace(/^#/, "") ?? "";
|
|
632
|
-
if (
|
|
849
|
+
if (value === void 0) throw usage(`Missing the ${what}.`, { reason: "missing_arguments" });
|
|
850
|
+
if (!/^[^\s/]+$/.test(id)) throw usage(`${value} isn't a ${what}.`);
|
|
633
851
|
return id;
|
|
634
852
|
}
|
|
853
|
+
/** A file over what a direct upload takes. */
|
|
854
|
+
const tooLarge = (path) => new Failed({
|
|
855
|
+
class: "limit",
|
|
856
|
+
reason: "payload_too_large",
|
|
857
|
+
message: `Can't attach ${path}. It's over ${DIRECT_UPLOAD_BYTES_MAX / 1024 / 1024} MB, the most an attachment can be.`
|
|
858
|
+
});
|
|
635
859
|
/** Whether an argument names a message rather than a channel: `#<id>`, or a bare number. */
|
|
636
860
|
const isMessageRef = (value) => /^#[^\s/]+$/.test(value) || /^\d+$/.test(value);
|
|
637
861
|
/** A message's ID as it's passed back: bare when it's a number, `#<id>` when it could be a channel's name. */
|
|
@@ -640,13 +864,13 @@ function labelPairs(values) {
|
|
|
640
864
|
const pairs = {};
|
|
641
865
|
for (const value of values ?? []) {
|
|
642
866
|
const at = value.indexOf("=");
|
|
643
|
-
if (at <= 0) throw
|
|
867
|
+
if (at <= 0) throw usage(`Labels look like key=value, and ${value} isn't one.`);
|
|
644
868
|
pairs[value.slice(0, at)] = value.slice(at + 1);
|
|
645
869
|
}
|
|
646
870
|
return pairs;
|
|
647
871
|
}
|
|
648
872
|
async function text(value) {
|
|
649
|
-
if (value === void 0) throw
|
|
873
|
+
if (value === void 0) throw usage("Missing the message's text: pass it, or - to read it from stdin.", { reason: "missing_arguments" });
|
|
650
874
|
if (value !== "-") return value;
|
|
651
875
|
return (await io.stdin()).replace(/\n$/, "");
|
|
652
876
|
}
|
|
@@ -655,8 +879,11 @@ async function attachments(board) {
|
|
|
655
879
|
const files = [];
|
|
656
880
|
for (const path of flags.attach ?? []) {
|
|
657
881
|
const data = await io.readFile(path);
|
|
658
|
-
if (!data) throw
|
|
659
|
-
|
|
882
|
+
if (!data) throw usage(`Can't attach ${path}. There's no such file.`, {
|
|
883
|
+
reason: "file_not_found",
|
|
884
|
+
next: []
|
|
885
|
+
});
|
|
886
|
+
if (data.byteLength > 104857600) throw tooLarge(path);
|
|
660
887
|
files.push({
|
|
661
888
|
path,
|
|
662
889
|
data
|
|
@@ -672,13 +899,14 @@ async function attachments(board) {
|
|
|
672
899
|
/** `s12` or `12`. */
|
|
673
900
|
function scheduleId(value) {
|
|
674
901
|
const id = value?.replace(/^s(?=\d)/i, "") ?? "";
|
|
675
|
-
if (
|
|
902
|
+
if (value === void 0) throw usage("Missing the schedule ID, like s12.", { reason: "missing_arguments" });
|
|
903
|
+
if (!/^[^\s/]+$/.test(id)) throw usage(`${value} isn't a schedule ID like s12.`);
|
|
676
904
|
return id;
|
|
677
905
|
}
|
|
678
906
|
/** A time flag: anything `Date` reads, as ISO. */
|
|
679
907
|
function instant(flag, value) {
|
|
680
908
|
const time = new Date(value);
|
|
681
|
-
if (Number.isNaN(time.getTime())) throw
|
|
909
|
+
if (Number.isNaN(time.getTime())) throw usage(`--${flag} takes a time like 2026-10-08T09:00:00Z, and ${value} isn't one.`);
|
|
682
910
|
return time.toISOString();
|
|
683
911
|
}
|
|
684
912
|
function filters(channel) {
|
|
@@ -703,15 +931,201 @@ function configuredUrl() {
|
|
|
703
931
|
} catch (error) {
|
|
704
932
|
if (!(error instanceof BoardError) || error.code !== "no_board") throw error;
|
|
705
933
|
if (app.teamBoard || process.env.VERCEL_ORG_ID || vercelProjectFile()) return null;
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
934
|
+
throw new Failed({
|
|
935
|
+
class: "missing_input",
|
|
936
|
+
reason: "missing_scope",
|
|
937
|
+
actionRequired: true,
|
|
938
|
+
message: "No board to use. Nothing sets one, and this directory isn't in a Vercel project.",
|
|
939
|
+
next: [{
|
|
940
|
+
instruction: "Use a team's board",
|
|
941
|
+
argv: rerun("--scope", "<TEAM>")
|
|
942
|
+
}, {
|
|
943
|
+
instruction: "Or a board at a URL",
|
|
944
|
+
argv: rerun("--url", "<URL>")
|
|
945
|
+
}]
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
/** A board that predates what this run asked of it. */
|
|
950
|
+
const outdated = (message) => new Failed({
|
|
951
|
+
class: "platform",
|
|
952
|
+
reason: "board_outdated",
|
|
953
|
+
message: `${message} It's older than this CLI.`,
|
|
954
|
+
next: [{ instruction: "Update @vercel/board where the board is deployed" }]
|
|
955
|
+
});
|
|
956
|
+
/** A message ID as a failure names it: `#812`. */
|
|
957
|
+
const ref = (value) => value === void 0 ? "a message" : `#${value.replace(/^#/, "")}`;
|
|
958
|
+
/** What this run was doing, as a failure's headline says it: `post to dev`, `reply to #812`. */
|
|
959
|
+
function doing() {
|
|
960
|
+
const { command, args } = current;
|
|
961
|
+
const [a, b, c] = args;
|
|
962
|
+
const message = a !== void 0 && isMessageRef(a);
|
|
963
|
+
switch (command) {
|
|
964
|
+
case "read": return a === void 0 ? "read the board" : /^https?:\/\//i.test(a) ? "read that link" : `read ${message ? ref(a) : a}`;
|
|
965
|
+
case "post": return message ? `reply to ${ref(a)}` : `post to ${a ?? "a channel"}`;
|
|
966
|
+
case "reply": return `reply to ${ref(a)}`;
|
|
967
|
+
case "edit":
|
|
968
|
+
case "delete":
|
|
969
|
+
case "label": return `${command} ${ref(a)}`;
|
|
970
|
+
case "react": return `react to ${ref(a)}`;
|
|
971
|
+
case "typing": return `show typing in ${ref(a)}`;
|
|
972
|
+
case "sync": return b === void 0 ? `list where ${ref(a)} is synced` : `sync ${ref(a)} with ${b}`;
|
|
973
|
+
case "schedule": return a === "cancel" ? `cancel schedule ${b}` : `schedule a message in ${message ? ref(a) : a ?? "a channel"}`;
|
|
974
|
+
case "schedules":
|
|
975
|
+
case "channels":
|
|
976
|
+
case "actors": return `list ${command}`;
|
|
977
|
+
case "sources": return b === void 0 ? "list sources" : `${b} ${c ?? "it"} on ${a}`;
|
|
978
|
+
case "identity": return a === "link" ? `link your account on ${b}` : a === "unlink" ? `unlink ${b}` : "list your linked accounts";
|
|
979
|
+
case "download": return `download ${a ?? "the attachment"}`;
|
|
980
|
+
case "inbox": return `read ${a}'s inbox`;
|
|
981
|
+
case "tail": return `follow ${a ?? "the board"}`;
|
|
982
|
+
case "wait": return `wait on ${a ?? "the board"}`;
|
|
983
|
+
case "whoami": return "reach the board";
|
|
984
|
+
case "conformance": return "check the board";
|
|
985
|
+
default: return `run ${named()}${command ? ` ${command}` : ""}`;
|
|
713
986
|
}
|
|
714
987
|
}
|
|
988
|
+
/** The commands whose first argument is always a message. */
|
|
989
|
+
const ON_MESSAGE = /* @__PURE__ */ new Set([
|
|
990
|
+
"reply",
|
|
991
|
+
"edit",
|
|
992
|
+
"delete",
|
|
993
|
+
"react",
|
|
994
|
+
"label",
|
|
995
|
+
"typing",
|
|
996
|
+
"sync",
|
|
997
|
+
"schedules"
|
|
998
|
+
]);
|
|
999
|
+
/** What a 404 means this run couldn't find, when its arguments name it: `message #812`. */
|
|
1000
|
+
function missing() {
|
|
1001
|
+
const { command = "", args } = current;
|
|
1002
|
+
const [a, b] = args;
|
|
1003
|
+
if (a === void 0) return void 0;
|
|
1004
|
+
if (command === "read" && /^https?:\/\//i.test(a)) return "that link's message on this board";
|
|
1005
|
+
if (ON_MESSAGE.has(command) || [
|
|
1006
|
+
"read",
|
|
1007
|
+
"post",
|
|
1008
|
+
"schedule"
|
|
1009
|
+
].includes(command) && isMessageRef(a)) return `message ${ref(a)}`;
|
|
1010
|
+
if (command === "schedule" && a === "cancel" && b !== void 0) return `schedule ${b}`;
|
|
1011
|
+
if (command === "download") return `attachment ${a}`;
|
|
1012
|
+
}
|
|
1013
|
+
/**
|
|
1014
|
+
* What `error` means for this run, in the Vercel CLI's terms. The handle's own errors (`vercel
|
|
1015
|
+
* login` missing, Deployment Protection) say what and why themselves; the board's are said by what
|
|
1016
|
+
* this run was doing, with the board's own words under it.
|
|
1017
|
+
*/
|
|
1018
|
+
function failureOf(error) {
|
|
1019
|
+
if (error instanceof Failed) return error.failure;
|
|
1020
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1021
|
+
const { code, status, hint } = error;
|
|
1022
|
+
if (typeof code !== "string" || typeof status !== "number") return {
|
|
1023
|
+
class: "unknown",
|
|
1024
|
+
reason: "unexpected_error",
|
|
1025
|
+
message: `Failed to ${doing()}.`,
|
|
1026
|
+
details: [message]
|
|
1027
|
+
};
|
|
1028
|
+
const said = { error: {
|
|
1029
|
+
code,
|
|
1030
|
+
message,
|
|
1031
|
+
status
|
|
1032
|
+
} };
|
|
1033
|
+
const fix = typeof hint === "string" ? [{ instruction: hint }] : [];
|
|
1034
|
+
const own = (kind, reason, next = fix) => ({
|
|
1035
|
+
class: kind,
|
|
1036
|
+
reason,
|
|
1037
|
+
message,
|
|
1038
|
+
...next.length ? { next } : {},
|
|
1039
|
+
data: said
|
|
1040
|
+
});
|
|
1041
|
+
switch (code) {
|
|
1042
|
+
case "not_logged_in": return {
|
|
1043
|
+
class: "auth",
|
|
1044
|
+
reason: "login_required",
|
|
1045
|
+
message: "You're not logged in to Vercel.",
|
|
1046
|
+
next: [{
|
|
1047
|
+
instruction: "Log in",
|
|
1048
|
+
argv: ["vercel", "login"]
|
|
1049
|
+
}],
|
|
1050
|
+
data: said
|
|
1051
|
+
};
|
|
1052
|
+
case "no_access": return own("permission", "permission_denied");
|
|
1053
|
+
case "wrong_project": return own("permission", "wrong_project");
|
|
1054
|
+
case "protected": return own("permission", "deployment_protected");
|
|
1055
|
+
case "no_project": return own("platform", "no_project");
|
|
1056
|
+
case "no_token": return own("auth", "token_unavailable");
|
|
1057
|
+
case "no_board": return own("not_found", "board_not_found");
|
|
1058
|
+
case "too_many_redirects": return own("network", "network_error");
|
|
1059
|
+
case "invalid_duration": return own("usage", "invalid_arguments", [helpRecovery()]);
|
|
1060
|
+
case "wrong_audience": return own("usage", "wrong_url");
|
|
1061
|
+
case "ambiguous_agent": return own("usage", "ambiguous_agent", [{
|
|
1062
|
+
instruction: "Write as one of them",
|
|
1063
|
+
argv: rerun("--as", "<PROFILE>")
|
|
1064
|
+
}, {
|
|
1065
|
+
instruction: "Or as yourself",
|
|
1066
|
+
argv: rerun("--human")
|
|
1067
|
+
}]);
|
|
1068
|
+
case "too_large": return {
|
|
1069
|
+
...own("limit", "payload_too_large"),
|
|
1070
|
+
message: `Can't ${doing()}. ${sentence(message)}`
|
|
1071
|
+
};
|
|
1072
|
+
case "upload_failed": return {
|
|
1073
|
+
...own("platform", "upload_failed"),
|
|
1074
|
+
message: `Failed to ${doing()}.`,
|
|
1075
|
+
details: [message]
|
|
1076
|
+
};
|
|
1077
|
+
case "port_taken": return own("conflict", "port_taken", [{
|
|
1078
|
+
instruction: "Pick another port",
|
|
1079
|
+
argv: [
|
|
1080
|
+
...cli(),
|
|
1081
|
+
current.command ?? "serve",
|
|
1082
|
+
"--port",
|
|
1083
|
+
"<PORT>"
|
|
1084
|
+
]
|
|
1085
|
+
}]);
|
|
1086
|
+
}
|
|
1087
|
+
const theirs = (kind, reason, headline, next = []) => ({
|
|
1088
|
+
class: kind,
|
|
1089
|
+
reason,
|
|
1090
|
+
message: headline,
|
|
1091
|
+
details: [message],
|
|
1092
|
+
...reached && [
|
|
1093
|
+
"auth",
|
|
1094
|
+
"permission",
|
|
1095
|
+
"network",
|
|
1096
|
+
"platform"
|
|
1097
|
+
].includes(kind) ? { context: [["Board", reached]] } : {},
|
|
1098
|
+
...next.length ? { next } : {},
|
|
1099
|
+
data: said
|
|
1100
|
+
});
|
|
1101
|
+
const specific = (fallback) => /^[a-z]+(_[a-z]+)+$/.test(code) && code !== "invalid_request" && code !== "remote_error" ? code : fallback;
|
|
1102
|
+
if (status === 400) return theirs("usage", specific("invalid_arguments"), `Can't ${doing()}.`);
|
|
1103
|
+
if (status === 401) return theirs("auth", "login_required", "The board didn't accept your credentials.", [{
|
|
1104
|
+
instruction: "Log in again",
|
|
1105
|
+
argv: ["vercel", "login"]
|
|
1106
|
+
}]);
|
|
1107
|
+
if (status === 403) return theirs("permission", "permission_denied", `You can't ${doing()}.`);
|
|
1108
|
+
if (status === 404) {
|
|
1109
|
+
const thing = missing();
|
|
1110
|
+
return thing ? {
|
|
1111
|
+
...theirs("not_found", specific("not_found"), `Couldn't find ${thing}.`),
|
|
1112
|
+
details: []
|
|
1113
|
+
} : theirs("not_found", specific("not_found"), `Can't ${doing()}.`);
|
|
1114
|
+
}
|
|
1115
|
+
if (status === 409) return theirs("conflict", specific("conflict"), `Can't ${doing()}.`);
|
|
1116
|
+
if (status === 413) return theirs("limit", "payload_too_large", `Can't ${doing()}. It's too large for the board.`);
|
|
1117
|
+
if (status === 429) return theirs("rate_limit", "rate_limited", "Too many requests to the board. Try again in a minute.");
|
|
1118
|
+
if (status === 501) return theirs("platform", specific("not_supported"), `Can't ${doing()}. This board doesn't support it.`);
|
|
1119
|
+
if (code === "unreachable") return theirs("network", "network_error", "Couldn't reach the board.");
|
|
1120
|
+
return theirs("platform", "api_error", `Failed to ${doing()}.`);
|
|
1121
|
+
}
|
|
1122
|
+
/** How this run ends with `failure`: shown on stderr, or by the app, and its exit code. */
|
|
1123
|
+
function report(failure, json) {
|
|
1124
|
+
if (app.onError) app.onError(failure, { json });
|
|
1125
|
+
else if (json) io.error(`${JSON.stringify(failureDocument(failure))}\n`);
|
|
1126
|
+
else io.error(`${terminalText(formatFailure(failure))}\n`);
|
|
1127
|
+
return exitCode(failure);
|
|
1128
|
+
}
|
|
715
1129
|
/**
|
|
716
1130
|
* A team's board in Vercel's API, by the URL its discovery names it with
|
|
717
1131
|
* (`https://api.vercel.com/v1/messageboard/boards/<board>?teamId=<team>`): its board and team.
|
|
@@ -759,6 +1173,7 @@ function connection(url = flags.url, options = {}) {
|
|
|
759
1173
|
};
|
|
760
1174
|
}
|
|
761
1175
|
const at = configured.replace(/\/+$/, "");
|
|
1176
|
+
reached = at;
|
|
762
1177
|
const onVercel = vercelBoard(at);
|
|
763
1178
|
if (onVercel) {
|
|
764
1179
|
const board$3 = board({ backend: vercel(onVercel) });
|
|
@@ -797,24 +1212,40 @@ async function readStdin() {
|
|
|
797
1212
|
}
|
|
798
1213
|
function hookAgents(names) {
|
|
799
1214
|
const known = Object.keys(AGENTS);
|
|
800
|
-
for (const name of names) if (!known.includes(name)) throw
|
|
1215
|
+
for (const name of names) if (!known.includes(name)) throw usage(`Unknown agent ${name}. The agents are ${known.join(", ")}.`);
|
|
801
1216
|
return names.length ? names : known;
|
|
802
1217
|
}
|
|
1218
|
+
/** Says what a write that went through had to say: in its JSON result with `--json`, on stderr otherwise. */
|
|
1219
|
+
function warn(warning) {
|
|
1220
|
+
if (flags.json) warnings.push(warning);
|
|
1221
|
+
else if (app.onWarning) app.onWarning(warning);
|
|
1222
|
+
else io.error(`${terminalText(`warning: ${warning.message}`)}\n`);
|
|
1223
|
+
}
|
|
803
1224
|
/** A write went through, but some handles in it matched no one: they stayed plain text. */
|
|
804
1225
|
function warnUnknownMentions(handles) {
|
|
805
1226
|
if (!handles?.length) return;
|
|
806
1227
|
const names = handles.map((handle) => `@${handle}`);
|
|
807
1228
|
const list = names.length === 1 ? names[0] : `${names.slice(0, -1).join(", ")} and ${names.at(-1)}`;
|
|
808
1229
|
const one = names.length === 1;
|
|
809
|
-
|
|
1230
|
+
warn({
|
|
1231
|
+
reason: "unknown_mention",
|
|
1232
|
+
message: `${list} ${one ? "matches" : "match"} no one on the board, so ${one ? "it stays" : "they stay"} plain text and ${one ? "notifies" : "notify"} no one.`
|
|
1233
|
+
});
|
|
810
1234
|
}
|
|
1235
|
+
/** A post or reply: its ID, or with `--json`, the message. */
|
|
811
1236
|
function printMessage(message) {
|
|
812
|
-
if (flags.json)
|
|
1237
|
+
if (flags.json) result({
|
|
1238
|
+
message,
|
|
1239
|
+
outcome: "posted"
|
|
1240
|
+
});
|
|
813
1241
|
else out(messageRef(message.id));
|
|
814
1242
|
}
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
1243
|
+
/** A write that answers nothing but that it went through: with `--json`, which message, and what happened to it. */
|
|
1244
|
+
function printDone(id, outcome) {
|
|
1245
|
+
if (flags.json) result({
|
|
1246
|
+
message: { id },
|
|
1247
|
+
outcome
|
|
1248
|
+
});
|
|
818
1249
|
}
|
|
819
1250
|
/** Best-effort: the URL is printed either way. */
|
|
820
1251
|
/** `url`, when it's an http or https URL as `new URL` reads it: the only kind the CLI opens, so a board can't hand the opener a flag or another scheme. */
|
|
@@ -844,7 +1275,7 @@ function openBrowser(url) {
|
|
|
844
1275
|
}
|
|
845
1276
|
function portFlag() {
|
|
846
1277
|
const port = flags.port === void 0 ? void 0 : Number(flags.port);
|
|
847
|
-
if (port !== void 0 && !(Number.isInteger(port) && port >= 0 && port <= 65535)) throw
|
|
1278
|
+
if (port !== void 0 && !(Number.isInteger(port) && port >= 0 && port <= 65535)) throw usage(`${flags.port} isn't a port.`, { reason: "invalid_port" });
|
|
848
1279
|
return port;
|
|
849
1280
|
}
|
|
850
1281
|
/** How long `identity link` waits for the sign-in or the code. */
|
|
@@ -854,26 +1285,39 @@ async function identity(args) {
|
|
|
854
1285
|
const [sub, target] = args;
|
|
855
1286
|
if (sub === void 0) {
|
|
856
1287
|
const view = await board.identity();
|
|
857
|
-
if (flags.json) return
|
|
1288
|
+
if (flags.json) return result(view), 0;
|
|
858
1289
|
if (view.links.length === 0) out("no linked accounts");
|
|
859
1290
|
for (const link of view.links) out(`${link.handle ? `@${link.handle} ` : ""}${link.actor} by ${link.method === "code" ? "code" : "sign-in"} · ${link.at}`);
|
|
860
1291
|
if (view.unlinked?.length) out(`not linked ${view.unlinked.join(", ")} (${named()} identity link <key>)`);
|
|
861
1292
|
return 0;
|
|
862
1293
|
}
|
|
863
1294
|
if (sub === "unlink") {
|
|
864
|
-
if (!target
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
1295
|
+
if (!target) throw usage(`Missing the account to unlink: a handle or actor ID from \`${named()} identity\`.`, { reason: "missing_arguments" });
|
|
1296
|
+
if (args.length !== 2) throw usage("identity unlink takes one account.");
|
|
1297
|
+
const unlinked = await board.unlinkAccount(target);
|
|
1298
|
+
if (flags.json) result({
|
|
1299
|
+
...unlinked,
|
|
1300
|
+
outcome: "unlinked"
|
|
1301
|
+
});
|
|
1302
|
+
else out(`unlinked ${unlinked.actor}`);
|
|
868
1303
|
return 0;
|
|
869
1304
|
}
|
|
870
|
-
if (sub !== "link"
|
|
1305
|
+
if (sub !== "link") throw usage(`Unknown command ${sub} for ${named()} identity.`, { reason: "unknown_command" });
|
|
1306
|
+
if (!target) throw usage("Missing the sync entry to link your account on, such as slack.", { reason: "missing_arguments" });
|
|
1307
|
+
if (args.length !== 2) throw usage("identity link takes one sync entry.");
|
|
871
1308
|
const before = new Set((await board.identity()).links.map((link) => `${link.actor}|${link.at}`));
|
|
872
1309
|
const platform = (await board.discovery().catch(() => null))?.sync.find((entry) => entry.key === target)?.platform ?? target;
|
|
873
1310
|
const { code, expiresAt, signIn } = await board.syncCode(target);
|
|
874
1311
|
const consent = signIn !== void 0 && !flags.code;
|
|
875
|
-
if (consent && browsable(signIn) === null) throw new
|
|
876
|
-
|
|
1312
|
+
if (consent && browsable(signIn) === null) throw new Failed({
|
|
1313
|
+
class: "platform",
|
|
1314
|
+
reason: "invalid_sign_in",
|
|
1315
|
+
message: "The board's sign-in isn't an http or https URL.",
|
|
1316
|
+
details: [JSON.stringify(signIn)],
|
|
1317
|
+
data: { signIn }
|
|
1318
|
+
});
|
|
1319
|
+
if (flags.json) line({
|
|
1320
|
+
type: "code",
|
|
877
1321
|
sync: target,
|
|
878
1322
|
platform,
|
|
879
1323
|
code,
|
|
@@ -899,13 +1343,28 @@ async function identity(args) {
|
|
|
899
1343
|
await sleep();
|
|
900
1344
|
const linked = await linkedSince();
|
|
901
1345
|
if (linked) {
|
|
902
|
-
if (flags.json)
|
|
1346
|
+
if (flags.json) line({
|
|
1347
|
+
type: "linked",
|
|
1348
|
+
account: linked
|
|
1349
|
+
});
|
|
903
1350
|
else out(`linked ${linked.handle ? `@${linked.handle} ` : ""}(${linked.actor})`);
|
|
904
1351
|
return 0;
|
|
905
1352
|
}
|
|
906
1353
|
}
|
|
907
|
-
|
|
908
|
-
|
|
1354
|
+
throw new Failed({
|
|
1355
|
+
class: "timeout",
|
|
1356
|
+
reason: "timeout",
|
|
1357
|
+
message: consent ? "Nothing linked an account within 10 minutes." : "The code expired before a message carried it.",
|
|
1358
|
+
next: [{
|
|
1359
|
+
instruction: "Try again",
|
|
1360
|
+
argv: [
|
|
1361
|
+
...cli(),
|
|
1362
|
+
"identity",
|
|
1363
|
+
"link",
|
|
1364
|
+
target
|
|
1365
|
+
]
|
|
1366
|
+
}]
|
|
1367
|
+
});
|
|
909
1368
|
}
|
|
910
1369
|
async function run(command, args) {
|
|
911
1370
|
switch (command) {
|
|
@@ -919,7 +1378,7 @@ async function run(command, args) {
|
|
|
919
1378
|
const url = given ?? discovered.url;
|
|
920
1379
|
const source = transport.source();
|
|
921
1380
|
if (flags.json) {
|
|
922
|
-
|
|
1381
|
+
result({
|
|
923
1382
|
...me,
|
|
924
1383
|
board: url,
|
|
925
1384
|
source
|
|
@@ -941,15 +1400,15 @@ async function run(command, args) {
|
|
|
941
1400
|
return 0;
|
|
942
1401
|
}
|
|
943
1402
|
case "conformance": {
|
|
944
|
-
if (args.length > 0) throw
|
|
1403
|
+
if (args.length > 0) throw usage("conformance takes no arguments: pass its channel with --channel.");
|
|
945
1404
|
const report = await conformance(client(), flags.channel ? { channel: flags.channel } : {});
|
|
946
|
-
if (flags.json)
|
|
1405
|
+
if (flags.json) result(report);
|
|
947
1406
|
else out(formatConformance(report));
|
|
948
1407
|
return report.ok ? 0 : 1;
|
|
949
1408
|
}
|
|
950
1409
|
case "channels": {
|
|
951
1410
|
const channels = await client().channels();
|
|
952
|
-
if (flags.json) return channels
|
|
1411
|
+
if (flags.json) return result({ channels }), 0;
|
|
953
1412
|
if (channels.length === 0) return out("no channels yet"), 0;
|
|
954
1413
|
let group = "";
|
|
955
1414
|
for (const channel of channels) {
|
|
@@ -965,7 +1424,7 @@ async function run(command, args) {
|
|
|
965
1424
|
}
|
|
966
1425
|
case "actors": {
|
|
967
1426
|
const actors = await client().actors();
|
|
968
|
-
if (flags.json) return actors
|
|
1427
|
+
if (flags.json) return result({ actors }), 0;
|
|
969
1428
|
for (const actor of actors) out(`@${actor.handle} ${actor.name} ${actor.id}${actor.person ? ` → @${actor.person.handle}` : ""}`);
|
|
970
1429
|
return 0;
|
|
971
1430
|
}
|
|
@@ -976,14 +1435,17 @@ async function run(command, args) {
|
|
|
976
1435
|
const link = target !== void 0 && /^https?:\/\//i.test(target);
|
|
977
1436
|
if (target && (link || isMessageRef(target))) {
|
|
978
1437
|
const thread = link ? await board.linked(target) : await board.thread(messageId(target));
|
|
979
|
-
if (link && !Array.isArray(thread.replies)) throw
|
|
1438
|
+
if (link && !Array.isArray(thread.replies)) throw outdated("This board can't read links.");
|
|
980
1439
|
const narrowed = Object.keys(filters()).length > 0;
|
|
981
1440
|
const replies = narrowed ? (await board.messages({
|
|
982
1441
|
...filters(),
|
|
983
1442
|
re: thread.message.id,
|
|
984
1443
|
limit: 500
|
|
985
1444
|
})).messages.filter((m) => m.re !== null) : thread.replies;
|
|
986
|
-
if (flags.json) return
|
|
1445
|
+
if (flags.json) return result({
|
|
1446
|
+
...thread,
|
|
1447
|
+
replies
|
|
1448
|
+
}), 0;
|
|
987
1449
|
const authors = new Set([thread.message, ...replies].map((m) => m.by.actor));
|
|
988
1450
|
const unnamed = reactionNames([thread.message, ...replies]).filter((id) => !authors.has(id));
|
|
989
1451
|
const known = unnamed.length ? new Map((await board.actors().catch(() => [])).filter((a) => unnamed.includes(a.id)).map((a) => [a.id, actorName(a)])) : /* @__PURE__ */ new Map();
|
|
@@ -995,13 +1457,27 @@ async function run(command, args) {
|
|
|
995
1457
|
...filters(target),
|
|
996
1458
|
...flags.limit ? { limit: Number(flags.limit) } : {}
|
|
997
1459
|
};
|
|
998
|
-
const old =
|
|
1460
|
+
const old = outdated("This board can't list threads.");
|
|
999
1461
|
const page = await board.threads(query).catch((error) => {
|
|
1000
1462
|
throw error instanceof BoardError && error.status === 404 ? old : error;
|
|
1001
1463
|
});
|
|
1002
1464
|
if (!Array.isArray(page.threads)) throw old;
|
|
1003
|
-
if (!page.threads.length && target
|
|
1004
|
-
|
|
1465
|
+
if (!page.threads.length && target) {
|
|
1466
|
+
const channels = (await board.channels()).map((c) => c.path);
|
|
1467
|
+
if (!channels.some((path) => path === target || path.startsWith(target.replace(/\/?$/, "/")))) {
|
|
1468
|
+
const guess = closest(target, channels);
|
|
1469
|
+
throw new Failed({
|
|
1470
|
+
class: "not_found",
|
|
1471
|
+
reason: "channel_not_found",
|
|
1472
|
+
message: `Couldn't find channel ${target}.${guess ? ` Did you mean ${guess}?` : ""}`,
|
|
1473
|
+
next: [{
|
|
1474
|
+
instruction: "See its channels",
|
|
1475
|
+
argv: [...cli(), "channels"]
|
|
1476
|
+
}]
|
|
1477
|
+
});
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
if (flags.json) return result(page), 0;
|
|
1005
1481
|
const count = `${page.threads.length} ${page.threads.length === 1 ? "thread" : "threads"}`;
|
|
1006
1482
|
out(`${target ?? "all channels"} · ${count} · cursor ${page.cursor}${page.threads.length ? "\n" : ""}`);
|
|
1007
1483
|
if (page.threads.length) out(formatThreadSummaries(page.threads, { channel: !target || target.endsWith("/") }));
|
|
@@ -1009,10 +1485,18 @@ async function run(command, args) {
|
|
|
1009
1485
|
}
|
|
1010
1486
|
case "post": {
|
|
1011
1487
|
const [target, body] = args;
|
|
1012
|
-
if (!target) throw
|
|
1488
|
+
if (!target) throw usage("Missing the channel or message to post to.", { reason: "missing_arguments" });
|
|
1013
1489
|
const re = isMessageRef(target) ? messageId(target) : void 0;
|
|
1014
|
-
if (re !== void 0 && flags.sync?.length) throw
|
|
1015
|
-
|
|
1490
|
+
if (re !== void 0 && flags.sync?.length) throw usage("Can't use --sync on a reply. It syncs whole threads.", { next: [{
|
|
1491
|
+
instruction: "Sync the thread instead",
|
|
1492
|
+
argv: [
|
|
1493
|
+
...cli(),
|
|
1494
|
+
"sync",
|
|
1495
|
+
re,
|
|
1496
|
+
"<TARGET>"
|
|
1497
|
+
]
|
|
1498
|
+
}] });
|
|
1499
|
+
if (flags.sync?.length && flags["no-sync"]) throw usage("Can't use --sync with --no-sync.");
|
|
1016
1500
|
const board = client();
|
|
1017
1501
|
const content = body === void 0 && flags.attach?.length ? "" : await text(body);
|
|
1018
1502
|
const files = await attachments(board);
|
|
@@ -1049,16 +1533,19 @@ async function run(command, args) {
|
|
|
1049
1533
|
case "schedule": {
|
|
1050
1534
|
if (args[0] === "cancel" && args.length === 2) {
|
|
1051
1535
|
const { schedule, canceled } = await client().cancelSchedule(scheduleId(args[1]));
|
|
1052
|
-
if (flags.json)
|
|
1536
|
+
if (flags.json) result({
|
|
1537
|
+
schedule,
|
|
1538
|
+
outcome: canceled ? "canceled" : "unchanged"
|
|
1539
|
+
});
|
|
1053
1540
|
else out(canceled ? `canceled s${schedule.id}` : `s${schedule.id} is already ${schedule.state}`);
|
|
1054
1541
|
return 0;
|
|
1055
1542
|
}
|
|
1056
1543
|
const [target, body] = args;
|
|
1057
|
-
if (!target) throw
|
|
1058
|
-
if (!flags.in && !flags.at && !flags.every) throw
|
|
1059
|
-
if (flags.sync?.length) throw
|
|
1544
|
+
if (!target) throw usage("Missing the channel or message to schedule a message in.", { reason: "missing_arguments" });
|
|
1545
|
+
if (!flags.in && !flags.at && !flags.every) throw usage("Can't schedule a message without a time: pass --in 1d, --at <time>, or --every 30m.", { reason: "missing_arguments" });
|
|
1546
|
+
if (flags.sync?.length) throw usage("Can't use --sync with schedule. A schedule posts where you point it.");
|
|
1060
1547
|
const times = flags.times === void 0 ? void 0 : Number(flags.times);
|
|
1061
|
-
if (times !== void 0 && !(Number.isInteger(times) && times > 0)) throw
|
|
1548
|
+
if (times !== void 0 && !(Number.isInteger(times) && times > 0)) throw usage(`--times takes a count, and ${flags.times} isn't one.`);
|
|
1062
1549
|
const board = client();
|
|
1063
1550
|
const content = body === void 0 && flags.attach?.length ? "" : await text(body);
|
|
1064
1551
|
const files = await attachments(board);
|
|
@@ -1076,7 +1563,10 @@ async function run(command, args) {
|
|
|
1076
1563
|
...times !== void 0 ? { times } : {}
|
|
1077
1564
|
});
|
|
1078
1565
|
warnUnknownMentions(schedule.unknownMentions);
|
|
1079
|
-
if (flags.json)
|
|
1566
|
+
if (flags.json) result({
|
|
1567
|
+
schedule,
|
|
1568
|
+
outcome: "scheduled"
|
|
1569
|
+
});
|
|
1080
1570
|
else out(formatSchedule(schedule));
|
|
1081
1571
|
return 0;
|
|
1082
1572
|
}
|
|
@@ -1087,102 +1577,135 @@ async function run(command, args) {
|
|
|
1087
1577
|
...flags.channel ? { channel: flags.channel } : {},
|
|
1088
1578
|
...flags.all ? { all: true } : {}
|
|
1089
1579
|
});
|
|
1090
|
-
if (flags.json) return list
|
|
1580
|
+
if (flags.json) return result({ schedules: list }), 0;
|
|
1091
1581
|
if (list.length === 0) return out(flags.all ? "no schedules" : "no pending schedules"), 0;
|
|
1092
1582
|
out(list.map((schedule) => formatSchedule(schedule)).join("\n"));
|
|
1093
1583
|
return 0;
|
|
1094
1584
|
}
|
|
1095
1585
|
case "hooks": {
|
|
1096
1586
|
const [action, ...names] = args;
|
|
1097
|
-
if (action !== "uninstall") throw
|
|
1587
|
+
if (action !== "uninstall") throw usage(`\`${`${named()} hooks ${action ?? ""}`.trimEnd()}\` is gone. Turn-end hooks were removed.`, {
|
|
1588
|
+
reason: "unknown_command",
|
|
1589
|
+
next: [{
|
|
1590
|
+
instruction: "Clear an earlier install",
|
|
1591
|
+
argv: [
|
|
1592
|
+
...cli(),
|
|
1593
|
+
"hooks",
|
|
1594
|
+
"uninstall"
|
|
1595
|
+
]
|
|
1596
|
+
}]
|
|
1597
|
+
});
|
|
1098
1598
|
const agents = hookAgents(names);
|
|
1099
1599
|
const width = Math.max(...agents.map((agent) => AGENTS[agent].name.length));
|
|
1600
|
+
const hooks = [];
|
|
1100
1601
|
for (const agent of agents) {
|
|
1101
1602
|
const installed = installedHook(agent);
|
|
1102
|
-
const
|
|
1103
|
-
const rules =
|
|
1603
|
+
const by = installed ? cliOf(installed) : null;
|
|
1604
|
+
const rules = by ? allowRules(agent, by) : null;
|
|
1104
1605
|
if (rules) disallow(rules.file, rules.rules);
|
|
1105
|
-
|
|
1606
|
+
const removed = uninstallHook(agent);
|
|
1607
|
+
hooks.push({
|
|
1608
|
+
agent,
|
|
1609
|
+
outcome: removed ? "removed" : "unchanged",
|
|
1610
|
+
file: installed ? AGENTS[agent].file() : null
|
|
1611
|
+
});
|
|
1612
|
+
if (!flags.json) out([
|
|
1106
1613
|
AGENTS[agent].name.padEnd(width),
|
|
1107
|
-
|
|
1614
|
+
removed ? "removed" : "not installed",
|
|
1108
1615
|
installed ? tilde(AGENTS[agent].file()) : ""
|
|
1109
1616
|
].join(" ").trimEnd());
|
|
1110
1617
|
}
|
|
1618
|
+
if (flags.json) result({ hooks });
|
|
1111
1619
|
return 0;
|
|
1112
1620
|
}
|
|
1113
1621
|
case "hook": return 0;
|
|
1114
1622
|
case "edit": {
|
|
1115
|
-
const
|
|
1623
|
+
const id = messageId(args[0]);
|
|
1624
|
+
const { unknownMentions } = await client().edit(id, await text(args[1]));
|
|
1116
1625
|
warnUnknownMentions(unknownMentions);
|
|
1626
|
+
printDone(id, "edited");
|
|
1117
1627
|
return 0;
|
|
1118
1628
|
}
|
|
1119
|
-
case "delete":
|
|
1120
|
-
|
|
1629
|
+
case "delete": {
|
|
1630
|
+
const id = messageId(args[0]);
|
|
1631
|
+
await client().delete(id);
|
|
1632
|
+
printDone(id, "deleted");
|
|
1121
1633
|
return 0;
|
|
1634
|
+
}
|
|
1122
1635
|
case "react": {
|
|
1123
1636
|
const id = messageId(args[0]);
|
|
1124
|
-
if (!args[1]) throw
|
|
1637
|
+
if (!args[1]) throw usage("Missing the emoji to react with, like eyes or thumbs_up.", { reason: "missing_arguments" });
|
|
1125
1638
|
let emoji;
|
|
1126
1639
|
try {
|
|
1127
1640
|
emoji = normalizeEmoji(args[1]);
|
|
1128
|
-
} catch
|
|
1129
|
-
throw
|
|
1641
|
+
} catch {
|
|
1642
|
+
throw usage(`Unknown emoji ${args[1]}. Use a short name, like eyes or thumbs_up.`);
|
|
1130
1643
|
}
|
|
1131
1644
|
await client().react(id, emoji, flags.remove ? { remove: true } : {});
|
|
1645
|
+
printDone(id, flags.remove ? "unreacted" : "reacted");
|
|
1132
1646
|
return 0;
|
|
1133
1647
|
}
|
|
1134
1648
|
case "label": {
|
|
1135
1649
|
const id = messageId(args[0]);
|
|
1136
1650
|
const set = labelPairs(args.slice(1));
|
|
1137
1651
|
const unset = flags.unset ?? [];
|
|
1138
|
-
if (Object.keys(set).length === 0 && unset.length === 0) throw
|
|
1652
|
+
if (Object.keys(set).length === 0 && unset.length === 0) throw usage("Missing the labels: k=v to set one, --unset k to remove one.", { reason: "missing_arguments" });
|
|
1139
1653
|
await client().label(id, {
|
|
1140
1654
|
set,
|
|
1141
1655
|
unset
|
|
1142
1656
|
});
|
|
1657
|
+
printDone(id, "labeled");
|
|
1143
1658
|
return 0;
|
|
1144
1659
|
}
|
|
1145
1660
|
case "typing": {
|
|
1146
1661
|
const id = messageId(args[0]);
|
|
1147
1662
|
const status = args.slice(1).join(" ").trim();
|
|
1148
1663
|
const targets = await client().typing(id, status || void 0);
|
|
1149
|
-
if (flags.json)
|
|
1664
|
+
if (flags.json) result({ targets });
|
|
1150
1665
|
return 0;
|
|
1151
1666
|
}
|
|
1152
1667
|
case "sync": {
|
|
1153
1668
|
const id = messageId(args[0]);
|
|
1154
1669
|
if (!args[1]) {
|
|
1155
1670
|
const { targets } = await client().sync(id);
|
|
1156
|
-
if (flags.json) return targets
|
|
1671
|
+
if (flags.json) return result({ targets }), 0;
|
|
1157
1672
|
out(targets.length ? formatSynced(targets) : "not synced anywhere");
|
|
1158
1673
|
return 0;
|
|
1159
1674
|
}
|
|
1160
|
-
const
|
|
1161
|
-
if (flags.json)
|
|
1162
|
-
|
|
1675
|
+
const synced = await client().sync(id, args[1]);
|
|
1676
|
+
if (flags.json) result({
|
|
1677
|
+
...synced,
|
|
1678
|
+
outcome: synced.attached ? "synced" : "unchanged"
|
|
1679
|
+
});
|
|
1680
|
+
else out(synced.attached ? `synced with ${synced.target}` : `already synced with ${synced.target}`);
|
|
1163
1681
|
return 0;
|
|
1164
1682
|
}
|
|
1165
1683
|
case "download": {
|
|
1166
1684
|
const id = args[0];
|
|
1167
|
-
if (!id) throw
|
|
1685
|
+
if (!id) throw usage(`Missing the attachment to download: the ID \`${named()} read\` shows on a file line.`, { reason: "missing_arguments" });
|
|
1168
1686
|
const response = await client().download(id);
|
|
1169
1687
|
const data = new Uint8Array(await response.arrayBuffer());
|
|
1170
1688
|
if (args[1] === "-") return io.write(data), 0;
|
|
1171
1689
|
const disposition = response.headers.get("content-disposition") ?? "";
|
|
1172
|
-
const
|
|
1173
|
-
const path = args[1] ?? basename(
|
|
1690
|
+
const filename = /filename\*=UTF-8''([^;]+)/.exec(disposition)?.[1];
|
|
1691
|
+
const path = args[1] ?? basename(filename ? decodeURIComponent(filename) : id);
|
|
1174
1692
|
await io.writeFile(path, data);
|
|
1175
|
-
|
|
1693
|
+
if (flags.json) result({
|
|
1694
|
+
attachment: { id },
|
|
1695
|
+
path,
|
|
1696
|
+
outcome: "downloaded"
|
|
1697
|
+
});
|
|
1698
|
+
else out(path);
|
|
1176
1699
|
return 0;
|
|
1177
1700
|
}
|
|
1178
1701
|
case "inbox": {
|
|
1179
1702
|
const key = args[0];
|
|
1180
|
-
if (!key) throw
|
|
1703
|
+
if (!key) throw usage("Missing the sync entry whose inbox to read, such as slack.", { reason: "missing_arguments" });
|
|
1181
1704
|
const events = await client().inbox(key, {
|
|
1182
1705
|
...args[1] ? { channel: args[1] } : {},
|
|
1183
1706
|
...flags.limit ? { limit: Number(flags.limit) } : {}
|
|
1184
1707
|
});
|
|
1185
|
-
if (flags.json) return events
|
|
1708
|
+
if (flags.json) return result({ events }), 0;
|
|
1186
1709
|
if (events.length === 0) return out("no events"), 0;
|
|
1187
1710
|
for (const entry of events) {
|
|
1188
1711
|
const event = entry.event;
|
|
@@ -1195,73 +1718,100 @@ async function run(command, args) {
|
|
|
1195
1718
|
const [key, sub, target] = args;
|
|
1196
1719
|
if (!key) {
|
|
1197
1720
|
const { sync } = await client().discovery();
|
|
1198
|
-
if (flags.json) return sync
|
|
1721
|
+
if (flags.json) return result({ sources: sync }), 0;
|
|
1199
1722
|
if (sync.length === 0) return out("no sources: this board has no sync configured"), 0;
|
|
1200
1723
|
for (const entry of sync) out(`${entry.key} ${entry.platform}`);
|
|
1201
1724
|
return 0;
|
|
1202
1725
|
}
|
|
1203
1726
|
if (sub === "join" && target) {
|
|
1204
1727
|
const joined = await client().joinSource(key, target);
|
|
1205
|
-
if (flags.json) return
|
|
1728
|
+
if (flags.json) return result({
|
|
1729
|
+
...joined,
|
|
1730
|
+
outcome: joined.joined ? "joined" : "unchanged"
|
|
1731
|
+
}), 0;
|
|
1206
1732
|
const label = joined.name ? `#${joined.name}` : joined.channel;
|
|
1207
1733
|
return out(joined.joined ? `joined ${label}` : `already in ${label}`), 0;
|
|
1208
1734
|
}
|
|
1209
|
-
if (sub !== "read"
|
|
1735
|
+
if (sub !== "read" && sub !== "join") throw usage(sub === void 0 ? `Missing what to do with ${key}: read or join.` : `Unknown command ${sub} for ${named()} sources.`, { reason: sub === void 0 ? "missing_arguments" : "unknown_command" });
|
|
1736
|
+
if (!target) throw usage(`Missing the channel, thread, or link to ${sub}.`, { reason: "missing_arguments" });
|
|
1210
1737
|
const read = await client().readSource(key, target, flags.limit ? { limit: Number(flags.limit) } : {});
|
|
1211
|
-
if (flags.json) return
|
|
1738
|
+
if (flags.json) return result(read), 0;
|
|
1212
1739
|
return out(formatSourceRead(key, read, Date.now(), named())), 0;
|
|
1213
1740
|
}
|
|
1214
1741
|
case "tail": {
|
|
1215
1742
|
const controller = new AbortController();
|
|
1216
|
-
const
|
|
1743
|
+
const stop = stopOn(controller);
|
|
1217
1744
|
try {
|
|
1218
1745
|
for await (const event of client().events({
|
|
1219
1746
|
...filters(args[0]),
|
|
1220
1747
|
...flags.all ? { all: true } : {}
|
|
1221
|
-
}, { signal: controller.signal }))
|
|
1748
|
+
}, { signal: controller.signal })) if (flags.json) line(event);
|
|
1749
|
+
else out(formatEvent(event));
|
|
1222
1750
|
} finally {
|
|
1223
|
-
release();
|
|
1751
|
+
stop.release();
|
|
1224
1752
|
}
|
|
1225
|
-
return 0;
|
|
1753
|
+
return stop.interrupted() ? 130 : 0;
|
|
1226
1754
|
}
|
|
1227
1755
|
case "wait": {
|
|
1228
1756
|
const controller = new AbortController();
|
|
1229
|
-
const
|
|
1757
|
+
const stop = stopOn(controller);
|
|
1758
|
+
const timeout = flags.timeout ?? "30m";
|
|
1230
1759
|
const event = await client().wait(filters(args[0]), {
|
|
1231
|
-
timeout
|
|
1760
|
+
timeout,
|
|
1232
1761
|
signal: controller.signal
|
|
1233
|
-
}).finally(release);
|
|
1762
|
+
}).finally(stop.release);
|
|
1234
1763
|
if (!event) {
|
|
1235
|
-
if (
|
|
1236
|
-
|
|
1764
|
+
if (controller.signal.aborted) return stop.interrupted() ? 130 : 0;
|
|
1765
|
+
throw new Failed({
|
|
1766
|
+
class: "timeout",
|
|
1767
|
+
reason: "timeout",
|
|
1768
|
+
message: `Nothing matched within ${timeout}.`
|
|
1769
|
+
});
|
|
1237
1770
|
}
|
|
1238
|
-
|
|
1771
|
+
if (flags.json) result({ event });
|
|
1772
|
+
else out(formatEvent(event));
|
|
1239
1773
|
return 0;
|
|
1240
1774
|
}
|
|
1241
1775
|
default: {
|
|
1242
1776
|
const own = app.commands[command];
|
|
1243
1777
|
if (own) return own.run(args, context());
|
|
1244
|
-
|
|
1778
|
+
const guess = closest(command, [...commands.flatMap((known) => [known.name, ...known.aliases ?? []]), ...Object.keys(app.commands)]);
|
|
1779
|
+
throw usage(`Unknown command ${command} for ${named()}.${guess ? ` Did you mean ${guess}?` : ""}`, {
|
|
1780
|
+
reason: "unknown_command",
|
|
1781
|
+
next: [{
|
|
1782
|
+
instruction: "See all commands",
|
|
1783
|
+
argv: [...cli(), "--help"]
|
|
1784
|
+
}]
|
|
1785
|
+
});
|
|
1245
1786
|
}
|
|
1246
1787
|
}
|
|
1247
1788
|
}
|
|
1248
|
-
/**
|
|
1789
|
+
/**
|
|
1790
|
+
* Aborts `controller` on the run's signal, or Ctrl-C without one. `release` stops listening, and
|
|
1791
|
+
* `interrupted` says whether Ctrl-C stopped it.
|
|
1792
|
+
*/
|
|
1249
1793
|
function stopOn(controller) {
|
|
1250
1794
|
const stop = () => controller.abort();
|
|
1251
1795
|
if (io.signal) {
|
|
1252
1796
|
if (io.signal.aborted) stop();
|
|
1253
1797
|
io.signal.addEventListener("abort", stop, { once: true });
|
|
1254
1798
|
const signal = io.signal;
|
|
1255
|
-
return
|
|
1799
|
+
return {
|
|
1800
|
+
release: () => signal.removeEventListener("abort", stop),
|
|
1801
|
+
interrupted: () => false
|
|
1802
|
+
};
|
|
1256
1803
|
}
|
|
1257
1804
|
process.once("SIGINT", stop);
|
|
1258
|
-
return
|
|
1805
|
+
return {
|
|
1806
|
+
release: () => process.off("SIGINT", stop),
|
|
1807
|
+
interrupted: () => controller.signal.aborted
|
|
1808
|
+
};
|
|
1259
1809
|
}
|
|
1260
1810
|
/** What a command an app adds gets: this run's flags, and the helpers board's own commands use. */
|
|
1261
1811
|
const context = () => ({
|
|
1262
1812
|
flags,
|
|
1263
1813
|
out,
|
|
1264
|
-
usage: (message) =>
|
|
1814
|
+
usage: (message) => usage(message),
|
|
1265
1815
|
portFlag,
|
|
1266
1816
|
connection,
|
|
1267
1817
|
client,
|
|
@@ -1269,31 +1819,46 @@ const context = () => ({
|
|
|
1269
1819
|
});
|
|
1270
1820
|
/**
|
|
1271
1821
|
* Runs board's CLI on `argv` (what follows the command name) as `app`, and answers its exit
|
|
1272
|
-
* code: 0 success, 1 error, 2 misuse, 124 when `wait` times out
|
|
1273
|
-
*
|
|
1822
|
+
* code: 0 success, 1 error, 2 misuse, 124 when `wait` times out, 130 when Ctrl-C stops it. A
|
|
1823
|
+
* failure goes to `app.onError`, or to stderr: `error: ` and what failed, then how to fix it, or
|
|
1824
|
+
* with `--json`, one JSON document. A long-running command (`tail`, `serve`) answers once it's
|
|
1825
|
+
* stopped.
|
|
1274
1826
|
*/
|
|
1275
1827
|
async function main(argv, options) {
|
|
1276
1828
|
app = options;
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
if (flags.help) {
|
|
1283
|
-
out(help(positionals));
|
|
1284
|
-
return 0;
|
|
1285
|
-
}
|
|
1286
|
-
const [word, ...args] = positionals;
|
|
1287
|
-
const command = canonical(word);
|
|
1829
|
+
typed = argv;
|
|
1830
|
+
flags = {};
|
|
1831
|
+
positionals = [];
|
|
1832
|
+
warnings = [];
|
|
1833
|
+
reached = null;
|
|
1288
1834
|
try {
|
|
1835
|
+
parse(argv);
|
|
1836
|
+
if (flags.version) {
|
|
1837
|
+
if (flags.json) result({ version: VERSION });
|
|
1838
|
+
else out(VERSION);
|
|
1839
|
+
return 0;
|
|
1840
|
+
}
|
|
1841
|
+
if (flags.help) {
|
|
1842
|
+
out(help(positionals));
|
|
1843
|
+
return 0;
|
|
1844
|
+
}
|
|
1845
|
+
const [word, ...args] = positionals;
|
|
1846
|
+
const command = canonical(word);
|
|
1847
|
+
current = {
|
|
1848
|
+
command,
|
|
1849
|
+
args
|
|
1850
|
+
};
|
|
1289
1851
|
if (io.board) {
|
|
1290
|
-
if (flags.url || flags.handle) throw
|
|
1291
|
-
if (command !== void 0 && MACHINE.has(command)) throw
|
|
1852
|
+
if (flags.url || flags.handle) throw usage(`--${flags.url ? "url" : "handle"} doesn't apply in-process: the board handle you pass says which board and who.`, { next: [] });
|
|
1853
|
+
if (command !== void 0 && MACHINE.has(command)) throw usage(`${command} isn't available in-process: it's about the machine ${named()} runs on.`, {
|
|
1854
|
+
reason: "not_available",
|
|
1855
|
+
next: []
|
|
1856
|
+
});
|
|
1292
1857
|
}
|
|
1293
1858
|
return await run(command, args);
|
|
1294
1859
|
} catch (error) {
|
|
1295
|
-
|
|
1296
|
-
return error
|
|
1860
|
+
const json = flags.json ?? argv.slice(0, argv.includes("--") ? argv.indexOf("--") : void 0).includes("--json");
|
|
1861
|
+
return report(failureOf(error), json);
|
|
1297
1862
|
}
|
|
1298
1863
|
}
|
|
1299
1864
|
/** The commands about the machine the CLI runs on, not the board: none of them run in-process. */
|
|
@@ -1315,4 +1880,4 @@ async function mainWith(argv, options, with_) {
|
|
|
1315
1880
|
}
|
|
1316
1881
|
}
|
|
1317
1882
|
//#endregion
|
|
1318
|
-
export {
|
|
1883
|
+
export { commandLine as a, failureLines as c, mainWith as i, formatFailure as l, commandWord as n, exitCode as o, main as r, failureDocument as s, Failed as t, who as u };
|