happyuptime 1.0.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.
Files changed (3) hide show
  1. package/README.md +263 -0
  2. package/dist/index.js +956 -0
  3. package/package.json +58 -0
package/README.md ADDED
@@ -0,0 +1,263 @@
1
+ # happyuptime
2
+
3
+ The official CLI for [Happy Uptime](https://happyuptime.com) — monitor uptime, manage incidents, and check site speed from your terminal.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g happyuptime
9
+ ```
10
+
11
+ Or use without installing:
12
+
13
+ ```bash
14
+ npx happyuptime status
15
+ ```
16
+
17
+ ## Quick Start
18
+
19
+ ```bash
20
+ # 1. Authenticate with your API key
21
+ happy login --api-key hu_your_key_here
22
+
23
+ # 2. Check your monitors
24
+ happy status
25
+
26
+ # 3. Speed test any URL
27
+ happy speed-test https://example.com
28
+ ```
29
+
30
+ Get an API key from your [Happy Uptime dashboard](https://happyuptime.com/dashboard/settings) under Settings > API Keys.
31
+
32
+ ## Commands
33
+
34
+ ### Authentication
35
+
36
+ ```bash
37
+ happy login --api-key hu_xxx # Save API key
38
+ happy whoami # Show current auth status
39
+ happy logout # Remove saved credentials
40
+ ```
41
+
42
+ You can also set the `HAPPYUPTIME_API_KEY` environment variable instead of using `happy login`.
43
+
44
+ ### Status Overview
45
+
46
+ ```bash
47
+ happy status # Quick overview of all monitors
48
+ happy status --json # JSON output for scripting
49
+ ```
50
+
51
+ Example output:
52
+
53
+ ```
54
+ ● All systems operational
55
+
56
+ Monitors
57
+ ● Production API
58
+ ● Marketing Site
59
+ ▲ Staging API degraded
60
+ ○ Dev Server (paused)
61
+
62
+ 10 monitors | 8 up | 0 down | 1 degraded
63
+ ```
64
+
65
+ ### Monitors
66
+
67
+ ```bash
68
+ happy monitors list # List all monitors
69
+ happy monitors list --status down # Filter by status
70
+ happy monitors list --type http # Filter by type
71
+ happy monitors get <id> # Show monitor details
72
+
73
+ happy monitors create \
74
+ --name "Production API" \
75
+ --url https://api.example.com/health \
76
+ --type http \
77
+ --interval 60 \
78
+ --regions us-east,eu-west # Create a monitor
79
+
80
+ happy monitors pause <id> # Pause monitoring
81
+ happy monitors resume <id> # Resume monitoring
82
+ happy monitors delete <id> # Delete (with confirmation)
83
+ happy monitors check https://example.com # Quick-check a URL
84
+ ```
85
+
86
+ ### Incidents
87
+
88
+ ```bash
89
+ happy incidents list # List incidents
90
+ happy incidents list --status investigating # Filter by status
91
+ happy incidents get <id> # Show incident detail + timeline
92
+
93
+ happy incidents create "API is slow" \
94
+ --severity major \
95
+ --message "Investigating elevated latency" # Create incident
96
+
97
+ happy incidents update <id> \
98
+ --status identified \
99
+ --message "Found root cause: database" # Add status update
100
+
101
+ happy incidents resolve <id> \
102
+ --message "Fix deployed, monitoring" # Resolve incident
103
+ ```
104
+
105
+ ### Alerts
106
+
107
+ ```bash
108
+ happy alerts list # List alert channels
109
+
110
+ happy alerts create \
111
+ --name "Slack Engineering" \
112
+ --type slack \
113
+ --webhook-url https://hooks.slack.com/... # Create channel
114
+
115
+ happy alerts delete <id> # Delete channel
116
+ ```
117
+
118
+ ### Speed Test
119
+
120
+ Test any URL from 6 global regions with timing waterfall:
121
+
122
+ ```bash
123
+ happy speed-test https://example.com
124
+ ```
125
+
126
+ Output includes a waterfall visualization:
127
+
128
+ ```
129
+ Speed Test: https://example.com
130
+ Avg: 245ms Min: 180ms Max: 420ms
131
+
132
+ ┌──────────┬────────┬──────┬───────┬─────┬─────────┬─────┬──────┐
133
+ │ Region │ Status │ Code │ Total │ DNS │ Connect │ TLS │ TTFB │
134
+ ├──────────┼────────┼──────┼───────┼─────┼─────────┼─────┼──────┤
135
+ │ us-east │ up │ 200 │ 180ms │ 12ms│ 25ms │ 35ms│ 108ms│
136
+ │ eu-west │ up │ 200 │ 245ms │ 15ms│ 45ms │ 52ms│ 133ms│
137
+ └──────────┴────────┴──────┴───────┴─────┴─────────┴─────┴──────┘
138
+
139
+ Waterfall
140
+ us-east ██████████████████████ 180ms
141
+ eu-west ████████████████████████████ 245ms
142
+
143
+ ■ DNS ■ Connect ■ TLS ■ TTFB ■ Transfer
144
+ ```
145
+
146
+ No authentication required for speed tests.
147
+
148
+ ### Config-as-Code
149
+
150
+ Manage monitors declaratively with `happyuptime.yml`:
151
+
152
+ ```bash
153
+ happy config pull # Export current monitors to YAML
154
+ happy config push # Sync YAML to Happy Uptime
155
+ happy config push --dry-run # Preview changes without applying
156
+ happy config validate # Validate YAML syntax
157
+ ```
158
+
159
+ Example `happyuptime.yml`:
160
+
161
+ ```yaml
162
+ monitors:
163
+ - name: Production API
164
+ url: https://api.example.com/health
165
+ type: http
166
+ interval: 60
167
+ regions:
168
+ - us-east
169
+ - eu-west
170
+ - ap-southeast
171
+ tags:
172
+ - production
173
+ - critical
174
+
175
+ - name: Marketing Site
176
+ url: https://example.com
177
+ type: http
178
+ interval: 300
179
+ regions:
180
+ - us-east
181
+ - eu-west
182
+
183
+ - name: Database Backup
184
+ type: heartbeat
185
+ interval: 3600
186
+ ```
187
+
188
+ Environment variable substitution is supported:
189
+
190
+ ```yaml
191
+ monitors:
192
+ - name: API
193
+ url: ${API_URL}/health
194
+ headers:
195
+ Authorization: Bearer ${API_TOKEN}
196
+ ```
197
+
198
+ ## Environment Variables
199
+
200
+ | Variable | Description |
201
+ |---|---|
202
+ | `HAPPYUPTIME_API_KEY` | API key (alternative to `happy login`) |
203
+ | `HAPPYUPTIME_API_URL` | Custom API base URL (default: `https://happyuptime.com`) |
204
+
205
+ ## JSON Output
206
+
207
+ All commands support `--json` for machine-readable output:
208
+
209
+ ```bash
210
+ happy status --json | jq '.monitors | length'
211
+ happy monitors list --json | jq '.data[] | select(.status == "down")'
212
+ ```
213
+
214
+ ## CI/CD Usage
215
+
216
+ Use in GitHub Actions or other CI pipelines:
217
+
218
+ ```yaml
219
+ # .github/workflows/deploy.yml
220
+ - name: Check monitors after deploy
221
+ run: |
222
+ npx happyuptime status --json
223
+ env:
224
+ HAPPYUPTIME_API_KEY: ${{ secrets.HAPPYUPTIME_API_KEY }}
225
+ ```
226
+
227
+ ```yaml
228
+ # Create incident on deploy failure
229
+ - name: Report deploy failure
230
+ if: failure()
231
+ run: |
232
+ npx happyuptime incidents create "Deploy failed" \
233
+ --severity major \
234
+ --message "Deploy to production failed in CI"
235
+ env:
236
+ HAPPYUPTIME_API_KEY: ${{ secrets.HAPPYUPTIME_API_KEY }}
237
+ ```
238
+
239
+ ## Config File
240
+
241
+ Credentials are stored in `~/.happyuptime/config.json`:
242
+
243
+ ```json
244
+ {
245
+ "apiKey": "hu_xxx",
246
+ "apiUrl": "https://happyuptime.com"
247
+ }
248
+ ```
249
+
250
+ ## Requirements
251
+
252
+ - Node.js 18+
253
+
254
+ ## Links
255
+
256
+ - [Happy Uptime](https://happyuptime.com)
257
+ - [API Documentation](https://happyuptime.com/docs)
258
+ - [Dashboard](https://happyuptime.com/dashboard)
259
+ - [GitHub](https://github.com/seangeng/happyuptime)
260
+
261
+ ## License
262
+
263
+ MIT
package/dist/index.js ADDED
@@ -0,0 +1,956 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import chalk6 from 'chalk';
4
+ import { writeFileSync, existsSync, readFileSync, mkdirSync } from 'fs';
5
+ import { homedir } from 'os';
6
+ import { join } from 'path';
7
+ import Table from 'cli-table3';
8
+
9
+ // src/lib/constants.ts
10
+ var CONFIG_DIR = ".happyuptime";
11
+ var CONFIG_FILE = "config.json";
12
+ var VERSION = "1.0.0";
13
+ function configPath() {
14
+ return join(homedir(), CONFIG_DIR, CONFIG_FILE);
15
+ }
16
+ function configDir() {
17
+ return join(homedir(), CONFIG_DIR);
18
+ }
19
+ function loadConfig() {
20
+ const path = configPath();
21
+ if (!existsSync(path)) return {};
22
+ try {
23
+ return JSON.parse(readFileSync(path, "utf-8"));
24
+ } catch {
25
+ return {};
26
+ }
27
+ }
28
+ function saveConfig(config) {
29
+ const dir = configDir();
30
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
31
+ writeFileSync(configPath(), JSON.stringify(config, null, 2) + "\n");
32
+ }
33
+ function getApiKey() {
34
+ const envKey = process.env.HAPPYUPTIME_API_KEY;
35
+ if (envKey) return envKey;
36
+ const config = loadConfig();
37
+ if (config.apiKey) return config.apiKey;
38
+ console.error(
39
+ "No API key found. Run `happy login --api-key hu_xxx` or set HAPPYUPTIME_API_KEY."
40
+ );
41
+ process.exit(1);
42
+ }
43
+ function getApiUrl() {
44
+ const config = loadConfig();
45
+ return config.apiUrl || process.env.HAPPYUPTIME_API_URL || "https://happyuptime.com";
46
+ }
47
+
48
+ // src/lib/api.ts
49
+ var ApiError = class extends Error {
50
+ constructor(status, code, message) {
51
+ super(message);
52
+ this.status = status;
53
+ this.code = code;
54
+ this.name = "ApiError";
55
+ }
56
+ };
57
+ async function api(path, opts = {}) {
58
+ const baseUrl = getApiUrl();
59
+ const url = new URL(path, baseUrl);
60
+ if (opts.params) {
61
+ for (const [key, val] of Object.entries(opts.params)) {
62
+ if (val !== void 0) url.searchParams.set(key, String(val));
63
+ }
64
+ }
65
+ const headers = {
66
+ "Content-Type": "application/json",
67
+ "User-Agent": "happyuptime-cli/1.0.0"
68
+ };
69
+ if (!opts.noAuth) {
70
+ headers["Authorization"] = `Bearer ${getApiKey()}`;
71
+ }
72
+ const res = await fetch(url.toString(), {
73
+ method: opts.method || "GET",
74
+ headers,
75
+ body: opts.body ? JSON.stringify(opts.body) : void 0
76
+ });
77
+ if (!res.ok) {
78
+ let errBody = {};
79
+ try {
80
+ errBody = await res.json();
81
+ } catch {
82
+ }
83
+ throw new ApiError(
84
+ res.status,
85
+ errBody.error?.code || "unknown",
86
+ errBody.error?.message || `HTTP ${res.status}`
87
+ );
88
+ }
89
+ return res.json();
90
+ }
91
+ function table(headers, rows, opts) {
92
+ const t = new Table({
93
+ head: headers.map((h) => chalk6.bold.cyan(h)),
94
+ style: { head: [], border: ["gray"] },
95
+ ...{}
96
+ });
97
+ for (const row of rows) {
98
+ t.push(row.map(String));
99
+ }
100
+ console.log(t.toString());
101
+ }
102
+ function statusColor(status) {
103
+ switch (status.toLowerCase()) {
104
+ case "up":
105
+ case "operational":
106
+ case "resolved":
107
+ return chalk6.green(status);
108
+ case "degraded":
109
+ case "monitoring":
110
+ case "identified":
111
+ case "minor":
112
+ return chalk6.yellow(status);
113
+ case "down":
114
+ case "investigating":
115
+ case "major":
116
+ case "critical":
117
+ return chalk6.red(status);
118
+ case "paused":
119
+ return chalk6.gray(status);
120
+ default:
121
+ return status;
122
+ }
123
+ }
124
+ function kv(label, value) {
125
+ console.log(` ${chalk6.gray(label + ":")} ${value ?? chalk6.dim("\u2014")}`);
126
+ }
127
+ function heading(text) {
128
+ console.log(chalk6.bold("\n" + text));
129
+ }
130
+ function success(text) {
131
+ console.log(chalk6.green("\u2713") + " " + text);
132
+ }
133
+ function warn(text) {
134
+ console.log(chalk6.yellow("!") + " " + text);
135
+ }
136
+ function error(text) {
137
+ console.error(chalk6.red("\u2717") + " " + text);
138
+ }
139
+ function formatMs(ms) {
140
+ if (ms < 1e3) return `${Math.round(ms)}ms`;
141
+ return `${(ms / 1e3).toFixed(2)}s`;
142
+ }
143
+ function formatUptime(pct) {
144
+ const s = pct.toFixed(2) + "%";
145
+ if (pct >= 99.9) return chalk6.green(s);
146
+ if (pct >= 99) return chalk6.yellow(s);
147
+ return chalk6.red(s);
148
+ }
149
+ function timeAgo(dateStr) {
150
+ const diff = Date.now() - new Date(dateStr).getTime();
151
+ const mins = Math.floor(diff / 6e4);
152
+ if (mins < 1) return "just now";
153
+ if (mins < 60) return `${mins}m ago`;
154
+ const hours = Math.floor(mins / 60);
155
+ if (hours < 24) return `${hours}h ago`;
156
+ const days = Math.floor(hours / 24);
157
+ return `${days}d ago`;
158
+ }
159
+
160
+ // src/commands/login.ts
161
+ var loginCommand = new Command("login").description("Authenticate with Happy Uptime").option("--api-key <key>", "API key (starts with hu_)").option("--api-url <url>", "Custom API URL (default: https://happyuptime.com)").action(async (opts) => {
162
+ if (!opts.apiKey) {
163
+ error("Please provide an API key: happy login --api-key hu_xxx");
164
+ console.log(
165
+ chalk6.dim("\nCreate one at https://happyuptime.com/dashboard/settings")
166
+ );
167
+ process.exit(1);
168
+ }
169
+ if (!opts.apiKey.startsWith("hu_")) {
170
+ error("Invalid API key format. Keys start with hu_");
171
+ process.exit(1);
172
+ }
173
+ const config = loadConfig();
174
+ config.apiKey = opts.apiKey;
175
+ if (opts.apiUrl) config.apiUrl = opts.apiUrl;
176
+ saveConfig(config);
177
+ try {
178
+ const res = await api(
179
+ "/api/v1/analytics/summary"
180
+ );
181
+ success(
182
+ `Authenticated! ${res.data.monitors.total} monitors found.`
183
+ );
184
+ } catch (e) {
185
+ const apiErr = e;
186
+ if (apiErr.status === 401) {
187
+ warn("Key saved but could not verify \u2014 check that it's valid.");
188
+ } else {
189
+ success("API key saved.");
190
+ warn("Could not reach API to verify.");
191
+ }
192
+ }
193
+ console.log(chalk6.dim(`Config saved to ~/.happyuptime/config.json`));
194
+ });
195
+ var whoamiCommand = new Command("whoami").description("Show current auth status").action(async () => {
196
+ try {
197
+ const key = getApiKey();
198
+ const url = getApiUrl();
199
+ const masked = key.slice(0, 7) + "..." + key.slice(-4);
200
+ heading("Happy Uptime CLI");
201
+ kv("API Key", masked);
202
+ kv("API URL", url);
203
+ const res = await api(
204
+ "/api/v1/analytics/summary"
205
+ );
206
+ kv("Monitors", `${res.data.monitors.total} total`);
207
+ console.log();
208
+ } catch {
209
+ error("Could not fetch account info. Check your API key.");
210
+ process.exit(1);
211
+ }
212
+ });
213
+ var logoutCommand = new Command("logout").description("Remove saved credentials").action(() => {
214
+ saveConfig({});
215
+ success("Logged out. API key removed from ~/.happyuptime/config.json");
216
+ });
217
+ var monitorsCommand = new Command("monitors").description("Manage monitors").addCommand(listMonitors()).addCommand(getMonitor()).addCommand(createMonitor()).addCommand(deleteMonitor()).addCommand(pauseMonitor()).addCommand(resumeMonitor()).addCommand(checkMonitor());
218
+ function listMonitors() {
219
+ return new Command("list").alias("ls").description("List all monitors").option("-s, --status <status>", "Filter by status (up, down, degraded)").option("-t, --type <type>", "Filter by type (http, ping, tcp, dns, keyword, heartbeat)").option("--tag <tag>", "Filter by tag").option("-p, --page <n>", "Page number", "1").option("--per-page <n>", "Results per page", "25").option("--json", "Output raw JSON").action(async (opts) => {
220
+ try {
221
+ const res = await api(
222
+ "/api/v1/monitors",
223
+ { params: { status: opts.status, type: opts.type, tag: opts.tag, page: opts.page, per_page: opts.perPage } }
224
+ );
225
+ if (opts.json) {
226
+ console.log(JSON.stringify(res, null, 2));
227
+ return;
228
+ }
229
+ if (res.data.length === 0) {
230
+ warn("No monitors found.");
231
+ return;
232
+ }
233
+ heading(`Monitors (${res.pagination.total} total)`);
234
+ table(
235
+ ["ID", "Name", "Type", "Status", "Interval", "URL"],
236
+ res.data.map((m) => [
237
+ chalk6.dim(m.id.slice(0, 8)),
238
+ m.name,
239
+ m.type,
240
+ m.paused ? statusColor("paused") : statusColor(m.status),
241
+ `${m.interval_seconds}s`,
242
+ m.url?.length > 40 ? m.url.slice(0, 37) + "..." : m.url || "\u2014"
243
+ ])
244
+ );
245
+ if (res.pagination.total_pages > 1) {
246
+ console.log(
247
+ chalk6.dim(` Page ${res.pagination.page}/${res.pagination.total_pages}`)
248
+ );
249
+ }
250
+ } catch (e) {
251
+ handleError(e);
252
+ }
253
+ });
254
+ }
255
+ function getMonitor() {
256
+ return new Command("get").description("Show monitor details").argument("<id>", "Monitor ID").option("--json", "Output raw JSON").action(async (id, opts) => {
257
+ try {
258
+ const res = await api(
259
+ `/api/v1/monitors/${id}`
260
+ );
261
+ if (opts.json) {
262
+ console.log(JSON.stringify(res, null, 2));
263
+ return;
264
+ }
265
+ const m = res.data;
266
+ heading(m.name);
267
+ kv("ID", m.id);
268
+ kv("Type", m.type);
269
+ kv("URL", m.url);
270
+ kv("Status", statusColor(m.paused ? "paused" : m.status));
271
+ kv("Interval", `${m.interval_seconds}s`);
272
+ kv("Regions", m.regions?.join(", ") || "\u2014");
273
+ if (m.uptime_30d !== void 0) {
274
+ kv("Uptime (30d)", formatUptime(m.uptime_30d));
275
+ }
276
+ kv("Created", timeAgo(m.created_at));
277
+ console.log();
278
+ } catch (e) {
279
+ handleError(e);
280
+ }
281
+ });
282
+ }
283
+ function createMonitor() {
284
+ return new Command("create").description("Create a new monitor").requiredOption("-n, --name <name>", "Monitor name").requiredOption("-u, --url <url>", "URL to monitor").option("-t, --type <type>", "Monitor type", "http").option("-m, --method <method>", "HTTP method", "GET").option("-i, --interval <seconds>", "Check interval in seconds", "60").option("--timeout <ms>", "Timeout in milliseconds", "30000").option("-r, --regions <regions>", "Comma-separated regions", "us-east,eu-west").option("--tag <tags>", "Comma-separated tags").option("--json", "Output raw JSON").action(async (opts) => {
285
+ try {
286
+ const body = {
287
+ name: opts.name,
288
+ url: opts.url,
289
+ type: opts.type,
290
+ method: opts.method,
291
+ interval_seconds: parseInt(opts.interval),
292
+ timeout_ms: parseInt(opts.timeout),
293
+ regions: opts.regions.split(",").map((r) => r.trim())
294
+ };
295
+ if (opts.tag) body.tags = opts.tag.split(",").map((t) => t.trim());
296
+ const res = await api("/api/v1/monitors", {
297
+ method: "POST",
298
+ body
299
+ });
300
+ if (opts.json) {
301
+ console.log(JSON.stringify(res, null, 2));
302
+ return;
303
+ }
304
+ success(`Monitor "${res.data.name}" created (${res.data.id})`);
305
+ } catch (e) {
306
+ handleError(e);
307
+ }
308
+ });
309
+ }
310
+ function deleteMonitor() {
311
+ return new Command("delete").alias("rm").description("Delete a monitor").argument("<id>", "Monitor ID").option("-y, --yes", "Skip confirmation").action(async (id, opts) => {
312
+ if (!opts.yes) {
313
+ const { default: inquirer } = await import('inquirer');
314
+ const { confirm } = await inquirer.prompt([
315
+ { type: "confirm", name: "confirm", message: `Delete monitor ${id}?`, default: false }
316
+ ]);
317
+ if (!confirm) return;
318
+ }
319
+ try {
320
+ await api(`/api/v1/monitors/${id}`, { method: "DELETE" });
321
+ success(`Monitor ${id} deleted.`);
322
+ } catch (e) {
323
+ handleError(e);
324
+ }
325
+ });
326
+ }
327
+ function pauseMonitor() {
328
+ return new Command("pause").description("Pause a monitor").argument("<id>", "Monitor ID").action(async (id) => {
329
+ try {
330
+ await api(`/api/v1/monitors/${id}`, {
331
+ method: "PUT",
332
+ body: { paused: true }
333
+ });
334
+ success(`Monitor ${id} paused.`);
335
+ } catch (e) {
336
+ handleError(e);
337
+ }
338
+ });
339
+ }
340
+ function resumeMonitor() {
341
+ return new Command("resume").description("Resume a paused monitor").argument("<id>", "Monitor ID").action(async (id) => {
342
+ try {
343
+ await api(`/api/v1/monitors/${id}`, {
344
+ method: "PUT",
345
+ body: { paused: false }
346
+ });
347
+ success(`Monitor ${id} resumed.`);
348
+ } catch (e) {
349
+ handleError(e);
350
+ }
351
+ });
352
+ }
353
+ function checkMonitor() {
354
+ return new Command("check").description("Quick-check a URL from multiple regions").argument("<url>", "URL to check").option("--json", "Output raw JSON").action(async (url, opts) => {
355
+ const ora = (await import('ora')).default;
356
+ const spinner = ora(`Testing ${url}...`).start();
357
+ try {
358
+ const res = await api("/api/speed-test/run", {
359
+ method: "POST",
360
+ body: { url },
361
+ noAuth: true
362
+ });
363
+ spinner.stop();
364
+ if (opts.json) {
365
+ console.log(JSON.stringify(res, null, 2));
366
+ return;
367
+ }
368
+ heading(`Results for ${url}`);
369
+ table(
370
+ ["Region", "Status", "Code", "Total", "DNS", "Connect", "TLS", "TTFB"],
371
+ res.results.map((r) => [
372
+ r.region,
373
+ statusColor(r.status),
374
+ r.statusCode || "\u2014",
375
+ formatMs(r.responseTimeMs),
376
+ formatMs(r.timingDnsMs),
377
+ formatMs(r.timingConnectMs),
378
+ formatMs(r.timingTlsMs),
379
+ formatMs(r.timingTtfbMs)
380
+ ])
381
+ );
382
+ if (res.id) {
383
+ console.log(
384
+ chalk6.dim(`
385
+ Share: https://happyuptime.com/speed-test?id=${res.id}`)
386
+ );
387
+ }
388
+ } catch (e) {
389
+ spinner.stop();
390
+ handleError(e);
391
+ }
392
+ });
393
+ }
394
+ function handleError(e) {
395
+ if (e instanceof ApiError) {
396
+ error(`${e.message} (${e.status})`);
397
+ } else if (e instanceof Error) {
398
+ error(e.message);
399
+ }
400
+ process.exit(1);
401
+ }
402
+ var statusCommand = new Command("status").description("Quick overview of all monitors").option("--json", "Output raw JSON").action(async (opts) => {
403
+ try {
404
+ const [summary, monitors] = await Promise.all([
405
+ api("/api/v1/analytics/summary"),
406
+ api("/api/v1/monitors", {
407
+ params: { per_page: 100 }
408
+ })
409
+ ]);
410
+ if (opts.json) {
411
+ console.log(JSON.stringify({ summary: summary.data, monitors: monitors.data }, null, 2));
412
+ return;
413
+ }
414
+ const s = summary.data;
415
+ const allUp = s.monitors.down === 0 && s.monitors.degraded === 0;
416
+ console.log();
417
+ if (allUp) {
418
+ console.log(chalk6.green.bold(" \u25CF All systems operational"));
419
+ } else {
420
+ if (s.monitors.down > 0) {
421
+ console.log(chalk6.red.bold(` \u25CF ${s.monitors.down} monitor${s.monitors.down > 1 ? "s" : ""} down`));
422
+ }
423
+ if (s.monitors.degraded > 0) {
424
+ console.log(chalk6.yellow.bold(` \u25B2 ${s.monitors.degraded} monitor${s.monitors.degraded > 1 ? "s" : ""} degraded`));
425
+ }
426
+ }
427
+ if (s.active_incidents > 0) {
428
+ console.log(chalk6.red(` ! ${s.active_incidents} active incident${s.active_incidents > 1 ? "s" : ""}`));
429
+ }
430
+ heading("Monitors");
431
+ const sorted = [...monitors.data].sort((a, b) => {
432
+ const order = { down: 0, degraded: 1, up: 2 };
433
+ return (order[a.status] ?? 3) - (order[b.status] ?? 3);
434
+ });
435
+ for (const m of sorted) {
436
+ if (m.paused) {
437
+ console.log(` ${chalk6.gray("\u25CB")} ${chalk6.gray(m.name)} ${chalk6.dim("(paused)")}`);
438
+ } else if (m.status === "up") {
439
+ console.log(` ${chalk6.green("\u25CF")} ${m.name}`);
440
+ } else if (m.status === "degraded") {
441
+ console.log(` ${chalk6.yellow("\u25B2")} ${m.name} ${chalk6.yellow("degraded")}`);
442
+ } else {
443
+ console.log(` ${chalk6.red("\u25CF")} ${m.name} ${chalk6.red("DOWN")}`);
444
+ }
445
+ }
446
+ console.log(
447
+ chalk6.dim(`
448
+ ${s.monitors.total} monitors | ${s.monitors.up} up | ${s.monitors.down} down | ${s.monitors.degraded} degraded
449
+ `)
450
+ );
451
+ } catch (e) {
452
+ if (e instanceof Error) error(e.message);
453
+ process.exit(1);
454
+ }
455
+ });
456
+ var incidentsCommand = new Command("incidents").description("Manage incidents").addCommand(listIncidents()).addCommand(getIncident()).addCommand(createIncident()).addCommand(updateIncident()).addCommand(resolveIncident());
457
+ function listIncidents() {
458
+ return new Command("list").alias("ls").description("List incidents").option("-s, --status <status>", "Filter (investigating, identified, monitoring, resolved)").option("--severity <severity>", "Filter (critical, major, minor)").option("-p, --page <n>", "Page number", "1").option("--json", "Output raw JSON").action(async (opts) => {
459
+ try {
460
+ const res = await api(
461
+ "/api/v1/incidents",
462
+ { params: { status: opts.status, severity: opts.severity, page: opts.page } }
463
+ );
464
+ if (opts.json) {
465
+ console.log(JSON.stringify(res, null, 2));
466
+ return;
467
+ }
468
+ if (res.data.length === 0) {
469
+ success("No incidents found.");
470
+ return;
471
+ }
472
+ heading(`Incidents (${res.pagination.total})`);
473
+ table(
474
+ ["ID", "Title", "Status", "Severity", "Started"],
475
+ res.data.map((i) => [
476
+ chalk6.dim(i.id.slice(0, 8)),
477
+ i.title.length > 40 ? i.title.slice(0, 37) + "..." : i.title,
478
+ statusColor(i.status),
479
+ statusColor(i.severity),
480
+ timeAgo(i.started_at || i.created_at)
481
+ ])
482
+ );
483
+ } catch (e) {
484
+ handleError2(e);
485
+ }
486
+ });
487
+ }
488
+ function getIncident() {
489
+ return new Command("get").description("Show incident details").argument("<id>", "Incident ID").option("--json", "Output raw JSON").action(async (id, opts) => {
490
+ try {
491
+ const res = await api(`/api/v1/incidents/${id}`);
492
+ if (opts.json) {
493
+ console.log(JSON.stringify(res, null, 2));
494
+ return;
495
+ }
496
+ const inc = res.data;
497
+ heading(inc.title);
498
+ kv("ID", inc.id);
499
+ kv("Status", statusColor(inc.status));
500
+ kv("Severity", statusColor(inc.severity));
501
+ kv("Started", inc.started_at || inc.created_at);
502
+ if (inc.resolved_at) kv("Resolved", inc.resolved_at);
503
+ if (inc.affected_monitors?.length) {
504
+ kv(
505
+ "Affected",
506
+ inc.affected_monitors.map((m) => m.name).join(", ")
507
+ );
508
+ }
509
+ if (inc.updates?.length) {
510
+ heading("Timeline");
511
+ for (const u of inc.updates) {
512
+ console.log(
513
+ ` ${chalk6.dim(timeAgo(u.created_at))} ${statusColor(u.status)} \u2014 ${u.message}`
514
+ );
515
+ }
516
+ }
517
+ console.log();
518
+ } catch (e) {
519
+ handleError2(e);
520
+ }
521
+ });
522
+ }
523
+ function createIncident() {
524
+ return new Command("create").description("Create a new incident").argument("<title>", "Incident title").option("-s, --severity <severity>", "Severity (critical, major, minor)", "major").option("--status <status>", "Initial status", "investigating").option("-m, --message <message>", "Initial status update message").option("--json", "Output raw JSON").action(async (title, opts) => {
525
+ try {
526
+ const body = {
527
+ title,
528
+ severity: opts.severity,
529
+ status: opts.status
530
+ };
531
+ if (opts.message) body.message = opts.message;
532
+ const res = await api("/api/v1/incidents", {
533
+ method: "POST",
534
+ body
535
+ });
536
+ if (opts.json) {
537
+ console.log(JSON.stringify(res, null, 2));
538
+ return;
539
+ }
540
+ success(`Incident created: ${res.data.id}`);
541
+ } catch (e) {
542
+ handleError2(e);
543
+ }
544
+ });
545
+ }
546
+ function updateIncident() {
547
+ return new Command("update").description("Add a status update to an incident").argument("<id>", "Incident ID").requiredOption("-m, --message <message>", "Update message").option("-s, --status <status>", "New status (investigating, identified, monitoring)").option("--json", "Output raw JSON").action(async (id, opts) => {
548
+ try {
549
+ const body = { message: opts.message };
550
+ if (opts.status) body.status = opts.status;
551
+ const res = await api(`/api/v1/incidents/${id}/updates`, {
552
+ method: "POST",
553
+ body
554
+ });
555
+ if (opts.json) {
556
+ console.log(JSON.stringify(res, null, 2));
557
+ return;
558
+ }
559
+ success("Incident updated.");
560
+ } catch (e) {
561
+ handleError2(e);
562
+ }
563
+ });
564
+ }
565
+ function resolveIncident() {
566
+ return new Command("resolve").description("Resolve an incident").argument("<id>", "Incident ID").option("-m, --message <message>", "Resolution message").option("--json", "Output raw JSON").action(async (id, opts) => {
567
+ try {
568
+ const body = {};
569
+ if (opts.message) body.message = opts.message;
570
+ const res = await api(`/api/v1/incidents/${id}/resolve`, {
571
+ method: "POST",
572
+ body
573
+ });
574
+ if (opts.json) {
575
+ console.log(JSON.stringify(res, null, 2));
576
+ return;
577
+ }
578
+ success(`Incident ${id} resolved.`);
579
+ } catch (e) {
580
+ handleError2(e);
581
+ }
582
+ });
583
+ }
584
+ function handleError2(e) {
585
+ if (e instanceof ApiError) error(`${e.message} (${e.status})`);
586
+ else if (e instanceof Error) error(e.message);
587
+ process.exit(1);
588
+ }
589
+ var speedTestCommand = new Command("speed-test").description("Test any URL from 6 global regions").argument("<url>", "URL to test").option("--json", "Output raw JSON").action(async (url, opts) => {
590
+ const ora = (await import('ora')).default;
591
+ const spinner = ora(`Testing ${url} from 6 regions...`).start();
592
+ try {
593
+ const res = await api("/api/speed-test/run", {
594
+ method: "POST",
595
+ body: { url },
596
+ noAuth: true
597
+ });
598
+ spinner.stop();
599
+ if (opts.json) {
600
+ console.log(JSON.stringify(res, null, 2));
601
+ return;
602
+ }
603
+ heading(`Speed Test: ${url}`);
604
+ const times = res.results.filter((r) => r.status === "up").map((r) => r.responseTimeMs);
605
+ if (times.length > 0) {
606
+ const avg = times.reduce((a, b) => a + b, 0) / times.length;
607
+ const min = Math.min(...times);
608
+ const max = Math.max(...times);
609
+ console.log(
610
+ ` ${chalk6.dim("Avg:")} ${formatMs(avg)} ${chalk6.dim("Min:")} ${formatMs(min)} ${chalk6.dim("Max:")} ${formatMs(max)}`
611
+ );
612
+ }
613
+ console.log();
614
+ table(
615
+ ["Region", "Status", "Code", "Total", "DNS", "Connect", "TLS", "TTFB", "Transfer"],
616
+ res.results.map((r) => [
617
+ r.region,
618
+ r.errorMessage ? chalk6.red("fail") : statusColor(r.status),
619
+ r.statusCode || "\u2014",
620
+ formatMs(r.responseTimeMs),
621
+ formatMs(r.timingDnsMs),
622
+ formatMs(r.timingConnectMs),
623
+ formatMs(r.timingTlsMs),
624
+ formatMs(r.timingTtfbMs),
625
+ formatMs(r.timingTransferMs)
626
+ ])
627
+ );
628
+ heading("Waterfall");
629
+ const maxTime = Math.max(...res.results.map((r) => r.responseTimeMs), 1);
630
+ for (const r of res.results) {
631
+ const barLen = Math.round(r.responseTimeMs / maxTime * 40);
632
+ const dnsBar = Math.max(1, Math.round(r.timingDnsMs / maxTime * 40));
633
+ const connectBar = Math.max(1, Math.round(r.timingConnectMs / maxTime * 40));
634
+ const tlsBar = Math.max(1, Math.round(r.timingTlsMs / maxTime * 40));
635
+ const ttfbBar = Math.max(1, Math.round(r.timingTtfbMs / maxTime * 40));
636
+ const transferBar = Math.max(0, barLen - dnsBar - connectBar - tlsBar - ttfbBar);
637
+ const bar = chalk6.blue("\u2588".repeat(dnsBar)) + chalk6.cyan("\u2588".repeat(connectBar)) + chalk6.magenta("\u2588".repeat(tlsBar)) + chalk6.yellow("\u2588".repeat(ttfbBar)) + chalk6.green("\u2588".repeat(transferBar));
638
+ console.log(` ${r.region.padEnd(15)} ${bar} ${formatMs(r.responseTimeMs)}`);
639
+ }
640
+ console.log(
641
+ chalk6.dim(
642
+ `
643
+ ${chalk6.blue("\u25A0")} DNS ${chalk6.cyan("\u25A0")} Connect ${chalk6.magenta("\u25A0")} TLS ${chalk6.yellow("\u25A0")} TTFB ${chalk6.green("\u25A0")} Transfer`
644
+ )
645
+ );
646
+ const sslResult = res.results.find((r) => r.sslExpiryDays !== void 0);
647
+ if (sslResult?.sslExpiryDays) {
648
+ const days = sslResult.sslExpiryDays;
649
+ const sslColor = days > 30 ? chalk6.green : days > 7 ? chalk6.yellow : chalk6.red;
650
+ console.log(`
651
+ ${chalk6.dim("SSL expiry:")} ${sslColor(days + " days")}`);
652
+ }
653
+ if (res.id) {
654
+ console.log(
655
+ chalk6.dim(`
656
+ Share: https://happyuptime.com/speed-test?id=${res.id}`)
657
+ );
658
+ }
659
+ console.log();
660
+ } catch (e) {
661
+ spinner.stop();
662
+ if (e instanceof ApiError) error(`${e.message} (${e.status})`);
663
+ else if (e instanceof Error) error(e.message);
664
+ process.exit(1);
665
+ }
666
+ });
667
+ var CONFIG_FILENAME = "happyuptime.yml";
668
+ var configCommand = new Command("config").description("Config-as-code (happyuptime.yml)").addCommand(configPull()).addCommand(configPush()).addCommand(configValidate());
669
+ function configPull() {
670
+ return new Command("pull").description("Generate happyuptime.yml from current monitors").option("-o, --output <file>", "Output file", CONFIG_FILENAME).action(async (opts) => {
671
+ const ora = (await import('ora')).default;
672
+ const spinner = ora("Fetching monitors...").start();
673
+ try {
674
+ const res = await api("/api/v1/monitors", {
675
+ params: { per_page: 200 }
676
+ });
677
+ spinner.stop();
678
+ const yaml = (await import('yaml')).default;
679
+ const config = {
680
+ monitors: res.data.map((m) => {
681
+ const mon = {
682
+ name: m.name,
683
+ url: m.url,
684
+ type: m.type,
685
+ interval: m.interval_seconds,
686
+ regions: m.regions
687
+ };
688
+ if (m.method && m.method !== "GET") mon.method = m.method;
689
+ if (m.timeout_ms !== 3e4) mon.timeout = m.timeout_ms;
690
+ if (m.tags?.length) mon.tags = m.tags;
691
+ return mon;
692
+ })
693
+ };
694
+ const content = yaml.stringify(config, { indent: 2 });
695
+ writeFileSync(opts.output, content);
696
+ success(`Config written to ${opts.output} (${res.data.length} monitors)`);
697
+ } catch (e) {
698
+ spinner.stop();
699
+ handleError3(e);
700
+ }
701
+ });
702
+ }
703
+ function configPush() {
704
+ return new Command("push").description("Sync happyuptime.yml to Happy Uptime").option("-f, --file <file>", "Config file path", CONFIG_FILENAME).option("-y, --yes", "Skip confirmation").option("--dry-run", "Show what would change without applying").action(async (opts) => {
705
+ if (!existsSync(opts.file)) {
706
+ error(`${opts.file} not found. Run \`happy config pull\` first.`);
707
+ process.exit(1);
708
+ }
709
+ const yaml = (await import('yaml')).default;
710
+ let config;
711
+ try {
712
+ const raw = readFileSync(opts.file, "utf-8");
713
+ const substituted = raw.replace(/\$\{(\w+)\}/g, (_, name) => {
714
+ const val = process.env[name];
715
+ if (!val) {
716
+ warn(`Environment variable ${name} is not set`);
717
+ return "";
718
+ }
719
+ return val;
720
+ });
721
+ config = yaml.parse(substituted);
722
+ } catch (e) {
723
+ error(`Invalid YAML: ${e.message}`);
724
+ process.exit(1);
725
+ }
726
+ if (!config.monitors?.length) {
727
+ warn("No monitors defined in config.");
728
+ return;
729
+ }
730
+ const ora = (await import('ora')).default;
731
+ const spinner = ora("Comparing with remote...").start();
732
+ try {
733
+ const existing = await api("/api/v1/monitors", {
734
+ params: { per_page: 200 }
735
+ });
736
+ spinner.stop();
737
+ const existingByName = new Map(existing.data.map((m) => [m.name, m]));
738
+ const toCreate = [];
739
+ const toUpdate = [];
740
+ for (const mon of config.monitors) {
741
+ const ex = existingByName.get(mon.name);
742
+ if (!ex) {
743
+ toCreate.push(mon);
744
+ } else {
745
+ const changes = {};
746
+ if (mon.url && mon.url !== ex.url) changes.url = mon.url;
747
+ if (mon.type && mon.type !== ex.type) changes.type = mon.type;
748
+ if (mon.interval && mon.interval !== ex.interval_seconds)
749
+ changes.interval_seconds = mon.interval;
750
+ if (mon.regions && JSON.stringify(mon.regions) !== JSON.stringify(ex.regions))
751
+ changes.regions = mon.regions;
752
+ if (Object.keys(changes).length > 0) {
753
+ toUpdate.push({ id: ex.id, body: changes });
754
+ }
755
+ }
756
+ }
757
+ console.log();
758
+ if (toCreate.length === 0 && toUpdate.length === 0) {
759
+ success("Everything is in sync.");
760
+ return;
761
+ }
762
+ if (toCreate.length > 0) {
763
+ console.log(chalk6.green(` + ${toCreate.length} monitor(s) to create`));
764
+ for (const m of toCreate) console.log(chalk6.green(` + ${m.name}`));
765
+ }
766
+ if (toUpdate.length > 0) {
767
+ console.log(chalk6.yellow(` ~ ${toUpdate.length} monitor(s) to update`));
768
+ }
769
+ if (opts.dryRun) {
770
+ console.log(chalk6.dim("\n (dry run \u2014 no changes applied)"));
771
+ return;
772
+ }
773
+ if (!opts.yes) {
774
+ const { default: inquirer } = await import('inquirer');
775
+ const { confirm } = await inquirer.prompt([
776
+ { type: "confirm", name: "confirm", message: "Apply changes?", default: true }
777
+ ]);
778
+ if (!confirm) return;
779
+ }
780
+ const applySpinner = ora("Applying...").start();
781
+ let created = 0;
782
+ let updated = 0;
783
+ for (const mon of toCreate) {
784
+ await api("/api/v1/monitors", {
785
+ method: "POST",
786
+ body: {
787
+ name: mon.name,
788
+ url: mon.url,
789
+ type: mon.type || "http",
790
+ method: mon.method || "GET",
791
+ interval_seconds: mon.interval || 60,
792
+ timeout_ms: mon.timeout || 3e4,
793
+ regions: mon.regions || ["us-east", "eu-west"],
794
+ tags: mon.tags,
795
+ headers: mon.headers,
796
+ assertions: mon.assertions
797
+ }
798
+ });
799
+ created++;
800
+ }
801
+ for (const { id, body } of toUpdate) {
802
+ await api(`/api/v1/monitors/${id}`, { method: "PUT", body });
803
+ updated++;
804
+ }
805
+ applySpinner.stop();
806
+ success(
807
+ `Done. ${created} created, ${updated} updated.`
808
+ );
809
+ } catch (e) {
810
+ spinner.stop();
811
+ handleError3(e);
812
+ }
813
+ });
814
+ }
815
+ function configValidate() {
816
+ return new Command("validate").description("Validate happyuptime.yml syntax").option("-f, --file <file>", "Config file path", CONFIG_FILENAME).action(async (opts) => {
817
+ if (!existsSync(opts.file)) {
818
+ error(`${opts.file} not found.`);
819
+ process.exit(1);
820
+ }
821
+ const yaml = (await import('yaml')).default;
822
+ try {
823
+ const raw = readFileSync(opts.file, "utf-8");
824
+ const config = yaml.parse(raw);
825
+ let errors = 0;
826
+ if (!config.monitors || !Array.isArray(config.monitors)) {
827
+ error("Missing or invalid 'monitors' array.");
828
+ errors++;
829
+ } else {
830
+ for (let i = 0; i < config.monitors.length; i++) {
831
+ const m = config.monitors[i];
832
+ if (!m.name) {
833
+ error(`monitors[${i}]: missing 'name'`);
834
+ errors++;
835
+ }
836
+ if (!m.url && m.type !== "heartbeat") {
837
+ error(`monitors[${i}] (${m.name || "unnamed"}): missing 'url'`);
838
+ errors++;
839
+ }
840
+ if (m.type && !["http", "ping", "tcp", "dns", "keyword", "heartbeat"].includes(m.type)) {
841
+ error(`monitors[${i}] (${m.name}): invalid type '${m.type}'`);
842
+ errors++;
843
+ }
844
+ }
845
+ }
846
+ if (errors === 0) {
847
+ success(
848
+ `${opts.file} is valid (${config.monitors?.length || 0} monitors defined)`
849
+ );
850
+ } else {
851
+ error(`${errors} error(s) found.`);
852
+ process.exit(1);
853
+ }
854
+ } catch (e) {
855
+ error(`Parse error: ${e.message}`);
856
+ process.exit(1);
857
+ }
858
+ });
859
+ }
860
+ function handleError3(e) {
861
+ if (e instanceof ApiError) error(`${e.message} (${e.status})`);
862
+ else if (e instanceof Error) error(e.message);
863
+ process.exit(1);
864
+ }
865
+ var alertsCommand = new Command("alerts").description("Manage alert channels").addCommand(listAlerts()).addCommand(createAlert()).addCommand(deleteAlert());
866
+ function listAlerts() {
867
+ return new Command("list").alias("ls").description("List alert channels").option("--json", "Output raw JSON").action(async (opts) => {
868
+ try {
869
+ const res = await api("/api/v1/alerts/channels");
870
+ if (opts.json) {
871
+ console.log(JSON.stringify(res, null, 2));
872
+ return;
873
+ }
874
+ if (res.data.length === 0) {
875
+ warn("No alert channels configured.");
876
+ return;
877
+ }
878
+ heading("Alert Channels");
879
+ table(
880
+ ["ID", "Name", "Type", "Default", "Created"],
881
+ res.data.map((ch) => [
882
+ chalk6.dim(ch.id.slice(0, 8)),
883
+ ch.name,
884
+ ch.type,
885
+ ch.is_default ? chalk6.green("yes") : "no",
886
+ timeAgo(ch.created_at)
887
+ ])
888
+ );
889
+ } catch (e) {
890
+ handleError4(e);
891
+ }
892
+ });
893
+ }
894
+ function createAlert() {
895
+ return new Command("create").description("Create an alert channel").requiredOption("-n, --name <name>", "Channel name").requiredOption("-t, --type <type>", "Type (slack, discord, webhook, email, telegram)").option("--webhook-url <url>", "Webhook URL (for slack, discord, webhook)").option("--email <email>", "Email address (for email type)").option("--default", "Set as default channel").option("--json", "Output raw JSON").action(async (opts) => {
896
+ try {
897
+ const config = {};
898
+ if (opts.webhookUrl) config.webhook_url = opts.webhookUrl;
899
+ if (opts.email) config.email = opts.email;
900
+ const res = await api("/api/v1/alerts/channels", {
901
+ method: "POST",
902
+ body: {
903
+ name: opts.name,
904
+ type: opts.type,
905
+ config,
906
+ is_default: opts.default || false
907
+ }
908
+ });
909
+ if (opts.json) {
910
+ console.log(JSON.stringify(res, null, 2));
911
+ return;
912
+ }
913
+ success(`Alert channel "${res.data.name}" created (${res.data.id})`);
914
+ } catch (e) {
915
+ handleError4(e);
916
+ }
917
+ });
918
+ }
919
+ function deleteAlert() {
920
+ return new Command("delete").alias("rm").description("Delete an alert channel").argument("<id>", "Channel ID").option("-y, --yes", "Skip confirmation").action(async (id, opts) => {
921
+ if (!opts.yes) {
922
+ const { default: inquirer } = await import('inquirer');
923
+ const { confirm } = await inquirer.prompt([
924
+ { type: "confirm", name: "confirm", message: `Delete alert channel ${id}?`, default: false }
925
+ ]);
926
+ if (!confirm) return;
927
+ }
928
+ try {
929
+ await api(`/api/v1/alerts/channels/${id}`, { method: "DELETE" });
930
+ success(`Alert channel ${id} deleted.`);
931
+ } catch (e) {
932
+ handleError4(e);
933
+ }
934
+ });
935
+ }
936
+ function handleError4(e) {
937
+ if (e instanceof ApiError) error(`${e.message} (${e.status})`);
938
+ else if (e instanceof Error) error(e.message);
939
+ process.exit(1);
940
+ }
941
+
942
+ // src/index.ts
943
+ var program = new Command().name("happy").description("Happy Uptime CLI \u2014 monitor uptime, manage incidents, check speed").version(VERSION, "-v, --version");
944
+ program.addCommand(loginCommand);
945
+ program.addCommand(logoutCommand);
946
+ program.addCommand(whoamiCommand);
947
+ program.addCommand(statusCommand);
948
+ program.addCommand(monitorsCommand);
949
+ program.addCommand(incidentsCommand);
950
+ program.addCommand(alertsCommand);
951
+ program.addCommand(speedTestCommand);
952
+ program.addCommand(configCommand);
953
+ program.parseAsync(process.argv).catch((err) => {
954
+ console.error(err);
955
+ process.exit(1);
956
+ });
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "happyuptime",
3
+ "version": "1.0.0",
4
+ "description": "CLI for Happy Uptime — monitor uptime, manage incidents, and check site speed from the terminal.",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "happyuptime": "dist/index.js",
9
+ "happy": "dist/index.js"
10
+ },
11
+ "files": [
12
+ "dist",
13
+ "README.md"
14
+ ],
15
+ "scripts": {
16
+ "build": "tsup",
17
+ "dev": "tsup --watch",
18
+ "prepublishOnly": "npm run build"
19
+ },
20
+ "keywords": [
21
+ "uptime",
22
+ "monitoring",
23
+ "status-page",
24
+ "cli",
25
+ "devops",
26
+ "incident-management",
27
+ "speed-test",
28
+ "alerting",
29
+ "sla"
30
+ ],
31
+ "author": "Happy Uptime <hello@happyuptime.com>",
32
+ "license": "MIT",
33
+ "homepage": "https://happyuptime.com",
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "git+https://github.com/seangeng/happyuptime.git",
37
+ "directory": "cli"
38
+ },
39
+ "bugs": {
40
+ "url": "https://github.com/seangeng/happyuptime/issues"
41
+ },
42
+ "engines": {
43
+ "node": ">=18"
44
+ },
45
+ "dependencies": {
46
+ "chalk": "^5.4.1",
47
+ "cli-table3": "^0.6.5",
48
+ "commander": "^13.1.0",
49
+ "inquirer": "^12.6.0",
50
+ "ora": "^8.2.0",
51
+ "yaml": "^2.7.1"
52
+ },
53
+ "devDependencies": {
54
+ "@types/node": "^22.15.0",
55
+ "tsup": "^8.4.0",
56
+ "typescript": "^5.9.3"
57
+ }
58
+ }