@theholocron/cli 5.0.0-alpha.202 → 5.0.0-alpha.203

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -538,18 +538,19 @@ A `notifications` provider can declare a whole server in `holocron.config.ts`
538
538
  onboarding, the bot application) and the CLI applies and checks it. The
539
539
  commands and the config are provider-neutral; Discord is the first provider.
540
540
 
541
- | Command | What it does |
542
- | ---------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
543
- | `holocron notifications setup` | Applies the layout; looks everything up by name first, so a re-run changes nothing that matches |
544
- | `holocron notifications channel add <name>` | Creates one channel (optionally with a webhook whose URL is stored as a secret) |
545
- | `holocron notifications link` | Links your account on the provider to your GitHub account by signing in with the provider (OAuth, nothing secret kept) |
546
- | `holocron notifications sync-roles` | Makes who holds a role match a GitHub team, through the links file (`--keep` only adds; `--provider` picks one) |
547
- | `holocron doctor notifications [--built-only] [--deliver]` | Read-only, step by step: does the server match the layout, and do the secrets exist (`--deliver` also posts a test message through each webhook and removes it) |
541
+ | Command | What it does |
542
+ | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
543
+ | `holocron notifications setup` | Applies the layout; looks everything up by name first, so a re-run changes nothing that matches |
544
+ | `holocron notifications channel add <name>` | Creates one channel (optionally with a webhook whose URL is stored as a secret) |
545
+ | `holocron notifications link` | Links your account on the provider to your GitHub account by signing in with the provider (OAuth, nothing secret kept) |
546
+ | `holocron notifications sync-roles` | Makes who holds a role match a GitHub team, through the links file (`--keep` only adds; `--provider` picks one) |
547
+ | `holocron doctor notifications [--built-only] [--deliver]` | Step by step: does the server match the layout, and do the secrets exist. A full run also leaves one `🧪 test:` post in the layout's `testChannel`; `--built-only` posts nothing; `--deliver` posts to every channel and removes each post |
548
548
 
549
549
  Config shape added for role sync and sign-in (all under the provider's `layout`):
550
550
 
551
551
  | Field | Meaning |
552
552
  | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
553
+ | `testChannel` | The channel (it must declare a webhook) the doctor posts its one self-removing test message to. |
553
554
  | `roles[].team` | A GitHub team slug. The role follows that team: given to linked members, taken from linked people who left. Never touches holders nobody has linked. |
554
555
  | `links.hostedUrl` | A hosted sign-in service (Sentinel). `link` asks `<hostedUrl>/api/link` and uses it when it answers `{ "service": "link" }`; otherwise it signs in locally. |
555
556
  | `links.localPort` | The port of the local sign-in listener (default `53682`, bound to `127.0.0.1` only). |
package/dist/cli.mjs CHANGED
@@ -3551,7 +3551,7 @@ const shown = (s) => sanitizeDetail(s);
3551
3551
  const WELCOME_TITLE = "welcome screen and onboarding set";
3552
3552
  const APP_TITLE = "bot description, tags, install link and URLs set";
3553
3553
  const AVATAR_TITLE = "webhook names and avatars set";
3554
- const DELIVERY_TITLE = "test message delivered through every webhook";
3554
+ const DELIVERY_TITLE = "test message delivered through a webhook";
3555
3555
  const SERVER_TITLE = "server verification, notifications and community set";
3556
3556
  const ACCESS_TITLE = "channel and category access set";
3557
3557
  const ROLE_APPEARANCE_TITLE = "role colour, hoist and permissions set";
@@ -3584,11 +3584,10 @@ async function runDoctorNotifications(input) {
3584
3584
  problems: []
3585
3585
  });
3586
3586
  };
3587
- let deliverSkipped = false;
3588
3587
  const summarise = () => ({
3589
3588
  pass: checks.filter((c) => c.status === "pass").length,
3590
3589
  fail: checks.filter((c) => c.status === "fail").length,
3591
- skip: (input.builtOnly ? PLANNED_CHECKS.length : 0) + (deliverSkipped ? 1 : 0)
3590
+ skip: input.builtOnly ? PLANNED_CHECKS.length : 0
3592
3591
  });
3593
3592
  const finish = () => {
3594
3593
  const summary = summarise();
@@ -3985,8 +3984,12 @@ async function runDoctorNotifications(input) {
3985
3984
  cat,
3986
3985
  channel
3987
3986
  })));
3988
- if (!input.deliver) deliverSkipped = true;
3989
- else if (withHook.length === 0) checks.push({
3987
+ const testName = layout.testChannel;
3988
+ const mode = input.deliver ? "all" : !input.builtOnly && testName !== void 0 ? "test" : "none";
3989
+ const targets = mode === "all" ? withHook : mode === "test" ? withHook.filter(({ channel }) => same(channel.name, testName)).slice(0, 1) : [];
3990
+ let deliveryNote;
3991
+ if (mode === "none") deliveryNote = !input.builtOnly && testName === void 0 ? "the delivery check needs a test channel: name one in the layout's `testChannel` (a channel with a webhook)" : "the delivery check posts test messages, so --built-only skips it; add --deliver to post to every channel";
3992
+ else if (targets.length === 0) checks.push({
3990
3993
  id: "post-works",
3991
3994
  title: DELIVERY_TITLE,
3992
3995
  status: "pass",
@@ -3997,22 +4000,24 @@ async function runDoctorNotifications(input) {
3997
4000
  else if (!notifications.checkDelivery) failed("post-works", DELIVERY_TITLE, `the \`${notifications.providerName}\` provider cannot check delivery`);
3998
4001
  else {
3999
4002
  const problems = [];
4000
- for (const { cat, channel } of withHook) {
4003
+ for (const { cat, channel } of targets) {
4001
4004
  const cfg = channel.webhook === true ? {} : channel.webhook;
4005
+ const keep = mode === "test";
4002
4006
  try {
4003
4007
  const result = await notifications.checkDelivery({
4004
4008
  channel: channel.name,
4005
4009
  name: cfg.name ?? channel.name,
4006
4010
  category: cat.name,
4007
- text: `holocron doctor delivery check for #${channel.name}: safe to ignore, this message removes itself`
4011
+ text: keep ? "🧪 test: `holocron doctor notifications` check for {channel}. This post was a test of the delivery system; it is safe to delete." : "🧪 test: `holocron doctor notifications --deliver` check for {channel}. This post was a test of the delivery system; it removes itself.",
4012
+ ...keep ? { keep: true } : {}
4008
4013
  });
4009
4014
  for (const problem of result.problems) problems.push(`#${shown(channel.name)}: ${shown(problem)}`);
4010
- if (result.problems.length === 0 && !(result.posted && result.readBack && result.removed)) problems.push(`#${shown(channel.name)}: the check did not complete`);
4015
+ if (result.problems.length === 0 && !(result.posted && result.readBack && (keep ? result.kept : result.removed))) problems.push(`#${shown(channel.name)}: the check did not complete`);
4011
4016
  } catch (err) {
4012
4017
  problems.push(`#${shown(channel.name)}: ${shown(describeProviderError(err))}`);
4013
4018
  }
4014
4019
  }
4015
- add("post-works", DELIVERY_TITLE, problems, `${withHook.length} of ${withHook.length} delivered`);
4020
+ add("post-works", DELIVERY_TITLE, problems, mode === "all" ? `${targets.length} of ${targets.length} delivered` : `delivered through #${shown(targets[0].channel.name)} (left there)`);
4016
4021
  }
4017
4022
  if (!input.builtOnly) for (const planned of PLANNED_CHECKS) checks.push({
4018
4023
  id: planned.id,
@@ -4027,7 +4032,10 @@ async function runDoctorNotifications(input) {
4027
4032
  print(statusRow(check.status === "pass" ? "ok" : "fail", [check.title.padEnd(width), check.detail]));
4028
4033
  for (const problem of check.problems) print(` - ${problem}`);
4029
4034
  }
4030
- if (deliverSkipped) print(` ${style.skip(`${DELIVERY_TITLE}: not run; add --deliver to post a test message to each channel`)}`);
4035
+ if (deliveryNote) {
4036
+ print("");
4037
+ print(` ${style.skip(`note: ${deliveryNote}`)}`);
4038
+ }
4031
4039
  const summary = summarise();
4032
4040
  print("");
4033
4041
  print(style.header(`${summary.pass} ok, ${summary.fail} fail, ${summary.skip} skipped`));
@@ -4700,12 +4708,18 @@ function accessRoleProblem(layout) {
4700
4708
  function serverLayoutProblem(layout) {
4701
4709
  const counts = /* @__PURE__ */ new Map();
4702
4710
  for (const name of (layout.categories ?? []).flatMap((c) => (c.channels ?? []).map((ch) => ch.name.toLowerCase()))) counts.set(name, (counts.get(name) ?? 0) + 1);
4711
+ const declared = (layout.categories ?? []).flatMap((c) => c.channels ?? []);
4703
4712
  const roles = new Set((layout.roles ?? []).map((r) => r.name.toLowerCase()));
4704
4713
  const channel = (where, name) => {
4705
4714
  const count = counts.get(name.toLowerCase()) ?? 0;
4706
4715
  if (count === 0) return `${where} names the channel \`${name}\`, which is not in the layout's channels`;
4707
4716
  if (count > 1) return `${where} names \`${name}\`, but more than one channel in the layout has that name`;
4708
4717
  };
4718
+ if (layout.testChannel !== void 0) {
4719
+ const problem = channel("`testChannel`", layout.testChannel);
4720
+ if (problem) return problem;
4721
+ if (!declared.some((ch) => ch.name.toLowerCase() === layout.testChannel.toLowerCase() && ch.webhook)) return `\`testChannel\` names \`${layout.testChannel}\`, which declares no webhook`;
4722
+ }
4709
4723
  const roleTeams = roleTeamProblem(layout);
4710
4724
  if (roleTeams) return roleTeams;
4711
4725
  const emptyTeam = (layout.roles ?? []).find((r) => r.team !== void 0 && r.team.trim() === "");
@@ -11114,14 +11128,14 @@ const COMMAND_REGISTRY = [
11114
11128
  },
11115
11129
  {
11116
11130
  name: "doctor notifications",
11117
- description: "Check the layout against the server and the secrets store, step by step (read-only unless --deliver posts test messages)",
11131
+ description: "Check the layout against the server and the secrets store, step by step (a full run leaves one test post in the test channel; --built-only posts nothing unless --deliver)",
11118
11132
  positionals: [],
11119
11133
  options: [{
11120
11134
  key: "deliver",
11121
11135
  flag: "--deliver",
11122
11136
  type: "confirm",
11123
11137
  default: false,
11124
- message: "Also post a test message through every channel's webhook (it removes itself)?"
11138
+ message: "Post a test message through every channel's webhook (each removes itself)?"
11125
11139
  }],
11126
11140
  supportsDryRun: false
11127
11141
  },
@@ -11982,7 +11996,7 @@ try {
11982
11996
  }).command("doctor", "Load the config and run a smoke check against every provider", (y) => y.option("repo", {
11983
11997
  type: "string",
11984
11998
  describe: "Repo coords (\"owner/name\"). Defaults to plugin-specific resolution."
11985
- }).command("notifications", "Check the layout against the server and the secrets store, step by step (read-only unless --deliver posts test messages)", (y) => y.option("scope", {
11999
+ }).command("notifications", "Check the layout against the server and the secrets store, step by step (a full run leaves one test post in the test channel; --built-only posts nothing unless --deliver)", (y) => y.option("scope", {
11986
12000
  type: "string",
11987
12001
  describe: "Where the layout's secrets live: \"repo\", \"env=<name>\" or \"org=<name>\" (default: the org)"
11988
12002
  }).option("provider", {
@@ -11995,7 +12009,7 @@ try {
11995
12009
  }).option("deliver", {
11996
12010
  type: "boolean",
11997
12011
  default: false,
11998
- describe: "Also post a test message through every channel's webhook, read it back and remove it"
12012
+ describe: "Post a test message through every channel's webhook, read it back and remove it (works with --built-only)"
11999
12013
  }), async (argv) => {
12000
12014
  const tokens = tokenContext(argv.token);
12001
12015
  if (!tokens) return;