@vercel/board 0.24.0 → 0.25.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{a2-BxfKZFvJ.mjs → a2-A830f5NS.mjs} +6 -6
- package/dist/access.d.mts +1 -2
- package/dist/access.mjs +3 -3
- package/dist/{actors-rPnEQLZu.mjs → actors-BLXVp4oZ.mjs} +2 -2
- package/dist/{auth-5nLsx9qB.mjs → auth-lTkOgvcE.mjs} +1 -1
- package/dist/{backend-D0GDYiFr.d.mts → backend-ZKYTe3xa.d.mts} +5 -11
- package/dist/backend-a2/blob-vercel.d.mts +1 -1
- package/dist/backend-a2/contract.d.mts +1 -2
- package/dist/backend-a2/store-memory.d.mts +1 -1
- package/dist/backend-a2/store-postgres.d.mts +1 -1
- package/dist/backend-a2/store-redis-http.d.mts +1 -1
- package/dist/backend-a2/store-redis.d.mts +1 -1
- package/dist/backend-a2/store-sqlite.d.mts +1 -1
- package/dist/backend-a2.d.mts +5 -6
- package/dist/backend-a2.mjs +1 -1
- package/dist/{backend-http-tGUo0e8j.mjs → backend-http-CJd4Fu0U.mjs} +2 -2
- package/dist/{backend-http-rfiG-AKP.mjs → backend-http-CYtfwqAW.mjs} +9 -6
- package/dist/{backend-http-Dvq2p__1.d.mts → backend-http-E7YU0qhn.d.mts} +3 -4
- 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-DM5PyMc2.mjs → backend-vercel-BhwPExmE.mjs} +3 -3
- package/dist/{backend-vercel-C3zuxmcD.mjs → backend-vercel-Bqg2QfTd.mjs} +7 -4
- package/dist/{backend-vercel-01QKDobk.d.mts → backend-vercel-CIRObA0t.d.mts} +3 -4
- 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/{blob-C0J3wbGv.d.mts → blob-B-9AvYis.d.mts} +2 -1
- package/dist/{chat-provider-D0k3y8zY.d.mts → chat-provider-DsNEBQiA.d.mts} +3 -2
- 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-D0Px7KzF.mjs} +26 -8
- package/dist/{provider-D11Dfda0.d.mts → client-xLxeDz0f.d.mts} +21 -688
- package/dist/{conformance-DQ9qVzWP.mjs → conformance-DlnfuzEB.mjs} +1 -1
- package/dist/conformance.d.mts +1 -1
- package/dist/conformance.mjs +1 -1
- package/dist/{engine-kHfSOXnQ.mjs → engine-ROa7-IQY.mjs} +2 -2
- package/dist/errors-CB4tnZGz.d.mts +16 -0
- package/dist/{errors-CQ-fwxM3.mjs → errors-CusN4x2l.mjs} +18 -6
- package/dist/exports-D22M68rk.d.mts +5 -0
- package/dist/{exports-Bw81oDDl.mjs → exports-DCqgO_mr.mjs} +6 -6
- package/dist/{folds-CRYY14kc.d.mts → folds-jS9wlh9_.d.mts} +49 -2
- package/dist/{index-B_hWanKF.d.mts → index-C05it0mC.d.mts} +2 -2
- package/dist/index.d.mts +5 -4
- package/dist/{index.default-B6oPlvEa.mjs → index.default-BejExznB.mjs} +1 -1
- package/dist/index.mjs +6 -6
- package/dist/index.node.d.mts +5 -4
- package/dist/index.node.mjs +4 -4
- package/dist/inspect/app.js +108 -108
- package/dist/{main-CzWvRy91.mjs → main-DOhNCWMd.mjs} +712 -142
- package/dist/{middleware-DUjyQP_a.mjs → middleware-ChKM1zuu.mjs} +1 -1
- package/dist/{node-CirFYqw0.mjs → node-Bkd1x0QI.mjs} +56 -28
- package/dist/provider-Cdg1qadh.d.mts +688 -0
- package/dist/{serve-DHyA-6FR.mjs → serve-D2O0C6Kk.mjs} +6 -5
- package/dist/{server-Cp2oIQOj.mjs → server-BF4WJ7qi.mjs} +4 -4
- package/dist/{serving-B307gppu.mjs → serving-CvOWEym2.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 +3 -3
- package/dist/sync.chat.d.mts +1 -1
- package/dist/sync.d.mts +2 -2
- package/dist/{tokens-DUXaXwOj.mjs → tokens-BuCU-61R.mjs} +2 -2
- package/dist/{types-8yq2VnKI.d.mts → types-aCAf6lGU.d.mts} +1 -1
- package/dist/{watchers-hash-C_1jGA53.mjs → watchers-hash-BHNXy_N7.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 +6 -1
- package/docs/04-reference/03-http.md +4 -0
- package/package.json +1 -1
- package/dist/exports-87_yESpp.d.mts +0 -4
- package/dist/schemas-CWxs3ECJ.d.mts +0 -50
|
@@ -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-BHNXy_N7.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-D0Px7KzF.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-Bkd1x0QI.mjs";
|
|
8
|
+
import { t as http } from "./backend-http-CJd4Fu0U.mjs";
|
|
9
|
+
import { t as board } from "./server-BF4WJ7qi.mjs";
|
|
10
|
+
import { t as conformance } from "./conformance-DlnfuzEB.mjs";
|
|
11
|
+
import { n as vercel, t as credentialSource } from "./backend-vercel-Bqg2QfTd.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,14 +931,205 @@ 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}` : ""}`;
|
|
986
|
+
}
|
|
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. What the handle raised itself says
|
|
1015
|
+
* what and why, and its hint what fixes it. What a board answered is said by what this run was
|
|
1016
|
+
* doing, with the board's words under it and in `data`, never in what to run: a board can answer
|
|
1017
|
+
* with any code, the handle's own included.
|
|
1018
|
+
*/
|
|
1019
|
+
function failureOf(error) {
|
|
1020
|
+
if (error instanceof Failed) return error.failure;
|
|
1021
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1022
|
+
const { code, status, hint, origin } = error;
|
|
1023
|
+
if (typeof code !== "string" || typeof status !== "number") return {
|
|
1024
|
+
class: "unknown",
|
|
1025
|
+
reason: "unexpected_error",
|
|
1026
|
+
message: `Failed to ${doing()}.`,
|
|
1027
|
+
details: [message]
|
|
1028
|
+
};
|
|
1029
|
+
const said = { error: {
|
|
1030
|
+
code,
|
|
1031
|
+
message,
|
|
1032
|
+
status,
|
|
1033
|
+
...typeof hint === "string" ? { hint } : {}
|
|
1034
|
+
} };
|
|
1035
|
+
if (origin === "handle") {
|
|
1036
|
+
const fix = typeof hint === "string" ? [{ instruction: hint }] : [];
|
|
1037
|
+
const own = (kind, reason, next = fix) => ({
|
|
1038
|
+
class: kind,
|
|
1039
|
+
reason,
|
|
1040
|
+
message,
|
|
1041
|
+
...next.length ? { next } : {},
|
|
1042
|
+
data: said
|
|
1043
|
+
});
|
|
1044
|
+
switch (code) {
|
|
1045
|
+
case "not_logged_in": return {
|
|
1046
|
+
class: "auth",
|
|
1047
|
+
reason: "login_required",
|
|
1048
|
+
message: "You're not logged in to Vercel.",
|
|
1049
|
+
next: [{
|
|
1050
|
+
instruction: "Log in",
|
|
1051
|
+
argv: ["vercel", "login"]
|
|
1052
|
+
}],
|
|
1053
|
+
data: said
|
|
1054
|
+
};
|
|
1055
|
+
case "no_access": return own("permission", "permission_denied");
|
|
1056
|
+
case "wrong_project": return own("permission", "wrong_project");
|
|
1057
|
+
case "protected": return own("permission", "deployment_protected");
|
|
1058
|
+
case "no_project": return own("platform", "no_project");
|
|
1059
|
+
case "no_token": return own("auth", "token_unavailable");
|
|
1060
|
+
case "no_board": return own("not_found", "board_not_found");
|
|
1061
|
+
case "too_many_redirects": return own("network", "network_error");
|
|
1062
|
+
case "invalid_duration": return own("usage", "invalid_arguments", [helpRecovery()]);
|
|
1063
|
+
case "wrong_audience":
|
|
1064
|
+
case "redirected": return own("usage", "wrong_url");
|
|
1065
|
+
case "ambiguous_agent": return own("usage", "ambiguous_agent", [{
|
|
1066
|
+
instruction: "Write as one of them",
|
|
1067
|
+
argv: rerun("--as", "<PROFILE>")
|
|
1068
|
+
}, {
|
|
1069
|
+
instruction: "Or as yourself",
|
|
1070
|
+
argv: rerun("--human")
|
|
1071
|
+
}]);
|
|
1072
|
+
case "too_large": return {
|
|
1073
|
+
...own("limit", "payload_too_large"),
|
|
1074
|
+
message: `Can't ${doing()}. ${sentence(message)}`
|
|
1075
|
+
};
|
|
1076
|
+
case "upload_failed": return {
|
|
1077
|
+
...own("platform", "upload_failed"),
|
|
1078
|
+
message: `Failed to ${doing()}.`,
|
|
1079
|
+
details: [message]
|
|
1080
|
+
};
|
|
1081
|
+
case "port_taken": return own("conflict", "port_taken", [{
|
|
1082
|
+
instruction: "Pick another port",
|
|
1083
|
+
argv: [
|
|
1084
|
+
...cli(),
|
|
1085
|
+
current.command ?? "serve",
|
|
1086
|
+
"--port",
|
|
1087
|
+
"<PORT>"
|
|
1088
|
+
]
|
|
1089
|
+
}]);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
const theirs = (kind, reason, headline, next = []) => ({
|
|
1093
|
+
class: kind,
|
|
1094
|
+
reason,
|
|
1095
|
+
message: headline,
|
|
1096
|
+
details: [message],
|
|
1097
|
+
...reached && [
|
|
1098
|
+
"auth",
|
|
1099
|
+
"permission",
|
|
1100
|
+
"network",
|
|
1101
|
+
"platform"
|
|
1102
|
+
].includes(kind) ? { context: [["Board", reached]] } : {},
|
|
1103
|
+
...next.length ? { next } : {},
|
|
1104
|
+
data: said
|
|
1105
|
+
});
|
|
1106
|
+
const specific = (fallback) => /^[a-z]+(_[a-z]+)+$/.test(code) && code !== "invalid_request" && code !== "remote_error" ? code : fallback;
|
|
1107
|
+
if (status === 400) return theirs("usage", specific("invalid_arguments"), `Can't ${doing()}.`);
|
|
1108
|
+
if (status === 401) return theirs("auth", "login_required", "The board didn't accept your credentials.", [{
|
|
1109
|
+
instruction: "Log in again",
|
|
1110
|
+
argv: ["vercel", "login"]
|
|
1111
|
+
}]);
|
|
1112
|
+
if (status === 403) return theirs("permission", "permission_denied", `You can't ${doing()}.`);
|
|
1113
|
+
if (status === 404) {
|
|
1114
|
+
const thing = missing();
|
|
1115
|
+
return thing ? {
|
|
1116
|
+
...theirs("not_found", specific("not_found"), `Couldn't find ${thing}.`),
|
|
1117
|
+
details: []
|
|
1118
|
+
} : theirs("not_found", specific("not_found"), `Can't ${doing()}.`);
|
|
713
1119
|
}
|
|
1120
|
+
if (status === 409) return theirs("conflict", specific("conflict"), `Can't ${doing()}.`);
|
|
1121
|
+
if (status === 413) return theirs("limit", "payload_too_large", `Can't ${doing()}. It's too large for the board.`);
|
|
1122
|
+
if (status === 429) return theirs("rate_limit", "rate_limited", "Too many requests to the board. Try again in a minute.");
|
|
1123
|
+
if (status === 501) return theirs("platform", specific("not_supported"), `Can't ${doing()}. This board doesn't support it.`);
|
|
1124
|
+
if (code === "unreachable") return theirs("network", "network_error", "Couldn't reach the board.");
|
|
1125
|
+
return theirs("platform", "api_error", `Failed to ${doing()}.`);
|
|
1126
|
+
}
|
|
1127
|
+
/** How this run ends with `failure`: shown on stderr, or by the app, and its exit code. */
|
|
1128
|
+
function report(failure, json) {
|
|
1129
|
+
if (app.onError) app.onError(failure, { json });
|
|
1130
|
+
else if (json) io.error(`${JSON.stringify(failureDocument(failure))}\n`);
|
|
1131
|
+
else io.error(`${terminalText(formatFailure(failure))}\n`);
|
|
1132
|
+
return exitCode(failure);
|
|
714
1133
|
}
|
|
715
1134
|
/**
|
|
716
1135
|
* A team's board in Vercel's API, by the URL its discovery names it with
|
|
@@ -759,6 +1178,7 @@ function connection(url = flags.url, options = {}) {
|
|
|
759
1178
|
};
|
|
760
1179
|
}
|
|
761
1180
|
const at = configured.replace(/\/+$/, "");
|
|
1181
|
+
reached = at;
|
|
762
1182
|
const onVercel = vercelBoard(at);
|
|
763
1183
|
if (onVercel) {
|
|
764
1184
|
const board$3 = board({ backend: vercel(onVercel) });
|
|
@@ -797,24 +1217,40 @@ async function readStdin() {
|
|
|
797
1217
|
}
|
|
798
1218
|
function hookAgents(names) {
|
|
799
1219
|
const known = Object.keys(AGENTS);
|
|
800
|
-
for (const name of names) if (!known.includes(name)) throw
|
|
1220
|
+
for (const name of names) if (!known.includes(name)) throw usage(`Unknown agent ${name}. The agents are ${known.join(", ")}.`);
|
|
801
1221
|
return names.length ? names : known;
|
|
802
1222
|
}
|
|
1223
|
+
/** Says what a write that went through had to say: in its JSON result with `--json`, on stderr otherwise. */
|
|
1224
|
+
function warn(warning) {
|
|
1225
|
+
if (flags.json) warnings.push(warning);
|
|
1226
|
+
else if (app.onWarning) app.onWarning(warning);
|
|
1227
|
+
else io.error(`${terminalText(`warning: ${warning.message}`)}\n`);
|
|
1228
|
+
}
|
|
803
1229
|
/** A write went through, but some handles in it matched no one: they stayed plain text. */
|
|
804
1230
|
function warnUnknownMentions(handles) {
|
|
805
1231
|
if (!handles?.length) return;
|
|
806
1232
|
const names = handles.map((handle) => `@${handle}`);
|
|
807
1233
|
const list = names.length === 1 ? names[0] : `${names.slice(0, -1).join(", ")} and ${names.at(-1)}`;
|
|
808
1234
|
const one = names.length === 1;
|
|
809
|
-
|
|
1235
|
+
warn({
|
|
1236
|
+
reason: "unknown_mention",
|
|
1237
|
+
message: `${list} ${one ? "matches" : "match"} no one on the board, so ${one ? "it stays" : "they stay"} plain text and ${one ? "notifies" : "notify"} no one.`
|
|
1238
|
+
});
|
|
810
1239
|
}
|
|
1240
|
+
/** A post or reply: its ID, or with `--json`, the message. */
|
|
811
1241
|
function printMessage(message) {
|
|
812
|
-
if (flags.json)
|
|
1242
|
+
if (flags.json) result({
|
|
1243
|
+
message,
|
|
1244
|
+
outcome: "posted"
|
|
1245
|
+
});
|
|
813
1246
|
else out(messageRef(message.id));
|
|
814
1247
|
}
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
1248
|
+
/** A write that answers nothing but that it went through: with `--json`, which message, and what happened to it. */
|
|
1249
|
+
function printDone(id, outcome) {
|
|
1250
|
+
if (flags.json) result({
|
|
1251
|
+
message: { id },
|
|
1252
|
+
outcome
|
|
1253
|
+
});
|
|
818
1254
|
}
|
|
819
1255
|
/** Best-effort: the URL is printed either way. */
|
|
820
1256
|
/** `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 +1280,7 @@ function openBrowser(url) {
|
|
|
844
1280
|
}
|
|
845
1281
|
function portFlag() {
|
|
846
1282
|
const port = flags.port === void 0 ? void 0 : Number(flags.port);
|
|
847
|
-
if (port !== void 0 && !(Number.isInteger(port) && port >= 0 && port <= 65535)) throw
|
|
1283
|
+
if (port !== void 0 && !(Number.isInteger(port) && port >= 0 && port <= 65535)) throw usage(`${flags.port} isn't a port.`, { reason: "invalid_port" });
|
|
848
1284
|
return port;
|
|
849
1285
|
}
|
|
850
1286
|
/** How long `identity link` waits for the sign-in or the code. */
|
|
@@ -854,26 +1290,39 @@ async function identity(args) {
|
|
|
854
1290
|
const [sub, target] = args;
|
|
855
1291
|
if (sub === void 0) {
|
|
856
1292
|
const view = await board.identity();
|
|
857
|
-
if (flags.json) return
|
|
1293
|
+
if (flags.json) return result(view), 0;
|
|
858
1294
|
if (view.links.length === 0) out("no linked accounts");
|
|
859
1295
|
for (const link of view.links) out(`${link.handle ? `@${link.handle} ` : ""}${link.actor} by ${link.method === "code" ? "code" : "sign-in"} · ${link.at}`);
|
|
860
1296
|
if (view.unlinked?.length) out(`not linked ${view.unlinked.join(", ")} (${named()} identity link <key>)`);
|
|
861
1297
|
return 0;
|
|
862
1298
|
}
|
|
863
1299
|
if (sub === "unlink") {
|
|
864
|
-
if (!target
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
1300
|
+
if (!target) throw usage(`Missing the account to unlink: a handle or actor ID from \`${named()} identity\`.`, { reason: "missing_arguments" });
|
|
1301
|
+
if (args.length !== 2) throw usage("identity unlink takes one account.");
|
|
1302
|
+
const unlinked = await board.unlinkAccount(target);
|
|
1303
|
+
if (flags.json) result({
|
|
1304
|
+
...unlinked,
|
|
1305
|
+
outcome: "unlinked"
|
|
1306
|
+
});
|
|
1307
|
+
else out(`unlinked ${unlinked.actor}`);
|
|
868
1308
|
return 0;
|
|
869
1309
|
}
|
|
870
|
-
if (sub !== "link"
|
|
1310
|
+
if (sub !== "link") throw usage(`Unknown command ${sub} for ${named()} identity.`, { reason: "unknown_command" });
|
|
1311
|
+
if (!target) throw usage("Missing the sync entry to link your account on, such as slack.", { reason: "missing_arguments" });
|
|
1312
|
+
if (args.length !== 2) throw usage("identity link takes one sync entry.");
|
|
871
1313
|
const before = new Set((await board.identity()).links.map((link) => `${link.actor}|${link.at}`));
|
|
872
1314
|
const platform = (await board.discovery().catch(() => null))?.sync.find((entry) => entry.key === target)?.platform ?? target;
|
|
873
1315
|
const { code, expiresAt, signIn } = await board.syncCode(target);
|
|
874
1316
|
const consent = signIn !== void 0 && !flags.code;
|
|
875
|
-
if (consent && browsable(signIn) === null) throw new
|
|
876
|
-
|
|
1317
|
+
if (consent && browsable(signIn) === null) throw new Failed({
|
|
1318
|
+
class: "platform",
|
|
1319
|
+
reason: "invalid_sign_in",
|
|
1320
|
+
message: "The board's sign-in isn't an http or https URL.",
|
|
1321
|
+
details: [JSON.stringify(signIn)],
|
|
1322
|
+
data: { signIn }
|
|
1323
|
+
});
|
|
1324
|
+
if (flags.json) line({
|
|
1325
|
+
type: "code",
|
|
877
1326
|
sync: target,
|
|
878
1327
|
platform,
|
|
879
1328
|
code,
|
|
@@ -899,13 +1348,28 @@ async function identity(args) {
|
|
|
899
1348
|
await sleep();
|
|
900
1349
|
const linked = await linkedSince();
|
|
901
1350
|
if (linked) {
|
|
902
|
-
if (flags.json)
|
|
1351
|
+
if (flags.json) line({
|
|
1352
|
+
type: "linked",
|
|
1353
|
+
account: linked
|
|
1354
|
+
});
|
|
903
1355
|
else out(`linked ${linked.handle ? `@${linked.handle} ` : ""}(${linked.actor})`);
|
|
904
1356
|
return 0;
|
|
905
1357
|
}
|
|
906
1358
|
}
|
|
907
|
-
|
|
908
|
-
|
|
1359
|
+
throw new Failed({
|
|
1360
|
+
class: "timeout",
|
|
1361
|
+
reason: "timeout",
|
|
1362
|
+
message: consent ? "Nothing linked an account within 10 minutes." : "The code expired before a message carried it.",
|
|
1363
|
+
next: [{
|
|
1364
|
+
instruction: "Try again",
|
|
1365
|
+
argv: [
|
|
1366
|
+
...cli(),
|
|
1367
|
+
"identity",
|
|
1368
|
+
"link",
|
|
1369
|
+
target
|
|
1370
|
+
]
|
|
1371
|
+
}]
|
|
1372
|
+
});
|
|
909
1373
|
}
|
|
910
1374
|
async function run(command, args) {
|
|
911
1375
|
switch (command) {
|
|
@@ -919,7 +1383,7 @@ async function run(command, args) {
|
|
|
919
1383
|
const url = given ?? discovered.url;
|
|
920
1384
|
const source = transport.source();
|
|
921
1385
|
if (flags.json) {
|
|
922
|
-
|
|
1386
|
+
result({
|
|
923
1387
|
...me,
|
|
924
1388
|
board: url,
|
|
925
1389
|
source
|
|
@@ -941,15 +1405,15 @@ async function run(command, args) {
|
|
|
941
1405
|
return 0;
|
|
942
1406
|
}
|
|
943
1407
|
case "conformance": {
|
|
944
|
-
if (args.length > 0) throw
|
|
1408
|
+
if (args.length > 0) throw usage("conformance takes no arguments: pass its channel with --channel.");
|
|
945
1409
|
const report = await conformance(client(), flags.channel ? { channel: flags.channel } : {});
|
|
946
|
-
if (flags.json)
|
|
1410
|
+
if (flags.json) result(report);
|
|
947
1411
|
else out(formatConformance(report));
|
|
948
1412
|
return report.ok ? 0 : 1;
|
|
949
1413
|
}
|
|
950
1414
|
case "channels": {
|
|
951
1415
|
const channels = await client().channels();
|
|
952
|
-
if (flags.json) return channels
|
|
1416
|
+
if (flags.json) return result({ channels }), 0;
|
|
953
1417
|
if (channels.length === 0) return out("no channels yet"), 0;
|
|
954
1418
|
let group = "";
|
|
955
1419
|
for (const channel of channels) {
|
|
@@ -965,7 +1429,7 @@ async function run(command, args) {
|
|
|
965
1429
|
}
|
|
966
1430
|
case "actors": {
|
|
967
1431
|
const actors = await client().actors();
|
|
968
|
-
if (flags.json) return actors
|
|
1432
|
+
if (flags.json) return result({ actors }), 0;
|
|
969
1433
|
for (const actor of actors) out(`@${actor.handle} ${actor.name} ${actor.id}${actor.person ? ` → @${actor.person.handle}` : ""}`);
|
|
970
1434
|
return 0;
|
|
971
1435
|
}
|
|
@@ -976,14 +1440,17 @@ async function run(command, args) {
|
|
|
976
1440
|
const link = target !== void 0 && /^https?:\/\//i.test(target);
|
|
977
1441
|
if (target && (link || isMessageRef(target))) {
|
|
978
1442
|
const thread = link ? await board.linked(target) : await board.thread(messageId(target));
|
|
979
|
-
if (link && !Array.isArray(thread.replies)) throw
|
|
1443
|
+
if (link && !Array.isArray(thread.replies)) throw outdated("This board can't read links.");
|
|
980
1444
|
const narrowed = Object.keys(filters()).length > 0;
|
|
981
1445
|
const replies = narrowed ? (await board.messages({
|
|
982
1446
|
...filters(),
|
|
983
1447
|
re: thread.message.id,
|
|
984
1448
|
limit: 500
|
|
985
1449
|
})).messages.filter((m) => m.re !== null) : thread.replies;
|
|
986
|
-
if (flags.json) return
|
|
1450
|
+
if (flags.json) return result({
|
|
1451
|
+
...thread,
|
|
1452
|
+
replies
|
|
1453
|
+
}), 0;
|
|
987
1454
|
const authors = new Set([thread.message, ...replies].map((m) => m.by.actor));
|
|
988
1455
|
const unnamed = reactionNames([thread.message, ...replies]).filter((id) => !authors.has(id));
|
|
989
1456
|
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 +1462,27 @@ async function run(command, args) {
|
|
|
995
1462
|
...filters(target),
|
|
996
1463
|
...flags.limit ? { limit: Number(flags.limit) } : {}
|
|
997
1464
|
};
|
|
998
|
-
const old =
|
|
1465
|
+
const old = outdated("This board can't list threads.");
|
|
999
1466
|
const page = await board.threads(query).catch((error) => {
|
|
1000
|
-
throw error instanceof BoardError && error.status === 404 ? old : error;
|
|
1467
|
+
throw error instanceof BoardError && error.status === 404 && /^no route: /.test(error.message) ? old : error;
|
|
1001
1468
|
});
|
|
1002
1469
|
if (!Array.isArray(page.threads)) throw old;
|
|
1003
|
-
if (!page.threads.length && target
|
|
1004
|
-
|
|
1470
|
+
if (!page.threads.length && target) {
|
|
1471
|
+
const channels = (await board.channels()).map((c) => c.path);
|
|
1472
|
+
if (!channels.some((path) => path === target || path.startsWith(target.replace(/\/?$/, "/")))) {
|
|
1473
|
+
const guess = closest(target, channels);
|
|
1474
|
+
throw new Failed({
|
|
1475
|
+
class: "not_found",
|
|
1476
|
+
reason: "channel_not_found",
|
|
1477
|
+
message: `Couldn't find channel ${target}.${guess ? ` Did you mean ${guess}?` : ""}`,
|
|
1478
|
+
next: [{
|
|
1479
|
+
instruction: "See its channels",
|
|
1480
|
+
argv: [...cli(), "channels"]
|
|
1481
|
+
}]
|
|
1482
|
+
});
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
if (flags.json) return result(page), 0;
|
|
1005
1486
|
const count = `${page.threads.length} ${page.threads.length === 1 ? "thread" : "threads"}`;
|
|
1006
1487
|
out(`${target ?? "all channels"} · ${count} · cursor ${page.cursor}${page.threads.length ? "\n" : ""}`);
|
|
1007
1488
|
if (page.threads.length) out(formatThreadSummaries(page.threads, { channel: !target || target.endsWith("/") }));
|
|
@@ -1009,10 +1490,18 @@ async function run(command, args) {
|
|
|
1009
1490
|
}
|
|
1010
1491
|
case "post": {
|
|
1011
1492
|
const [target, body] = args;
|
|
1012
|
-
if (!target) throw
|
|
1493
|
+
if (!target) throw usage("Missing the channel or message to post to.", { reason: "missing_arguments" });
|
|
1013
1494
|
const re = isMessageRef(target) ? messageId(target) : void 0;
|
|
1014
|
-
if (re !== void 0 && flags.sync?.length) throw
|
|
1015
|
-
|
|
1495
|
+
if (re !== void 0 && flags.sync?.length) throw usage("Can't use --sync on a reply. It syncs whole threads.", { next: [{
|
|
1496
|
+
instruction: "Sync the thread instead",
|
|
1497
|
+
argv: [
|
|
1498
|
+
...cli(),
|
|
1499
|
+
"sync",
|
|
1500
|
+
re,
|
|
1501
|
+
"<TARGET>"
|
|
1502
|
+
]
|
|
1503
|
+
}] });
|
|
1504
|
+
if (flags.sync?.length && flags["no-sync"]) throw usage("Can't use --sync with --no-sync.");
|
|
1016
1505
|
const board = client();
|
|
1017
1506
|
const content = body === void 0 && flags.attach?.length ? "" : await text(body);
|
|
1018
1507
|
const files = await attachments(board);
|
|
@@ -1049,16 +1538,19 @@ async function run(command, args) {
|
|
|
1049
1538
|
case "schedule": {
|
|
1050
1539
|
if (args[0] === "cancel" && args.length === 2) {
|
|
1051
1540
|
const { schedule, canceled } = await client().cancelSchedule(scheduleId(args[1]));
|
|
1052
|
-
if (flags.json)
|
|
1541
|
+
if (flags.json) result({
|
|
1542
|
+
schedule,
|
|
1543
|
+
outcome: canceled ? "canceled" : "unchanged"
|
|
1544
|
+
});
|
|
1053
1545
|
else out(canceled ? `canceled s${schedule.id}` : `s${schedule.id} is already ${schedule.state}`);
|
|
1054
1546
|
return 0;
|
|
1055
1547
|
}
|
|
1056
1548
|
const [target, body] = args;
|
|
1057
|
-
if (!target) throw
|
|
1058
|
-
if (!flags.in && !flags.at && !flags.every) throw
|
|
1059
|
-
if (flags.sync?.length) throw
|
|
1549
|
+
if (!target) throw usage("Missing the channel or message to schedule a message in.", { reason: "missing_arguments" });
|
|
1550
|
+
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" });
|
|
1551
|
+
if (flags.sync?.length) throw usage("Can't use --sync with schedule. A schedule posts where you point it.");
|
|
1060
1552
|
const times = flags.times === void 0 ? void 0 : Number(flags.times);
|
|
1061
|
-
if (times !== void 0 && !(Number.isInteger(times) && times > 0)) throw
|
|
1553
|
+
if (times !== void 0 && !(Number.isInteger(times) && times > 0)) throw usage(`--times takes a count, and ${flags.times} isn't one.`);
|
|
1062
1554
|
const board = client();
|
|
1063
1555
|
const content = body === void 0 && flags.attach?.length ? "" : await text(body);
|
|
1064
1556
|
const files = await attachments(board);
|
|
@@ -1076,7 +1568,10 @@ async function run(command, args) {
|
|
|
1076
1568
|
...times !== void 0 ? { times } : {}
|
|
1077
1569
|
});
|
|
1078
1570
|
warnUnknownMentions(schedule.unknownMentions);
|
|
1079
|
-
if (flags.json)
|
|
1571
|
+
if (flags.json) result({
|
|
1572
|
+
schedule,
|
|
1573
|
+
outcome: "scheduled"
|
|
1574
|
+
});
|
|
1080
1575
|
else out(formatSchedule(schedule));
|
|
1081
1576
|
return 0;
|
|
1082
1577
|
}
|
|
@@ -1087,102 +1582,135 @@ async function run(command, args) {
|
|
|
1087
1582
|
...flags.channel ? { channel: flags.channel } : {},
|
|
1088
1583
|
...flags.all ? { all: true } : {}
|
|
1089
1584
|
});
|
|
1090
|
-
if (flags.json) return list
|
|
1585
|
+
if (flags.json) return result({ schedules: list }), 0;
|
|
1091
1586
|
if (list.length === 0) return out(flags.all ? "no schedules" : "no pending schedules"), 0;
|
|
1092
1587
|
out(list.map((schedule) => formatSchedule(schedule)).join("\n"));
|
|
1093
1588
|
return 0;
|
|
1094
1589
|
}
|
|
1095
1590
|
case "hooks": {
|
|
1096
1591
|
const [action, ...names] = args;
|
|
1097
|
-
if (action !== "uninstall") throw
|
|
1592
|
+
if (action !== "uninstall") throw usage(`\`${`${named()} hooks ${action ?? ""}`.trimEnd()}\` is gone. Turn-end hooks were removed.`, {
|
|
1593
|
+
reason: "unknown_command",
|
|
1594
|
+
next: [{
|
|
1595
|
+
instruction: "Clear an earlier install",
|
|
1596
|
+
argv: [
|
|
1597
|
+
...cli(),
|
|
1598
|
+
"hooks",
|
|
1599
|
+
"uninstall"
|
|
1600
|
+
]
|
|
1601
|
+
}]
|
|
1602
|
+
});
|
|
1098
1603
|
const agents = hookAgents(names);
|
|
1099
1604
|
const width = Math.max(...agents.map((agent) => AGENTS[agent].name.length));
|
|
1605
|
+
const hooks = [];
|
|
1100
1606
|
for (const agent of agents) {
|
|
1101
1607
|
const installed = installedHook(agent);
|
|
1102
|
-
const
|
|
1103
|
-
const rules =
|
|
1608
|
+
const by = installed ? cliOf(installed) : null;
|
|
1609
|
+
const rules = by ? allowRules(agent, by) : null;
|
|
1104
1610
|
if (rules) disallow(rules.file, rules.rules);
|
|
1105
|
-
|
|
1611
|
+
const removed = uninstallHook(agent);
|
|
1612
|
+
hooks.push({
|
|
1613
|
+
agent,
|
|
1614
|
+
outcome: removed ? "removed" : "unchanged",
|
|
1615
|
+
file: installed ? AGENTS[agent].file() : null
|
|
1616
|
+
});
|
|
1617
|
+
if (!flags.json) out([
|
|
1106
1618
|
AGENTS[agent].name.padEnd(width),
|
|
1107
|
-
|
|
1619
|
+
removed ? "removed" : "not installed",
|
|
1108
1620
|
installed ? tilde(AGENTS[agent].file()) : ""
|
|
1109
1621
|
].join(" ").trimEnd());
|
|
1110
1622
|
}
|
|
1623
|
+
if (flags.json) result({ hooks });
|
|
1111
1624
|
return 0;
|
|
1112
1625
|
}
|
|
1113
1626
|
case "hook": return 0;
|
|
1114
1627
|
case "edit": {
|
|
1115
|
-
const
|
|
1628
|
+
const id = messageId(args[0]);
|
|
1629
|
+
const { unknownMentions } = await client().edit(id, await text(args[1]));
|
|
1116
1630
|
warnUnknownMentions(unknownMentions);
|
|
1631
|
+
printDone(id, "edited");
|
|
1117
1632
|
return 0;
|
|
1118
1633
|
}
|
|
1119
|
-
case "delete":
|
|
1120
|
-
|
|
1634
|
+
case "delete": {
|
|
1635
|
+
const id = messageId(args[0]);
|
|
1636
|
+
await client().delete(id);
|
|
1637
|
+
printDone(id, "deleted");
|
|
1121
1638
|
return 0;
|
|
1639
|
+
}
|
|
1122
1640
|
case "react": {
|
|
1123
1641
|
const id = messageId(args[0]);
|
|
1124
|
-
if (!args[1]) throw
|
|
1642
|
+
if (!args[1]) throw usage("Missing the emoji to react with, like eyes or thumbs_up.", { reason: "missing_arguments" });
|
|
1125
1643
|
let emoji;
|
|
1126
1644
|
try {
|
|
1127
1645
|
emoji = normalizeEmoji(args[1]);
|
|
1128
|
-
} catch
|
|
1129
|
-
throw
|
|
1646
|
+
} catch {
|
|
1647
|
+
throw usage(`Unknown emoji ${args[1]}. Use a short name, like eyes or thumbs_up.`);
|
|
1130
1648
|
}
|
|
1131
1649
|
await client().react(id, emoji, flags.remove ? { remove: true } : {});
|
|
1650
|
+
printDone(id, flags.remove ? "unreacted" : "reacted");
|
|
1132
1651
|
return 0;
|
|
1133
1652
|
}
|
|
1134
1653
|
case "label": {
|
|
1135
1654
|
const id = messageId(args[0]);
|
|
1136
1655
|
const set = labelPairs(args.slice(1));
|
|
1137
1656
|
const unset = flags.unset ?? [];
|
|
1138
|
-
if (Object.keys(set).length === 0 && unset.length === 0) throw
|
|
1657
|
+
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
1658
|
await client().label(id, {
|
|
1140
1659
|
set,
|
|
1141
1660
|
unset
|
|
1142
1661
|
});
|
|
1662
|
+
printDone(id, "labeled");
|
|
1143
1663
|
return 0;
|
|
1144
1664
|
}
|
|
1145
1665
|
case "typing": {
|
|
1146
1666
|
const id = messageId(args[0]);
|
|
1147
1667
|
const status = args.slice(1).join(" ").trim();
|
|
1148
1668
|
const targets = await client().typing(id, status || void 0);
|
|
1149
|
-
if (flags.json)
|
|
1669
|
+
if (flags.json) result({ targets });
|
|
1150
1670
|
return 0;
|
|
1151
1671
|
}
|
|
1152
1672
|
case "sync": {
|
|
1153
1673
|
const id = messageId(args[0]);
|
|
1154
1674
|
if (!args[1]) {
|
|
1155
1675
|
const { targets } = await client().sync(id);
|
|
1156
|
-
if (flags.json) return targets
|
|
1676
|
+
if (flags.json) return result({ targets }), 0;
|
|
1157
1677
|
out(targets.length ? formatSynced(targets) : "not synced anywhere");
|
|
1158
1678
|
return 0;
|
|
1159
1679
|
}
|
|
1160
|
-
const
|
|
1161
|
-
if (flags.json)
|
|
1162
|
-
|
|
1680
|
+
const synced = await client().sync(id, args[1]);
|
|
1681
|
+
if (flags.json) result({
|
|
1682
|
+
...synced,
|
|
1683
|
+
outcome: synced.attached ? "synced" : "unchanged"
|
|
1684
|
+
});
|
|
1685
|
+
else out(synced.attached ? `synced with ${synced.target}` : `already synced with ${synced.target}`);
|
|
1163
1686
|
return 0;
|
|
1164
1687
|
}
|
|
1165
1688
|
case "download": {
|
|
1166
1689
|
const id = args[0];
|
|
1167
|
-
if (!id) throw
|
|
1690
|
+
if (!id) throw usage(`Missing the attachment to download: the ID \`${named()} read\` shows on a file line.`, { reason: "missing_arguments" });
|
|
1168
1691
|
const response = await client().download(id);
|
|
1169
1692
|
const data = new Uint8Array(await response.arrayBuffer());
|
|
1170
1693
|
if (args[1] === "-") return io.write(data), 0;
|
|
1171
1694
|
const disposition = response.headers.get("content-disposition") ?? "";
|
|
1172
|
-
const
|
|
1173
|
-
const path = args[1] ?? basename(
|
|
1695
|
+
const filename = /filename\*=UTF-8''([^;]+)/.exec(disposition)?.[1];
|
|
1696
|
+
const path = args[1] ?? basename(filename ? decodeURIComponent(filename) : id);
|
|
1174
1697
|
await io.writeFile(path, data);
|
|
1175
|
-
|
|
1698
|
+
if (flags.json) result({
|
|
1699
|
+
attachment: { id },
|
|
1700
|
+
path,
|
|
1701
|
+
outcome: "downloaded"
|
|
1702
|
+
});
|
|
1703
|
+
else out(path);
|
|
1176
1704
|
return 0;
|
|
1177
1705
|
}
|
|
1178
1706
|
case "inbox": {
|
|
1179
1707
|
const key = args[0];
|
|
1180
|
-
if (!key) throw
|
|
1708
|
+
if (!key) throw usage("Missing the sync entry whose inbox to read, such as slack.", { reason: "missing_arguments" });
|
|
1181
1709
|
const events = await client().inbox(key, {
|
|
1182
1710
|
...args[1] ? { channel: args[1] } : {},
|
|
1183
1711
|
...flags.limit ? { limit: Number(flags.limit) } : {}
|
|
1184
1712
|
});
|
|
1185
|
-
if (flags.json) return events
|
|
1713
|
+
if (flags.json) return result({ events }), 0;
|
|
1186
1714
|
if (events.length === 0) return out("no events"), 0;
|
|
1187
1715
|
for (const entry of events) {
|
|
1188
1716
|
const event = entry.event;
|
|
@@ -1195,73 +1723,100 @@ async function run(command, args) {
|
|
|
1195
1723
|
const [key, sub, target] = args;
|
|
1196
1724
|
if (!key) {
|
|
1197
1725
|
const { sync } = await client().discovery();
|
|
1198
|
-
if (flags.json) return sync
|
|
1726
|
+
if (flags.json) return result({ sources: sync }), 0;
|
|
1199
1727
|
if (sync.length === 0) return out("no sources: this board has no sync configured"), 0;
|
|
1200
1728
|
for (const entry of sync) out(`${entry.key} ${entry.platform}`);
|
|
1201
1729
|
return 0;
|
|
1202
1730
|
}
|
|
1203
1731
|
if (sub === "join" && target) {
|
|
1204
1732
|
const joined = await client().joinSource(key, target);
|
|
1205
|
-
if (flags.json) return
|
|
1733
|
+
if (flags.json) return result({
|
|
1734
|
+
...joined,
|
|
1735
|
+
outcome: joined.joined ? "joined" : "unchanged"
|
|
1736
|
+
}), 0;
|
|
1206
1737
|
const label = joined.name ? `#${joined.name}` : joined.channel;
|
|
1207
1738
|
return out(joined.joined ? `joined ${label}` : `already in ${label}`), 0;
|
|
1208
1739
|
}
|
|
1209
|
-
if (sub !== "read"
|
|
1740
|
+
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" });
|
|
1741
|
+
if (!target) throw usage(`Missing the channel, thread, or link to ${sub}.`, { reason: "missing_arguments" });
|
|
1210
1742
|
const read = await client().readSource(key, target, flags.limit ? { limit: Number(flags.limit) } : {});
|
|
1211
|
-
if (flags.json) return
|
|
1743
|
+
if (flags.json) return result(read), 0;
|
|
1212
1744
|
return out(formatSourceRead(key, read, Date.now(), named())), 0;
|
|
1213
1745
|
}
|
|
1214
1746
|
case "tail": {
|
|
1215
1747
|
const controller = new AbortController();
|
|
1216
|
-
const
|
|
1748
|
+
const stop = stopOn(controller);
|
|
1217
1749
|
try {
|
|
1218
1750
|
for await (const event of client().events({
|
|
1219
1751
|
...filters(args[0]),
|
|
1220
1752
|
...flags.all ? { all: true } : {}
|
|
1221
|
-
}, { signal: controller.signal }))
|
|
1753
|
+
}, { signal: controller.signal })) if (flags.json) line(event);
|
|
1754
|
+
else out(formatEvent(event));
|
|
1222
1755
|
} finally {
|
|
1223
|
-
release();
|
|
1756
|
+
stop.release();
|
|
1224
1757
|
}
|
|
1225
|
-
return 0;
|
|
1758
|
+
return stop.interrupted() ? 130 : 0;
|
|
1226
1759
|
}
|
|
1227
1760
|
case "wait": {
|
|
1228
1761
|
const controller = new AbortController();
|
|
1229
|
-
const
|
|
1762
|
+
const stop = stopOn(controller);
|
|
1763
|
+
const timeout = flags.timeout ?? "30m";
|
|
1230
1764
|
const event = await client().wait(filters(args[0]), {
|
|
1231
|
-
timeout
|
|
1765
|
+
timeout,
|
|
1232
1766
|
signal: controller.signal
|
|
1233
|
-
}).finally(release);
|
|
1767
|
+
}).finally(stop.release);
|
|
1234
1768
|
if (!event) {
|
|
1235
|
-
if (
|
|
1236
|
-
|
|
1769
|
+
if (controller.signal.aborted) return stop.interrupted() ? 130 : 0;
|
|
1770
|
+
throw new Failed({
|
|
1771
|
+
class: "timeout",
|
|
1772
|
+
reason: "timeout",
|
|
1773
|
+
message: `Nothing matched within ${timeout}.`
|
|
1774
|
+
});
|
|
1237
1775
|
}
|
|
1238
|
-
|
|
1776
|
+
if (flags.json) result({ event });
|
|
1777
|
+
else out(formatEvent(event));
|
|
1239
1778
|
return 0;
|
|
1240
1779
|
}
|
|
1241
1780
|
default: {
|
|
1242
1781
|
const own = app.commands[command];
|
|
1243
1782
|
if (own) return own.run(args, context());
|
|
1244
|
-
|
|
1783
|
+
const guess = closest(command, [...commands.flatMap((known) => [known.name, ...known.aliases ?? []]), ...Object.keys(app.commands)]);
|
|
1784
|
+
throw usage(`Unknown command ${command} for ${named()}.${guess ? ` Did you mean ${guess}?` : ""}`, {
|
|
1785
|
+
reason: "unknown_command",
|
|
1786
|
+
next: [{
|
|
1787
|
+
instruction: "See all commands",
|
|
1788
|
+
argv: [...cli(), "--help"]
|
|
1789
|
+
}]
|
|
1790
|
+
});
|
|
1245
1791
|
}
|
|
1246
1792
|
}
|
|
1247
1793
|
}
|
|
1248
|
-
/**
|
|
1794
|
+
/**
|
|
1795
|
+
* Aborts `controller` on the run's signal, or Ctrl-C without one. `release` stops listening, and
|
|
1796
|
+
* `interrupted` says whether Ctrl-C stopped it.
|
|
1797
|
+
*/
|
|
1249
1798
|
function stopOn(controller) {
|
|
1250
1799
|
const stop = () => controller.abort();
|
|
1251
1800
|
if (io.signal) {
|
|
1252
1801
|
if (io.signal.aborted) stop();
|
|
1253
1802
|
io.signal.addEventListener("abort", stop, { once: true });
|
|
1254
1803
|
const signal = io.signal;
|
|
1255
|
-
return
|
|
1804
|
+
return {
|
|
1805
|
+
release: () => signal.removeEventListener("abort", stop),
|
|
1806
|
+
interrupted: () => false
|
|
1807
|
+
};
|
|
1256
1808
|
}
|
|
1257
1809
|
process.once("SIGINT", stop);
|
|
1258
|
-
return
|
|
1810
|
+
return {
|
|
1811
|
+
release: () => process.off("SIGINT", stop),
|
|
1812
|
+
interrupted: () => controller.signal.aborted
|
|
1813
|
+
};
|
|
1259
1814
|
}
|
|
1260
1815
|
/** What a command an app adds gets: this run's flags, and the helpers board's own commands use. */
|
|
1261
1816
|
const context = () => ({
|
|
1262
1817
|
flags,
|
|
1263
1818
|
out,
|
|
1264
|
-
usage: (message) =>
|
|
1819
|
+
usage: (message) => usage(message),
|
|
1265
1820
|
portFlag,
|
|
1266
1821
|
connection,
|
|
1267
1822
|
client,
|
|
@@ -1269,31 +1824,46 @@ const context = () => ({
|
|
|
1269
1824
|
});
|
|
1270
1825
|
/**
|
|
1271
1826
|
* 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
|
-
*
|
|
1827
|
+
* code: 0 success, 1 error, 2 misuse, 124 when `wait` times out, 130 when Ctrl-C stops it. A
|
|
1828
|
+
* failure goes to `app.onError`, or to stderr: `error: ` and what failed, then how to fix it, or
|
|
1829
|
+
* with `--json`, one JSON document. A long-running command (`tail`, `serve`) answers once it's
|
|
1830
|
+
* stopped.
|
|
1274
1831
|
*/
|
|
1275
1832
|
async function main(argv, options) {
|
|
1276
1833
|
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);
|
|
1834
|
+
typed = argv;
|
|
1835
|
+
flags = {};
|
|
1836
|
+
positionals = [];
|
|
1837
|
+
warnings = [];
|
|
1838
|
+
reached = null;
|
|
1288
1839
|
try {
|
|
1840
|
+
parse(argv);
|
|
1841
|
+
if (flags.version) {
|
|
1842
|
+
if (flags.json) result({ version: VERSION });
|
|
1843
|
+
else out(VERSION);
|
|
1844
|
+
return 0;
|
|
1845
|
+
}
|
|
1846
|
+
if (flags.help) {
|
|
1847
|
+
out(help(positionals));
|
|
1848
|
+
return 0;
|
|
1849
|
+
}
|
|
1850
|
+
const [word, ...args] = positionals;
|
|
1851
|
+
const command = canonical(word);
|
|
1852
|
+
current = {
|
|
1853
|
+
command,
|
|
1854
|
+
args
|
|
1855
|
+
};
|
|
1289
1856
|
if (io.board) {
|
|
1290
|
-
if (flags.url || flags.handle) throw
|
|
1291
|
-
if (command !== void 0 && MACHINE.has(command)) throw
|
|
1857
|
+
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: [] });
|
|
1858
|
+
if (command !== void 0 && MACHINE.has(command)) throw usage(`${command} isn't available in-process: it's about the machine ${named()} runs on.`, {
|
|
1859
|
+
reason: "not_available",
|
|
1860
|
+
next: []
|
|
1861
|
+
});
|
|
1292
1862
|
}
|
|
1293
1863
|
return await run(command, args);
|
|
1294
1864
|
} catch (error) {
|
|
1295
|
-
|
|
1296
|
-
return error
|
|
1865
|
+
const json = flags.json ?? argv.slice(0, argv.includes("--") ? argv.indexOf("--") : void 0).includes("--json");
|
|
1866
|
+
return report(failureOf(error), json);
|
|
1297
1867
|
}
|
|
1298
1868
|
}
|
|
1299
1869
|
/** The commands about the machine the CLI runs on, not the board: none of them run in-process. */
|
|
@@ -1315,4 +1885,4 @@ async function mainWith(argv, options, with_) {
|
|
|
1315
1885
|
}
|
|
1316
1886
|
}
|
|
1317
1887
|
//#endregion
|
|
1318
|
-
export {
|
|
1888
|
+
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 };
|