@kud/incidents-cli 0.0.0-stage → 0.1.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Erwann Mest
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,140 @@
1
- # Temporary Holding Version
1
+ # incidents-cli
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Which of the services you depend on are declaring trouble right now: vendor status pages in one terminal view.
4
+
5
+ It reads each vendor's own status page, worst first, and tells you what they have **declared**. A green row is not proof your calls work; a page it cannot read is shown as `? unreadable` and counts as not green, never as fine.
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ npm install -g @kud/incidents-cli
11
+ ```
12
+
13
+ Node 22 or later.
14
+
15
+ ## Use
16
+
17
+ | Command | What it does |
18
+ | -------------------------- | ------------------------------------------------------------------------ |
19
+ | `incidents` | Interactive view: live list, worst first, rows fill in as they answer |
20
+ | `incidents <service>` | Interactive view opened on that service's detail |
21
+ | `incidents list [service]` | One-shot static output: the overview, or one service's detail |
22
+ | `incidents services` | One service per line: id, name, aliases (comma-separated), tab-separated |
23
+
24
+ Keys in the interactive view: `↑↓` or `j k` move, `⏎` opens a service, `esc` or `⌫` goes back, `r` refreshes, `q` quits.
25
+
26
+ Add `--json` to any form for JSON records. With no TTY on stdin or stdout, or `CI` set, every form falls back to the static output (or JSON), so pipes, `$(...)` and cron never hang.
27
+
28
+ ### Exit codes
29
+
30
+ | Code | Meaning |
31
+ | ---- | ----------------------------------------------------------- |
32
+ | 0 | All clear (maintenance and manual-link rows count as clear) |
33
+ | 1 | Something is degraded, or a page could not be read |
34
+ | 2 | Usage or configuration error |
35
+
36
+ The interactive view exits 0 on quit.
37
+
38
+ ## Configuration
39
+
40
+ `${XDG_CONFIG_HOME:-~/.config}/incidents/config.yml`, then every `config.d/*.yml` merged over it by service id, in lexical order. `INCIDENTS_CONFIG=<file>` replaces both.
41
+
42
+ ```yaml
43
+ timeout: 4000 # milliseconds per request, default 4000
44
+ services:
45
+ github: {} # take the built-in entry as it is
46
+ aws:
47
+ regions: [us-east-1, us-west-2] # override fields of a built-in entry
48
+ npm:
49
+ enabled: false # drop one
50
+ acme:
51
+ name: Acme # a full entry defines a new service
52
+ kind: statuspage
53
+ url: https://status.acme.example
54
+ aliases: [ac]
55
+ ```
56
+
57
+ With no config file the defaults are `github`, `npm`, `cloudflare` and `claude`, and one muted line names the config path.
58
+
59
+ `list`, `services` and `help` are reserved words and cannot be a service id or alias; loading fails with exit 2 and a message naming the file and the id. Any other configuration error also exits 2.
60
+
61
+ ### Built-in services
62
+
63
+ `github`, `claude`, `openai`, `vercel`, `cloudflare`, `npm`, `docker`, `figma`, `linear`, `notion`, `gandi`, `todoist`, `bluesky`, `jira`, `confluence`, `bitbucket`, `sentry`, `harness`, `datadog`, `slack`, `aws`.
64
+
65
+ ### Kinds
66
+
67
+ | Kind | Reads |
68
+ | ------------ | -------------------------------------------------------------------------------------------------------- |
69
+ | `statuspage` | Atlassian Statuspage v2: `<url>/api/v2/summary.json`, history from `<url>/history.rss` |
70
+ | `rss` | An incident feed only. Current state comes from how recent the last post is (`window`, hours, default 6) |
71
+ | `slack` | Slack's status API |
72
+ | `aws` | AWS Health's public event list; `regions` narrows it, global events always count |
73
+ | `link` | No machine-readable page: the row points at `url` and claims nothing |
74
+
75
+ Other fields: `aliases`, `note` (printed under the detail), `timeout` (per service, milliseconds).
76
+
77
+ ## JSON records
78
+
79
+ `incidents list --json` prints an array, worst first. `incidents list <service> --json` prints one record, with its history loaded.
80
+
81
+ ```json
82
+ {
83
+ "id": "github",
84
+ "name": "GitHub",
85
+ "url": "https://www.githubstatus.com",
86
+ "kind": "statuspage",
87
+ "fetched": "ok",
88
+ "indicator": "minor",
89
+ "description": "Partially Degraded Service",
90
+ "components": [{ "name": "API", "status": "degraded_performance" }],
91
+ "active": [
92
+ {
93
+ "title": "Elevated API errors",
94
+ "link": "https://stspg.io/abc",
95
+ "date": "2026-01-01T10:00:00Z",
96
+ "impact": "minor"
97
+ }
98
+ ],
99
+ "recent": [{ "title": "Earlier incident", "link": "https://…", "date": "…" }],
100
+ "historyLoaded": true,
101
+ "historyFailed": false,
102
+ "note": ""
103
+ }
104
+ ```
105
+
106
+ | Field | Meaning |
107
+ | --------------- | --------------------------------------------------------------------------------------------------------- |
108
+ | `fetched` | `ok`, or `failed` when the page could not be read |
109
+ | `indicator` | `critical`, `major`, `unknown`, `minor`, `maintenance`, `none` or `link`, worst first in that order |
110
+ | `components` | Components with their raw vendor status; `operational` ones are included |
111
+ | `active` | Incidents open now |
112
+ | `recent` | Past incidents, newest first; empty until history is loaded |
113
+ | `historyLoaded` | `true` once history was requested for this record |
114
+ | `historyFailed` | `true` when the history feed could not be read, so an empty `recent` is not a claim that nothing happened |
115
+
116
+ `unknown` means the page could not be read. It sorts above every healthy row and fails the exit code.
117
+
118
+ ## Environment
119
+
120
+ | Variable | Effect |
121
+ | ------------------ | -------------------------------------------------- |
122
+ | `INCIDENTS_CONFIG` | A single config file, replacing the default lookup |
123
+ | `NO_COLOR` | Plain output |
124
+ | `CI` | Forces the static output |
125
+
126
+ Requests identify themselves as `incidents-cli/<version>`.
127
+
128
+ ## Development
129
+
130
+ ```sh
131
+ npm install
132
+ npm run dev -- --mock
133
+ npm test
134
+ ```
135
+
136
+ `--mock` swaps every data source for fixtures; `--screen <name>` opens straight on a screen (`--screen list` prints the names).
137
+
138
+ ## Licence
139
+
140
+ MIT
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { Command } from "commander";
6
+ import { list, open, run, services } from "../src/cli.js";
7
+ const resolveVersion = () => {
8
+ let dir = dirname(fileURLToPath(import.meta.url));
9
+ for (let i = 0; i < 6; i++) {
10
+ try {
11
+ return JSON.parse(readFileSync(join(dir, "package.json"), "utf8")).version;
12
+ }
13
+ catch {
14
+ dir = dirname(dir);
15
+ }
16
+ }
17
+ return "0.0.0";
18
+ };
19
+ const version = resolveVersion();
20
+ const program = new Command();
21
+ program
22
+ .name("incidents")
23
+ .description("Which of the services you depend on are declaring trouble right now")
24
+ .version(version)
25
+ .option("--json", "print JSON records, never the interactive view", false)
26
+ .option("--mock", "use built-in fixtures instead of live status pages", false)
27
+ .option("--screen <name>", "open directly on a screen (`list` prints them)")
28
+ .argument("[service]", "open the interactive view on one service")
29
+ .showHelpAfterError()
30
+ .action((word, flags) => run(() => open(word, flags, version)));
31
+ program
32
+ .command("list")
33
+ .argument("[service]", "one service's detail instead of the overview")
34
+ .description("print the status once and exit: 0 all clear, 1 degraded or unreadable")
35
+ .action((word, _flags, command) => run(() => list(word, command.optsWithGlobals(), version)));
36
+ program
37
+ .command("services")
38
+ .description("print configured service ids and aliases, one per line")
39
+ .action((_flags, command) => run(() => services(command.optsWithGlobals(), version)));
40
+ await program.parseAsync();
@@ -0,0 +1,106 @@
1
+ import { loadConfig, defaultsNote } from "./config.js";
2
+ import { ConfigError, exitCode, resolveService, worstFirst, } from "./core/index.js";
3
+ import { canBeInteractive } from "./mode.js";
4
+ import { renderDetail, renderOverview } from "./render.js";
5
+ import { liveSource, mockSource } from "./source.js";
6
+ export const SCREENS = ["overview", "detail"];
7
+ const makeContext = (flags, version) => {
8
+ const source = flags.mock
9
+ ? mockSource()
10
+ : (() => {
11
+ const config = loadConfig();
12
+ return liveSource(config, version, defaultsNote(config));
13
+ })();
14
+ return {
15
+ source,
16
+ registry: Object.fromEntries(source.services.map((s) => [s.id, s])),
17
+ };
18
+ };
19
+ const write = (text) => {
20
+ process.stdout.write(text.endsWith("\n") ? text : `${text}\n`);
21
+ };
22
+ const fail = (message) => {
23
+ process.stderr.write(`incidents: ${message}\n`);
24
+ return 2;
25
+ };
26
+ const pretty = () => {
27
+ const color = Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
28
+ return { color, links: color };
29
+ };
30
+ const unknownService = (word, registry) => fail(`no service '${word}'. Known: ${Object.keys(registry).sort().join(", ")}`);
31
+ const classic = async (word, flags, { source, registry }) => {
32
+ if (word !== undefined) {
33
+ const service = resolveService(registry, word);
34
+ if (!service)
35
+ return unknownService(word, registry);
36
+ const record = await source.readDetail(service, {});
37
+ write(flags.json
38
+ ? JSON.stringify(record, null, 2)
39
+ : renderDetail(record, { ...pretty(), note: source.note }));
40
+ return exitCode([record]);
41
+ }
42
+ if (source.services.length === 0) {
43
+ write("No services configured.");
44
+ return 0;
45
+ }
46
+ const records = worstFirst(await source.readAll({}));
47
+ write(flags.json
48
+ ? JSON.stringify(records, null, 2)
49
+ : renderOverview(records, { ...pretty(), note: source.note }));
50
+ return exitCode(records);
51
+ };
52
+ const interactive = async (word, flags, { source, registry }) => {
53
+ const target = word !== undefined ? resolveService(registry, word) : undefined;
54
+ if (word !== undefined && !target)
55
+ return unknownService(word, registry);
56
+ const initialDetail = target?.id ??
57
+ (flags.screen === "detail" ? source.services[0]?.id : undefined);
58
+ const { render } = await import("ink");
59
+ const { createElement } = await import("react");
60
+ const { App } = await import("./ui/app.js");
61
+ const app = render(createElement(App, { source, initialDetail }));
62
+ await app.waitUntilExit();
63
+ return 0;
64
+ };
65
+ export const open = async (word, flags, version) => {
66
+ if (flags.screen === "list") {
67
+ write(SCREENS.join("\n"));
68
+ return 0;
69
+ }
70
+ if (flags.screen !== undefined && !SCREENS.includes(flags.screen))
71
+ return fail(`no screen '${flags.screen}'. Known: ${SCREENS.join(", ")}`);
72
+ const context = makeContext(flags, version);
73
+ const tty = canBeInteractive({
74
+ stdinTTY: Boolean(process.stdin.isTTY),
75
+ stdoutTTY: Boolean(process.stdout.isTTY),
76
+ env: process.env,
77
+ json: Boolean(flags.json),
78
+ });
79
+ return tty
80
+ ? interactive(word, flags, context)
81
+ : classic(word ??
82
+ (flags.screen === "detail"
83
+ ? context.source.services[0]?.id
84
+ : undefined), flags, context);
85
+ };
86
+ export const list = async (word, flags, version) => classic(word, flags, makeContext(flags, version));
87
+ export const services = (flags, version) => {
88
+ const { source } = makeContext(flags, version);
89
+ write(source.services
90
+ .map((s) => [s.id, s.name, s.aliases.join(",")].join("\t"))
91
+ .join("\n"));
92
+ return 0;
93
+ };
94
+ export const run = async (action) => {
95
+ try {
96
+ process.exitCode = await action();
97
+ }
98
+ catch (error) {
99
+ if (error instanceof ConfigError)
100
+ process.exitCode = fail(error.message);
101
+ else {
102
+ process.stderr.write(`incidents: ${error.message}\n`);
103
+ process.exitCode = 1;
104
+ }
105
+ }
106
+ };
@@ -0,0 +1,75 @@
1
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { load } from "js-yaml";
5
+ import { ConfigError, DEFAULT_IDS, RESERVED_WORDS, DEFAULT_TIMEOUT_MS, mergeServices, selectServices, } from "./core/index.js";
6
+ const configHome = (env) => join(env.XDG_CONFIG_HOME || join(homedir(), ".config"), "incidents");
7
+ const readFile = (path) => {
8
+ try {
9
+ const parsed = load(readFileSync(path, "utf8"));
10
+ if (parsed === undefined || parsed === null)
11
+ return {};
12
+ if (typeof parsed !== "object" || Array.isArray(parsed))
13
+ throw new Error("expected a mapping at the top level");
14
+ return parsed;
15
+ }
16
+ catch (error) {
17
+ throw new ConfigError(`${path}: ${error.message}`);
18
+ }
19
+ };
20
+ const rejectReserved = (path, services) => {
21
+ for (const [id, fields] of Object.entries(services)) {
22
+ const aliases = Array.isArray(fields?.aliases) ? fields.aliases : [];
23
+ const word = [id, ...aliases]
24
+ .map(String)
25
+ .find((w) => RESERVED_WORDS.includes(w.toLowerCase()));
26
+ if (word)
27
+ throw new ConfigError(`${path}: service '${id}' uses the reserved word '${word}'`);
28
+ }
29
+ };
30
+ const filesFor = (env) => {
31
+ if (env.INCIDENTS_CONFIG) {
32
+ if (!existsSync(env.INCIDENTS_CONFIG))
33
+ throw new ConfigError(`INCIDENTS_CONFIG: no file at ${env.INCIDENTS_CONFIG}`);
34
+ return [env.INCIDENTS_CONFIG];
35
+ }
36
+ const dir = configHome(env);
37
+ const main = join(dir, "config.yml");
38
+ const dropIns = join(dir, "config.d");
39
+ return [
40
+ ...(existsSync(main) ? [main] : []),
41
+ ...(existsSync(dropIns)
42
+ ? readdirSync(dropIns)
43
+ .filter((f) => f.endsWith(".yml"))
44
+ .sort()
45
+ .map((f) => join(dropIns, f))
46
+ : []),
47
+ ];
48
+ };
49
+ export const defaultConfigPath = (env = process.env) => env.INCIDENTS_CONFIG || join(configHome(env), "config.yml");
50
+ export const loadConfig = (env = process.env) => {
51
+ const files = filesFor(env);
52
+ if (files.length === 0)
53
+ return {
54
+ services: selectServices(Object.fromEntries(DEFAULT_IDS.map((id) => [id, {}]))),
55
+ timeout: DEFAULT_TIMEOUT_MS,
56
+ source: "default",
57
+ path: defaultConfigPath(env),
58
+ };
59
+ const parsed = files.map(readFile);
60
+ parsed.forEach((file, i) => rejectReserved(files[i], file.services ?? {}));
61
+ const selected = parsed.reduce((acc, file) => mergeServices(acc, file.services ?? {}), {});
62
+ const timeout = parsed
63
+ .map((f) => f.timeout)
64
+ .filter((t) => typeof t === "number")
65
+ .at(-1) ?? DEFAULT_TIMEOUT_MS;
66
+ return {
67
+ services: selectServices(selected, undefined, files.join(", ")),
68
+ timeout,
69
+ source: "file",
70
+ path: files[0],
71
+ };
72
+ };
73
+ export const defaultsNote = (config) => config.source === "default"
74
+ ? `No config at ${config.path.replace(homedir(), "~")}: showing ${DEFAULT_IDS.join(", ")}.`
75
+ : null;
@@ -0,0 +1,58 @@
1
+ import { decodeUtf16, fromAws, fromFeed, fromSlack, fromStatuspageSummary, newestFirst, parseRss, } from "./parse.js";
2
+ export const DEFAULT_TIMEOUT_MS = 4000;
3
+ const fetchWith = async (url, service, opts) => {
4
+ const timeout = AbortSignal.timeout(service.timeout ?? opts.timeout ?? DEFAULT_TIMEOUT_MS);
5
+ const signal = opts.signal ? AbortSignal.any([opts.signal, timeout]) : timeout;
6
+ const res = await (opts.fetch ?? fetch)(url, {
7
+ signal,
8
+ headers: opts.userAgent ? { "user-agent": opts.userAgent } : {},
9
+ });
10
+ if (!res.ok)
11
+ throw new Error(`${res.status} from ${url}`);
12
+ return res;
13
+ };
14
+ const json = async (url, service, opts) => (await fetchWith(url, service, opts)).json();
15
+ const text = async (url, service, opts) => (await fetchWith(url, service, opts)).text();
16
+ const readHistory = async (url, service, opts) => {
17
+ try {
18
+ return {
19
+ recent: newestFirst(parseRss(await text(url, service, opts))),
20
+ historyFailed: false,
21
+ };
22
+ }
23
+ catch (error) {
24
+ if (opts.signal?.aborted)
25
+ throw error;
26
+ return { recent: [], historyFailed: true };
27
+ }
28
+ };
29
+ export const ADAPTERS = {
30
+ statuspage: async (service, opts) => {
31
+ const base = (service.url ?? "").replace(/\/$/, "");
32
+ const [summary, history] = await Promise.all([
33
+ json(`${base}/api/v2/summary.json`, service, opts),
34
+ opts.detail
35
+ ? readHistory(`${base}/history.rss`, service, opts)
36
+ : { recent: [], historyFailed: false },
37
+ ]);
38
+ return { ...fromStatuspageSummary(summary), ...history };
39
+ },
40
+ rss: async (service, opts) => {
41
+ const items = newestFirst(parseRss(await text(service.feed ?? "", service, opts)));
42
+ return {
43
+ ...fromFeed(items, { windowHours: service.window ?? 6 }),
44
+ recent: items,
45
+ };
46
+ },
47
+ slack: async (service, opts) => fromSlack(await json("https://slack-status.com/api/v2.0.0/current", service, opts)),
48
+ aws: async (service, opts) => {
49
+ const res = await fetchWith("https://health.aws.amazon.com/public/currentevents", service, opts);
50
+ return fromAws(JSON.parse(decodeUtf16(await res.arrayBuffer())), {
51
+ regions: service.regions,
52
+ });
53
+ },
54
+ link: async () => ({
55
+ indicator: "link",
56
+ description: "No machine-readable status",
57
+ }),
58
+ };
@@ -0,0 +1,33 @@
1
+ export const age = (ms) => {
2
+ const hours = Math.floor(ms / 3_600_000);
3
+ if (hours < 1)
4
+ return `${Math.max(1, Math.floor(ms / 60_000))}m`;
5
+ if (hours < 48)
6
+ return hours < 10 && ms % 3_600_000 >= 60_000
7
+ ? `${hours}h ${Math.floor((ms % 3_600_000) / 60_000)}m`
8
+ : `${hours}h`;
9
+ return `${Math.floor(hours / 24)}d`;
10
+ };
11
+ export const ageOf = (raw, now = Date.now()) => {
12
+ const ms = now - new Date(raw).getTime();
13
+ return raw && !Number.isNaN(ms) ? age(Math.max(0, ms)) : "";
14
+ };
15
+ export const formatWhen = (raw) => {
16
+ const date = new Date(raw);
17
+ return Number.isNaN(date.getTime())
18
+ ? raw
19
+ : date.toLocaleString(undefined, {
20
+ day: "numeric",
21
+ month: "short",
22
+ hour: "2-digit",
23
+ minute: "2-digit",
24
+ });
25
+ };
26
+ export const whenLabel = (raw, now = Date.now()) => {
27
+ const label = ageOf(raw, now);
28
+ if (!label)
29
+ return raw ? formatWhen(raw) : "";
30
+ return label.endsWith("d") && parseInt(label, 10) >= 7
31
+ ? formatWhen(raw)
32
+ : `${label} ago`;
33
+ };
@@ -0,0 +1,102 @@
1
+ export const CATALOGUE = {
2
+ github: {
3
+ name: "GitHub",
4
+ kind: "statuspage",
5
+ url: "https://www.githubstatus.com",
6
+ aliases: ["gh"],
7
+ note: "A green board is not a promise your calls succeed: scattered 5xx errors never reach an incident.",
8
+ },
9
+ claude: {
10
+ name: "Claude",
11
+ kind: "statuspage",
12
+ url: "https://status.claude.com",
13
+ aliases: ["anthropic"],
14
+ },
15
+ openai: {
16
+ name: "OpenAI",
17
+ kind: "statuspage",
18
+ url: "https://status.openai.com",
19
+ },
20
+ vercel: {
21
+ name: "Vercel",
22
+ kind: "statuspage",
23
+ url: "https://www.vercel-status.com",
24
+ },
25
+ cloudflare: {
26
+ name: "Cloudflare",
27
+ kind: "statuspage",
28
+ url: "https://www.cloudflarestatus.com",
29
+ aliases: ["cf"],
30
+ },
31
+ npm: { name: "npm", kind: "statuspage", url: "https://status.npmjs.org" },
32
+ docker: {
33
+ name: "Docker",
34
+ kind: "statuspage",
35
+ url: "https://www.dockerstatus.com",
36
+ },
37
+ figma: { name: "Figma", kind: "statuspage", url: "https://status.figma.com" },
38
+ linear: {
39
+ name: "Linear",
40
+ kind: "statuspage",
41
+ url: "https://linearstatus.com",
42
+ },
43
+ notion: {
44
+ name: "Notion",
45
+ kind: "statuspage",
46
+ url: "https://www.notion-status.com",
47
+ },
48
+ gandi: { name: "Gandi", kind: "statuspage", url: "https://status.gandi.net" },
49
+ todoist: {
50
+ name: "Todoist",
51
+ kind: "rss",
52
+ url: "https://status.todoist.net",
53
+ feed: "https://status.todoist.net/history.rss",
54
+ },
55
+ bluesky: {
56
+ name: "Bluesky",
57
+ kind: "rss",
58
+ url: "https://status.bsky.app",
59
+ feed: "https://status.bsky.app/history.rss",
60
+ aliases: ["bsky"],
61
+ },
62
+ jira: {
63
+ name: "Jira",
64
+ kind: "statuspage",
65
+ url: "https://jira-software.status.atlassian.com",
66
+ },
67
+ confluence: {
68
+ name: "Confluence",
69
+ kind: "statuspage",
70
+ url: "https://confluence.status.atlassian.com",
71
+ },
72
+ bitbucket: {
73
+ name: "Bitbucket",
74
+ kind: "statuspage",
75
+ url: "https://bitbucket.status.atlassian.com",
76
+ },
77
+ sentry: {
78
+ name: "Sentry",
79
+ kind: "statuspage",
80
+ url: "https://status.sentry.io",
81
+ },
82
+ harness: {
83
+ name: "Harness",
84
+ kind: "statuspage",
85
+ url: "https://status.harness.io",
86
+ },
87
+ datadog: {
88
+ name: "Datadog",
89
+ kind: "rss",
90
+ url: "https://status.datadoghq.com",
91
+ feed: "https://status.datadoghq.com/history.rss",
92
+ aliases: ["dd"],
93
+ },
94
+ slack: { name: "Slack", kind: "slack", url: "https://slack-status.com" },
95
+ aws: {
96
+ name: "AWS",
97
+ kind: "aws",
98
+ url: "https://health.aws.amazon.com/health/status",
99
+ note: "Every region counts unless `regions` narrows it; global events always do.",
100
+ },
101
+ };
102
+ export const DEFAULT_IDS = ["github", "npm", "cloudflare", "claude"];
@@ -0,0 +1,8 @@
1
+ export * from "./adapters.js";
2
+ export * from "./age.js";
3
+ export * from "./catalogue.js";
4
+ export * from "./parse.js";
5
+ export * from "./read.js";
6
+ export * from "./registry.js";
7
+ export * from "./severity.js";
8
+ export * from "./types.js";