@theholocron/cli 5.0.0-alpha.20 → 5.0.0-alpha.201

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,5 +1,5 @@
1
1
  import { ProviderApiError } from "@theholocron/http-client";
2
- //#region src/plugin/capabilities.d.ts
2
+ //#region src/plugin/capabilities/capabilities.type.d.ts
3
3
  /**
4
4
  * Capability interfaces — the contracts that providers implement.
5
5
  *
@@ -12,33 +12,8 @@ import { ProviderApiError } from "@theholocron/http-client";
12
12
  * See `docs/wiki/specifications/tech-architecture.spec.md` for the design narrative
13
13
  * (issue: #74).
14
14
  */
15
- type CapabilityKey = "source" | "ci" | "secrets" | "environments" | "issues" | "deployment" | "storage" | "auth" | "vault" | "dns" | "tooling" | "notifications" | "analytics" | "errors" | "logs" | "wiki" | "workers";
15
+ type CapabilityKey = "source" | "ci" | "secrets" | "environments" | "issues" | "deployment" | "storage" | "auth" | "vault" | "dns" | "tooling" | "notifications" | "analytics" | "errors" | "logs" | "monitoring" | "wiki" | "workers";
16
16
  type Cardinality = "single" | "many";
17
- declare const CARDINALITY: {
18
- readonly source: "single";
19
- readonly ci: "single";
20
- readonly secrets: "single";
21
- readonly environments: "single";
22
- readonly issues: "single";
23
- readonly deployment: "single";
24
- readonly storage: "single";
25
- readonly auth: "single";
26
- readonly vault: "single";
27
- readonly dns: "single";
28
- readonly tooling: "many";
29
- readonly notifications: "many";
30
- readonly analytics: "many";
31
- readonly errors: "single";
32
- readonly logs: "single";
33
- readonly wiki: "single";
34
- readonly workers: "single";
35
- };
36
- /**
37
- * No capabilities are strictly required — repos without secrets (e.g. org
38
- * community health repos) legitimately omit vault. Plugins validate their
39
- * own requirements at call time.
40
- */
41
- declare const REQUIRED_CAPABILITIES: readonly CapabilityKey[];
42
17
  interface ProviderIdentity {
43
18
  readonly key: CapabilityKey;
44
19
  readonly providerName: string;
@@ -79,6 +54,19 @@ type TeamEntry = string | {
79
54
  slug: string;
80
55
  permission: TeamPermission;
81
56
  };
57
+ /** A member of a team, as {@link Source.teamMembers} reports it. */
58
+ interface SourceTeamMember {
59
+ /** The provider's stable id for the person; for GitHub, the numeric user id as a string. */
60
+ id: string;
61
+ /** What they are called now. Can change; never use it as a key. */
62
+ login: string;
63
+ }
64
+ /** A file read by {@link Source.readFile}. */
65
+ interface SourceFile {
66
+ content: string;
67
+ /** Opaque; hand it back to {@link Source.writeFile} to replace this exact version. */
68
+ version: string;
69
+ }
82
70
  interface Source extends ProviderIdentity {
83
71
  readonly key: "source";
84
72
  /** Auth sanity-check. Throws ProviderApiError on auth failure. */
@@ -151,6 +139,29 @@ interface Source extends ProviderIdentity {
151
139
  * Optional — providers without a team concept omit this.
152
140
  */
153
141
  syncTeams?(teams: TeamEntry[]): Promise<string>;
142
+ /**
143
+ * The members of a team in `org` (default: the repo's organisation), by the provider's own stable id (for GitHub, the
144
+ * numeric user id, which survives a rename) and current login.
145
+ * Optional — providers without a team concept omit this.
146
+ */
147
+ teamMembers?(teamSlug: string, org?: string): Promise<SourceTeamMember[]>;
148
+ /**
149
+ * Read one file from a repository (default: this one), with the `version` a later {@link writeFile} must
150
+ * hand back. `null` when the file does not exist.
151
+ * Optional — providers that cannot read file contents omit this.
152
+ */
153
+ readFile?(path: string, repo?: string): Promise<SourceFile | null>;
154
+ /**
155
+ * Create or replace one file in a single commit. Pass the `version` {@link readFile} returned when
156
+ * replacing, and leave it out when creating: a write on top of a version that has since changed is
157
+ * refused (throws a `ProviderApiError` with status 409 or 422), so two writers cannot overwrite each other.
158
+ * Optional — providers that cannot write file contents omit this.
159
+ */
160
+ writeFile?(path: string, content: string, opts: {
161
+ message: string;
162
+ version?: string;
163
+ repo?: string;
164
+ }): Promise<void>;
154
165
  /**
155
166
  * Set the repository description.
156
167
  * Optional — providers that don't support setting descriptions omit this.
@@ -364,9 +375,35 @@ interface DeploymentRecord {
364
375
  target?: DeploymentTrigger;
365
376
  status: "queued" | "building" | "ready" | "error" | "cancelled";
366
377
  createdAt?: string;
378
+ /** The provider's own reason when `status` is `"error"` (e.g. Vercel's `Command "npm install" exited with 1`). */
379
+ errorMessage?: string;
380
+ }
381
+ /** Config for `Deployment.deployFunction()` — source files, not a Git ref. */
382
+ interface DeployFunctionConfig {
383
+ /**
384
+ * Files to deploy, keyed by path relative to the project root (e.g.
385
+ * `"api/webhook.js"`, `"package.json"`). Content is plain text — the
386
+ * provider handles any wire-format encoding.
387
+ */
388
+ files: Record<string, string>;
389
+ /**
390
+ * Named deployment target. Omit for a preview deployment — same
391
+ * semantics as `triggerDeployment`'s `target`.
392
+ */
393
+ target?: DeploymentTrigger;
394
+ }
395
+ interface DeployFunctionResult {
396
+ deploymentId: string;
397
+ url: string;
367
398
  }
368
399
  interface Deployment extends ProviderIdentity {
369
400
  readonly key: "deployment";
401
+ /**
402
+ * Custom domain declared in the deployment provider's configuration.
403
+ * `holocron repo setup` attaches it to the configured project and hands any
404
+ * returned verification record to the `dns` capability.
405
+ */
406
+ readonly domain?: string;
370
407
  listProjects(): Promise<DeploymentProject[]>;
371
408
  /** Create if missing, otherwise return existing. Idempotent. */
372
409
  ensureProject(input: {
@@ -390,6 +427,16 @@ interface Deployment extends ProviderIdentity {
390
427
  target?: DeploymentTrigger;
391
428
  }): Promise<DeploymentRecord>;
392
429
  getDeployment(deploymentId: string): Promise<DeploymentRecord>;
430
+ /**
431
+ * Deploy (or update) a project directly from source files, bypassing
432
+ * Git entirely — the non-git counterpart to `triggerDeployment`, for a
433
+ * consumer with no repo to link (a webhook receiver shipped as an npm
434
+ * package, for instance, rather than deployed from its own Git
435
+ * history). `projectId` may be a project name for providers that
436
+ * accept either. Optional — providers without a files-based deploy API
437
+ * omit this (e.g. Cloudflare Pages, git-source only today).
438
+ */
439
+ deployFunction?(projectId: string, config: DeployFunctionConfig): Promise<DeployFunctionResult>;
393
440
  /**
394
441
  * List preview deployments for a specific branch alias (e.g. "repo-pr-42").
395
442
  * Optional — providers that don't support listing by branch omit this.
@@ -402,10 +449,18 @@ interface Deployment extends ProviderIdentity {
402
449
  deletePreviewDeployments?(projectId: string, deploymentIds: string[]): Promise<number>;
403
450
  /**
404
451
  * Add a custom domain (or wildcard) to the project. Idempotent — no-op
405
- * when the domain is already present. Optional: providers without a custom
406
- * domain API omit this (e.g. Vercel manages domains separately).
452
+ * when the domain is already present. Optional: providers without a
453
+ * custom domain API omit this.
454
+ *
455
+ * Returns the DNS record needed to finish verification — `null` when
456
+ * the domain is already verified, or when the provider's API doesn't
457
+ * surface DNS-challenge details at all (e.g. Cloudflare Pages, whose
458
+ * custom-domain flow doesn't expose the same per-request verification
459
+ * handshake Vercel's does). Callers hand a non-null result straight
460
+ * to `dns.upsertRecord(result.zone, result.record)` — the exact
461
+ * pattern `setup` already uses for `Wiki.dnsRecord()`.
407
462
  */
408
- ensureCustomDomain?(projectId: string, hostname: string): Promise<void>;
463
+ ensureCustomDomain?(projectId: string, hostname: string): Promise<DnsRecordRequest | null>;
409
464
  }
410
465
  interface StorageBranch {
411
466
  id: string;
@@ -517,9 +572,6 @@ interface ParseWebhookInput {
517
572
  /** The signing secret the auth provider issued for this webhook endpoint. */
518
573
  signingSecret: string;
519
574
  }
520
- declare class WebhookVerificationError extends Error {
521
- name: string;
522
- }
523
575
  interface EnsureResult {
524
576
  /** True when the resource already existed (idempotent no-op). */
525
577
  alreadyExists: boolean;
@@ -573,11 +625,31 @@ interface DnsRecord {
573
625
  priority?: number;
574
626
  proxied?: boolean;
575
627
  }
628
+ /**
629
+ * A DNS record a capability wants created, plus which zone it belongs
630
+ * to — the shape `Wiki.dnsRecord()` and `Deployment.ensureCustomDomain()`
631
+ * both hand off to `Dns.upsertRecord()`. Not tied to either capability;
632
+ * any provider that needs "here's a record, go create it" returns this.
633
+ */
634
+ interface DnsRecordRequest {
635
+ /** Zone apex passed as the first argument to `dns.upsertRecord`. */
636
+ zone: string;
637
+ record: DnsRecord;
638
+ }
576
639
  interface Dns extends ProviderIdentity {
577
640
  readonly key: "dns";
578
641
  listRecords(domain: string): Promise<DnsRecord[]>;
579
642
  upsertRecord(domain: string, record: DnsRecord): Promise<DnsRecord>;
580
643
  deleteRecord(domain: string, id: string): Promise<void>;
644
+ /**
645
+ * A `holocron doctor` smoke check with no specific domain to ask about —
646
+ * `listRecords` needs one, but doctor doesn't have one on hand. Verifies
647
+ * the token and reports how many zones it can see.
648
+ */
649
+ whoami?(): Promise<{
650
+ ok: boolean;
651
+ zones: number;
652
+ }>;
581
653
  }
582
654
  interface Errors extends ProviderIdentity {
583
655
  readonly key: "errors";
@@ -586,9 +658,17 @@ interface Errors extends ProviderIdentity {
586
658
  provider: string;
587
659
  envKeys: string[];
588
660
  }>;
661
+ /**
662
+ * Reachability probe. `null` means the check genuinely can't run right
663
+ * now on an otherwise-valid configuration (e.g. Sentry's optional `org`
664
+ * setting is unset — error ingestion still works from the DSN alone,
665
+ * `org` is only needed for the management-API calls `whoami` makes) —
666
+ * callers should fall back to `describe()`, not treat it as a failure.
667
+ * Still throws for a genuine failure (bad token, network error, etc.).
668
+ */
589
669
  whoami?(): Promise<{
590
670
  org: string;
591
- }>;
671
+ } | null>;
592
672
  ensureProject?(input: {
593
673
  name: string;
594
674
  platform?: string;
@@ -614,6 +694,74 @@ interface Logs extends ProviderIdentity {
614
694
  alreadyExists: boolean;
615
695
  }>;
616
696
  }
697
+ type MonitoringAssertionSource = "status_code" | "response_time" | "body" | "header";
698
+ type MonitoringAssertionComparison = "equals" | "not_equals" | "contains" | "not_contains" | "greater_than" | "less_than";
699
+ interface MonitoringAssertion {
700
+ source: MonitoringAssertionSource;
701
+ comparison: MonitoringAssertionComparison;
702
+ /** Header name / JSON path — required when `source` needs one (e.g. `"header"`). */
703
+ property?: string;
704
+ target?: string;
705
+ }
706
+ type MonitoringHttpMethod = "GET" | "POST" | "PUT" | "HEAD" | "DELETE" | "PATCH";
707
+ interface MonitoringCheckInput {
708
+ /** Also the idempotency key — `ensureCheck` finds-or-creates by this name. */
709
+ name: string;
710
+ url: string;
711
+ method?: MonitoringHttpMethod;
712
+ /** Minutes between runs. Adapters round to their own supported values. */
713
+ frequency?: number;
714
+ assertions?: MonitoringAssertion[];
715
+ activated?: boolean;
716
+ /**
717
+ * Provider-specific region/datacenter ids to run the check from (e.g.
718
+ * Checkly's `"us-east-1"`). Required by some providers — there's no
719
+ * universal default; an account with none configured rejects a check
720
+ * that omits this entirely.
721
+ */
722
+ locations?: string[];
723
+ /**
724
+ * Inverts pass/fail: set this when the check's expected-healthy
725
+ * response is itself an HTTP error status (e.g. asserting a 404/405
726
+ * on an endpoint that should reject certain requests). Without it,
727
+ * most providers classify any 4xx/5xx response as a failure at the
728
+ * platform level *before* evaluating `assertions` at all — a custom
729
+ * assertion that matches the "expected" error status does not
730
+ * override that baseline classification on its own.
731
+ */
732
+ shouldFail?: boolean;
733
+ /**
734
+ * Raw request body to send with the check — e.g. simulating a webhook
735
+ * delivery to verify signature/auth checking is wired correctly
736
+ * end-to-end, rather than just confirming the endpoint is reachable.
737
+ * Omitted entirely by default (no body sent).
738
+ */
739
+ body?: string;
740
+ }
741
+ interface MonitoringCheck {
742
+ id: string;
743
+ name: string;
744
+ }
745
+ interface Monitoring extends ProviderIdentity {
746
+ readonly key: "monitoring";
747
+ /**
748
+ * Checks this provider is configured to ensure exist — populated from
749
+ * the provider's own config (`holocron.config.ts`'s provider tuple),
750
+ * the same place `Deployment.domain` comes from. `holocron repo setup`
751
+ * loops this list and calls `ensureCheck` for each; empty when none
752
+ * are configured.
753
+ */
754
+ readonly checks: readonly MonitoringCheckInput[];
755
+ /** Auth sanity-check. Throws ProviderApiError on auth failure. */
756
+ whoami?(): Promise<{
757
+ ok: boolean;
758
+ }>;
759
+ /** Idempotent find-or-create-or-update by `input.name`. */
760
+ ensureCheck(input: MonitoringCheckInput): Promise<{
761
+ check: MonitoringCheck;
762
+ alreadyExists: boolean;
763
+ }>;
764
+ }
617
765
  interface ToolingDoctorReport {
618
766
  ok: boolean;
619
767
  message: string;
@@ -624,6 +772,329 @@ interface Tooling extends ProviderIdentity {
624
772
  sync(): Promise<void>;
625
773
  doctor(): Promise<ToolingDoctorReport>;
626
774
  }
775
+ /** What an `ensure*` call found or made. `created: false` means it already existed. */
776
+ interface EnsuredResource {
777
+ id: string;
778
+ created: boolean;
779
+ /** For something that already existed: the settings that disagreed with the layout and were changed. */
780
+ updated?: string[];
781
+ }
782
+ /** A webhook (or equivalent inbound URL) for a channel. */
783
+ interface EnsureWebhookResult {
784
+ created: boolean;
785
+ /**
786
+ * The posting URL — a credential. Absent when the webhook already existed and
787
+ * the provider will not reveal it again (Discord returns the token only to
788
+ * the application that created the webhook, and only on creation).
789
+ */
790
+ url?: string;
791
+ /** For a webhook that already existed, or was just made: the settings that were set (`"avatar"`). */
792
+ updated?: string[];
793
+ /** Set when the webhook is fine but setting its avatar failed; the webhook and its `url` are still good. */
794
+ avatarError?: string;
795
+ }
796
+ /**
797
+ * Who may see a channel and post in it. Roles are named as in the layout's `roles`.
798
+ * A channel inherits its category's access; stating `view` or `post` on the channel
799
+ * replaces that field only.
800
+ */
801
+ interface NotificationsAccess {
802
+ /** `"everyone"` (the default), or the roles that can see it: everyone else cannot. */
803
+ view?: "everyone" | string[];
804
+ /** `"everyone"` (the default), or the roles that can post; `[]` means only bots and webhooks. */
805
+ post?: "everyone" | string[];
806
+ }
807
+ /** One channel in a {@link NotificationsLayout}. */
808
+ interface NotificationsChannelLayout {
809
+ name: string;
810
+ topic?: string;
811
+ /** Who may see and post. Overrides the category's, field by field. */
812
+ access?: NotificationsAccess;
813
+ /**
814
+ * Create an inbound webhook for the channel and store its URL in a secret.
815
+ * `true` uses the defaults; the object form overrides the webhook's display
816
+ * name and the secret's name.
817
+ */
818
+ webhook?: boolean | {
819
+ name?: string;
820
+ secret?: string;
821
+ avatar?: string;
822
+ };
823
+ }
824
+ /**
825
+ * Settings of the server itself. Channels are named as in the layout's `categories`; each field
826
+ * left out is left alone.
827
+ */
828
+ interface NotificationsServerLayout {
829
+ /** How established an account must be to talk: `none`, `low` (verified email), `medium` (5 minutes old), `high` (10 minutes in the server), `highest` (verified phone). */
830
+ verification?: "none" | "low" | "medium" | "high" | "highest";
831
+ /** Whether members are notified for every message or only when mentioned. */
832
+ defaultNotifications?: "all" | "mentions";
833
+ /** Which members' media is scanned: `off`, `noRoles` (members without a role) or `all`. */
834
+ contentFilter?: "off" | "noRoles" | "all";
835
+ /** Where the provider posts its own notices (member joins, boosts). */
836
+ systemChannel?: string;
837
+ /** The channel holding the server rules; required to turn community features on. */
838
+ rulesChannel?: string;
839
+ /** Where the provider sends its notices to the owners; required to turn community features on. */
840
+ updatesChannel?: string;
841
+ /** Turn community features (welcome screen, onboarding, discovery) on or off. */
842
+ community?: boolean;
843
+ /** The server's public description. Only providers that support one for the server's current state accept it. */
844
+ description?: string;
845
+ /** Preferred language, as a locale such as `en-US`. */
846
+ locale?: string;
847
+ }
848
+ /**
849
+ * The screen a new member sees on joining: a short text and the channels worth visiting first.
850
+ * Declaring it describes the whole screen: the text and the channels are set to exactly this, so to
851
+ * switch it off, state them as well.
852
+ */
853
+ interface NotificationsWelcomeLayout {
854
+ /** Whether the screen is shown. Defaults to `true`. */
855
+ enabled?: boolean;
856
+ /** A sentence or two about the server (Discord allows up to 140 characters). */
857
+ description: string;
858
+ /** The channels it points to, in order (Discord allows up to five). */
859
+ channels: Array<{
860
+ /** A channel in the layout. */
861
+ channel: string;
862
+ /** Why to visit it (Discord allows up to 50 characters). */
863
+ description: string;
864
+ /** A single emoji shown beside it. */
865
+ emoji?: string;
866
+ }>;
867
+ }
868
+ /** The questions a new member answers to be shown the channels that fit them. */
869
+ interface NotificationsOnboardingLayout {
870
+ /** Turn onboarding on or off. Left out, it is left as it is (Discord has requirements for turning it on). */
871
+ enabled?: boolean;
872
+ /** Channels every member starts with, by name. */
873
+ defaultChannels?: string[];
874
+ prompts?: Array<{
875
+ title: string;
876
+ /** One answer only (`true`) or any number. Defaults to `false`. */
877
+ singleSelect?: boolean;
878
+ /** The member must answer. Defaults to `false`. */
879
+ required?: boolean;
880
+ options: Array<{
881
+ title: string;
882
+ description?: string;
883
+ emoji?: string;
884
+ /** Channels this answer shows, by name. */
885
+ channels?: string[];
886
+ /** Roles this answer gives, by name from the layout's `roles`. */
887
+ roles?: string[];
888
+ }>;
889
+ }>;
890
+ }
891
+ /**
892
+ * Settings of the bot's own application (for Discord, what the developer portal shows). A field left
893
+ * out is left alone.
894
+ */
895
+ interface NotificationsApplicationLayout {
896
+ /** What the application's page says it is (Discord allows up to 400 characters). */
897
+ description?: string;
898
+ /** Short labels, up to five of up to twenty characters each. */
899
+ tags?: string[];
900
+ /** The default install link: what the bot is added with. Permissions are named, as in a role's. */
901
+ install?: {
902
+ scopes: string[];
903
+ permissions: string[];
904
+ };
905
+ /** `https://` link to the terms of service. */
906
+ termsUrl?: string;
907
+ /** `https://` link to the privacy policy. */
908
+ privacyUrl?: string;
909
+ /** `https://` link the provider calls to verify linked roles. */
910
+ roleConnectionsUrl?: string;
911
+ /**
912
+ * Privileged intents the bot needs. `members` lets it list who is in the server, which `sync-roles` needs to
913
+ * find who holds a role. `true` switches it on from code; `false` switches it off.
914
+ */
915
+ intents?: {
916
+ members?: boolean;
917
+ };
918
+ /**
919
+ * OAuth2 redirect URIs that must be registered, for `notifications link`. The provider's API does not allow
920
+ * setting them, so these are only checked: `setup` and the doctor say what to add and where.
921
+ */
922
+ redirectUris?: string[];
923
+ }
924
+ /**
925
+ * Permissions of everyone on the server (for Discord, the @everyone role). Unlike a role's `permissions`,
926
+ * these name only what to change: the rest of the set is left as it is.
927
+ */
928
+ interface NotificationsEveryoneLayout {
929
+ /** Permissions to grant. */
930
+ allow?: string[];
931
+ /** Permissions to take away. */
932
+ deny?: string[];
933
+ }
934
+ /** Where the person-to-member links live: one file in a repo, written by the sign-in flow and read by `sync-roles`. */
935
+ interface NotificationsLinksLayout {
936
+ /**
937
+ * The base URL of the hosted sign-in service (Sentinel), for example `https://sentinel.theholocron.dev`.
938
+ * `notifications link` asks it for `/api/link`; when it answers, sign-in happens there, and when it does not
939
+ * (down, out of quota, no route yet) the command signs in on this machine instead.
940
+ */
941
+ hostedUrl?: string;
942
+ /** The port the local sign-in listens on, which must be registered as `http://localhost:<port>/callback`. Defaults to 53682. */
943
+ localPort?: number;
944
+ /** `owner/name`. Defaults to `<org>/.github-private`. Keep it private: the file pairs two identities. */
945
+ repo?: string;
946
+ /** Defaults to `notifications/<provider>-links.json`. */
947
+ path?: string;
948
+ }
949
+ /** A declared server layout, applied idempotently by `holocron notifications setup`. */
950
+ interface NotificationsLayout {
951
+ /** Where `sync-roles` finds which server member is which GitHub account. Only used by roles with a `team`. */
952
+ links?: NotificationsLinksLayout;
953
+ /** The bot's own application settings, applied with the server's. */
954
+ application?: NotificationsApplicationLayout;
955
+ /** What everyone on the server may do, applied with the server's settings. */
956
+ everyone?: NotificationsEveryoneLayout;
957
+ /**
958
+ * The picture every webhook gets unless its own `webhook.avatar` says otherwise: a path to a PNG,
959
+ * JPEG or GIF (from where the CLI runs) or an `https://` URL. It is set only on a webhook that has
960
+ * no picture yet -- a provider cannot say whether the one it holds is the same image.
961
+ */
962
+ webhookAvatar?: string;
963
+ /** What a new member sees on joining, applied after the channels exist. */
964
+ welcome?: NotificationsWelcomeLayout;
965
+ /** The questions a new member answers, applied after the channels and roles exist. */
966
+ onboarding?: NotificationsOnboardingLayout;
967
+ /** Settings of the server itself, applied after its channels exist. */
968
+ server?: NotificationsServerLayout;
969
+ categories?: Array<{
970
+ name: string;
971
+ access?: NotificationsAccess;
972
+ channels?: NotificationsChannelLayout[];
973
+ }>;
974
+ roles?: Array<{
975
+ name: string;
976
+ mentionable?: boolean;
977
+ /** `#rrggbb`. Omit to leave the colour alone. */
978
+ color?: string;
979
+ /** Show the role's members as their own group in the member list. */
980
+ hoist?: boolean;
981
+ /**
982
+ * The permissions the role holds, by the provider's own names (for Discord, `ViewChannel`,
983
+ * `SendMessages`, …). The list is exact: a permission not named is not granted.
984
+ */
985
+ permissions?: string[];
986
+ /**
987
+ * Keep this role's members equal to a team of the `source` provider (for GitHub, a team slug in
988
+ * the org): `holocron notifications sync-roles` gives the role to every team member who has linked
989
+ * their account, and takes it from linked people who have left the team.
990
+ */
991
+ team?: string;
992
+ /**
993
+ * Store the role's id in this secret, so a workflow can ping the role
994
+ * without anyone copying the id by hand. The id is not sensitive; a secret
995
+ * is used because the same token that writes the webhook secrets can write
996
+ * it, with no extra permission.
997
+ */
998
+ secret?: string;
999
+ }>;
1000
+ }
1001
+ /**
1002
+ * What a provider found on the server, limited to what a {@link NotificationsLayout}
1003
+ * asks about. Read-only: this is what `holocron doctor notifications` compares the
1004
+ * layout against.
1005
+ */
1006
+ interface ServerInspection {
1007
+ /** The bot's application as the provider has it, in the layout's terms, when the layout declares `application`. */
1008
+ application?: NotificationsApplicationLayout;
1009
+ /** Set when reading the application failed; `application` is then absent and means nothing. */
1010
+ applicationError?: string;
1011
+ /** What everyone may do, by permission name, when the layout declares `everyone`. */
1012
+ everyone?: {
1013
+ permissions: string[];
1014
+ };
1015
+ /** Set when reading everyone's permissions failed; `everyone` is then absent and means nothing. */
1016
+ everyoneError?: string;
1017
+ /** The welcome screen as the server has it, in the layout's terms, when the layout declares `welcome`. */
1018
+ welcome?: NotificationsWelcomeLayout;
1019
+ /** Set when reading the welcome screen failed; `welcome` is then absent and means nothing. */
1020
+ welcomeError?: string;
1021
+ /** Onboarding as the server has it, in the layout's terms, when the layout declares `onboarding`. */
1022
+ onboarding?: NotificationsOnboardingLayout;
1023
+ /** Set when reading onboarding failed; `onboarding` is then absent and means nothing. */
1024
+ onboardingError?: string;
1025
+ /** The server's own settings, in the layout's terms, when the layout declares `server`. */
1026
+ server?: NotificationsServerLayout;
1027
+ /** Set when reading the server's settings failed; `server` is then absent and means nothing. */
1028
+ serverError?: string;
1029
+ categories: string[];
1030
+ /**
1031
+ * Who can see and post in each category. `"custom"` means its overwrites are not an access the layout
1032
+ * can state (a role-specific deny, a member overwrite, only some of the posting permissions).
1033
+ */
1034
+ categoryAccess?: Record<string, Required<NotificationsAccess> | "custom">;
1035
+ channels: Array<{
1036
+ name: string;
1037
+ /** The category it sits under, if any. */
1038
+ category?: string;
1039
+ topic?: string;
1040
+ /** Names of the webhooks on the channel. Only filled in for channels the layout gives a webhook. */
1041
+ webhooks: string[];
1042
+ /** Which of those webhooks have a picture of their own. */
1043
+ webhookAvatars?: string[];
1044
+ /** Set when listing this channel's webhooks failed; `webhooks` is then empty and means nothing. */
1045
+ webhooksError?: string;
1046
+ /** Who can see and post; `"custom"` as for `categoryAccess`. */
1047
+ access?: Required<NotificationsAccess> | "custom";
1048
+ }>;
1049
+ roles: Array<{
1050
+ name: string;
1051
+ id: string;
1052
+ mentionable: boolean;
1053
+ /** `#rrggbb`, or absent for the default colour. */
1054
+ color?: string;
1055
+ hoist?: boolean;
1056
+ permissions?: string[];
1057
+ }>;
1058
+ /** Set when reading the roles failed; `roles` is then empty and means nothing. Not read at all when the layout declares none. */
1059
+ rolesError?: string;
1060
+ }
1061
+ /** Who a member turned out to be after signing in with the provider. */
1062
+ interface NotificationsSignedIn {
1063
+ /** Their id on the provider; for Discord, the user id. */
1064
+ member: string;
1065
+ name?: string;
1066
+ /** The GitHub account the provider has verified they own; absent when they have none linked. */
1067
+ github?: {
1068
+ id: string;
1069
+ login: string;
1070
+ };
1071
+ }
1072
+ /** The provider's "sign in" half: build the page a member opens, then learn who they are from the code it sends back. */
1073
+ interface NotificationsSignIn {
1074
+ authorizeUrl(input: {
1075
+ redirectUri: string;
1076
+ state: string;
1077
+ }): Promise<string>;
1078
+ /** Trades the one-use `code` for who they are, and discards the token it was given. */
1079
+ complete(input: {
1080
+ redirectUri: string;
1081
+ code: string;
1082
+ }): Promise<NotificationsSignedIn>;
1083
+ }
1084
+ /** A member of the server, as {@link Notifications.roleMembers} reports it. */
1085
+ /** What {@link Notifications.checkDelivery} saw: each step that went wrong is named in `problems`. */
1086
+ interface NotificationsDelivery {
1087
+ posted: boolean;
1088
+ readBack: boolean;
1089
+ removed: boolean;
1090
+ problems: string[];
1091
+ }
1092
+ interface NotificationsMember {
1093
+ /** The provider's id for them; for Discord, the user id. */
1094
+ id: string;
1095
+ /** A display name for reports; absent when the provider does not give one. */
1096
+ name?: string;
1097
+ }
627
1098
  interface Notifications extends ProviderIdentity {
628
1099
  readonly key: "notifications";
629
1100
  /**
@@ -631,6 +1102,112 @@ interface Notifications extends ProviderIdentity {
631
1102
  * Discord webhook url-name, etc.); adapters resolve from config.
632
1103
  */
633
1104
  send(channel: string, message: string): Promise<void>;
1105
+ /**
1106
+ * The server layout the owner declared for this provider, if any. Read by
1107
+ * `holocron notifications setup`; the provider just carries it from its options.
1108
+ */
1109
+ readonly layout?: NotificationsLayout;
1110
+ /**
1111
+ * Provisioning. All optional, all idempotent (look up by name, create only
1112
+ * what is missing), and all need provider credentials beyond the posting
1113
+ * URL — so a provider that only posts simply omits them.
1114
+ */
1115
+ ensureCategory?(name: string, opts?: {
1116
+ access?: NotificationsAccess;
1117
+ }): Promise<EnsuredResource>;
1118
+ ensureChannel?(input: {
1119
+ name: string;
1120
+ category?: string;
1121
+ topic?: string;
1122
+ access?: NotificationsAccess;
1123
+ }): Promise<EnsuredResource>;
1124
+ ensureWebhook?(input: {
1125
+ channel: string;
1126
+ name: string;
1127
+ category?: string;
1128
+ /** An image as a `data:` URI, set when the webhook has none. */
1129
+ avatar?: string;
1130
+ }): Promise<EnsureWebhookResult>;
1131
+ /**
1132
+ * Proves a channel's webhook delivers: posts `text` through it, reads the post back from the channel and
1133
+ * removes it again. Never throws for a failed step; the result says which one failed. Posts a visible
1134
+ * message for a moment, so callers ask before using it.
1135
+ */
1136
+ checkDelivery?(input: {
1137
+ channel: string;
1138
+ name: string;
1139
+ category?: string;
1140
+ text: string;
1141
+ }): Promise<NotificationsDelivery>;
1142
+ /**
1143
+ * Who holds a role, by the provider's member id. Needs whatever permission the provider asks for to list
1144
+ * members (for Discord, the privileged Server Members Intent). Throws if the role does not exist.
1145
+ */
1146
+ roleMembers?(roleName: string): Promise<NotificationsMember[]>;
1147
+ /** Give a member the role; one who has it already is left alone. */
1148
+ addRoleMember?(roleName: string, memberId: string): Promise<void>;
1149
+ /** Take the role from a member; one who lacks it is left alone. */
1150
+ removeRoleMember?(roleName: string, memberId: string): Promise<void>;
1151
+ /** Sign-in used to learn which GitHub account a member is. Optional; needs an OAuth client secret. */
1152
+ signIn?(): NotificationsSignIn;
1153
+ ensureRole?(input: {
1154
+ name: string;
1155
+ mentionable?: boolean;
1156
+ color?: string;
1157
+ hoist?: boolean;
1158
+ permissions?: string[];
1159
+ }): Promise<EnsuredResource>;
1160
+ /**
1161
+ * Bring the server's own settings in line with the layout, changing only what differs. Returns the
1162
+ * names of what it changed, empty when the server already matched.
1163
+ */
1164
+ ensureServer?(settings: NotificationsServerLayout): Promise<{
1165
+ updated: string[];
1166
+ }>;
1167
+ /** Bring the welcome screen in line with the layout, changing only what differs. Returns what it changed. */
1168
+ ensureWelcome?(welcome: NotificationsWelcomeLayout): Promise<{
1169
+ updated: string[];
1170
+ }>;
1171
+ /** Bring onboarding in line with the layout, changing only what differs. Returns what it changed. */
1172
+ ensureOnboarding?(onboarding: NotificationsOnboardingLayout): Promise<{
1173
+ updated: string[];
1174
+ }>;
1175
+ /** Bring the bot's application settings in line with the layout, changing only what differs. Returns what it changed. */
1176
+ ensureApplication?(application: NotificationsApplicationLayout): Promise<{
1177
+ updated: string[];
1178
+ todo?: string[];
1179
+ }>;
1180
+ /** Grant or take away the named permissions from everyone, changing only what differs. Returns what it changed. */
1181
+ ensureEveryone?(everyone: NotificationsEveryoneLayout): Promise<{
1182
+ updated: string[];
1183
+ }>;
1184
+ /**
1185
+ * Before anything is created, prove the credentials can do the parts of the layout that need
1186
+ * more than the rest (for Discord, change the server's settings). Throws with the reason and how
1187
+ * to fix it, so `setup` stops before it leaves a half-applied layout. Not called on a dry run.
1188
+ */
1189
+ preflight?(layout: NotificationsLayout): Promise<void>;
1190
+ /**
1191
+ * Check the provider-specific parts of the layout (colours, permission names, …) without
1192
+ * touching the server, so `setup` can refuse a bad layout before it creates anything.
1193
+ * Returns the first problem, or `undefined` when the layout is fine.
1194
+ */
1195
+ validateLayout?(layout: NotificationsLayout): string | undefined;
1196
+ /**
1197
+ * Smoke check for `holocron doctor`: prove the provisioning credentials work
1198
+ * and name what they reach. Returns `null` when there is nothing to check (a
1199
+ * config that only posts has no provisioning credentials), which doctor shows
1200
+ * as "loaded" rather than as a failure. Throws when it is configured to
1201
+ * provision but cannot (missing token, no access).
1202
+ */
1203
+ whoami?(): Promise<{
1204
+ target: string;
1205
+ } | null>;
1206
+ /**
1207
+ * Read what exists on the server, for the parts the layout cares about, without
1208
+ * changing anything. Backs `holocron doctor notifications`.
1209
+ */
1210
+ inspect?(layout: NotificationsLayout): Promise<ServerInspection>;
634
1211
  }
635
1212
  interface Analytics extends ProviderIdentity {
636
1213
  readonly key: "analytics";
@@ -651,18 +1228,6 @@ interface WikiProvisionOpts {
651
1228
  /** Project display name used in the wiki title (e.g. "Holocron"). */
652
1229
  name?: string;
653
1230
  }
654
- /**
655
- * DNS record that `setup` should create for a wiki custom domain.
656
- * Returned by `Wiki.dnsRecord()` when a custom domain is configured.
657
- */
658
- interface WikiDnsRecord {
659
- /** Zone apex passed as the first argument to `dns.upsertRecord`. */
660
- zone: string;
661
- /** Full hostname for the CNAME (e.g. "wiki.theholocron.dev"). */
662
- cname: string;
663
- /** CNAME target (e.g. "holocron.docs.buildwithfern.com"). */
664
- target: string;
665
- }
666
1231
  /**
667
1232
  * Reverse-proxy configuration returned by `Wiki.proxyConfig()`.
668
1233
  * Used by `setup` to deploy a Worker that forwards traffic to the wiki
@@ -691,13 +1256,24 @@ interface Wiki extends ProviderIdentity {
691
1256
  * Returns null when no custom domain is set.
692
1257
  * Called by `setup` to provision the CNAME via the `dns` capability.
693
1258
  */
694
- dnsRecord?(): WikiDnsRecord | null;
1259
+ dnsRecord?(): DnsRecordRequest | null;
695
1260
  /**
696
1261
  * Reverse-proxy config when the wiki provider requires a Worker-level
697
1262
  * proxy in addition to the CNAME. Returns null when no proxy is needed.
698
1263
  * Called by `setup` to deploy the proxy via the `workers` capability.
699
1264
  */
700
1265
  proxyConfig?(): WikiProxyConfig | null;
1266
+ /**
1267
+ * Confirms a provider token resolves. Unlike `dns`/`workers`/`monitoring`'s
1268
+ * `whoami()`, this is NOT a live reachability check — Fern (the only
1269
+ * current `wiki` provider) exposes no lightweight endpoint to verify a
1270
+ * token actually works; the only token-consuming operation is a full
1271
+ * `fern generate` (holocron#1079). `ok: true` means "a token was found",
1272
+ * not "the token works".
1273
+ */
1274
+ whoami?(): Promise<{
1275
+ ok: boolean;
1276
+ }>;
701
1277
  }
702
1278
  /** Config for `Workers.deployScript()` — an arbitrary Worker, not a generated reverse-proxy. */
703
1279
  interface DeployScriptConfig {
@@ -738,6 +1314,15 @@ interface Workers extends ProviderIdentity {
738
1314
  upsertProxy(hostname: string, config: WikiProxyConfig): Promise<void>;
739
1315
  /** Deploy (or update) an arbitrary Worker script, optionally with secrets and routes. */
740
1316
  deployScript(name: string, config: DeployScriptConfig): Promise<DeployScriptResult>;
1317
+ /**
1318
+ * A `holocron doctor` smoke check — every other method either mutates or
1319
+ * needs a specific script/zone doctor has no way to supply up front.
1320
+ * Reports how many Worker scripts the token can see.
1321
+ */
1322
+ whoami?(): Promise<{
1323
+ ok: boolean;
1324
+ scripts: number;
1325
+ }>;
741
1326
  }
742
1327
  interface CapabilityImpls {
743
1328
  source: Source;
@@ -755,12 +1340,44 @@ interface CapabilityImpls {
755
1340
  analytics: Analytics;
756
1341
  errors: Errors;
757
1342
  logs: Logs;
1343
+ monitoring: Monitoring;
758
1344
  wiki: Wiki;
759
1345
  workers: Workers;
760
1346
  }
761
1347
  type CardinalityFor<K extends CapabilityKey> = (typeof CARDINALITY)[K];
762
1348
  /** Resolved runtime shape: single → one impl; many → array. */
763
1349
  type ResolvedCapability<K extends CapabilityKey> = CardinalityFor<K> extends "many" ? CapabilityImpls[K][] : CapabilityImpls[K];
1350
+ //#endregion
1351
+ //#region src/plugin/capabilities/capabilities.d.ts
1352
+ declare const CARDINALITY: {
1353
+ readonly source: "single";
1354
+ readonly ci: "single";
1355
+ readonly secrets: "single";
1356
+ readonly environments: "single";
1357
+ readonly issues: "single";
1358
+ readonly deployment: "single";
1359
+ readonly storage: "single";
1360
+ readonly auth: "single";
1361
+ readonly vault: "single";
1362
+ readonly dns: "single";
1363
+ readonly tooling: "many";
1364
+ readonly notifications: "many";
1365
+ readonly analytics: "many";
1366
+ readonly errors: "single";
1367
+ readonly logs: "single";
1368
+ readonly monitoring: "single";
1369
+ readonly wiki: "single";
1370
+ readonly workers: "single";
1371
+ };
1372
+ /**
1373
+ * No capabilities are strictly required — repos without secrets (e.g. org
1374
+ * community health repos) legitimately omit vault. Plugins validate their
1375
+ * own requirements at call time.
1376
+ */
1377
+ declare const REQUIRED_CAPABILITIES: readonly CapabilityKey[];
1378
+ declare class WebhookVerificationError extends Error {
1379
+ name: string;
1380
+ }
764
1381
  declare function isMulti<K extends CapabilityKey>(key: K): CardinalityFor<K> extends "many" ? true : false;
765
1382
  //#endregion
766
- export { Analytics, Auth, AuthDescription, AuthEvent, AuthEventType, AuthIdentity, AuthUser, CARDINALITY, CapabilityImpls, CapabilityKey, Cardinality, CardinalityFor, Ci, CiRun, CiRunFilter, CiRunStatus, ConnectionStringOptions, CreateAuthUserInput, DeployScriptConfig, DeployScriptResult, Deployment, DeploymentProject, DeploymentProjectSettings, DeploymentRecord, DeploymentTarget, DeploymentTrigger, Dns, DnsRecord, DnsRecordType, EnsureResult, Environment, EnvironmentReviewer, Environments, Errors, Issue, IssueSearchFilter, Issues, LabelDef, LifecycleResult, LifecycleSlot, Logs, NormalizedAuthUser, Notifications, PagesConfig, ParseWebhookInput, ProviderApiError, ProviderIdentity, PullRequest, REQUIRED_CAPABILITIES, RepoRef, RepoSettings, ResolvedCapability, Ruleset, SecretScope, Secrets, Source, StatusCategory, Storage, StorageBranch, TeamEntry, TeamPermission, Tooling, ToolingDoctorReport, TrackerDoctorReport, TrackerUser, Vault, WebhookDashboardInfo, WebhookVerificationError, Wiki, WikiDnsRecord, WikiProvisionOpts, WikiProxyConfig, Workers, isMulti };
1383
+ export { Analytics, Auth, AuthDescription, AuthEvent, AuthEventType, AuthIdentity, AuthUser, CARDINALITY, CapabilityImpls, CapabilityKey, Cardinality, CardinalityFor, Ci, CiRun, CiRunFilter, CiRunStatus, ConnectionStringOptions, CreateAuthUserInput, DeployFunctionConfig, DeployFunctionResult, DeployScriptConfig, DeployScriptResult, Deployment, DeploymentProject, DeploymentProjectSettings, DeploymentRecord, DeploymentTarget, DeploymentTrigger, Dns, DnsRecord, DnsRecordRequest, DnsRecordType, EnsureResult, EnsureWebhookResult, EnsuredResource, Environment, EnvironmentReviewer, Environments, Errors, Issue, IssueSearchFilter, Issues, LabelDef, LifecycleResult, LifecycleSlot, Logs, Monitoring, MonitoringAssertion, MonitoringAssertionComparison, MonitoringAssertionSource, MonitoringCheck, MonitoringCheckInput, MonitoringHttpMethod, NormalizedAuthUser, Notifications, NotificationsAccess, NotificationsApplicationLayout, NotificationsChannelLayout, NotificationsDelivery, NotificationsEveryoneLayout, NotificationsLayout, NotificationsLinksLayout, NotificationsMember, NotificationsOnboardingLayout, NotificationsServerLayout, NotificationsSignIn, NotificationsSignedIn, NotificationsWelcomeLayout, PagesConfig, ParseWebhookInput, ProviderApiError, ProviderIdentity, PullRequest, REQUIRED_CAPABILITIES, RepoRef, RepoSettings, ResolvedCapability, Ruleset, SecretScope, Secrets, ServerInspection, Source, SourceFile, SourceTeamMember, StatusCategory, Storage, StorageBranch, TeamEntry, TeamPermission, Tooling, ToolingDoctorReport, TrackerDoctorReport, TrackerUser, Vault, WebhookDashboardInfo, WebhookVerificationError, Wiki, WikiProvisionOpts, WikiProxyConfig, Workers, isMulti };