@vercel/board 0.21.0 → 0.22.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.
@@ -1,13 +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 { a as withMentions, f as VERSION, i as FILTER_FLAGS } from "./watchers-hash-9sxfL1Gd.mjs";
3
+ import { d as VERSION, i as withMentions } from "./watchers-hash-D_VYDwvu.mjs";
4
4
  import { n as displayEmoji, r as normalizeEmoji } from "./emoji-CTLpvH-A.mjs";
5
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";
6
7
  import { n as configDir, o as projectDir, r as connect, t as boardUrl } from "./node-BeKPcokJ.mjs";
7
- import { t as http } from "./backend-http-Dzb4lpMG.mjs";
8
- import { t as board } from "./server-Du0rSpcW.mjs";
8
+ import { t as http } from "./backend-http-DM1T_CP6.mjs";
9
+ import { t as board } from "./server-B8nEnlMa.mjs";
9
10
  import { t as conformance } from "./conformance-DQ9qVzWP.mjs";
10
- import { n as vercel, t as credentialSource } from "./backend-vercel-BjUpoLR9.mjs";
11
+ import { n as vercel, t as credentialSource } from "./backend-vercel-IEtvtvuH.mjs";
11
12
  import { t as displayText } from "./bots-CGkO76qH.mjs";
12
13
  import { spawn } from "node:child_process";
13
14
  import { appendFile, mkdir, readFile, stat, writeFile } from "node:fs/promises";
@@ -137,9 +138,11 @@ function formatThreadSummaries(threads, options = {}) {
137
138
  const where = options.channel ? `${message.channel} ` : "";
138
139
  const head = `#${message.id} ${where}${authorName(message)} · ${ago(message.createdAt, now)}${labels(message)}`;
139
140
  const activity = last ? `${replies} ${replies === 1 ? "reply" : "replies"} · last #${last.id} ${authorName(last)} · ${ago(last.createdAt, now)}` : "no replies";
141
+ const counted = Object.entries(message.reactions).filter(([, actors]) => actors.length > 0).map(([emoji, actors]) => `${displayEmoji(emoji)} ${actors.length}`).join(" ");
140
142
  return [
141
143
  head,
142
144
  ` ${firstLine(message)}`,
145
+ ...counted ? [` ${counted}`] : [],
143
146
  ` ${activity}`
144
147
  ].join("\n");
145
148
  }).join("\n\n");
@@ -409,119 +412,121 @@ function disallow(file, rules) {
409
412
  return true;
410
413
  }
411
414
  //#endregion
415
+ //#region src/cli/help.ts
416
+ /** The widest a row's subject pads to before its value moves to the next line. */
417
+ const SUBJECT_MAX = 34;
418
+ /** Help fits 80 columns: a value wraps under its column. */
419
+ const COLUMNS = 80;
420
+ /** The padding `entries`' subjects line up at: the widest, up to `SUBJECT_MAX`. */
421
+ const padOf = (entries) => Math.min(SUBJECT_MAX, Math.max(0, ...entries.map(([subject]) => subject.length)));
422
+ /**
423
+ * `text` in lines of at most `width`, split where `gap` separates its words (two spaces, for a
424
+ * list of flags); a word longer than that keeps its own line.
425
+ */
426
+ function wrap(text, width, gap = " ") {
427
+ const lines = [];
428
+ let line = "";
429
+ for (const word of text.split(gap)) if (line && line.length + gap.length + word.length > width) {
430
+ lines.push(line);
431
+ line = word;
432
+ } else line = line ? `${line}${gap}${word}` : word;
433
+ return line ? [...lines, line] : lines;
434
+ }
435
+ /** A line headed `label` (`filters:`), its value wrapped at 80 columns under where it starts. */
436
+ const labeled = (label, value, gap = " ") => {
437
+ const indent = " ".repeat(10);
438
+ return wrap(value, COLUMNS - indent.length, gap).map((line, index) => `${index === 0 ? label.padEnd(indent.length) : indent}${line}`);
439
+ };
440
+ /** Rows as columns: each subject padded to `pad`, then two spaces and its value, wrapped under it. */
441
+ function rows(entries, pad = padOf(entries)) {
442
+ const indent = " ".repeat(2 + pad + 2);
443
+ return entries.map(([subject, value]) => {
444
+ const [first = "", ...rest] = wrap(value, Math.max(20, COLUMNS - indent.length));
445
+ return [subject.length > pad ? ` ${subject}\n${indent}${first}` : ` ${subject.padEnd(pad)} ${first}`.trimEnd(), ...rest.map((line) => `${indent}${line}`)].join("\n");
446
+ });
447
+ }
448
+ /** An option as help writes it: `--limit <N>`, `-h, --help`, `--label <k[=v|w]>...`. */
449
+ const flag = (option) => `${option.short ? `-${option.short}, ` : ""}--${option.name}${option.value ? option.optionalValue ? ` [${option.value}]` : ` <${option.value}>` : ""}${option.multiple ? "..." : ""}`;
450
+ const shown = (list) => list.filter((option) => !option.hidden);
451
+ /** A command's rows in the list: itself, when it runs alone, and each of its subcommands. */
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])];
454
+ }
455
+ /** Every command, by group, then what the app adds (`serve`, `inspect`), and the options they share. */
456
+ function rootHelp(app, version) {
457
+ const sections = [...groups.map((group) => commands.filter((command) => command.group === group)), app.commands].map((list) => list.filter((command) => !command.hidden));
458
+ const lines = [
459
+ `${app.name} ${version}: a message board you mount in one route`,
460
+ "",
461
+ `usage: ${app.name} <command> [options]${app.name === "board" ? " (also installed as `vb`)" : ""}`
462
+ ];
463
+ const listedSections = sections.filter((list) => list.length > 0).map((list) => list.flatMap(listed));
464
+ const pad = padOf(listedSections.flat());
465
+ for (const section of listedSections) lines.push("", ...rows(section, pad));
466
+ const everywhere = shown(globalOptions).filter((option) => option.name !== "help" && option.name !== "version");
467
+ lines.push("", ...labeled("filters:", shown(filters$1).map(flag).join(" "), " "), ...labeled("", "labels match a message or its thread's root; repeated flags all must match"), ...labeled("global:", everywhere.map(flag).join(" "), " "), ...labeled("json:", "one object per line (JSONL): { message, replies, last } for read <channel>, a message for read <id>"), ...labeled("text:", "pass \"-\" to read it from stdin; it may be left out when you --attach"), ...labeled("env:", "BOARD_URL BOARD_TOKEN BOARD_AS BOARD_AVATAR BOARD_HANDLE", " "), "", `a command's own help: ${app.name} <command> --help`);
468
+ return lines.join("\n");
469
+ }
470
+ /** One command's page: its usage, what it does, and its arguments, subcommands, options, and examples. */
471
+ function commandHelp(name, path) {
472
+ const command = path.at(-1);
473
+ const options = shown(command.options);
474
+ const subcommands = (command.subcommands ?? []).filter((sub) => !sub.hidden);
475
+ const lines = [
476
+ `usage: ${name} ${usage(path)}${options.length || command.filters ? " [options]" : ""}`,
477
+ "",
478
+ ...wrap(command.description, COLUMNS)
479
+ ];
480
+ if (command.aliases?.length) lines.push("", `also: ${command.aliases.map((alias) => `${name} ${alias}`).join(", ")}`);
481
+ const section = (title, entries) => {
482
+ if (entries.length) lines.push("", `${title}:`, ...rows(entries));
483
+ };
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]));
486
+ section("options", options.map((option) => [flag(option), option.description]));
487
+ if (command.filters) {
488
+ section("filters", shown(filters$1).map((option) => [flag(option), option.description]));
489
+ lines.push(" labels match a message or its thread's root; repeated flags all must match");
490
+ }
491
+ if (command.examples.length) lines.push("", "examples:", ...command.examples.flatMap((example, index) => [
492
+ ...index ? [""] : [],
493
+ ` ${example.name}`,
494
+ ` ${name} ${example.value}`
495
+ ]));
496
+ lines.push("", `options every command takes: ${name} help`);
497
+ return lines.join("\n");
498
+ }
499
+ /**
500
+ * The command `words` name, and its subcommand when they name one (`identity link`, `sources slack
501
+ * read`): what `--help` shows the page of. Null when the first word names no command.
502
+ */
503
+ function commandPath(words, all) {
504
+ const command = all.find((candidate) => candidate.name === words[0] || candidate.aliases?.includes(words[0]));
505
+ if (!command) return null;
506
+ const at = command.argumentsBeforeSubcommand ? command.arguments.length + 1 : 1;
507
+ const sub = command.subcommands?.find((candidate) => candidate.name === words[at]);
508
+ return sub ? [command, sub] : [command];
509
+ }
510
+ //#endregion
412
511
  //#region src/cli/main.ts
413
- const help = () => `${app.name} ${VERSION}: a message board you mount in one route
414
-
415
- usage: ${app.name} <command> [options]${app.name === "board" ? " (also installed as `vb`)" : ""}
416
-
417
- whoami who am I, on which board, with which token, and what I can do
418
- link <url> link this project to a board (.board/config.json)
419
- use <url> save the default board
420
-
421
- channels channels, grouped by path prefix
422
- actors actors and their handles
423
- identity your linked Slack and GitHub accounts
424
- identity link <key> [--code] [--no-wait] link your account on sync entry <key> (e.g. slack): sign in, or post a code from it
425
- identity unlink <account> unlink one, by handle or actor ID
426
-
427
- read [channel] [filters] [--limit N] the latest threads, one each: root, replies, latest reply
428
- (no channel: every channel)
429
- read <id> one thread, every message (filters: the root and the replies that match)
430
- read <link> the thread a Slack or GitHub link names, if the board synced it
431
-
432
- post <channel|id> ["<text>"] [--label k=v]... [--attach <file>]... [--sync <target>]... [--no-sync] [--id <key>]
433
- a new thread in a channel, or with an id, a reply in its thread
434
- reply <id> ["<text>"] [--attach <file>]... [--id <key>] [--no-sync]
435
- schedule <channel|id> ["<text>"] (--in 1d | --at <time> | --every 30m) [--until <time> | --for 24h | --times N]
436
- [--label k=v]... [--attach <file>]... [--no-sync]
437
- post later, as you: once, or repeating until a stop or until canceled
438
- schedules [id] [--by <handle>] [--channel <c>] [--all]
439
- pending schedules (a thread's, with an id); --all adds done and canceled
440
- schedule cancel <schedule> stop one, e.g. schedule cancel s12 (yours, or with manage)
441
- edit <id> "<text>"
442
- delete <id>
443
- react <id> <emoji> [--remove]
444
- label <id> k=v... [--unset k]...
445
- typing <id> [status] show typing in the thread's synced platform threads (marks WhatsApp read);
446
- Slack shows the status, if given, instead of "Typing…"
447
- sync <id> where the thread is synced, with this message's link on each
448
- sync <id> <target> attach the thread to e.g. slack:#incidents, backfilling it
449
- inbox <key> [channel] [--limit N] raw platform events, for debugging sync
450
- sources the board's sources (its sync entries: slack, github, …)
451
- sources <key> read <channel|thread|link> [--limit N]
452
- read a source's channel, thread, or message link without syncing it,
453
- e.g. sources slack read '#general', or a Slack permalink (full access only)
454
- sources <key> join <channel|link> add the app to a public channel so it can be read, where the board
455
- opted in (slack({ join: true })); full access only
456
- download <attachment> [path] save an attachment (its name, here, by default; - for stdout)
457
- conformance [--channel <c>] check that the board speaks the protocol: posts to conformance,
458
- kept on the board, and deletes what it posted (1 if a check fails)
459
-
460
- hooks uninstall [claude|codex|cursor|pi]...
461
- remove the turn-end hooks an earlier board installed
462
-
463
- tail [channel] [filters] [--all] live, until Ctrl-C
464
- wait [channel] [filters] [--timeout 30m] block for the next match, print it, then exit (124 on timeout)${Object.keys(app.commands).length > 0 ? `\n\n${Object.values(app.commands).map((command) => command.help).join("\n")}` : ""}
465
-
466
- filters: --re <id> --by <handle> --from <source> --label k[=v|w] --text <words> --mentions [@handle] --after <id>
467
- labels match a message or its thread's root; repeated flags all must match
468
- global: --url <url> --scope <team> --as <profile> --avatar <url> --human --handle <h> --json
469
- json: one object per line (JSONL): { message, replies, last } for read <channel>, a message for read <id>
470
- text: pass "-" to read it from stdin; it may be left out when you --attach
471
- env: BOARD_URL BOARD_TOKEN BOARD_AS BOARD_AVATAR BOARD_HANDLE`;
472
512
  var UsageError = class extends Error {};
473
- const OPTIONS = {
474
- ...FILTER_FLAGS,
475
- url: { type: "string" },
476
- as: { type: "string" },
477
- avatar: { type: "string" },
478
- human: { type: "boolean" },
479
- handle: { type: "string" },
480
- json: { type: "boolean" },
481
- scope: { type: "string" },
482
- team: { type: "string" },
483
- help: {
484
- type: "boolean",
485
- short: "h"
486
- },
487
- version: {
488
- type: "boolean",
489
- short: "v"
490
- },
491
- unset: {
492
- type: "string",
493
- multiple: true
494
- },
495
- id: { type: "string" },
496
- "no-sync": { type: "boolean" },
497
- sync: {
498
- type: "string",
499
- multiple: true
500
- },
501
- attach: {
502
- type: "string",
503
- multiple: true
504
- },
505
- channel: { type: "string" },
506
- agent: { type: "string" },
507
- remove: { type: "boolean" },
508
- after: { type: "string" },
509
- limit: { type: "string" },
510
- all: { type: "boolean" },
511
- timeout: { type: "string" },
512
- port: { type: "string" },
513
- "no-open": { type: "boolean" },
514
- "no-inspect": { type: "boolean" },
515
- dir: { type: "string" },
516
- code: { type: "boolean" },
517
- "no-wait": { type: "boolean" },
518
- in: { type: "string" },
519
- at: { type: "string" },
520
- every: { type: "string" },
521
- until: { type: "string" },
522
- for: { type: "string" },
523
- times: { type: "string" }
524
- };
513
+ /** What the parser accepts: every option, by name (`commands.ts`). */
514
+ const OPTIONS = options;
515
+ /** Options by their short letter: `n` is `limit`. */
516
+ const SHORT = Object.fromEntries(Object.entries(OPTIONS).flatMap(([name, option]) => "short" in option ? [[option.short, name]] : []));
517
+ /**
518
+ * Whether `arg` is a string option written apart from its value, whose value is the next argument:
519
+ * `--limit 5`, `-n 5`, or the last of a group of short ones, `-an 5`.
520
+ */
521
+ function takesValue(arg) {
522
+ if (arg.includes("=")) return false;
523
+ const string = (name) => name !== void 0 && OPTIONS[name]?.type === "string";
524
+ if (arg.startsWith("--")) return string(arg.slice(2));
525
+ const letters = arg.slice(1);
526
+ return [...letters].findIndex((letter) => string(SHORT[letter])) === letters.length - 1;
527
+ }
528
+ /** A command's own name for `word`, which may be one of its aliases (`rm` is `delete`). */
529
+ const canonical = (word) => word === void 0 ? word : commands.find((command) => command.aliases?.includes(word))?.name ?? word;
525
530
  const MARK = "\0";
526
531
  /**
527
532
  * Attachment IDs are base64url, so boards minted some that start with `-`,
@@ -539,7 +544,7 @@ function markAttachmentId(args) {
539
544
  `${MARK}${arg}`,
540
545
  ...args.slice(i + 1)
541
546
  ];
542
- else if (!arg.includes("=") && OPTIONS[arg.slice(2)]?.type === "string") i++;
547
+ else if (takesValue(arg)) i++;
543
548
  }
544
549
  return args;
545
550
  }
@@ -552,15 +557,19 @@ function removedCommand(word) {
552
557
  };
553
558
  return Object.hasOwn(removed, word) ? removed[word] : void 0;
554
559
  }
555
- /** The command word: the first argument that isn't a flag or a string flag's value (a bare `--mentions` takes none). */
560
+ /**
561
+ * The command `argv` runs, by its own name (`delete` for `rm`), as the parser reads it: the
562
+ * first positional, past every flag and value however they're written (`-an 1`, `--limit=1`). A
563
+ * bare `--mentions` takes none.
564
+ */
556
565
  function commandWord(argv) {
557
- const args = withMentions(argv);
558
- for (let i = 0; i < args.length; i++) {
559
- const arg = args[i];
560
- if (arg === "--") return args[i + 1];
561
- if (!arg.startsWith("-") || arg === "-") return arg;
562
- if (!arg.includes("=") && OPTIONS[arg.slice(2)]?.type === "string") i++;
563
- }
566
+ const { positionals } = parseArgs({
567
+ args: markAttachmentId(withMentions(argv)),
568
+ allowPositionals: true,
569
+ options: OPTIONS,
570
+ strict: false
571
+ });
572
+ return canonical(positionals[0]);
564
573
  }
565
574
  const parseAll = (args) => parseArgs({
566
575
  args,
@@ -573,6 +582,15 @@ let app = {
573
582
  };
574
583
  let flags = {};
575
584
  let positionals = [];
585
+ /** The help `words` ask for: a command's page when they name one (`read`, `identity link`), or else every command. */
586
+ function help(words = []) {
587
+ const own = Object.values(app.commands).map((command) => command.spec);
588
+ const path = commandPath(words, [...commands, ...own]);
589
+ return path ? commandHelp(app.name, path) : rootHelp({
590
+ name: app.name,
591
+ commands: own
592
+ }, VERSION);
593
+ }
576
594
  /** How messages name this CLI: `vb` for the standalone one, as they always have, or the app's own name. */
577
595
  const named = () => app.name === "board" ? "vb" : app.name;
578
596
  /** Parses `argv` for this run: false, once the error line is out, when it doesn't. */
@@ -684,6 +702,7 @@ function configuredUrl() {
684
702
  if (!(error instanceof BoardError) || error.code !== "no_board") throw error;
685
703
  if (app.teamBoard) return null;
686
704
  const ways = [
705
+ "pass --scope <team> for a team's board in Vercel's API",
687
706
  `run \`${app.name} link <url>\``,
688
707
  "set BOARD_URL",
689
708
  `run \`${app.name} use <url>\``,
@@ -692,6 +711,25 @@ function configuredUrl() {
692
711
  throw new BoardError("no_board", `no board URL: ${ways.slice(0, -1).join(", ")}, or ${ways.at(-1)}`, 0);
693
712
  }
694
713
  }
714
+ /**
715
+ * A team's board in Vercel's API, by the URL its discovery names it with
716
+ * (`https://api.vercel.com/v1/messageboard/boards/<board>?teamId=<team>`): its board and team.
717
+ */
718
+ function vercelBoard(url) {
719
+ let parsed;
720
+ try {
721
+ parsed = new URL(url);
722
+ } catch {
723
+ return null;
724
+ }
725
+ const found = /^\/v1\/messageboard\/boards\/([^/]+)\/?$/.exec(parsed.pathname);
726
+ if (parsed.origin !== "https://api.vercel.com" || !found) return null;
727
+ const team = parsed.searchParams.get("teamId");
728
+ return {
729
+ board: decodeURIComponent(found[1]),
730
+ ...team ? { team } : {}
731
+ };
732
+ }
695
733
  /** Who the handle writes as: `--as`'s profile (with `--avatar`), yourself with `--human`, or what the environment says. */
696
734
  function profileFlag() {
697
735
  if (flags.human) return null;
@@ -720,6 +758,15 @@ function connection(url = flags.url, options = {}) {
720
758
  };
721
759
  }
722
760
  const at = configured.replace(/\/+$/, "");
761
+ const onVercel = vercelBoard(at);
762
+ if (onVercel) {
763
+ const board$3 = board({ backend: vercel(onVercel) });
764
+ return {
765
+ url: at,
766
+ transport: { source: credentialSource },
767
+ board: profile !== void 0 ? board$3.as(profile) : board$3
768
+ };
769
+ }
723
770
  const handle = options.handle ?? flags.handle;
724
771
  const transport = connect(at, {
725
772
  ...options.token !== void 0 ? { token: options.token } : {},
@@ -891,7 +938,7 @@ async function run(command, args) {
891
938
  switch (command) {
892
939
  case void 0:
893
940
  case "help":
894
- out(help());
941
+ out(help(args));
895
942
  return 0;
896
943
  case "link":
897
944
  case "use": {
@@ -1272,10 +1319,11 @@ async function main(argv, options) {
1272
1319
  return 0;
1273
1320
  }
1274
1321
  if (flags.help) {
1275
- out(help());
1322
+ out(help(positionals));
1276
1323
  return 0;
1277
1324
  }
1278
- const [command, ...args] = positionals;
1325
+ const [word, ...args] = positionals;
1326
+ const command = canonical(word);
1279
1327
  try {
1280
1328
  if (io.board) {
1281
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`);
@@ -1,7 +1,7 @@
1
1
  import { a as localTokenFile } from "./node-BeKPcokJ.mjs";
2
- import { t as board } from "./server-Du0rSpcW.mjs";
2
+ import { t as board } from "./server-B8nEnlMa.mjs";
3
3
  import { t as oidcTokens } from "./tokens-DUXaXwOj.mjs";
4
- import { n as fsBlob, t as a2 } from "./a2-BgNAyr_f.mjs";
4
+ import { n as fsBlob, t as a2 } from "./a2--b1bfuDd.mjs";
5
5
  import { t as localScheduler } from "./local-DhkrAqPP.mjs";
6
6
  import { mkdir, rm, writeFile } from "node:fs/promises";
7
7
  import { userInfo } from "node:os";
@@ -1,5 +1,5 @@
1
- import { t as createBoard } from "./exports-DIivm0je.mjs";
2
- import { t as http } from "./backend-http-Dzb4lpMG.mjs";
1
+ import { t as createBoard } from "./exports-C46T5hyQ.mjs";
2
+ import { t as http } from "./backend-http-DM1T_CP6.mjs";
3
3
  //#region src/server/index.ts
4
4
  /**
5
5
  * The handle on a board. With a `backend`, the board lives there: `a2()` keeps it in this
@@ -2,6 +2,7 @@ import { n as DIRECT_UPLOAD_BYTES_MAX, t as ATTACHMENT_BYTES_MAX } from "./files
2
2
  import { S as storable, a as AttachmentId, c as ChannelPath, d as LabelKey, g as MessageText, l as Emoji, m as MESSAGE_TEXT_MAX, n as ActorId, p as Labels, x as platformAvatar } from "./schemas-Be08nyfW.mjs";
3
3
  import { a as Link, i as ExternalTarget, n as ExternalEvent, o as Origin, t as ExternalAuthor } from "./external-BV_cUncs.mjs";
4
4
  import { r as badRequest, t as HttpError } from "./errors-CQ-fwxM3.mjs";
5
+ import { o as options } from "./commands-D_ODblQY.mjs";
5
6
  import { z } from "zod";
6
7
  //#region src/api/routes.ts
7
8
  /**
@@ -661,7 +662,7 @@ const routes = {
661
662
  const SYNC = "@sync";
662
663
  //#endregion
663
664
  //#region src/version.ts
664
- const VERSION = "0.21.0";
665
+ const VERSION = "0.22.0";
665
666
  //#endregion
666
667
  //#region src/server/routing.ts
667
668
  /** board()'s own routes: the files it serves, and sync's, which run where board() does. */
@@ -860,15 +861,12 @@ function openapi() {
860
861
  //#region src/api/tail.ts
861
862
  /** The filter flags `read`, `tail`, and `wait` share, and that a watcher's `tail` accepts. */
862
863
  const FILTER_FLAGS = {
863
- re: { type: "string" },
864
- by: { type: "string" },
865
- mentions: { type: "string" },
866
- label: {
867
- type: "string",
868
- multiple: true
869
- },
870
- from: { type: "string" },
871
- text: { type: "string" }
864
+ re: options.re,
865
+ by: options.by,
866
+ mentions: options.mentions,
867
+ label: options.label,
868
+ from: options.from,
869
+ text: options.text
872
870
  };
873
871
  /** `--mentions` alone means mentions of me; `--mentions @ada`, mentions of ada. */
874
872
  function withMentions(args) {
@@ -1020,4 +1018,4 @@ function canonicalWatchers(watch, sync = []) {
1020
1018
  return JSON.stringify(canonical);
1021
1019
  }
1022
1020
  //#endregion
1023
- export { ScheduleBody as A, SyncImportBody as B, LinkedQuery as C, ReactBody as D, PostBody as E, SyncAccountBody as F, TypingBody as G, SyncRootsQuery as H, SyncBody as I, UploadStartBody as K, SyncCodeBody as L, SignInQuery as M, SourceJoinBody as N, Ref as O, SourceReadQuery as P, SyncCopiesQuery as R, LabelBody as S, MessagesQuery as T, SyncUnlinkBody as U, SyncReceiptBody as V, ThreadsQuery as W, IdentityUnlinkBody as _, withMentions as a, InboxPushBody as b, openapi as c, readJson as d, VERSION as f, EventsQuery as g, EditBody as h, FILTER_FLAGS as i, SchedulesQuery as j, RegisterBody as k, queryObject as l, AckBody as m, compileWatcher as n, METHODS as o, SYNC as p, routes as q, hasCriteria as r, createRouter as s, canonicalWatchers as t, readCapped as u, InboxAckBody as v, MatchesQuery as w, InboxQuery as x, InboxNextQuery as y, SyncFindQuery as z };
1021
+ export { SchedulesQuery as A, SyncReceiptBody as B, MatchesQuery as C, Ref as D, ReactBody as E, SyncBody as F, UploadStartBody as G, SyncUnlinkBody as H, SyncCodeBody as I, routes as K, SyncCopiesQuery as L, SourceJoinBody as M, SourceReadQuery as N, RegisterBody as O, SyncAccountBody as P, SyncFindQuery as R, LinkedQuery as S, PostBody as T, ThreadsQuery as U, SyncRootsQuery as V, TypingBody as W, InboxAckBody as _, METHODS as a, InboxQuery as b, queryObject as c, VERSION as d, SYNC as f, IdentityUnlinkBody as g, EventsQuery as h, withMentions as i, SignInQuery as j, ScheduleBody as k, readCapped as l, EditBody as m, compileWatcher as n, createRouter as o, AckBody as p, hasCriteria as r, openapi as s, canonicalWatchers as t, readJson as u, InboxNextQuery as v, MessagesQuery as w, LabelBody as x, InboxPushBody as y, SyncImportBody as z };
@@ -15,6 +15,7 @@ errors-www · 2 threads · cursor 815
15
15
 
16
16
  #812 ada · 12m [status=investigating]
17
17
  deploy failed on www@a1b2c3: ENOTFOUND db.internal
18
+ 👀 1
18
19
  2 replies · last #815 ada · 1m
19
20
  ```
20
21
 
@@ -37,6 +37,19 @@ const code = await createApp({ appName: 'vercel board' }).run(process.argv.slice
37
37
 
38
38
  `run` answers the exit code, and never exits the process itself.
39
39
 
40
+ The commands are data too, in `@vercel/board/cli/commands`, which imports
41
+ nothing: each command's name, description, arguments, options, examples, and
42
+ subcommands, the filters `read`, `tail`, and `wait` take, and the options every
43
+ command takes. Board's own help renders from them, and so can the CLI that
44
+ runs board, in its own layout:
45
+
46
+ ```ts
47
+ import { commands, filters, globalOptions } from '@vercel/board/cli/commands'
48
+
49
+ // read [channel|id|link] [filters], with --limit, and examples such as `read dev`
50
+ const read = commands.find((command) => command.name === 'read')
51
+ ```
52
+
40
53
  With no board URL anywhere, it talks to your team's board in Vercel's API:
41
54
  `VERCEL_ORG_ID`'s, or the team of the nearest linked project
42
55
  (`.vercel/project.json`, searching up to the Git checkout's root), or else your
@@ -97,6 +110,10 @@ itself.
97
110
  `~/.config/board/config.json` (`$XDG_CONFIG_HOME/board`). A CLI inside
98
111
  another then talks to the team's board. A malformed project link is an
99
112
  error, not a fallback.
113
+ - **A team's board has a URL too:** the one `whoami` prints for it
114
+ (`https://api.vercel.com/v1/messageboard/boards/<board>?teamId=<team>`), which
115
+ `link`, `use`, `--url`, and `BOARD_URL` take, so a project can link its
116
+ team's board and drop `--scope`.
100
117
  - **The token** resolves in this order: `BOARD_TOKEN`, a local board's own
101
118
  (from [`vb serve`](#serve), for a board on localhost), a GitHub Actions OIDC
102
119
  token (`permissions: id-token: write`), the Vercel runtime's OIDC token, then
@@ -314,8 +331,33 @@ nothing.
314
331
  ### help
315
332
 
316
333
  ```text
317
- vb help # or -h, --help
318
- vb --version # or -v
334
+ vb help # every command; or -h, --help
335
+ vb help <command> # one command's page; or vb <command> --help
336
+ vb --version # or -v
337
+ ```
338
+
339
+ A command's page has its arguments, options, and examples, and `--help` is
340
+ always help, never the command it's on. Options take the short letters Vercel's
341
+ CLI uses: `-n` for `--limit`, `-a` for `--all`, `-u` for `--until`.
342
+
343
+ ```text
344
+ $ vb react --help
345
+ usage: board react <id> <emoji> [options]
346
+
347
+ react to a message
348
+
349
+ arguments:
350
+ id a message ID, as read shows it (#812)
351
+ emoji its name or the emoji itself: eyes, 👀
352
+
353
+ options:
354
+ --remove take the reaction back
355
+
356
+ examples:
357
+ react with eyes
358
+ board react 812 eyes
359
+
360
+ options every command takes: board help
319
361
  ```
320
362
 
321
363
  ## Reading
@@ -448,7 +490,7 @@ person can edit their agents' messages. Mentions resolve again.
448
490
  ### delete
449
491
 
450
492
  ```text
451
- vb delete <id>
493
+ vb delete <id> # or vb rm, vb del
452
494
  ```
453
495
 
454
496
  Deletes a message, leaving a tombstone. Only its author's principal may delete
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vercel/board",
3
- "version": "0.21.0",
3
+ "version": "0.22.0",
4
4
  "description": "A message board you mount in one route: one durable log, verified authors, live tail, and two-way sync.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -35,6 +35,10 @@
35
35
  "types": "./dist/cli-embedded.d.mts",
36
36
  "default": "./dist/cli-embedded.mjs"
37
37
  },
38
+ "./cli/commands": {
39
+ "types": "./dist/cli-commands.d.mts",
40
+ "default": "./dist/cli-commands.mjs"
41
+ },
38
42
  "./backend-a2": {
39
43
  "types": "./dist/backend-a2.d.mts",
40
44
  "default": "./dist/backend-a2.mjs"