@theholocron/cli 5.0.0-alpha.186 → 5.0.0-alpha.187

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/dist/cli.mjs CHANGED
@@ -409,6 +409,7 @@ const COMMAND_CONTEXTS = {
409
409
  "secrets sync": "workspace",
410
410
  "notifications channel add": "workspace",
411
411
  "notifications setup": "workspace",
412
+ "notifications verify": "workspace",
412
413
  deploy: "workspace",
413
414
  "deploy cleanup-preview": "workspace",
414
415
  "monitoring check": "workspace",
@@ -2402,8 +2403,12 @@ async function runDeployFromFiles(input) {
2402
2403
  * (newlines and the C1 range, whose 0x9b is an 8-bit CSI) so a crafted body
2403
2404
  * can't forge extra report lines or inject terminal escape sequences. */
2404
2405
  const MAX_DETAIL_LENGTH = 200;
2406
+ /** Control characters (newlines and the C1 range included) turned into spaces, whitespace collapsed, no length cap. */
2407
+ function stripControl(text) {
2408
+ return text.replace(/[\x00-\x1f\x7f-\x9f]/g, " ").replace(/\s+/g, " ").trim();
2409
+ }
2405
2410
  function sanitizeDetail(text) {
2406
- const stripped = text.replace(/[\x00-\x1f\x7f-\x9f]/g, " ").replace(/\s+/g, " ").trim();
2411
+ const stripped = stripControl(text);
2407
2412
  return stripped.length > MAX_DETAIL_LENGTH ? `${stripped.slice(0, MAX_DETAIL_LENGTH)}…` : stripped;
2408
2413
  }
2409
2414
  /**
@@ -2416,11 +2421,12 @@ function sanitizeDetail(text) {
2416
2421
  * able to overwhelm or manipulate the terminal.
2417
2422
  */
2418
2423
  function describeProviderError(err) {
2424
+ const base = stripControl(errorMessage(err));
2419
2425
  if (err instanceof ProviderApiError$1 && err.details) {
2420
2426
  const detail = sanitizeDetail(String(err.details));
2421
- if (detail) return `${errorMessage(err)}: ${detail}`;
2427
+ if (detail) return `${base}: ${detail}`;
2422
2428
  }
2423
- return errorMessage(err);
2429
+ return base;
2424
2430
  }
2425
2431
  /**
2426
2432
  * What to put in structured logs: the plain message of the underlying error,
@@ -3279,21 +3285,66 @@ async function assertSecretsReachable(secrets, scope) {
3279
3285
  /** Why a capability is not loaded: the plugin's own load error when there is one (e.g. a missing token). */
3280
3286
  function whyMissing(loader, key, fallback) {
3281
3287
  const failure = loader.loadFailures().find((f) => f.key === key);
3282
- return failure ? `${fallback} (\`${failure.provider}\` failed to load: ${failure.error.message})` : fallback;
3288
+ return failure ? `${fallback} (\`${failure.provider}\` failed to load: ${describeProviderError(failure.error)})` : fallback;
3283
3289
  }
3284
- /** Pick the provider that can provision; with several, `--provider` decides. */
3285
- function pickProvisioner(providers, wanted) {
3286
- const capable = providers.filter((p) => typeof p.ensureChannel === "function" || typeof p.ensureCategory === "function" || typeof p.ensureRole === "function");
3287
- if (capable.length === 0) throw new Error("no `notifications` provider can provision — add one that implements it (for example discord) to holocron.config.ts");
3290
+ /** The configured `notifications` providers, or an error that says why there are none (including a plugin that failed to load). */
3291
+ function notificationProviders(loader) {
3292
+ const providers = loader.has("notifications") ? loader.get("notifications") : [];
3293
+ if (providers.length === 0) throw new Error(whyMissing(loader, "notifications", "no `notifications` provider is available — add one to holocron.config.ts"));
3294
+ return providers;
3295
+ }
3296
+ /**
3297
+ * Pick the provider that can do what the command needs; with several, `--provider`
3298
+ * decides. `can` says what "able" means (a command that only reads needs `inspect`, not
3299
+ * provisioning), and `verb` words the errors.
3300
+ */
3301
+ function pickProvider(providers, wanted, can, verb) {
3302
+ const capable = providers.filter(can);
3303
+ if (capable.length === 0) throw new Error(`no \`notifications\` provider can ${verb.infinitive} — add one that implements it (for example discord) to holocron.config.ts`);
3288
3304
  if (wanted) {
3289
3305
  const match = capable.find((p) => p.providerName === wanted);
3290
- if (!match) throw new Error(`no provisioning \`notifications\` provider named \`${wanted}\` (have: ${names(capable)})`);
3306
+ if (!match) throw new Error(`no ${verb.gerund} \`notifications\` provider named \`${wanted}\` (have: ${names(capable)})`);
3291
3307
  return match;
3292
3308
  }
3293
- if (capable.length > 1) throw new Error(`several \`notifications\` providers can provision (${names(capable)}) — pass --provider <name>`);
3309
+ if (capable.length > 1) throw new Error(`several \`notifications\` providers can ${verb.infinitive} (${names(capable)}) — pass --provider <name>`);
3294
3310
  return capable[0];
3295
3311
  }
3312
+ /** Pick the provider that can provision. Any ensure* method counts: a role-only layout needs no channel support. */
3313
+ function pickProvisioner(providers, wanted) {
3314
+ return pickProvider(providers, wanted, (p) => typeof p.ensureChannel === "function" || typeof p.ensureCategory === "function" || typeof p.ensureRole === "function", {
3315
+ infinitive: "provision",
3316
+ gerund: "provisioning"
3317
+ });
3318
+ }
3296
3319
  const names = (ps) => ps.map((p) => p.providerName).join(", ");
3320
+ /**
3321
+ * The first reason the layout's secret names cannot work, or undefined. Two things
3322
+ * cannot share one secret: the second write would overwrite the first and callers
3323
+ * would read the wrong value. Punctuation normalising (release-notes vs
3324
+ * release_notes) makes that easy to hit by accident, GitHub secret names are
3325
+ * case-insensitive, and the preflight's own prefix is reserved. Shared by `setup`
3326
+ * (which refuses) and `verify` (which reports it).
3327
+ */
3328
+ function layoutSecretProblem(layout) {
3329
+ const taken = /* @__PURE__ */ new Map();
3330
+ const claim = (secret, owner, hint) => {
3331
+ if (isReservedSecret(secret)) return `${owner} would store into ${secret}, a name reserved for the setup preflight — choose another`;
3332
+ const key = secret.toUpperCase();
3333
+ const other = taken.get(key);
3334
+ if (other) return `${other} and ${owner} would both store into ${secret} — ${hint}`;
3335
+ taken.set(key, owner);
3336
+ };
3337
+ for (const category of layout.categories ?? []) for (const channel of category.channels ?? []) {
3338
+ if (!channel.webhook) continue;
3339
+ const clash = claim(channel.webhook !== true && channel.webhook.secret || defaultSecretName(channel.name), `channel \`${channel.name}\``, "give one a distinct `webhook.secret`");
3340
+ if (clash) return clash;
3341
+ }
3342
+ for (const role of layout.roles ?? []) {
3343
+ if (!role.secret) continue;
3344
+ const clash = claim(role.secret, `role \`${role.name}\``, "give one a distinct secret");
3345
+ if (clash) return clash;
3346
+ }
3347
+ }
3297
3348
  /** Ensure one channel and, if asked, its webhook + secret. Shared with `notifications setup`. */
3298
3349
  async function provisionChannel(deps, spec) {
3299
3350
  const { notifications, secrets, scope, dryRun, print } = deps;
@@ -3367,7 +3418,7 @@ async function runNotificationsChannelAdd(input) {
3367
3418
  const dryRun = input.context.dryRun ?? false;
3368
3419
  print(`Holocron notifications channel add — ${input.name}${dryRun ? " (dry-run)" : ""}`);
3369
3420
  try {
3370
- const notifications = pickProvisioner(loader.has("notifications") ? loader.get("notifications") : [], input.provider);
3421
+ const notifications = pickProvisioner(notificationProviders(loader), input.provider);
3371
3422
  let secrets;
3372
3423
  if (input.withWebhook && !dryRun) {
3373
3424
  if (!loader.has("secrets")) throw new Error(whyMissing(loader, "secrets", "`--with-webhook` needs a `secrets` capability — add a `secrets` provider to holocron.config.ts"));
@@ -3467,7 +3518,7 @@ async function runNotificationsSetup(input) {
3467
3518
  };
3468
3519
  let notifications;
3469
3520
  try {
3470
- notifications = pickProvisioner(loader.has("notifications") ? loader.get("notifications") : [], input.provider);
3521
+ notifications = pickProvisioner(notificationProviders(loader), input.provider);
3471
3522
  } catch (err) {
3472
3523
  return refuse(errorMessage(err));
3473
3524
  }
@@ -3490,24 +3541,8 @@ async function runNotificationsSetup(input) {
3490
3541
  } catch (err) {
3491
3542
  return refuse(errorMessage(err), logReason(err));
3492
3543
  }
3493
- const taken = /* @__PURE__ */ new Map();
3494
- const claim = (secret, owner, hint) => {
3495
- if (isReservedSecret(secret)) return `${owner} would store into ${secret}, a name reserved for the setup preflight — choose another`;
3496
- const key = secret.toUpperCase();
3497
- const other = taken.get(key);
3498
- if (other) return `${other} and ${owner} would both store into ${secret} — ${hint}`;
3499
- taken.set(key, owner);
3500
- };
3501
- for (const category of layout.categories ?? []) for (const channel of category.channels ?? []) {
3502
- if (!channel.webhook) continue;
3503
- const clash = claim(channel.webhook !== true && channel.webhook.secret || defaultSecretName(channel.name), `channel \`${channel.name}\``, "give one a distinct `webhook.secret`");
3504
- if (clash) return refuse(clash);
3505
- }
3506
- for (const role of layout.roles ?? []) {
3507
- if (!role.secret) continue;
3508
- const clash = claim(role.secret, `role \`${role.name}\``, "give one a distinct secret");
3509
- if (clash) return refuse(clash);
3510
- }
3544
+ const secretProblem = layoutSecretProblem(layout);
3545
+ if (secretProblem) return refuse(secretProblem);
3511
3546
  print(`Holocron notifications setup — ${notifications.providerName}${dryRun ? " (dry-run)" : ""}`);
3512
3547
  const attempt = async (step, fn) => {
3513
3548
  try {
@@ -3597,6 +3632,270 @@ async function runNotificationsSetup(input) {
3597
3632
  return report;
3598
3633
  }
3599
3634
  //#endregion
3635
+ //#region src/commands/notifications-verify/notifications-verify.ts
3636
+ /**
3637
+ * `holocron notifications verify` -- read-only: compare the layout a provider
3638
+ * declares in `holocron.config.ts` with what is actually on the server and in the
3639
+ * secrets store, step by step.
3640
+ *
3641
+ * It is the end-to-end check for `notifications setup`, and stays useful after
3642
+ * as a drift check. Everything the layout and `setup` cannot do yet is listed
3643
+ * too, as a failing "not built" step, so the command is red until the last
3644
+ * feature lands and each feature built turns exactly one step green. The list
3645
+ * is {@link PLANNED_CHECKS}; the numbers match the table in holocron#1098.
3646
+ */
3647
+ /** Features not built yet, in the order of the table in holocron#1098. Move an entry into a real check when it lands. */
3648
+ const PLANNED_CHECKS = [
3649
+ {
3650
+ step: 8,
3651
+ id: "permissions",
3652
+ title: "channel and category permissions"
3653
+ },
3654
+ {
3655
+ step: 9,
3656
+ id: "role-appearance",
3657
+ title: "role colour, hoist and permissions"
3658
+ },
3659
+ {
3660
+ step: 10,
3661
+ id: "server-settings",
3662
+ title: "server settings (verification, notifications, system channels)"
3663
+ },
3664
+ {
3665
+ step: 11,
3666
+ id: "welcome",
3667
+ title: "welcome and onboarding content"
3668
+ },
3669
+ {
3670
+ step: 12,
3671
+ id: "webhook-appearance",
3672
+ title: "webhook names and avatars"
3673
+ },
3674
+ {
3675
+ step: 13,
3676
+ id: "app-settings",
3677
+ title: "bot application settings (description, tags, install, links)"
3678
+ },
3679
+ {
3680
+ step: 14,
3681
+ id: "role-assignment",
3682
+ title: "role assigned to team members"
3683
+ },
3684
+ {
3685
+ step: 15,
3686
+ id: "post-works",
3687
+ title: "a test post reached every channel (platform.notifyCheck)"
3688
+ },
3689
+ {
3690
+ step: 16,
3691
+ id: "server-creation",
3692
+ title: "the server itself created from config"
3693
+ }
3694
+ ];
3695
+ const same = (a, b) => a.toLowerCase() === b.toLowerCase();
3696
+ const shown = (s) => sanitizeDetail(s);
3697
+ async function runNotificationsVerify(input) {
3698
+ const print = input.print ?? ((line) => console.log(line));
3699
+ const logger = input.logger ?? getLogger();
3700
+ /* v8 ignore next -- the real loader is only built outside tests, which always inject one */
3701
+ const loader = input.loader ?? new PluginLoader(input.loaded.resolved, input.context);
3702
+ await loader.load();
3703
+ assertPluginsResolvable(loader, "notifications verify");
3704
+ const checks = [];
3705
+ const add = (id, title, problems, pass) => {
3706
+ checks.push({
3707
+ id,
3708
+ title,
3709
+ status: problems.length === 0 ? "pass" : "fail",
3710
+ detail: problems.length === 0 ? pass : `${problems.length} problem(s)`,
3711
+ problems
3712
+ });
3713
+ };
3714
+ const byId = (id) => checks.find((c) => c.id === id);
3715
+ const failed = (id, title, reason) => {
3716
+ checks.push({
3717
+ id,
3718
+ title,
3719
+ status: "fail",
3720
+ detail: reason,
3721
+ problems: []
3722
+ });
3723
+ };
3724
+ const summarise = () => ({
3725
+ pass: checks.filter((c) => c.status === "pass").length,
3726
+ fail: checks.filter((c) => c.status === "fail").length,
3727
+ planned: checks.filter((c) => c.status === "planned").length
3728
+ });
3729
+ const finish = () => {
3730
+ const summary = summarise();
3731
+ const report = {
3732
+ status: summary.fail + summary.planned > 0 ? "fail" : "ok",
3733
+ checks,
3734
+ summary
3735
+ };
3736
+ logger.info({
3737
+ ...summary,
3738
+ status: report.status,
3739
+ checks: checks.map((c) => `${c.id}:${c.status}`)
3740
+ }, "notifications verify");
3741
+ return report;
3742
+ };
3743
+ const refuse = (message, reason = message) => {
3744
+ print(` ✗ ${message}`);
3745
+ logger.warn({ reason }, "notifications verify");
3746
+ return {
3747
+ status: "fail",
3748
+ checks,
3749
+ summary: summarise(),
3750
+ message
3751
+ };
3752
+ };
3753
+ let notifications;
3754
+ try {
3755
+ notifications = pickProvider(notificationProviders(loader), input.provider, (p) => typeof p.inspect === "function", {
3756
+ infinitive: "inspect a server",
3757
+ gerund: "inspecting"
3758
+ });
3759
+ } catch (err) {
3760
+ return refuse(describeProviderError(err));
3761
+ }
3762
+ const layout = notifications.layout;
3763
+ if (!layout) return refuse(`the \`${notifications.providerName}\` provider declares no layout — add \`layout\` to its options in holocron.config.ts`);
3764
+ print(`Holocron notifications verify — ${notifications.providerName}\n`);
3765
+ let found;
3766
+ let readError = "";
3767
+ try {
3768
+ found = await notifications.inspect(layout);
3769
+ } catch (err) {
3770
+ readError = describeProviderError(err);
3771
+ }
3772
+ if (notifications.whoami) try {
3773
+ const who = await notifications.whoami();
3774
+ if (who) checks.push({
3775
+ id: "server",
3776
+ title: "server",
3777
+ status: "pass",
3778
+ detail: shown(who.target),
3779
+ problems: []
3780
+ });
3781
+ else failed("server", "server", "no server configured (the provider's whoami has nothing to check)");
3782
+ } catch (err) {
3783
+ failed("server", "server", describeProviderError(err));
3784
+ }
3785
+ else if (found) checks.push({
3786
+ id: "server",
3787
+ title: "server",
3788
+ status: "pass",
3789
+ detail: "readable (the provider has no whoami)",
3790
+ problems: []
3791
+ });
3792
+ else failed("server", "server", readError);
3793
+ const declaredChannels = (layout.categories ?? []).flatMap((cat) => (cat.channels ?? []).map((channel) => ({
3794
+ cat: cat.name,
3795
+ channel
3796
+ })));
3797
+ const pick = (name, category) => {
3798
+ const named = found.channels.filter((c) => same(c.name, name));
3799
+ return {
3800
+ named,
3801
+ match: named.find((c) => c.category !== void 0 && same(c.category, category))
3802
+ };
3803
+ };
3804
+ if (!found) {
3805
+ if (byId("server")?.status !== "fail") failed("read", "read server", readError);
3806
+ for (const [id, title] of [
3807
+ ["categories", "categories"],
3808
+ ["channels", "channels"],
3809
+ ["webhooks", "webhooks"],
3810
+ ["roles", "roles"]
3811
+ ]) failed(id, title, "not checked: could not read the server");
3812
+ } else {
3813
+ const inspected = found;
3814
+ const categories = layout.categories ?? [];
3815
+ add("categories", "categories", categories.filter((cat) => !inspected.categories.some((name) => same(name, cat.name))).map((cat) => `${shown(cat.name)}: missing`), categories.length === 0 ? "none declared" : `${categories.length} of ${categories.length} exist`);
3816
+ const channelProblems = [];
3817
+ for (const { cat, channel } of declaredChannels) {
3818
+ const { named, match } = pick(channel.name, cat);
3819
+ if (named.length === 0) {
3820
+ channelProblems.push(`#${shown(channel.name)}: missing`);
3821
+ continue;
3822
+ }
3823
+ if (!match) {
3824
+ const where = named.map((c) => c.category ?? "no category").join(", ");
3825
+ channelProblems.push(`#${shown(channel.name)}: exists, but under ${shown(where)} and not ${shown(cat)}`);
3826
+ continue;
3827
+ }
3828
+ if (channel.topic && (match.topic ?? "") !== channel.topic) channelProblems.push(`#${shown(channel.name)}: topic is "${shown(match.topic ?? "")}" but the layout says "${shown(channel.topic)}"`);
3829
+ }
3830
+ add("channels", "channels", channelProblems, declaredChannels.length === 0 ? "none declared" : `${declaredChannels.length} of ${declaredChannels.length} as declared`);
3831
+ const withWebhook = declaredChannels.filter(({ channel }) => channel.webhook);
3832
+ const webhookProblems = [];
3833
+ for (const { cat, channel } of withWebhook) {
3834
+ const name = (channel.webhook === true ? {} : channel.webhook).name ?? channel.name;
3835
+ const { match } = pick(channel.name, cat);
3836
+ if (!match) webhookProblems.push(`#${shown(channel.name)}: no channel to hold the webhook`);
3837
+ else if (match.webhooksError) webhookProblems.push(`#${shown(channel.name)}: could not list its webhooks (${shown(match.webhooksError)})`);
3838
+ else if (!match.webhooks.some((w) => same(w, name))) webhookProblems.push(`#${shown(channel.name)}: no webhook named ${shown(name)}`);
3839
+ }
3840
+ add("webhooks", "webhooks", webhookProblems, withWebhook.length === 0 ? "none declared" : `${withWebhook.length} of ${withWebhook.length} exist`);
3841
+ const roles = layout.roles ?? [];
3842
+ const roleProblems = [];
3843
+ if (inspected.rolesError) roleProblems.push(`could not read the roles (${shown(inspected.rolesError)})`);
3844
+ for (const role of inspected.rolesError ? [] : roles) {
3845
+ const actual = inspected.roles.find((r) => same(r.name, role.name));
3846
+ if (!actual) roleProblems.push(`${shown(role.name)}: missing`);
3847
+ else if (role.mentionable !== void 0 && actual.mentionable !== role.mentionable) roleProblems.push(`${shown(role.name)}: mentionable is ${actual.mentionable} but the layout says ${role.mentionable}`);
3848
+ }
3849
+ add("roles", "roles", roleProblems, roles.length === 0 ? "none declared" : `${roles.length} of ${roles.length} as declared`);
3850
+ }
3851
+ const wanted = [...declaredChannels.filter(({ channel }) => channel.webhook).map(({ channel }) => channel.webhook !== true && channel.webhook && channel.webhook.secret ? channel.webhook.secret : defaultSecretName(channel.name)), ...(layout.roles ?? []).flatMap((r) => r.secret ? [r.secret] : [])];
3852
+ if (wanted.length === 0) checks.push({
3853
+ id: "secrets",
3854
+ title: "secrets",
3855
+ status: "pass",
3856
+ detail: "none declared",
3857
+ problems: []
3858
+ });
3859
+ else if (!loader.has("secrets")) failed("secrets", "secrets", whyMissing(loader, "secrets", "no `secrets` provider to check them with"));
3860
+ else {
3861
+ const org = input.context.org;
3862
+ const scope = input.scope ?? (org ? {
3863
+ kind: "organization",
3864
+ name: org
3865
+ } : void 0);
3866
+ if (!scope) failed("secrets", "secrets", "no org to look under — pass --scope or set `org` in holocron.config.ts");
3867
+ else try {
3868
+ const have = new Set((await loader.get("secrets").listSecrets(scope)).map((n) => n.toUpperCase()));
3869
+ const clash = layoutSecretProblem(layout);
3870
+ add("secrets", "secrets", [...clash ? [shown(clash)] : [], ...wanted.filter((n) => !have.has(n.toUpperCase())).map((n) => `${shown(n)}: missing`)], `${wanted.length} of ${wanted.length} exist`);
3871
+ } catch (err) {
3872
+ failed("secrets", "secrets", describeProviderError(err));
3873
+ }
3874
+ }
3875
+ if (!input.builtOnly) for (const planned of PLANNED_CHECKS) checks.push({
3876
+ id: planned.id,
3877
+ title: planned.title,
3878
+ status: "planned",
3879
+ detail: `not built yet (#1098, step ${planned.step})`,
3880
+ problems: [],
3881
+ step: planned.step
3882
+ });
3883
+ const built = checks.filter((c) => c.status !== "planned");
3884
+ const width = Math.max(...built.map((c) => c.title.length));
3885
+ for (const check of built) {
3886
+ print(` ${check.status === "pass" ? "✓" : "✗"} ${check.title.padEnd(width)} ${check.detail}`);
3887
+ for (const problem of check.problems) print(` - ${problem}`);
3888
+ }
3889
+ const planned = checks.filter((c) => c.status === "planned");
3890
+ if (planned.length > 0) {
3891
+ print("\nNot built yet (each fails until it lands, see #1098):");
3892
+ for (const check of planned) print(` ✗ ${String(check.step).padStart(2)} ${check.title}`);
3893
+ }
3894
+ const summary = summarise();
3895
+ print(`\n${summary.pass} passed, ${summary.fail} failed${input.builtOnly ? "" : `, ${summary.planned} not built`}`);
3896
+ return finish();
3897
+ }
3898
+ //#endregion
3600
3899
  //#region src/commands/npm-bump-versions/npm-bump-versions.ts
3601
3900
  async function runNpmBumpVersions(input) {
3602
3901
  const print = input.print ?? ((line) => console.log(line));
@@ -9733,6 +10032,12 @@ const COMMAND_REGISTRY = [
9733
10032
  }],
9734
10033
  supportsDryRun: true
9735
10034
  },
10035
+ {
10036
+ name: "notifications verify",
10037
+ description: "Check the layout against the server and the secrets store, step by step (read-only)",
10038
+ positionals: [],
10039
+ supportsDryRun: false
10040
+ },
9736
10041
  {
9737
10042
  name: "notifications setup",
9738
10043
  description: "Apply the server layout a notifications provider declares in holocron.config.ts",
@@ -11291,6 +11596,32 @@ try {
11291
11596
  ...argv.scope ? { scope: parseScope(argv.scope) } : {},
11292
11597
  ...argv.provider ? { provider: argv.provider } : {}
11293
11598
  })).status === "fail") process.exitCode = 1;
11599
+ }).command("verify", "Check the layout against the server and the secrets store, step by step (read-only)", (y) => y.option("scope", {
11600
+ type: "string",
11601
+ describe: "Where the layout's secrets live: \"repo\", \"env=<name>\" or \"org=<name>\" (default: the org)"
11602
+ }).option("provider", {
11603
+ type: "string",
11604
+ describe: "Which notifications provider, when several can inspect"
11605
+ }).option("built-only", {
11606
+ type: "boolean",
11607
+ default: false,
11608
+ describe: "Leave out the features that are not built yet (a routine drift check)"
11609
+ }), async (argv) => {
11610
+ const tokens = tokenContext(argv.token);
11611
+ if (!tokens) return;
11612
+ const loaded = await loadConfig(argv.cwd);
11613
+ applyResolvedConfig(argv, loaded.resolved);
11614
+ if ((await runNotificationsVerify({
11615
+ loaded,
11616
+ context: {
11617
+ repoRoot: argv.cwd,
11618
+ ...tokens,
11619
+ org: resolveOrg(argv, loaded.resolved)
11620
+ },
11621
+ ...argv.scope ? { scope: parseScope(argv.scope) } : {},
11622
+ ...argv.provider ? { provider: argv.provider } : {},
11623
+ ...argv.builtOnly ? { builtOnly: true } : {}
11624
+ })).status === "fail") process.exitCode = 1;
11294
11625
  }).command("$0", false, () => {}, async (argv) => {
11295
11626
  await launchGroupMenu("notifications", argv);
11296
11627
  }), () => {}).command("sync", "Sync generated files to other repos", (g) => g.command("github", "Sync workflow templates and composite actions to theholocron/.github", (y) => y.option("repo", {