@vercel/board 0.24.0 → 0.25.1

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