@vercel/board 0.23.0 → 0.25.0

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