@vercel/board 0.24.0 → 0.25.0

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