@griffin-app/griffin-cli 1.0.7 → 1.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -9,12 +9,14 @@ import { executeRunLocal } from "./commands/local/run.js";
9
9
  // Hub commands
10
10
  import { executeConnect } from "./commands/hub/connect.js";
11
11
  import { executeStatus } from "./commands/hub/status.js";
12
+ import { executeMetrics } from "./commands/hub/metrics.js";
12
13
  import { executeRuns } from "./commands/hub/runs.js";
13
14
  import { executeMonitor } from "./commands/hub/monitor.js";
14
15
  import { executeApply } from "./commands/hub/apply.js";
15
16
  import { executeRun } from "./commands/hub/run.js";
16
17
  import { executeLogin } from "./commands/hub/login.js";
17
18
  import { executeLogout } from "./commands/hub/logout.js";
19
+ import { executeNotificationsList, executeNotificationsAdd, executeNotificationsDelete, executeNotificationsTest, } from "./commands/hub/notifications.js";
18
20
  const program = new Command();
19
21
  program
20
22
  .name("griffin")
@@ -72,6 +74,19 @@ hub
72
74
  .action(async () => {
73
75
  await executeStatus();
74
76
  });
77
+ hub
78
+ .command("metrics")
79
+ .description("Show metrics summary from the hub")
80
+ .option("--period <period>", "Time window: 1h, 6h, 24h, 7d, 30d", "24h")
81
+ .option("--environment <env>", "Filter by environment")
82
+ .option("--json", "Output as JSON")
83
+ .action(async (options) => {
84
+ await executeMetrics({
85
+ period: options.period,
86
+ environment: options.environment,
87
+ json: options.json,
88
+ });
89
+ });
75
90
  hub
76
91
  .command("runs")
77
92
  .description("Show recent runs from the hub")
@@ -120,5 +135,58 @@ hub
120
135
  .action(async (options) => {
121
136
  await executeLogout(options);
122
137
  });
138
+ // Notifications command group
139
+ const notifications = hub
140
+ .command("notifications")
141
+ .description("Manage notification rules");
142
+ notifications
143
+ .command("list")
144
+ .description("List notification rules")
145
+ .option("--monitor <id>", "Filter by monitor ID")
146
+ .option("--enabled", "Filter by enabled status")
147
+ .option("--json", "Output as JSON")
148
+ .action(async (options) => {
149
+ await executeNotificationsList({
150
+ monitor: options.monitor,
151
+ enabled: options.enabled ? true : undefined,
152
+ json: options.json,
153
+ });
154
+ });
155
+ notifications
156
+ .command("add")
157
+ .description("Create a notification rule")
158
+ .requiredOption("--name <name>", "Rule name")
159
+ .option("--monitor <id>", "Monitor ID (optional, applies to all if not specified)")
160
+ .option("--environment <env>", "Environment filter")
161
+ .option("--location <loc>", "Location filter")
162
+ .requiredOption("--trigger <trigger>", "Trigger: run_failed, run_recovered, consecutive_failures:N, success_rate_below:threshold:window_min, latency_above:threshold_ms:percentile:window_min")
163
+ .requiredOption("--webhook <url>", "Webhook URL")
164
+ .option("--cooldown <minutes>", "Cooldown period in minutes", "15")
165
+ .action(async (options) => {
166
+ await executeNotificationsAdd({
167
+ name: options.name,
168
+ monitor: options.monitor,
169
+ environment: options.environment,
170
+ location: options.location,
171
+ trigger: options.trigger,
172
+ webhook: options.webhook,
173
+ cooldown: options.cooldown ? parseInt(options.cooldown, 10) : undefined,
174
+ });
175
+ });
176
+ notifications
177
+ .command("delete <id>")
178
+ .description("Delete a notification rule")
179
+ .action(async (id) => {
180
+ await executeNotificationsDelete({ id });
181
+ });
182
+ notifications
183
+ .command("test")
184
+ .description("Test a webhook configuration")
185
+ .requiredOption("--webhook <url>", "Webhook URL to test")
186
+ .action(async (options) => {
187
+ await executeNotificationsTest({
188
+ webhook: options.webhook,
189
+ });
190
+ });
123
191
  // Parse arguments
124
192
  program.parse();
@@ -0,0 +1,12 @@
1
+ declare const METRICS_PERIODS: readonly ["1h", "6h", "24h", "7d", "30d"];
2
+ type MetricsPeriod = (typeof METRICS_PERIODS)[number];
3
+ export interface MetricsOptions {
4
+ period?: MetricsPeriod;
5
+ environment?: string;
6
+ json?: boolean;
7
+ }
8
+ /**
9
+ * Show metrics summary from the hub
10
+ */
11
+ export declare function executeMetrics(options: MetricsOptions): Promise<void>;
12
+ export {};
@@ -0,0 +1,78 @@
1
+ import { loadState } from "../../core/state.js";
2
+ import { getHubCredentials } from "../../core/credentials.js";
3
+ import { terminal } from "../../utils/terminal.js";
4
+ const METRICS_PERIODS = ["1h", "6h", "24h", "7d", "30d"];
5
+ /**
6
+ * Show metrics summary from the hub
7
+ */
8
+ export async function executeMetrics(options) {
9
+ try {
10
+ const state = await loadState();
11
+ if (!state.hub?.baseUrl) {
12
+ terminal.error("No hub connection configured.");
13
+ terminal.dim("Connect with:");
14
+ terminal.dim(" griffin hub connect --url <url> --token <token>");
15
+ terminal.exit(1);
16
+ }
17
+ const credentials = await getHubCredentials();
18
+ const token = credentials?.token;
19
+ if (!token) {
20
+ terminal.error("No API token. Run 'griffin hub login' or connect with --token.");
21
+ terminal.exit(1);
22
+ }
23
+ const baseUrl = state.hub.baseUrl;
24
+ const period = options.period ?? "24h";
25
+ const url = new URL(baseUrl);
26
+ url.pathname = "/metrics/summary";
27
+ url.searchParams.set("period", period);
28
+ if (options.environment) {
29
+ url.searchParams.set("environment", options.environment);
30
+ }
31
+ const spinner = terminal.spinner("Fetching metrics...").start();
32
+ const response = await fetch(url.toString(), {
33
+ headers: { Authorization: `Bearer ${token}` },
34
+ });
35
+ spinner.stop();
36
+ if (!response.ok) {
37
+ const text = await response.text();
38
+ terminal.error(`Failed to fetch metrics: ${response.status} ${response.statusText}`);
39
+ if (text)
40
+ terminal.dim(text);
41
+ terminal.exit(1);
42
+ }
43
+ const body = (await response.json());
44
+ const data = body.data;
45
+ if (options.json) {
46
+ terminal.log(JSON.stringify(data, null, 2));
47
+ return;
48
+ }
49
+ terminal.info(`Metrics (${period})`);
50
+ terminal.dim(`${new Date(data.periodStart).toLocaleString()} – ${new Date(data.periodEnd).toLocaleString()}`);
51
+ terminal.blank();
52
+ terminal.log("Monitors:");
53
+ terminal.log(` Total: ${data.monitors.total} Passing: ${terminal.colors.green(String(data.monitors.passing))} Failing: ${data.monitors.failing > 0 ? terminal.colors.red(String(data.monitors.failing)) : "0"} No recent runs: ${data.monitors.noRecentRuns}`);
54
+ terminal.blank();
55
+ terminal.log("Runs:");
56
+ terminal.log(` Total: ${data.runs.total} Success rate: ${data.runs.successRate.toFixed(1)}%`);
57
+ terminal.blank();
58
+ if (data.latency.p50DurationMs != null ||
59
+ data.latency.p95DurationMs != null ||
60
+ data.latency.p99DurationMs != null) {
61
+ terminal.log("Latency (completed runs):");
62
+ terminal.log(` p50: ${data.latency.p50DurationMs ?? "-"} ms p95: ${data.latency.p95DurationMs ?? "-"} ms p99: ${data.latency.p99DurationMs ?? "-"} ms`);
63
+ terminal.blank();
64
+ }
65
+ terminal.log(`Uptime: ${data.uptimePercent.toFixed(1)}%`);
66
+ terminal.blank();
67
+ if (data.failingMonitors.length > 0) {
68
+ terminal.warn("Failing monitors:");
69
+ for (const m of data.failingMonitors) {
70
+ terminal.log(` ${terminal.colors.red("✗")} ${m.monitorName} (${m.monitorId}) – ${m.consecutiveFailures} consecutive failure(s), last at ${new Date(m.lastFailureAt).toLocaleString()}`);
71
+ }
72
+ }
73
+ }
74
+ catch (error) {
75
+ terminal.error(error.message);
76
+ terminal.exit(1);
77
+ }
78
+ }
@@ -0,0 +1,36 @@
1
+ export interface NotificationsListOptions {
2
+ monitor?: string;
3
+ enabled?: boolean;
4
+ json?: boolean;
5
+ }
6
+ export interface NotificationsAddOptions {
7
+ name: string;
8
+ monitor?: string;
9
+ environment?: string;
10
+ location?: string;
11
+ trigger: string;
12
+ webhook: string;
13
+ cooldown?: number;
14
+ }
15
+ export interface NotificationsDeleteOptions {
16
+ id: string;
17
+ }
18
+ export interface NotificationsTestOptions {
19
+ webhook: string;
20
+ }
21
+ /**
22
+ * List notification rules
23
+ */
24
+ export declare function executeNotificationsList(options: NotificationsListOptions): Promise<void>;
25
+ /**
26
+ * Add a notification rule
27
+ */
28
+ export declare function executeNotificationsAdd(options: NotificationsAddOptions): Promise<void>;
29
+ /**
30
+ * Delete a notification rule
31
+ */
32
+ export declare function executeNotificationsDelete(options: NotificationsDeleteOptions): Promise<void>;
33
+ /**
34
+ * Test a webhook
35
+ */
36
+ export declare function executeNotificationsTest(options: NotificationsTestOptions): Promise<void>;
@@ -0,0 +1,225 @@
1
+ import { loadState } from "../../core/state.js";
2
+ import { createSdkWithCredentials } from "../../core/sdk.js";
3
+ import { terminal } from "../../utils/terminal.js";
4
+ import { withSDKErrorHandling } from "../../utils/sdk-error.js";
5
+ /**
6
+ * List notification rules
7
+ */
8
+ export async function executeNotificationsList(options) {
9
+ try {
10
+ const state = await loadState();
11
+ if (!state.hub?.baseUrl) {
12
+ terminal.error("Hub connection not configured.");
13
+ terminal.dim("Connect with:");
14
+ terminal.dim(" griffin hub connect --url <url> --token <token>");
15
+ terminal.exit(1);
16
+ }
17
+ const sdk = await createSdkWithCredentials(state.hub.baseUrl);
18
+ const response = await withSDKErrorHandling(() => sdk.getNotificationsRules({
19
+ query: {
20
+ monitorId: options.monitor,
21
+ enabled: options.enabled,
22
+ },
23
+ }), "Failed to fetch notification rules");
24
+ const rules = response?.data?.data || [];
25
+ if (options.json) {
26
+ terminal.log(JSON.stringify(rules, null, 2));
27
+ return;
28
+ }
29
+ if (rules.length === 0) {
30
+ terminal.info("No notification rules found.");
31
+ return;
32
+ }
33
+ terminal.info("Notification Rules");
34
+ terminal.blank();
35
+ const table = terminal.table({
36
+ head: ["ID", "Name", "Monitor", "Trigger", "Channels", "Enabled"],
37
+ });
38
+ for (const rule of rules) {
39
+ const triggerDesc = formatTrigger(rule.trigger);
40
+ const channels = rule.channels
41
+ .map((ch) => (ch.type === "webhook" ? "webhook" : ch.type))
42
+ .join(", ");
43
+ const monitorName = rule.monitorId || "all monitors";
44
+ const enabled = rule.enabled ? "✓" : "✗";
45
+ table.push([
46
+ rule.id.substring(0, 8) + "...",
47
+ rule.name,
48
+ monitorName,
49
+ triggerDesc,
50
+ channels,
51
+ enabled,
52
+ ]);
53
+ }
54
+ terminal.log(table.toString());
55
+ }
56
+ catch (error) {
57
+ terminal.error(error.message);
58
+ terminal.exit(1);
59
+ }
60
+ }
61
+ /**
62
+ * Add a notification rule
63
+ */
64
+ export async function executeNotificationsAdd(options) {
65
+ try {
66
+ const state = await loadState();
67
+ if (!state.hub?.baseUrl) {
68
+ terminal.error("Hub connection not configured.");
69
+ terminal.dim("Connect with:");
70
+ terminal.dim(" griffin hub connect --url <url> --token <token>");
71
+ terminal.exit(1);
72
+ }
73
+ // Parse trigger
74
+ const trigger = parseTrigger(options.trigger);
75
+ if (!trigger) {
76
+ terminal.error(`Invalid trigger: ${options.trigger}. Expected format: type[:value]`);
77
+ terminal.dim("Examples:");
78
+ terminal.dim(" run_failed");
79
+ terminal.dim(" consecutive_failures:3");
80
+ terminal.dim(" success_rate_below:80:60");
81
+ terminal.exit(1);
82
+ }
83
+ const sdk = await createSdkWithCredentials(state.hub.baseUrl);
84
+ const response = await withSDKErrorHandling(() => sdk.postNotificationsRules({
85
+ body: {
86
+ name: options.name,
87
+ monitorId: options.monitor,
88
+ environment: options.environment,
89
+ location: options.location,
90
+ trigger,
91
+ channels: [
92
+ {
93
+ type: "webhook",
94
+ url: options.webhook,
95
+ },
96
+ ],
97
+ cooldownMinutes: options.cooldown,
98
+ },
99
+ }), "Failed to create notification rule");
100
+ const rule = response?.data?.data;
101
+ if (rule) {
102
+ terminal.success(`Created notification rule: ${rule.id}`);
103
+ }
104
+ }
105
+ catch (error) {
106
+ terminal.error(error.message);
107
+ terminal.exit(1);
108
+ }
109
+ }
110
+ /**
111
+ * Delete a notification rule
112
+ */
113
+ export async function executeNotificationsDelete(options) {
114
+ try {
115
+ const state = await loadState();
116
+ if (!state.hub?.baseUrl) {
117
+ terminal.error("Hub connection not configured.");
118
+ terminal.dim("Connect with:");
119
+ terminal.dim(" griffin hub connect --url <url> --token <token>");
120
+ terminal.exit(1);
121
+ }
122
+ const sdk = await createSdkWithCredentials(state.hub.baseUrl);
123
+ await withSDKErrorHandling(() => sdk.deleteNotificationsRulesById({ path: { id: options.id } }), "Failed to delete notification rule");
124
+ terminal.success(`Deleted notification rule: ${options.id}`);
125
+ }
126
+ catch (error) {
127
+ terminal.error(error.message);
128
+ terminal.exit(1);
129
+ }
130
+ }
131
+ /**
132
+ * Test a webhook
133
+ */
134
+ export async function executeNotificationsTest(options) {
135
+ try {
136
+ const state = await loadState();
137
+ if (!state.hub?.baseUrl) {
138
+ terminal.error("Hub connection not configured.");
139
+ terminal.dim("Connect with:");
140
+ terminal.dim(" griffin hub connect --url <url> --token <token>");
141
+ terminal.exit(1);
142
+ }
143
+ const sdk = await createSdkWithCredentials(state.hub.baseUrl);
144
+ const spinner = terminal.spinner("Sending test notification...").start();
145
+ const response = await withSDKErrorHandling(() => sdk.postNotificationsTest({
146
+ body: {
147
+ channel: {
148
+ type: "webhook",
149
+ url: options.webhook,
150
+ },
151
+ },
152
+ }), "Failed to send test notification");
153
+ spinner.stop();
154
+ const result = response?.data?.data;
155
+ if (result?.success) {
156
+ terminal.success(result.message || "Test notification delivered successfully");
157
+ }
158
+ else {
159
+ terminal.error("Test notification failed");
160
+ terminal.exit(1);
161
+ }
162
+ }
163
+ catch (error) {
164
+ terminal.error(error.message);
165
+ terminal.exit(1);
166
+ }
167
+ }
168
+ /**
169
+ * Parse trigger string into trigger object
170
+ */
171
+ function parseTrigger(triggerStr) {
172
+ const parts = triggerStr.split(":");
173
+ const type = parts[0];
174
+ switch (type) {
175
+ case "run_failed":
176
+ return { type: "run_failed" };
177
+ case "run_recovered":
178
+ return { type: "run_recovered" };
179
+ case "consecutive_failures":
180
+ if (parts.length < 2)
181
+ return null;
182
+ return {
183
+ type: "consecutive_failures",
184
+ threshold: parseInt(parts[1], 10),
185
+ };
186
+ case "success_rate_below":
187
+ if (parts.length < 3)
188
+ return null;
189
+ return {
190
+ type: "success_rate_below",
191
+ threshold: parseFloat(parts[1]),
192
+ window_minutes: parseInt(parts[2], 10),
193
+ };
194
+ case "latency_above":
195
+ if (parts.length < 4)
196
+ return null;
197
+ return {
198
+ type: "latency_above",
199
+ threshold_ms: parseInt(parts[1], 10),
200
+ percentile: parts[2],
201
+ window_minutes: parseInt(parts[3], 10),
202
+ };
203
+ default:
204
+ return null;
205
+ }
206
+ }
207
+ /**
208
+ * Format trigger for display
209
+ */
210
+ function formatTrigger(trigger) {
211
+ switch (trigger.type) {
212
+ case "run_failed":
213
+ return "Run failed";
214
+ case "run_recovered":
215
+ return "Run recovered";
216
+ case "consecutive_failures":
217
+ return `${trigger.threshold} consecutive failures`;
218
+ case "success_rate_below":
219
+ return `Success rate < ${trigger.threshold}% (${trigger.window_minutes}m)`;
220
+ case "latency_above":
221
+ return `${trigger.percentile} latency > ${trigger.threshold_ms}ms (${trigger.window_minutes}m)`;
222
+ default:
223
+ return String(trigger.type);
224
+ }
225
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Output format for CLI commands.
3
+ * - human: formatted, colored, spinners and progress
4
+ * - json: single JSON object to stdout, no progress output
5
+ */
6
+ export type OutputFormat = "human" | "json";
7
+ /**
8
+ * Set the output format (called from cli.ts when parsing --output).
9
+ */
10
+ export declare function setOutputFormat(format: OutputFormat): void;
11
+ /**
12
+ * Get the current output format.
13
+ */
14
+ export declare function getOutputFormat(): OutputFormat;
15
+ /**
16
+ * True when output should be JSON-only (no spinners, no progress).
17
+ */
18
+ export declare function isJsonOutput(): boolean;
@@ -0,0 +1,22 @@
1
+ let currentFormat = "human";
2
+ /**
3
+ * Set the output format (called from cli.ts when parsing --output).
4
+ */
5
+ export function setOutputFormat(format) {
6
+ if (format !== "human" && format !== "json") {
7
+ throw new Error(`Invalid output format: ${format}. Use 'human' or 'json'.`);
8
+ }
9
+ currentFormat = format;
10
+ }
11
+ /**
12
+ * Get the current output format.
13
+ */
14
+ export function getOutputFormat() {
15
+ return currentFormat;
16
+ }
17
+ /**
18
+ * True when output should be JSON-only (no spinners, no progress).
19
+ */
20
+ export function isJsonOutput() {
21
+ return currentFormat === "json";
22
+ }
@@ -0,0 +1,3 @@
1
+ export { type CommandResult, type InitResultData, type ConnectResultData, type LoginResultData, type LogoutResultData, type GenerateKeyResultData, type ValidateResultData, type ValidateMonitorInfo, type EnvListResultData, type StatusResultData, type RunSummary, type RunsResultData, type MetricsResultData, type MonitorResultData, type ApplyResultData, type LocalRunResultData, type HubRunResultData, type NotificationRuleSummary, type NotificationsListResultData, type NotificationsAddResultData, type NotificationsDeleteResultData, type NotificationsTestResultData, } from "./types.js";
2
+ export { setOutputFormat, getOutputFormat, isJsonOutput, type OutputFormat, } from "./context.js";
3
+ export { render, type CommandName } from "./renderer.js";
@@ -0,0 +1,2 @@
1
+ export { setOutputFormat, getOutputFormat, isJsonOutput, } from "./context.js";
2
+ export { render } from "./renderer.js";
@@ -0,0 +1,6 @@
1
+ import type { CommandResult } from "./types.js";
2
+ export type CommandName = "init" | "validate" | "generate-key" | "env-list" | "local-run" | "hub-connect" | "hub-status" | "hub-metrics" | "hub-runs" | "hub-monitor" | "hub-apply" | "hub-run" | "hub-login" | "hub-logout" | "notifications-list" | "notifications-add" | "notifications-delete" | "notifications-test";
3
+ /**
4
+ * Render command result to stdout (human or JSON based on --output).
5
+ */
6
+ export declare function render<T>(result: CommandResult<T>, command: CommandName): void;
@@ -0,0 +1,348 @@
1
+ import pc from "picocolors";
2
+ import { getOutputFormat } from "./context.js";
3
+ import { terminal } from "../utils/terminal.js";
4
+ import { formatDiff } from "../core/diff.js";
5
+ import { formatApplyResult } from "../core/apply.js";
6
+ function toJsonPayload(result) {
7
+ if (result.success) {
8
+ return {
9
+ success: true,
10
+ ...result.data,
11
+ ...(result.warnings?.length ? { warnings: result.warnings } : {}),
12
+ };
13
+ }
14
+ return {
15
+ success: false,
16
+ error: result.error,
17
+ ...(result.hint ? { hint: result.hint } : {}),
18
+ };
19
+ }
20
+ /**
21
+ * Render command result to stdout (human or JSON based on --output).
22
+ */
23
+ export function render(result, command) {
24
+ if (getOutputFormat() === "json") {
25
+ console.log(JSON.stringify(toJsonPayload(result), null, 2));
26
+ return;
27
+ }
28
+ renderHuman(result, command);
29
+ }
30
+ function renderHuman(result, command) {
31
+ if (!result.success) {
32
+ terminal.error(result.error);
33
+ if (result.hint) {
34
+ terminal.dim(result.hint);
35
+ }
36
+ return;
37
+ }
38
+ const data = result.data;
39
+ switch (command) {
40
+ case "init":
41
+ renderInit(data);
42
+ break;
43
+ case "hub-connect":
44
+ renderConnect(data);
45
+ break;
46
+ case "hub-login":
47
+ case "hub-logout":
48
+ terminal.success("Success.");
49
+ terminal.blank();
50
+ break;
51
+ case "generate-key":
52
+ renderGenerateKey(data);
53
+ break;
54
+ case "validate":
55
+ renderValidate(data);
56
+ break;
57
+ case "env-list":
58
+ renderEnvList(data);
59
+ break;
60
+ case "hub-status":
61
+ renderStatus(data);
62
+ break;
63
+ case "hub-runs":
64
+ renderRuns(data);
65
+ break;
66
+ case "hub-metrics":
67
+ renderMetrics(data);
68
+ break;
69
+ case "hub-monitor":
70
+ renderMonitor(data);
71
+ break;
72
+ case "hub-apply":
73
+ renderApply(data);
74
+ break;
75
+ case "local-run":
76
+ renderLocalRun(data);
77
+ break;
78
+ case "hub-run":
79
+ renderHubRun(data);
80
+ break;
81
+ case "notifications-list":
82
+ renderNotificationsList(data);
83
+ break;
84
+ case "notifications-add":
85
+ renderNotificationsAdd(data);
86
+ break;
87
+ case "notifications-delete":
88
+ renderNotificationsDelete(data);
89
+ break;
90
+ case "notifications-test":
91
+ renderNotificationsTest(data);
92
+ break;
93
+ default:
94
+ terminal.success("Done.");
95
+ }
96
+ }
97
+ function renderInit(d) {
98
+ terminal.blank();
99
+ terminal.success("Initialization complete!");
100
+ terminal.blank();
101
+ terminal.info("Next steps:");
102
+ for (const step of d.nextSteps) {
103
+ terminal.dim(` ${step}`);
104
+ }
105
+ terminal.blank();
106
+ }
107
+ function renderConnect(d) {
108
+ terminal.success("Hub connection configured");
109
+ terminal.log(` URL: ${pc.cyan(d.url)}`);
110
+ if (d.tokenSet) {
111
+ terminal.log(` API Token: ${pc.dim("***")} (saved to user credentials)`);
112
+ }
113
+ terminal.blank();
114
+ }
115
+ function renderGenerateKey(d) {
116
+ terminal.blank();
117
+ terminal.success("Generated API key:");
118
+ terminal.blank();
119
+ terminal.log(` ${pc.cyan(d.apiKey)}`);
120
+ terminal.blank();
121
+ terminal.warn("Store this key securely - it cannot be retrieved later.");
122
+ terminal.blank();
123
+ terminal.info("To use this key:");
124
+ terminal.dim(" 1. Add it to your runner's AUTH_API_KEYS environment variable:");
125
+ terminal.dim(` AUTH_API_KEYS=${d.apiKey}`);
126
+ terminal.dim(" 2. Or add it to your .griffinrc.json:");
127
+ terminal.dim(` { "runner": { "apiToken": "${d.apiKey}" } }`);
128
+ terminal.dim(" 3. Or pass it via environment variable:");
129
+ terminal.dim(` GRIFFIN_API_TOKEN=${d.apiKey}`);
130
+ terminal.blank();
131
+ }
132
+ function renderValidate(d) {
133
+ terminal.blank();
134
+ for (const m of d.monitors) {
135
+ const exportInfo = m.exportName === "default" ? "" : pc.dim(` (${m.exportName})`);
136
+ terminal.log(` ${pc.green("●")} ${pc.cyan(m.name)}${exportInfo}`);
137
+ terminal.dim(` ${m.filePath}`);
138
+ terminal.dim(` Nodes: ${m.nodes}, Edges: ${m.edges}`);
139
+ if (m.frequency) {
140
+ terminal.dim(` Schedule: Every ${m.frequency.every} ${m.frequency.unit}`);
141
+ }
142
+ terminal.blank();
143
+ }
144
+ terminal.success("All monitors are valid");
145
+ }
146
+ function renderEnvList(d) {
147
+ terminal.info("Available environments:");
148
+ terminal.blank();
149
+ for (const envName of d.environments) {
150
+ const isDefault = d.default === envName;
151
+ const marker = isDefault ? pc.green("●") : pc.dim("○");
152
+ const envDisplay = isDefault
153
+ ? pc.cyan(envName) + pc.dim(" (default)")
154
+ : envName;
155
+ terminal.log(` ${marker} ${envDisplay}`);
156
+ }
157
+ terminal.blank();
158
+ }
159
+ function renderStatus(d) {
160
+ terminal.info("Hub connection:");
161
+ terminal.log(` URL: ${pc.cyan(d.url)}`);
162
+ if (d.token) {
163
+ terminal.log(` API Token: ${pc.dim(d.token.substring(0, 8) + "...")}`);
164
+ if (d.updatedAt) {
165
+ terminal.log(` Updated: ${pc.dim(new Date(d.updatedAt).toLocaleString())}`);
166
+ }
167
+ }
168
+ else {
169
+ terminal.log(` API Token: ${pc.dim("(not set)")}`);
170
+ }
171
+ terminal.blank();
172
+ }
173
+ function renderRuns(d) {
174
+ if (d.runs.length === 0) {
175
+ terminal.info("No runs found.");
176
+ return;
177
+ }
178
+ terminal.blank();
179
+ const table = terminal.table({
180
+ head: ["Status", "Monitor", "Duration", "Started"],
181
+ });
182
+ for (const run of d.runs) {
183
+ const statusIcon = getStatusIcon(run.status, run.success);
184
+ const duration = run.duration_ms
185
+ ? `${(run.duration_ms / 1000).toFixed(2)}s`
186
+ : "-";
187
+ const started = new Date(run.startedAt).toLocaleString();
188
+ table.push([statusIcon, run.monitorId || "-", duration, started]);
189
+ }
190
+ terminal.log(table.toString());
191
+ const runsWithErrors = d.runs.filter((r) => r.errors && r.errors.length > 0);
192
+ if (runsWithErrors.length > 0) {
193
+ terminal.blank();
194
+ terminal.warn("Runs with errors:");
195
+ terminal.blank();
196
+ for (const run of runsWithErrors) {
197
+ terminal.log(`${pc.red("✗")} ${pc.cyan(run.monitorId || run.id)}`);
198
+ if (run.errors) {
199
+ for (const err of run.errors.slice(0, 3)) {
200
+ terminal.dim(` - ${err}`);
201
+ }
202
+ if (run.errors.length > 3) {
203
+ terminal.dim(` ... and ${run.errors.length - 3} more`);
204
+ }
205
+ }
206
+ terminal.blank();
207
+ }
208
+ }
209
+ }
210
+ function getStatusIcon(status, success) {
211
+ switch (status) {
212
+ case "pending":
213
+ return "⏳";
214
+ case "running":
215
+ return "🏃";
216
+ case "completed":
217
+ return success ? "✅" : "❌";
218
+ case "failed":
219
+ return "❌";
220
+ default:
221
+ return "•";
222
+ }
223
+ }
224
+ function renderMetrics(d) {
225
+ const period = d.period;
226
+ terminal.info(`Metrics (${period})`);
227
+ terminal.dim(`${new Date(d.periodStart).toLocaleString()} – ${new Date(d.periodEnd).toLocaleString()}`);
228
+ terminal.blank();
229
+ terminal.log("Monitors:");
230
+ terminal.log(` Total: ${d.monitors.total} Passing: ${pc.green(String(d.monitors.passing))} Failing: ${d.monitors.failing > 0 ? pc.red(String(d.monitors.failing)) : "0"} No recent runs: ${d.monitors.noRecentRuns}`);
231
+ terminal.blank();
232
+ terminal.log("Runs:");
233
+ terminal.log(` Total: ${d.runs.total} Success rate: ${d.runs.successRate.toFixed(1)}%`);
234
+ terminal.blank();
235
+ if (d.latency.p50DurationMs != null ||
236
+ d.latency.p95DurationMs != null ||
237
+ d.latency.p99DurationMs != null) {
238
+ terminal.log("Latency (completed runs):");
239
+ terminal.log(` p50: ${d.latency.p50DurationMs ?? "-"} ms p95: ${d.latency.p95DurationMs ?? "-"} ms p99: ${d.latency.p99DurationMs ?? "-"} ms`);
240
+ terminal.blank();
241
+ }
242
+ terminal.log(`Uptime: ${d.uptimePercent.toFixed(1)}%`);
243
+ terminal.blank();
244
+ if (d.failingMonitors.length > 0) {
245
+ terminal.warn("Failing monitors:");
246
+ for (const m of d.failingMonitors) {
247
+ terminal.log(` ${pc.red("✗")} ${m.monitorName} (${m.monitorId}) – ${m.consecutiveFailures} consecutive failure(s), last at ${new Date(m.lastFailureAt).toLocaleString()}`);
248
+ }
249
+ }
250
+ }
251
+ function renderMonitor(d) {
252
+ terminal.blank();
253
+ terminal.log(formatDiff(d.diff));
254
+ }
255
+ function renderApply(d) {
256
+ terminal.blank();
257
+ terminal.log(formatApplyResult(d.applied));
258
+ }
259
+ function renderLocalRun(d) {
260
+ terminal.blank();
261
+ if (d.failed === 0) {
262
+ terminal.success(`All tests passed (${pc.bold(String(d.passed))} / ${d.total})`);
263
+ }
264
+ else {
265
+ terminal.error(`${pc.bold(String(d.failed))} test(s) failed, ${pc.bold(String(d.passed))} passed`);
266
+ }
267
+ }
268
+ function renderHubRun(d) {
269
+ terminal.blank();
270
+ terminal.log(`Run ID: ${pc.dim(d.runId)}`);
271
+ terminal.log(`Status: ${pc.cyan(d.status)}`);
272
+ terminal.log(`Started: ${pc.dim(new Date(d.startedAt).toLocaleString())}`);
273
+ if (d.duration_ms != null) {
274
+ terminal.log(`Duration: ${pc.dim((d.duration_ms / 1000).toFixed(2) + "s")}`);
275
+ }
276
+ if (d.success !== undefined) {
277
+ const successText = d.success ? pc.green("Yes") : pc.red("No");
278
+ terminal.log(`Success: ${successText}`);
279
+ }
280
+ if (d.errors && d.errors.length > 0) {
281
+ terminal.blank();
282
+ terminal.error("Errors:");
283
+ for (const err of d.errors) {
284
+ terminal.dim(` - ${err}`);
285
+ }
286
+ }
287
+ if (!d.waited) {
288
+ terminal.blank();
289
+ terminal.dim("Run started. Use 'griffin hub runs' to check progress.");
290
+ }
291
+ }
292
+ function renderNotificationsList(d) {
293
+ if (d.rules.length === 0) {
294
+ terminal.info("No notification rules found.");
295
+ return;
296
+ }
297
+ terminal.info("Notification Rules");
298
+ terminal.blank();
299
+ const table = terminal.table({
300
+ head: ["ID", "Name", "Monitor", "Trigger", "Channels", "Enabled"],
301
+ });
302
+ for (const rule of d.rules) {
303
+ const triggerDesc = formatTriggerDesc(rule.trigger);
304
+ table.push([
305
+ rule.id.substring(0, 8) + "...",
306
+ rule.name,
307
+ rule.monitorId ?? "all monitors",
308
+ triggerDesc,
309
+ rule.channels.join(", "),
310
+ rule.enabled ? "✓" : "✗",
311
+ ]);
312
+ }
313
+ terminal.log(table.toString());
314
+ }
315
+ function formatTriggerDesc(trigger) {
316
+ if (trigger && typeof trigger === "object" && "type" in trigger) {
317
+ const t = trigger;
318
+ switch (t.type) {
319
+ case "run_failed":
320
+ return "Run failed";
321
+ case "run_recovered":
322
+ return "Run recovered";
323
+ case "consecutive_failures":
324
+ return `${t.threshold} consecutive failures`;
325
+ case "success_rate_below":
326
+ return `Success rate < ${t.threshold}% (${t.window_minutes}m)`;
327
+ case "latency_above":
328
+ return `${t.percentile} latency > ${t.threshold_ms}ms (${t.window_minutes}m)`;
329
+ default:
330
+ return String(t.type);
331
+ }
332
+ }
333
+ return String(trigger);
334
+ }
335
+ function renderNotificationsAdd(d) {
336
+ terminal.success(`Created notification rule: ${d.id}`);
337
+ }
338
+ function renderNotificationsDelete(d) {
339
+ terminal.success(`Deleted notification rule: ${d.id}`);
340
+ }
341
+ function renderNotificationsTest(d) {
342
+ if (d.success) {
343
+ terminal.success(d.message || "Test notification delivered successfully");
344
+ }
345
+ else {
346
+ terminal.error("Test notification failed");
347
+ }
348
+ }
@@ -0,0 +1,153 @@
1
+ import type { ApplyResult } from "../core/apply.js";
2
+ import type { DiffResult } from "../core/diff.js";
3
+ /**
4
+ * Base result type for all CLI commands.
5
+ * Discriminated union: success yields data; failure yields error and optional hint.
6
+ * exitCode: if set, used when exiting (default 0 for success, 1 for failure).
7
+ */
8
+ export type CommandResult<T> = {
9
+ success: true;
10
+ data: T;
11
+ warnings?: string[];
12
+ exitCode?: number;
13
+ } | {
14
+ success: false;
15
+ error: string;
16
+ hint?: string;
17
+ exitCode?: number;
18
+ };
19
+ export interface InitResultData {
20
+ projectId: string;
21
+ stateFile: string;
22
+ environments: string[];
23
+ variablesCreated: boolean;
24
+ nextSteps: string[];
25
+ }
26
+ export interface ConnectResultData {
27
+ url: string;
28
+ tokenSet: boolean;
29
+ }
30
+ export interface LoginResultData {
31
+ success: true;
32
+ }
33
+ export interface LogoutResultData {
34
+ success: true;
35
+ }
36
+ export interface GenerateKeyResultData {
37
+ apiKey: string;
38
+ }
39
+ export interface ValidateMonitorInfo {
40
+ name: string;
41
+ filePath: string;
42
+ exportName: string;
43
+ nodes: number;
44
+ edges: number;
45
+ frequency?: {
46
+ every: number;
47
+ unit: string;
48
+ };
49
+ }
50
+ export interface ValidateResultData {
51
+ monitors: ValidateMonitorInfo[];
52
+ valid: boolean;
53
+ }
54
+ export interface EnvListResultData {
55
+ environments: string[];
56
+ default: string | null;
57
+ }
58
+ export interface StatusResultData {
59
+ url: string;
60
+ token: string | null;
61
+ updatedAt: string | null;
62
+ }
63
+ export interface RunSummary {
64
+ id: string;
65
+ monitorId: string | null;
66
+ status: string;
67
+ success?: boolean;
68
+ duration_ms?: number;
69
+ startedAt: string;
70
+ errors?: string[];
71
+ }
72
+ export interface RunsResultData {
73
+ runs: RunSummary[];
74
+ total: number;
75
+ }
76
+ export interface MetricsResultData {
77
+ period: string;
78
+ periodStart: string;
79
+ periodEnd: string;
80
+ monitors: {
81
+ total: number;
82
+ passing: number;
83
+ failing: number;
84
+ noRecentRuns: number;
85
+ };
86
+ runs: {
87
+ total: number;
88
+ successful: number;
89
+ failed: number;
90
+ successRate: number;
91
+ };
92
+ latency: {
93
+ p50DurationMs: number | null;
94
+ p95DurationMs: number | null;
95
+ p99DurationMs: number | null;
96
+ };
97
+ uptimePercent: number;
98
+ failingMonitors: Array<{
99
+ monitorId: string;
100
+ monitorName: string;
101
+ lastFailureAt: string;
102
+ consecutiveFailures: number;
103
+ }>;
104
+ }
105
+ export interface MonitorResultData {
106
+ diff: DiffResult;
107
+ hasChanges: boolean;
108
+ }
109
+ export interface ApplyResultData {
110
+ diff: DiffResult;
111
+ applied: ApplyResult;
112
+ dryRun: boolean;
113
+ }
114
+ export interface LocalRunResultData {
115
+ results: Array<{
116
+ file: string;
117
+ success: boolean;
118
+ }>;
119
+ passed: number;
120
+ failed: number;
121
+ total: number;
122
+ }
123
+ export interface HubRunResultData {
124
+ runId: string;
125
+ status: string;
126
+ success?: boolean;
127
+ duration_ms?: number;
128
+ startedAt: string;
129
+ errors?: string[];
130
+ waited?: boolean;
131
+ }
132
+ export interface NotificationRuleSummary {
133
+ id: string;
134
+ name: string;
135
+ monitorId: string | null;
136
+ trigger: unknown;
137
+ channels: string[];
138
+ enabled: boolean;
139
+ }
140
+ export interface NotificationsListResultData {
141
+ rules: NotificationRuleSummary[];
142
+ }
143
+ export interface NotificationsAddResultData {
144
+ id: string;
145
+ name: string;
146
+ }
147
+ export interface NotificationsDeleteResultData {
148
+ id: string;
149
+ }
150
+ export interface NotificationsTestResultData {
151
+ success: boolean;
152
+ message: string;
153
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -109,6 +109,8 @@ export async function withSDKErrorHandling(fn, context) {
109
109
  return await fn();
110
110
  }
111
111
  catch (error) {
112
+ console.error("error");
113
+ console.error(error);
112
114
  handleSDKError(error, context);
113
115
  }
114
116
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@griffin-app/griffin-cli",
3
- "version": "1.0.7",
3
+ "version": "1.0.8",
4
4
  "description": "CLI tool for running and managing griffin API tests",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -23,7 +23,7 @@
23
23
  "author": "",
24
24
  "license": "MIT",
25
25
  "dependencies": {
26
- "@griffin-app/griffin-hub-sdk": "1.0.7",
26
+ "@griffin-app/griffin-hub-sdk": "1.0.8",
27
27
  "@griffin-app/griffin-plan-executor": "0.1.14",
28
28
  "@griffin-app/griffin-ts": "0.1.14",
29
29
  "better-auth": "^1.4.17",