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

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,13 +538,13 @@ 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]` | Read-only, step by step: does the server match the layout, and do the secrets exist |
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) |
548
548
 
549
549
  Config shape added for role sync and sign-in (all under the provider's `layout`):
550
550
 
@@ -566,6 +566,7 @@ Capability surface behind it (all optional, so providers without the concept omi
566
566
  - `Source.teamMembers(teamSlug, org?)`, `Source.readFile(path, repo?)` (returns the `version` a later write must present),
567
567
  `Source.writeFile(path, content, { message, version?, repo? })` (a changed `version` is refused: a real conflict check).
568
568
  - `Notifications.roleMembers(role)`, `addRoleMember(role, memberId)`, `removeRoleMember(role, memberId)`.
569
+ - `Notifications.checkDelivery({ channel, name, category?, text })`: posts through a channel's webhook, reads the post back and removes it, and says which step failed.
569
570
  - `Notifications.signIn()` returning `{ authorizeUrl, complete }`: the provider-neutral shape of "prove which account you own".
570
571
  - `Notifications.ensureApplication` returns `{ updated, todo? }`; each `todo` is a step only a person can do (a portal setting
571
572
  the API ignores) and is reported as a failure until done.
package/dist/cli.mjs CHANGED
@@ -3527,10 +3527,6 @@ async function runNotificationsLink(input) {
3527
3527
  */
3528
3528
  /** Features not built yet, in the order of the table in holocron#1098. Move an entry into a real check when it lands. */
3529
3529
  const PLANNED_CHECKS = [{
3530
- step: 15,
3531
- id: "post-works",
3532
- title: "test post reached every channel (platform.notifyCheck)"
3533
- }, {
3534
3530
  step: 16,
3535
3531
  id: "server-creation",
3536
3532
  title: "server created from config"
@@ -3555,6 +3551,7 @@ const shown = (s) => sanitizeDetail(s);
3555
3551
  const WELCOME_TITLE = "welcome screen and onboarding set";
3556
3552
  const APP_TITLE = "bot description, tags, install link and URLs set";
3557
3553
  const AVATAR_TITLE = "webhook names and avatars set";
3554
+ const DELIVERY_TITLE = "test message delivered through every webhook";
3558
3555
  const SERVER_TITLE = "server verification, notifications and community set";
3559
3556
  const ACCESS_TITLE = "channel and category access set";
3560
3557
  const ROLE_APPEARANCE_TITLE = "role colour, hoist and permissions set";
@@ -3587,10 +3584,11 @@ async function runDoctorNotifications(input) {
3587
3584
  problems: []
3588
3585
  });
3589
3586
  };
3587
+ let deliverSkipped = false;
3590
3588
  const summarise = () => ({
3591
3589
  pass: checks.filter((c) => c.status === "pass").length,
3592
3590
  fail: checks.filter((c) => c.status === "fail").length,
3593
- skip: input.builtOnly ? PLANNED_CHECKS.length : 0
3591
+ skip: (input.builtOnly ? PLANNED_CHECKS.length : 0) + (deliverSkipped ? 1 : 0)
3594
3592
  });
3595
3593
  const finish = () => {
3596
3594
  const summary = summarise();
@@ -3983,6 +3981,39 @@ async function runDoctorNotifications(input) {
3983
3981
  failed("role-members", ROLE_MEMBERS_TITLE, shown(describeProviderError(err)));
3984
3982
  }
3985
3983
  }
3984
+ const withHook = (layout.categories ?? []).flatMap((cat) => (cat.channels ?? []).filter((c) => c.webhook).map((channel) => ({
3985
+ cat,
3986
+ channel
3987
+ })));
3988
+ if (!input.deliver) deliverSkipped = true;
3989
+ else if (withHook.length === 0) checks.push({
3990
+ id: "post-works",
3991
+ title: DELIVERY_TITLE,
3992
+ status: "pass",
3993
+ detail: "none declared",
3994
+ problems: [],
3995
+ step: 15
3996
+ });
3997
+ else if (!notifications.checkDelivery) failed("post-works", DELIVERY_TITLE, `the \`${notifications.providerName}\` provider cannot check delivery`);
3998
+ else {
3999
+ const problems = [];
4000
+ for (const { cat, channel } of withHook) {
4001
+ const cfg = channel.webhook === true ? {} : channel.webhook;
4002
+ try {
4003
+ const result = await notifications.checkDelivery({
4004
+ channel: channel.name,
4005
+ name: cfg.name ?? channel.name,
4006
+ category: cat.name,
4007
+ text: `holocron doctor delivery check for #${channel.name}: safe to ignore, this message removes itself`
4008
+ });
4009
+ 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`);
4011
+ } catch (err) {
4012
+ problems.push(`#${shown(channel.name)}: ${shown(describeProviderError(err))}`);
4013
+ }
4014
+ }
4015
+ add("post-works", DELIVERY_TITLE, problems, `${withHook.length} of ${withHook.length} delivered`);
4016
+ }
3986
4017
  if (!input.builtOnly) for (const planned of PLANNED_CHECKS) checks.push({
3987
4018
  id: planned.id,
3988
4019
  title: planned.title,
@@ -3996,6 +4027,7 @@ async function runDoctorNotifications(input) {
3996
4027
  print(statusRow(check.status === "pass" ? "ok" : "fail", [check.title.padEnd(width), check.detail]));
3997
4028
  for (const problem of check.problems) print(` - ${problem}`);
3998
4029
  }
4030
+ if (deliverSkipped) print(` ${style.skip(`${DELIVERY_TITLE}: not run; add --deliver to post a test message to each channel`)}`);
3999
4031
  const summary = summarise();
4000
4032
  print("");
4001
4033
  print(style.header(`${summary.pass} ok, ${summary.fail} fail, ${summary.skip} skipped`));
@@ -11082,8 +11114,15 @@ const COMMAND_REGISTRY = [
11082
11114
  },
11083
11115
  {
11084
11116
  name: "doctor notifications",
11085
- description: "Check the layout against the server and the secrets store, step by step (read-only)",
11117
+ description: "Check the layout against the server and the secrets store, step by step (read-only unless --deliver posts test messages)",
11086
11118
  positionals: [],
11119
+ options: [{
11120
+ key: "deliver",
11121
+ flag: "--deliver",
11122
+ type: "confirm",
11123
+ default: false,
11124
+ message: "Also post a test message through every channel's webhook (it removes itself)?"
11125
+ }],
11087
11126
  supportsDryRun: false
11088
11127
  },
11089
11128
  {
@@ -11943,7 +11982,7 @@ try {
11943
11982
  }).command("doctor", "Load the config and run a smoke check against every provider", (y) => y.option("repo", {
11944
11983
  type: "string",
11945
11984
  describe: "Repo coords (\"owner/name\"). Defaults to plugin-specific resolution."
11946
- }).command("notifications", "Check the layout against the server and the secrets store, step by step (read-only)", (y) => y.option("scope", {
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", {
11947
11986
  type: "string",
11948
11987
  describe: "Where the layout's secrets live: \"repo\", \"env=<name>\" or \"org=<name>\" (default: the org)"
11949
11988
  }).option("provider", {
@@ -11953,6 +11992,10 @@ try {
11953
11992
  type: "boolean",
11954
11993
  default: false,
11955
11994
  describe: "Leave out the features that are not built yet (a routine drift check)"
11995
+ }).option("deliver", {
11996
+ type: "boolean",
11997
+ default: false,
11998
+ describe: "Also post a test message through every channel's webhook, read it back and remove it"
11956
11999
  }), async (argv) => {
11957
12000
  const tokens = tokenContext(argv.token);
11958
12001
  if (!tokens) return;
@@ -11967,7 +12010,8 @@ try {
11967
12010
  },
11968
12011
  ...argv.scope ? { scope: parseScope(argv.scope) } : {},
11969
12012
  ...argv.provider ? { provider: argv.provider } : {},
11970
- ...argv.builtOnly ? { builtOnly: true } : {}
12013
+ ...argv.builtOnly ? { builtOnly: true } : {},
12014
+ ...argv.deliver ? { deliver: true } : {}
11971
12015
  })).status === "fail") process.exitCode = 1;
11972
12016
  }), async (argv) => {
11973
12017
  const tokens = tokenContext(argv.token);