@vercel/board 0.23.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/README.md +14 -19
- package/dist/{a2-NMEbIKNZ.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-CWmd4UH-.d.mts → backend-DmXjVnil.d.mts} +7 -3
- package/dist/backend-a2.d.mts +2 -2
- package/dist/backend-a2.mjs +1 -1
- package/dist/{backend-http-DQUsVgML.mjs → backend-http-BJ7AdK0c.mjs} +6 -6
- package/dist/{backend-http-BfWQfvqB.d.mts → backend-http-BsP0Xq9e.d.mts} +2 -2
- package/dist/{backend-http-D_rjJVmW.mjs → backend-http-Cs-UKpYp.mjs} +3 -3
- package/dist/backend-http.d.mts +1 -1
- package/dist/backend-http.mjs +1 -1
- package/dist/backend-http.node.d.mts +2 -2
- package/dist/backend-http.node.mjs +1 -1
- package/dist/{backend-vercel-BSuIfOyF.mjs → backend-vercel-B0Q0K0O5.mjs} +3 -3
- package/dist/{backend-vercel-CYOqtLPc.d.mts → backend-vercel-CED673bx.d.mts} +2 -2
- package/dist/{backend-vercel-BWVAihZC.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-commands.mjs +1 -1
- package/dist/cli-embedded.d.mts +57 -6
- package/dist/cli-embedded.mjs +40 -20
- package/dist/cli.mjs +44 -37
- package/dist/{client-ClznIBMg.mjs → client-BY-BCggB.mjs} +13 -7
- package/dist/{commands-D_ODblQY.mjs → commands-BtL4jFq8.mjs} +0 -30
- 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-DAVHon9y.mjs → exports-ByvYtxFT.mjs} +6 -6
- package/dist/exports-CPrIkpz3.d.mts +4 -0
- package/dist/{index-DsMPCv_K.d.mts → index-k2LfZxl-.d.mts} +3 -3
- package/dist/index.d.mts +3 -3
- package/dist/{index.default-TBybqjGV.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-B5BCEQ0l.mjs → main-BNkywL2P.mjs} +713 -189
- package/dist/{middleware-DUjyQP_a.mjs → middleware-B0J7QBq7.mjs} +1 -1
- package/dist/{node-BeKPcokJ.mjs → node-C51_xzCb.mjs} +29 -52
- package/dist/{provider-D11Dfda0.d.mts → provider-C_sj4-GL.d.mts} +10 -1
- package/dist/{serve-BC218-NQ.mjs → serve-DYQ64Q5R.mjs} +6 -5
- package/dist/{server-BxUruJxv.mjs → server-BgK1sgj7.mjs} +5 -5
- 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-DYu8WWXj.mjs → watchers-hash-C4CJg7K1.mjs} +3 -3
- package/docs/01-board/01-mounting.md +4 -5
- 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/01-quickstart.md +8 -8
- package/docs/03-guides/01-coding-agents.md +21 -22
- package/docs/03-guides/02-inspector.md +1 -1
- package/docs/03-guides/03-ci.md +2 -2
- package/docs/04-reference/01-cli.md +95 -58
- package/docs/04-reference/02-board.md +4 -5
- package/docs/04-reference/03-http.md +4 -0
- package/docs/index.md +2 -3
- package/package.json +1 -1
- package/dist/exports-ggfE9jNv.d.mts +0 -4
|
@@ -1,17 +1,17 @@
|
|
|
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-
|
|
7
|
-
import { n as
|
|
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
|
-
import {
|
|
14
|
+
import { readFile, stat, writeFile } from "node:fs/promises";
|
|
15
15
|
import { mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
16
16
|
import { homedir } from "node:os";
|
|
17
17
|
import { basename, dirname, join } from "node:path";
|
|
@@ -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,12 +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
|
-
|
|
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)
|
|
557
700
|
};
|
|
558
|
-
return Object.hasOwn(removed, word) ? removed[word] : void 0;
|
|
701
|
+
return Object.hasOwn(removed, word) ? removed[word]() : void 0;
|
|
559
702
|
}
|
|
560
703
|
/**
|
|
561
704
|
* The command `argv` runs, by its own name (`delete` for `rm`), as the parser reads it: the
|
|
@@ -582,6 +725,17 @@ let app = {
|
|
|
582
725
|
};
|
|
583
726
|
let flags = {};
|
|
584
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;
|
|
585
739
|
/** The help `words` ask for: a command's page when they name one (`read`, `identity link`), or else every command. */
|
|
586
740
|
function help(words = []) {
|
|
587
741
|
const own = Object.values(app.commands).map((command) => command.spec);
|
|
@@ -593,43 +747,115 @@ function help(words = []) {
|
|
|
593
747
|
}
|
|
594
748
|
/** How messages name this CLI: `vb` for the standalone one, as they always have, or the app's own name. */
|
|
595
749
|
const named = () => app.name === "board" ? "vb" : app.name;
|
|
596
|
-
/**
|
|
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. */
|
|
597
758
|
function parse(argv) {
|
|
598
759
|
const args = withMentions(argv);
|
|
599
|
-
const
|
|
760
|
+
const word = commandWord(args);
|
|
761
|
+
current = {
|
|
762
|
+
command: word,
|
|
763
|
+
args: []
|
|
764
|
+
};
|
|
765
|
+
const removed = removedCommand(word ?? "");
|
|
766
|
+
if (removed) throw removed;
|
|
600
767
|
try {
|
|
601
|
-
if (removed) throw new UsageError(removed);
|
|
602
768
|
const parsed = parseAll(markAttachmentId(args));
|
|
603
769
|
flags = parsed.values;
|
|
604
770
|
positionals = parsed.positionals.map((arg) => arg.startsWith(MARK) ? arg.slice(1) : arg);
|
|
605
|
-
return true;
|
|
606
771
|
} catch (error) {
|
|
607
|
-
|
|
608
|
-
io.error(terminalText(`error: ${unknown ? `unknown option: ${unknown} (run \`${app.name} help\`; text starting with - goes after --, as in -- "${unknown}")` : error.message}`) + "\n");
|
|
609
|
-
return false;
|
|
772
|
+
throw parseFailure(error, argv, word);
|
|
610
773
|
}
|
|
611
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
|
+
};
|
|
612
824
|
const TERMINAL = {
|
|
613
825
|
write: (chunk) => void process.stdout.write(chunk),
|
|
614
826
|
error: (chunk) => void process.stderr.write(chunk),
|
|
615
827
|
async readFile(path) {
|
|
616
828
|
const info = await stat(path).catch(() => null);
|
|
617
829
|
if (!info?.isFile()) return null;
|
|
618
|
-
if (info.size > 104857600) throw
|
|
830
|
+
if (info.size > 104857600) throw tooLarge(path);
|
|
619
831
|
return new Uint8Array(await readFile(path));
|
|
620
832
|
},
|
|
621
833
|
writeFile: (path, data) => writeFile(path, data),
|
|
622
|
-
stdin: readStdin
|
|
834
|
+
stdin: readStdin,
|
|
835
|
+
tty: process.stdout.isTTY === true
|
|
623
836
|
};
|
|
624
837
|
let io = TERMINAL;
|
|
625
838
|
const out = (text) => io.write(`${terminalText(text)}\n`);
|
|
626
|
-
|
|
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`);
|
|
627
846
|
/** A message's ID as you'd write it: `#812` or `812`. It's passed to the board as it is. */
|
|
628
|
-
function messageId(value, what = "message
|
|
847
|
+
function messageId(value, what = "message ID") {
|
|
629
848
|
const id = value?.replace(/^#/, "") ?? "";
|
|
630
|
-
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}.`);
|
|
631
851
|
return id;
|
|
632
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
|
+
});
|
|
633
859
|
/** Whether an argument names a message rather than a channel: `#<id>`, or a bare number. */
|
|
634
860
|
const isMessageRef = (value) => /^#[^\s/]+$/.test(value) || /^\d+$/.test(value);
|
|
635
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. */
|
|
@@ -638,13 +864,13 @@ function labelPairs(values) {
|
|
|
638
864
|
const pairs = {};
|
|
639
865
|
for (const value of values ?? []) {
|
|
640
866
|
const at = value.indexOf("=");
|
|
641
|
-
if (at <= 0) throw
|
|
867
|
+
if (at <= 0) throw usage(`Labels look like key=value, and ${value} isn't one.`);
|
|
642
868
|
pairs[value.slice(0, at)] = value.slice(at + 1);
|
|
643
869
|
}
|
|
644
870
|
return pairs;
|
|
645
871
|
}
|
|
646
872
|
async function text(value) {
|
|
647
|
-
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" });
|
|
648
874
|
if (value !== "-") return value;
|
|
649
875
|
return (await io.stdin()).replace(/\n$/, "");
|
|
650
876
|
}
|
|
@@ -653,8 +879,11 @@ async function attachments(board) {
|
|
|
653
879
|
const files = [];
|
|
654
880
|
for (const path of flags.attach ?? []) {
|
|
655
881
|
const data = await io.readFile(path);
|
|
656
|
-
if (!data) throw
|
|
657
|
-
|
|
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);
|
|
658
887
|
files.push({
|
|
659
888
|
path,
|
|
660
889
|
data
|
|
@@ -670,13 +899,14 @@ async function attachments(board) {
|
|
|
670
899
|
/** `s12` or `12`. */
|
|
671
900
|
function scheduleId(value) {
|
|
672
901
|
const id = value?.replace(/^s(?=\d)/i, "") ?? "";
|
|
673
|
-
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.`);
|
|
674
904
|
return id;
|
|
675
905
|
}
|
|
676
906
|
/** A time flag: anything `Date` reads, as ISO. */
|
|
677
907
|
function instant(flag, value) {
|
|
678
908
|
const time = new Date(value);
|
|
679
|
-
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.`);
|
|
680
910
|
return time.toISOString();
|
|
681
911
|
}
|
|
682
912
|
function filters(channel) {
|
|
@@ -700,16 +930,201 @@ function configuredUrl() {
|
|
|
700
930
|
return boardUrl();
|
|
701
931
|
} catch (error) {
|
|
702
932
|
if (!(error instanceof BoardError) || error.code !== "no_board") throw error;
|
|
703
|
-
if (app.teamBoard) return null;
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
933
|
+
if (app.teamBoard || process.env.VERCEL_ORG_ID || vercelProjectFile()) return null;
|
|
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. 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()}.`);
|
|
712
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);
|
|
713
1128
|
}
|
|
714
1129
|
/**
|
|
715
1130
|
* A team's board in Vercel's API, by the URL its discovery names it with
|
|
@@ -742,8 +1157,8 @@ function profileFlag() {
|
|
|
742
1157
|
/**
|
|
743
1158
|
* The board this command talks to, as `--as` says: a handle on it, where it is (null until its
|
|
744
1159
|
* discovery says, for a team's board), and how it reaches it. It's `--url`'s; a team's board in
|
|
745
|
-
* Vercel's API for `--scope` or `--team`; the one the environment points at; or
|
|
746
|
-
*
|
|
1160
|
+
* Vercel's API for `--scope` or `--team`; the one the environment points at; or the team's board
|
|
1161
|
+
* of the Vercel project it runs in, or, for an app with a team board, your login's.
|
|
747
1162
|
*/
|
|
748
1163
|
function connection(url = flags.url, options = {}) {
|
|
749
1164
|
const profile = profileFlag();
|
|
@@ -758,6 +1173,7 @@ function connection(url = flags.url, options = {}) {
|
|
|
758
1173
|
};
|
|
759
1174
|
}
|
|
760
1175
|
const at = configured.replace(/\/+$/, "");
|
|
1176
|
+
reached = at;
|
|
761
1177
|
const onVercel = vercelBoard(at);
|
|
762
1178
|
if (onVercel) {
|
|
763
1179
|
const board$3 = board({ backend: vercel(onVercel) });
|
|
@@ -796,24 +1212,40 @@ async function readStdin() {
|
|
|
796
1212
|
}
|
|
797
1213
|
function hookAgents(names) {
|
|
798
1214
|
const known = Object.keys(AGENTS);
|
|
799
|
-
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(", ")}.`);
|
|
800
1216
|
return names.length ? names : known;
|
|
801
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
|
+
}
|
|
802
1224
|
/** A write went through, but some handles in it matched no one: they stayed plain text. */
|
|
803
1225
|
function warnUnknownMentions(handles) {
|
|
804
1226
|
if (!handles?.length) return;
|
|
805
1227
|
const names = handles.map((handle) => `@${handle}`);
|
|
806
1228
|
const list = names.length === 1 ? names[0] : `${names.slice(0, -1).join(", ")} and ${names.at(-1)}`;
|
|
807
1229
|
const one = names.length === 1;
|
|
808
|
-
|
|
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
|
+
});
|
|
809
1234
|
}
|
|
1235
|
+
/** A post or reply: its ID, or with `--json`, the message. */
|
|
810
1236
|
function printMessage(message) {
|
|
811
|
-
if (flags.json)
|
|
1237
|
+
if (flags.json) result({
|
|
1238
|
+
message,
|
|
1239
|
+
outcome: "posted"
|
|
1240
|
+
});
|
|
812
1241
|
else out(messageRef(message.id));
|
|
813
1242
|
}
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
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
|
+
});
|
|
817
1249
|
}
|
|
818
1250
|
/** Best-effort: the URL is printed either way. */
|
|
819
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. */
|
|
@@ -841,37 +1273,9 @@ function openBrowser(url) {
|
|
|
841
1273
|
child.on("error", () => {});
|
|
842
1274
|
child.unref();
|
|
843
1275
|
}
|
|
844
|
-
/** Writes the board link: the project's `.board/` (kept out of Git), or the saved default when `root` is null. */
|
|
845
|
-
async function saveLink(root, url, extra = {}) {
|
|
846
|
-
const directory = root === null ? configDir() : join(root, ".board");
|
|
847
|
-
if (root !== null) {
|
|
848
|
-
const path = join(root, ".gitignore");
|
|
849
|
-
const ignore = await readFile(path, "utf8").catch((error) => {
|
|
850
|
-
if (error.code === "ENOENT") return "";
|
|
851
|
-
throw error;
|
|
852
|
-
});
|
|
853
|
-
if (!ignore.split(/\r?\n/).some((line) => /^\/?\.board\/?$/.test(line))) await appendFile(path, `${ignore && !ignore.endsWith("\n") ? "\n" : ""}.board/\n`);
|
|
854
|
-
}
|
|
855
|
-
await mkdir(directory, {
|
|
856
|
-
recursive: true,
|
|
857
|
-
mode: 448
|
|
858
|
-
});
|
|
859
|
-
await writeFile(join(directory, "config.json"), `${JSON.stringify({
|
|
860
|
-
url,
|
|
861
|
-
...extra
|
|
862
|
-
}, null, 2)}\n`, { mode: 384 });
|
|
863
|
-
}
|
|
864
|
-
/** Whether the project's link was written by `vb serve` (`"serve": true`), so a new run may move it. */
|
|
865
|
-
function linkedByServe(root) {
|
|
866
|
-
try {
|
|
867
|
-
return JSON.parse(readFileSync(join(root, ".board", "config.json"), "utf8")).serve === true;
|
|
868
|
-
} catch {
|
|
869
|
-
return false;
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
1276
|
function portFlag() {
|
|
873
1277
|
const port = flags.port === void 0 ? void 0 : Number(flags.port);
|
|
874
|
-
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" });
|
|
875
1279
|
return port;
|
|
876
1280
|
}
|
|
877
1281
|
/** How long `identity link` waits for the sign-in or the code. */
|
|
@@ -881,26 +1285,39 @@ async function identity(args) {
|
|
|
881
1285
|
const [sub, target] = args;
|
|
882
1286
|
if (sub === void 0) {
|
|
883
1287
|
const view = await board.identity();
|
|
884
|
-
if (flags.json) return
|
|
1288
|
+
if (flags.json) return result(view), 0;
|
|
885
1289
|
if (view.links.length === 0) out("no linked accounts");
|
|
886
1290
|
for (const link of view.links) out(`${link.handle ? `@${link.handle} ` : ""}${link.actor} by ${link.method === "code" ? "code" : "sign-in"} · ${link.at}`);
|
|
887
1291
|
if (view.unlinked?.length) out(`not linked ${view.unlinked.join(", ")} (${named()} identity link <key>)`);
|
|
888
1292
|
return 0;
|
|
889
1293
|
}
|
|
890
1294
|
if (sub === "unlink") {
|
|
891
|
-
if (!target
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
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}`);
|
|
895
1303
|
return 0;
|
|
896
1304
|
}
|
|
897
|
-
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.");
|
|
898
1308
|
const before = new Set((await board.identity()).links.map((link) => `${link.actor}|${link.at}`));
|
|
899
1309
|
const platform = (await board.discovery().catch(() => null))?.sync.find((entry) => entry.key === target)?.platform ?? target;
|
|
900
1310
|
const { code, expiresAt, signIn } = await board.syncCode(target);
|
|
901
1311
|
const consent = signIn !== void 0 && !flags.code;
|
|
902
|
-
if (consent && browsable(signIn) === null) throw new
|
|
903
|
-
|
|
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",
|
|
904
1321
|
sync: target,
|
|
905
1322
|
platform,
|
|
906
1323
|
code,
|
|
@@ -926,13 +1343,28 @@ async function identity(args) {
|
|
|
926
1343
|
await sleep();
|
|
927
1344
|
const linked = await linkedSince();
|
|
928
1345
|
if (linked) {
|
|
929
|
-
if (flags.json)
|
|
1346
|
+
if (flags.json) line({
|
|
1347
|
+
type: "linked",
|
|
1348
|
+
account: linked
|
|
1349
|
+
});
|
|
930
1350
|
else out(`linked ${linked.handle ? `@${linked.handle} ` : ""}(${linked.actor})`);
|
|
931
1351
|
return 0;
|
|
932
1352
|
}
|
|
933
1353
|
}
|
|
934
|
-
|
|
935
|
-
|
|
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
|
+
});
|
|
936
1368
|
}
|
|
937
1369
|
async function run(command, args) {
|
|
938
1370
|
switch (command) {
|
|
@@ -940,23 +1372,13 @@ async function run(command, args) {
|
|
|
940
1372
|
case "help":
|
|
941
1373
|
out(help(args));
|
|
942
1374
|
return 0;
|
|
943
|
-
case "link":
|
|
944
|
-
case "use": {
|
|
945
|
-
const url = args[0];
|
|
946
|
-
if (!url || args.length !== 1) throw new UsageError(`usage: ${app.name} ${command} <url>`);
|
|
947
|
-
const discovery = await connection(url).board.discovery();
|
|
948
|
-
const root = command === "link" ? projectDir() : null;
|
|
949
|
-
await saveLink(root, discovery.url);
|
|
950
|
-
out(root === null ? `using ${discovery.url}` : `linked ${root} to ${discovery.url}`);
|
|
951
|
-
return 0;
|
|
952
|
-
}
|
|
953
1375
|
case "whoami": {
|
|
954
1376
|
const { board, url: given, transport } = connection();
|
|
955
|
-
const me = await board.me();
|
|
956
|
-
const url = given ??
|
|
1377
|
+
const [me, discovered] = await Promise.all([board.me(), board.discovery()]);
|
|
1378
|
+
const url = given ?? discovered.url;
|
|
957
1379
|
const source = transport.source();
|
|
958
1380
|
if (flags.json) {
|
|
959
|
-
|
|
1381
|
+
result({
|
|
960
1382
|
...me,
|
|
961
1383
|
board: url,
|
|
962
1384
|
source
|
|
@@ -978,15 +1400,15 @@ async function run(command, args) {
|
|
|
978
1400
|
return 0;
|
|
979
1401
|
}
|
|
980
1402
|
case "conformance": {
|
|
981
|
-
if (args.length > 0) throw
|
|
1403
|
+
if (args.length > 0) throw usage("conformance takes no arguments: pass its channel with --channel.");
|
|
982
1404
|
const report = await conformance(client(), flags.channel ? { channel: flags.channel } : {});
|
|
983
|
-
if (flags.json)
|
|
1405
|
+
if (flags.json) result(report);
|
|
984
1406
|
else out(formatConformance(report));
|
|
985
1407
|
return report.ok ? 0 : 1;
|
|
986
1408
|
}
|
|
987
1409
|
case "channels": {
|
|
988
1410
|
const channels = await client().channels();
|
|
989
|
-
if (flags.json) return channels
|
|
1411
|
+
if (flags.json) return result({ channels }), 0;
|
|
990
1412
|
if (channels.length === 0) return out("no channels yet"), 0;
|
|
991
1413
|
let group = "";
|
|
992
1414
|
for (const channel of channels) {
|
|
@@ -1002,7 +1424,7 @@ async function run(command, args) {
|
|
|
1002
1424
|
}
|
|
1003
1425
|
case "actors": {
|
|
1004
1426
|
const actors = await client().actors();
|
|
1005
|
-
if (flags.json) return actors
|
|
1427
|
+
if (flags.json) return result({ actors }), 0;
|
|
1006
1428
|
for (const actor of actors) out(`@${actor.handle} ${actor.name} ${actor.id}${actor.person ? ` → @${actor.person.handle}` : ""}`);
|
|
1007
1429
|
return 0;
|
|
1008
1430
|
}
|
|
@@ -1013,14 +1435,17 @@ async function run(command, args) {
|
|
|
1013
1435
|
const link = target !== void 0 && /^https?:\/\//i.test(target);
|
|
1014
1436
|
if (target && (link || isMessageRef(target))) {
|
|
1015
1437
|
const thread = link ? await board.linked(target) : await board.thread(messageId(target));
|
|
1016
|
-
if (link && !Array.isArray(thread.replies)) throw
|
|
1438
|
+
if (link && !Array.isArray(thread.replies)) throw outdated("This board can't read links.");
|
|
1017
1439
|
const narrowed = Object.keys(filters()).length > 0;
|
|
1018
1440
|
const replies = narrowed ? (await board.messages({
|
|
1019
1441
|
...filters(),
|
|
1020
1442
|
re: thread.message.id,
|
|
1021
1443
|
limit: 500
|
|
1022
1444
|
})).messages.filter((m) => m.re !== null) : thread.replies;
|
|
1023
|
-
if (flags.json) return
|
|
1445
|
+
if (flags.json) return result({
|
|
1446
|
+
...thread,
|
|
1447
|
+
replies
|
|
1448
|
+
}), 0;
|
|
1024
1449
|
const authors = new Set([thread.message, ...replies].map((m) => m.by.actor));
|
|
1025
1450
|
const unnamed = reactionNames([thread.message, ...replies]).filter((id) => !authors.has(id));
|
|
1026
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();
|
|
@@ -1032,13 +1457,27 @@ async function run(command, args) {
|
|
|
1032
1457
|
...filters(target),
|
|
1033
1458
|
...flags.limit ? { limit: Number(flags.limit) } : {}
|
|
1034
1459
|
};
|
|
1035
|
-
const old =
|
|
1460
|
+
const old = outdated("This board can't list threads.");
|
|
1036
1461
|
const page = await board.threads(query).catch((error) => {
|
|
1037
1462
|
throw error instanceof BoardError && error.status === 404 ? old : error;
|
|
1038
1463
|
});
|
|
1039
1464
|
if (!Array.isArray(page.threads)) throw old;
|
|
1040
|
-
if (!page.threads.length && target
|
|
1041
|
-
|
|
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;
|
|
1042
1481
|
const count = `${page.threads.length} ${page.threads.length === 1 ? "thread" : "threads"}`;
|
|
1043
1482
|
out(`${target ?? "all channels"} · ${count} · cursor ${page.cursor}${page.threads.length ? "\n" : ""}`);
|
|
1044
1483
|
if (page.threads.length) out(formatThreadSummaries(page.threads, { channel: !target || target.endsWith("/") }));
|
|
@@ -1046,10 +1485,18 @@ async function run(command, args) {
|
|
|
1046
1485
|
}
|
|
1047
1486
|
case "post": {
|
|
1048
1487
|
const [target, body] = args;
|
|
1049
|
-
if (!target) throw
|
|
1488
|
+
if (!target) throw usage("Missing the channel or message to post to.", { reason: "missing_arguments" });
|
|
1050
1489
|
const re = isMessageRef(target) ? messageId(target) : void 0;
|
|
1051
|
-
if (re !== void 0 && flags.sync?.length) throw
|
|
1052
|
-
|
|
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.");
|
|
1053
1500
|
const board = client();
|
|
1054
1501
|
const content = body === void 0 && flags.attach?.length ? "" : await text(body);
|
|
1055
1502
|
const files = await attachments(board);
|
|
@@ -1086,16 +1533,19 @@ async function run(command, args) {
|
|
|
1086
1533
|
case "schedule": {
|
|
1087
1534
|
if (args[0] === "cancel" && args.length === 2) {
|
|
1088
1535
|
const { schedule, canceled } = await client().cancelSchedule(scheduleId(args[1]));
|
|
1089
|
-
if (flags.json)
|
|
1536
|
+
if (flags.json) result({
|
|
1537
|
+
schedule,
|
|
1538
|
+
outcome: canceled ? "canceled" : "unchanged"
|
|
1539
|
+
});
|
|
1090
1540
|
else out(canceled ? `canceled s${schedule.id}` : `s${schedule.id} is already ${schedule.state}`);
|
|
1091
1541
|
return 0;
|
|
1092
1542
|
}
|
|
1093
1543
|
const [target, body] = args;
|
|
1094
|
-
if (!target) throw
|
|
1095
|
-
if (!flags.in && !flags.at && !flags.every) throw
|
|
1096
|
-
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.");
|
|
1097
1547
|
const times = flags.times === void 0 ? void 0 : Number(flags.times);
|
|
1098
|
-
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.`);
|
|
1099
1549
|
const board = client();
|
|
1100
1550
|
const content = body === void 0 && flags.attach?.length ? "" : await text(body);
|
|
1101
1551
|
const files = await attachments(board);
|
|
@@ -1113,7 +1563,10 @@ async function run(command, args) {
|
|
|
1113
1563
|
...times !== void 0 ? { times } : {}
|
|
1114
1564
|
});
|
|
1115
1565
|
warnUnknownMentions(schedule.unknownMentions);
|
|
1116
|
-
if (flags.json)
|
|
1566
|
+
if (flags.json) result({
|
|
1567
|
+
schedule,
|
|
1568
|
+
outcome: "scheduled"
|
|
1569
|
+
});
|
|
1117
1570
|
else out(formatSchedule(schedule));
|
|
1118
1571
|
return 0;
|
|
1119
1572
|
}
|
|
@@ -1124,102 +1577,135 @@ async function run(command, args) {
|
|
|
1124
1577
|
...flags.channel ? { channel: flags.channel } : {},
|
|
1125
1578
|
...flags.all ? { all: true } : {}
|
|
1126
1579
|
});
|
|
1127
|
-
if (flags.json) return list
|
|
1580
|
+
if (flags.json) return result({ schedules: list }), 0;
|
|
1128
1581
|
if (list.length === 0) return out(flags.all ? "no schedules" : "no pending schedules"), 0;
|
|
1129
1582
|
out(list.map((schedule) => formatSchedule(schedule)).join("\n"));
|
|
1130
1583
|
return 0;
|
|
1131
1584
|
}
|
|
1132
1585
|
case "hooks": {
|
|
1133
1586
|
const [action, ...names] = args;
|
|
1134
|
-
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
|
+
});
|
|
1135
1598
|
const agents = hookAgents(names);
|
|
1136
1599
|
const width = Math.max(...agents.map((agent) => AGENTS[agent].name.length));
|
|
1600
|
+
const hooks = [];
|
|
1137
1601
|
for (const agent of agents) {
|
|
1138
1602
|
const installed = installedHook(agent);
|
|
1139
|
-
const
|
|
1140
|
-
const rules =
|
|
1603
|
+
const by = installed ? cliOf(installed) : null;
|
|
1604
|
+
const rules = by ? allowRules(agent, by) : null;
|
|
1141
1605
|
if (rules) disallow(rules.file, rules.rules);
|
|
1142
|
-
|
|
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([
|
|
1143
1613
|
AGENTS[agent].name.padEnd(width),
|
|
1144
|
-
|
|
1614
|
+
removed ? "removed" : "not installed",
|
|
1145
1615
|
installed ? tilde(AGENTS[agent].file()) : ""
|
|
1146
1616
|
].join(" ").trimEnd());
|
|
1147
1617
|
}
|
|
1618
|
+
if (flags.json) result({ hooks });
|
|
1148
1619
|
return 0;
|
|
1149
1620
|
}
|
|
1150
1621
|
case "hook": return 0;
|
|
1151
1622
|
case "edit": {
|
|
1152
|
-
const
|
|
1623
|
+
const id = messageId(args[0]);
|
|
1624
|
+
const { unknownMentions } = await client().edit(id, await text(args[1]));
|
|
1153
1625
|
warnUnknownMentions(unknownMentions);
|
|
1626
|
+
printDone(id, "edited");
|
|
1154
1627
|
return 0;
|
|
1155
1628
|
}
|
|
1156
|
-
case "delete":
|
|
1157
|
-
|
|
1629
|
+
case "delete": {
|
|
1630
|
+
const id = messageId(args[0]);
|
|
1631
|
+
await client().delete(id);
|
|
1632
|
+
printDone(id, "deleted");
|
|
1158
1633
|
return 0;
|
|
1634
|
+
}
|
|
1159
1635
|
case "react": {
|
|
1160
1636
|
const id = messageId(args[0]);
|
|
1161
|
-
if (!args[1]) throw
|
|
1637
|
+
if (!args[1]) throw usage("Missing the emoji to react with, like eyes or thumbs_up.", { reason: "missing_arguments" });
|
|
1162
1638
|
let emoji;
|
|
1163
1639
|
try {
|
|
1164
1640
|
emoji = normalizeEmoji(args[1]);
|
|
1165
|
-
} catch
|
|
1166
|
-
throw
|
|
1641
|
+
} catch {
|
|
1642
|
+
throw usage(`Unknown emoji ${args[1]}. Use a short name, like eyes or thumbs_up.`);
|
|
1167
1643
|
}
|
|
1168
1644
|
await client().react(id, emoji, flags.remove ? { remove: true } : {});
|
|
1645
|
+
printDone(id, flags.remove ? "unreacted" : "reacted");
|
|
1169
1646
|
return 0;
|
|
1170
1647
|
}
|
|
1171
1648
|
case "label": {
|
|
1172
1649
|
const id = messageId(args[0]);
|
|
1173
1650
|
const set = labelPairs(args.slice(1));
|
|
1174
1651
|
const unset = flags.unset ?? [];
|
|
1175
|
-
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" });
|
|
1176
1653
|
await client().label(id, {
|
|
1177
1654
|
set,
|
|
1178
1655
|
unset
|
|
1179
1656
|
});
|
|
1657
|
+
printDone(id, "labeled");
|
|
1180
1658
|
return 0;
|
|
1181
1659
|
}
|
|
1182
1660
|
case "typing": {
|
|
1183
1661
|
const id = messageId(args[0]);
|
|
1184
1662
|
const status = args.slice(1).join(" ").trim();
|
|
1185
1663
|
const targets = await client().typing(id, status || void 0);
|
|
1186
|
-
if (flags.json)
|
|
1664
|
+
if (flags.json) result({ targets });
|
|
1187
1665
|
return 0;
|
|
1188
1666
|
}
|
|
1189
1667
|
case "sync": {
|
|
1190
1668
|
const id = messageId(args[0]);
|
|
1191
1669
|
if (!args[1]) {
|
|
1192
1670
|
const { targets } = await client().sync(id);
|
|
1193
|
-
if (flags.json) return targets
|
|
1671
|
+
if (flags.json) return result({ targets }), 0;
|
|
1194
1672
|
out(targets.length ? formatSynced(targets) : "not synced anywhere");
|
|
1195
1673
|
return 0;
|
|
1196
1674
|
}
|
|
1197
|
-
const
|
|
1198
|
-
if (flags.json)
|
|
1199
|
-
|
|
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}`);
|
|
1200
1681
|
return 0;
|
|
1201
1682
|
}
|
|
1202
1683
|
case "download": {
|
|
1203
1684
|
const id = args[0];
|
|
1204
|
-
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" });
|
|
1205
1686
|
const response = await client().download(id);
|
|
1206
1687
|
const data = new Uint8Array(await response.arrayBuffer());
|
|
1207
1688
|
if (args[1] === "-") return io.write(data), 0;
|
|
1208
1689
|
const disposition = response.headers.get("content-disposition") ?? "";
|
|
1209
|
-
const
|
|
1210
|
-
const path = args[1] ?? basename(
|
|
1690
|
+
const filename = /filename\*=UTF-8''([^;]+)/.exec(disposition)?.[1];
|
|
1691
|
+
const path = args[1] ?? basename(filename ? decodeURIComponent(filename) : id);
|
|
1211
1692
|
await io.writeFile(path, data);
|
|
1212
|
-
|
|
1693
|
+
if (flags.json) result({
|
|
1694
|
+
attachment: { id },
|
|
1695
|
+
path,
|
|
1696
|
+
outcome: "downloaded"
|
|
1697
|
+
});
|
|
1698
|
+
else out(path);
|
|
1213
1699
|
return 0;
|
|
1214
1700
|
}
|
|
1215
1701
|
case "inbox": {
|
|
1216
1702
|
const key = args[0];
|
|
1217
|
-
if (!key) throw
|
|
1703
|
+
if (!key) throw usage("Missing the sync entry whose inbox to read, such as slack.", { reason: "missing_arguments" });
|
|
1218
1704
|
const events = await client().inbox(key, {
|
|
1219
1705
|
...args[1] ? { channel: args[1] } : {},
|
|
1220
1706
|
...flags.limit ? { limit: Number(flags.limit) } : {}
|
|
1221
1707
|
});
|
|
1222
|
-
if (flags.json) return events
|
|
1708
|
+
if (flags.json) return result({ events }), 0;
|
|
1223
1709
|
if (events.length === 0) return out("no events"), 0;
|
|
1224
1710
|
for (const entry of events) {
|
|
1225
1711
|
const event = entry.event;
|
|
@@ -1232,113 +1718,151 @@ async function run(command, args) {
|
|
|
1232
1718
|
const [key, sub, target] = args;
|
|
1233
1719
|
if (!key) {
|
|
1234
1720
|
const { sync } = await client().discovery();
|
|
1235
|
-
if (flags.json) return sync
|
|
1721
|
+
if (flags.json) return result({ sources: sync }), 0;
|
|
1236
1722
|
if (sync.length === 0) return out("no sources: this board has no sync configured"), 0;
|
|
1237
1723
|
for (const entry of sync) out(`${entry.key} ${entry.platform}`);
|
|
1238
1724
|
return 0;
|
|
1239
1725
|
}
|
|
1240
1726
|
if (sub === "join" && target) {
|
|
1241
1727
|
const joined = await client().joinSource(key, target);
|
|
1242
|
-
if (flags.json) return
|
|
1728
|
+
if (flags.json) return result({
|
|
1729
|
+
...joined,
|
|
1730
|
+
outcome: joined.joined ? "joined" : "unchanged"
|
|
1731
|
+
}), 0;
|
|
1243
1732
|
const label = joined.name ? `#${joined.name}` : joined.channel;
|
|
1244
1733
|
return out(joined.joined ? `joined ${label}` : `already in ${label}`), 0;
|
|
1245
1734
|
}
|
|
1246
|
-
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" });
|
|
1247
1737
|
const read = await client().readSource(key, target, flags.limit ? { limit: Number(flags.limit) } : {});
|
|
1248
|
-
if (flags.json) return
|
|
1738
|
+
if (flags.json) return result(read), 0;
|
|
1249
1739
|
return out(formatSourceRead(key, read, Date.now(), named())), 0;
|
|
1250
1740
|
}
|
|
1251
1741
|
case "tail": {
|
|
1252
1742
|
const controller = new AbortController();
|
|
1253
|
-
const
|
|
1743
|
+
const stop = stopOn(controller);
|
|
1254
1744
|
try {
|
|
1255
1745
|
for await (const event of client().events({
|
|
1256
1746
|
...filters(args[0]),
|
|
1257
1747
|
...flags.all ? { all: true } : {}
|
|
1258
|
-
}, { signal: controller.signal }))
|
|
1748
|
+
}, { signal: controller.signal })) if (flags.json) line(event);
|
|
1749
|
+
else out(formatEvent(event));
|
|
1259
1750
|
} finally {
|
|
1260
|
-
release();
|
|
1751
|
+
stop.release();
|
|
1261
1752
|
}
|
|
1262
|
-
return 0;
|
|
1753
|
+
return stop.interrupted() ? 130 : 0;
|
|
1263
1754
|
}
|
|
1264
1755
|
case "wait": {
|
|
1265
1756
|
const controller = new AbortController();
|
|
1266
|
-
const
|
|
1757
|
+
const stop = stopOn(controller);
|
|
1758
|
+
const timeout = flags.timeout ?? "30m";
|
|
1267
1759
|
const event = await client().wait(filters(args[0]), {
|
|
1268
|
-
timeout
|
|
1760
|
+
timeout,
|
|
1269
1761
|
signal: controller.signal
|
|
1270
|
-
}).finally(release);
|
|
1762
|
+
}).finally(stop.release);
|
|
1271
1763
|
if (!event) {
|
|
1272
|
-
if (
|
|
1273
|
-
|
|
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
|
+
});
|
|
1274
1770
|
}
|
|
1275
|
-
|
|
1771
|
+
if (flags.json) result({ event });
|
|
1772
|
+
else out(formatEvent(event));
|
|
1276
1773
|
return 0;
|
|
1277
1774
|
}
|
|
1278
1775
|
default: {
|
|
1279
1776
|
const own = app.commands[command];
|
|
1280
1777
|
if (own) return own.run(args, context());
|
|
1281
|
-
|
|
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
|
+
});
|
|
1282
1786
|
}
|
|
1283
1787
|
}
|
|
1284
1788
|
}
|
|
1285
|
-
/**
|
|
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
|
+
*/
|
|
1286
1793
|
function stopOn(controller) {
|
|
1287
1794
|
const stop = () => controller.abort();
|
|
1288
1795
|
if (io.signal) {
|
|
1289
1796
|
if (io.signal.aborted) stop();
|
|
1290
1797
|
io.signal.addEventListener("abort", stop, { once: true });
|
|
1291
1798
|
const signal = io.signal;
|
|
1292
|
-
return
|
|
1799
|
+
return {
|
|
1800
|
+
release: () => signal.removeEventListener("abort", stop),
|
|
1801
|
+
interrupted: () => false
|
|
1802
|
+
};
|
|
1293
1803
|
}
|
|
1294
1804
|
process.once("SIGINT", stop);
|
|
1295
|
-
return
|
|
1805
|
+
return {
|
|
1806
|
+
release: () => process.off("SIGINT", stop),
|
|
1807
|
+
interrupted: () => controller.signal.aborted
|
|
1808
|
+
};
|
|
1296
1809
|
}
|
|
1297
1810
|
/** What a command an app adds gets: this run's flags, and the helpers board's own commands use. */
|
|
1298
1811
|
const context = () => ({
|
|
1299
1812
|
flags,
|
|
1300
1813
|
out,
|
|
1301
|
-
usage: (message) =>
|
|
1814
|
+
usage: (message) => usage(message),
|
|
1302
1815
|
portFlag,
|
|
1303
1816
|
connection,
|
|
1304
1817
|
client,
|
|
1305
|
-
openBrowser
|
|
1306
|
-
saveLink,
|
|
1307
|
-
linkedByServe
|
|
1818
|
+
openBrowser
|
|
1308
1819
|
});
|
|
1309
1820
|
/**
|
|
1310
1821
|
* Runs board's CLI on `argv` (what follows the command name) as `app`, and answers its exit
|
|
1311
|
-
* code: 0 success, 1 error, 2 misuse, 124 when `wait` times out
|
|
1312
|
-
*
|
|
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.
|
|
1313
1826
|
*/
|
|
1314
1827
|
async function main(argv, options) {
|
|
1315
1828
|
app = options;
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
if (flags.help) {
|
|
1322
|
-
out(help(positionals));
|
|
1323
|
-
return 0;
|
|
1324
|
-
}
|
|
1325
|
-
const [word, ...args] = positionals;
|
|
1326
|
-
const command = canonical(word);
|
|
1829
|
+
typed = argv;
|
|
1830
|
+
flags = {};
|
|
1831
|
+
positionals = [];
|
|
1832
|
+
warnings = [];
|
|
1833
|
+
reached = null;
|
|
1327
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
|
+
};
|
|
1328
1851
|
if (io.board) {
|
|
1329
|
-
if (flags.url || flags.handle) throw
|
|
1330
|
-
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
|
+
});
|
|
1331
1857
|
}
|
|
1332
1858
|
return await run(command, args);
|
|
1333
1859
|
} catch (error) {
|
|
1334
|
-
|
|
1335
|
-
return error
|
|
1860
|
+
const json = flags.json ?? argv.slice(0, argv.includes("--") ? argv.indexOf("--") : void 0).includes("--json");
|
|
1861
|
+
return report(failureOf(error), json);
|
|
1336
1862
|
}
|
|
1337
1863
|
}
|
|
1338
1864
|
/** The commands about the machine the CLI runs on, not the board: none of them run in-process. */
|
|
1339
1865
|
const MACHINE = /* @__PURE__ */ new Set([
|
|
1340
|
-
"link",
|
|
1341
|
-
"use",
|
|
1342
1866
|
"whoami",
|
|
1343
1867
|
"identity",
|
|
1344
1868
|
"hooks",
|
|
@@ -1356,4 +1880,4 @@ async function mainWith(argv, options, with_) {
|
|
|
1356
1880
|
}
|
|
1357
1881
|
}
|
|
1358
1882
|
//#endregion
|
|
1359
|
-
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 };
|