@griffin-app/griffin-cli 1.0.7 → 1.0.9

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,15 @@ 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, executeNotificationsTest, } from "./commands/hub/notifications.js";
20
+ import { executeSecretsList, executeSecretsSet, executeSecretsGet, executeSecretsDelete, } from "./commands/hub/secrets.js";
18
21
  const program = new Command();
19
22
  program
20
23
  .name("griffin")
@@ -51,7 +54,7 @@ env
51
54
  // Local command group
52
55
  const local = program.command("local").description("Local test execution");
53
56
  local
54
- .command("run <env>")
57
+ .command("run [env]")
55
58
  .description("Run tests locally against an environment")
56
59
  .action(async (env, options) => {
57
60
  await executeRunLocal({ env });
@@ -72,6 +75,18 @@ hub
72
75
  .action(async () => {
73
76
  await executeStatus();
74
77
  });
78
+ hub
79
+ .command("metrics [env]")
80
+ .description("Show metrics summary from the hub")
81
+ .option("--period <period>", "Time window: 1h, 6h, 24h, 7d, 30d", "24h")
82
+ .option("--json", "Output as JSON")
83
+ .action(async (env, options) => {
84
+ await executeMetrics({
85
+ period: options.period,
86
+ environment: env,
87
+ json: options.json,
88
+ });
89
+ });
75
90
  hub
76
91
  .command("runs")
77
92
  .description("Show recent runs from the hub")
@@ -84,14 +99,14 @@ hub
84
99
  });
85
100
  });
86
101
  hub
87
- .command("monitor <env>")
102
+ .command("plan [env]")
88
103
  .description("Show what changes would be applied")
89
104
  .option("--json", "Output in JSON format")
90
105
  .action(async (env, options) => {
91
106
  await executeMonitor({ ...options, env });
92
107
  });
93
108
  hub
94
- .command("apply <env>")
109
+ .command("apply [env]")
95
110
  .description("Apply changes to the hub")
96
111
  .option("--auto-approve", "Skip confirmation prompt")
97
112
  .option("--dry-run", "Show what would be done without making changes")
@@ -100,7 +115,7 @@ hub
100
115
  await executeApply({ ...options, env });
101
116
  });
102
117
  hub
103
- .command("run <env>")
118
+ .command("run [env]")
104
119
  .description("Trigger a monitor run on the hub")
105
120
  .requiredOption("--monitor <name>", "Monitor name to run")
106
121
  .option("--wait", "Wait for run to complete")
@@ -120,5 +135,82 @@ 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("View notification rules (rules are defined in monitor DSL and synced via griffin hub apply)");
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("test")
157
+ .description("Test a webhook configuration")
158
+ .requiredOption("--webhook <url>", "Webhook URL to test")
159
+ .action(async (options) => {
160
+ await executeNotificationsTest({
161
+ webhook: options.webhook,
162
+ });
163
+ });
164
+ // Secrets command group
165
+ const secrets = hub
166
+ .command("secrets")
167
+ .description("Manage secrets (platform storage)");
168
+ secrets
169
+ .command("list")
170
+ .description("List secrets for an environment")
171
+ .option("--environment <env>", "Environment name", "default")
172
+ .option("--json", "Output as JSON")
173
+ .action(async (options) => {
174
+ await executeSecretsList({
175
+ environment: options.environment,
176
+ json: options.json,
177
+ });
178
+ });
179
+ secrets
180
+ .command("set <name>")
181
+ .description("Create or update a secret (prompts for value)")
182
+ .option("--environment <env>", "Environment name", "default")
183
+ .option("--value <value>", "Secret value (avoid for sensitive data)")
184
+ .action(async (name, options) => {
185
+ await executeSecretsSet({
186
+ name,
187
+ environment: options.environment,
188
+ value: options.value,
189
+ });
190
+ });
191
+ secrets
192
+ .command("get <name>")
193
+ .description("Show secret metadata (not the value)")
194
+ .option("--environment <env>", "Environment name", "default")
195
+ .option("--json", "Output as JSON")
196
+ .action(async (name, options) => {
197
+ await executeSecretsGet({
198
+ name,
199
+ environment: options.environment,
200
+ json: options.json,
201
+ });
202
+ });
203
+ secrets
204
+ .command("delete <name>")
205
+ .description("Delete a secret")
206
+ .option("--environment <env>", "Environment name", "default")
207
+ .option("--force", "Skip confirmation prompt")
208
+ .action(async (name, options) => {
209
+ await executeSecretsDelete({
210
+ name,
211
+ environment: options.environment,
212
+ force: options.force,
213
+ });
214
+ });
123
215
  // Parse arguments
124
216
  program.parse();
@@ -16,13 +16,8 @@ export async function executeEnvList() {
16
16
  terminal.info("Available environments:");
17
17
  terminal.blank();
18
18
  for (const envName of environments) {
19
- const isDefault = state.defaultEnvironment === envName;
20
- const marker = isDefault
21
- ? terminal.colors.green("●")
22
- : terminal.colors.dim("○");
23
- const envDisplay = isDefault
24
- ? terminal.colors.cyan(envName) + terminal.colors.dim(" (default)")
25
- : envName;
19
+ const marker = terminal.colors.green("●");
20
+ const envDisplay = terminal.colors.cyan(envName);
26
21
  terminal.log(` ${marker} ${envDisplay}`);
27
22
  }
28
23
  terminal.blank();
@@ -1,5 +1,5 @@
1
1
  import { loadState, resolveEnvironment } from "../../core/state.js";
2
- import { discoverMonitors, formatDiscoveryErrors } from "../../core/discovery.js";
2
+ import { discoverMonitors, formatDiscoveryErrors, } from "../../core/discovery.js";
3
3
  import { computeDiff, formatDiff } from "../../core/diff.js";
4
4
  import { applyDiff, formatApplyResult } from "../../core/apply.js";
5
5
  import { createSdkWithCredentials } from "../../core/sdk.js";
@@ -42,7 +42,9 @@ export async function executeApply(options) {
42
42
  }
43
43
  spinner.succeed(`Found ${monitors.length} local monitor(s)`);
44
44
  // Fetch remote monitors for this project + environment
45
- const fetchSpinner = terminal.spinner("Fetching remote monitors...").start();
45
+ const fetchSpinner = terminal
46
+ .spinner("Fetching remote monitors...")
47
+ .start();
46
48
  const response = await withSDKErrorHandling(() => sdk.getMonitor({
47
49
  query: {
48
50
  projectId: state.projectId,
@@ -1,20 +1,20 @@
1
1
  // CLI implementation
2
2
  import { createAuthClient } from "better-auth/client";
3
3
  import { deviceAuthorizationClient, jwtClient, } from "better-auth/client/plugins";
4
- import { getProjectId, loadState, saveState } from "../../core/state.js";
4
+ import { loadState, saveState } from "../../core/state.js";
5
5
  import { saveHubCredentials } from "../../core/credentials.js";
6
6
  import { terminal } from "../../utils/terminal.js";
7
7
  import { randomBytes } from "crypto";
8
- import { createEmptyState } from "../../schemas/state.js";
9
- const baseURL = "http://localhost:4000/api/auth";
10
- const hubBaseUrl = "http://localhost:3000";
11
- //const baseURL = "https://cloud.griffin.app"
12
8
  const oauthGrant = "urn:ietf:params:oauth:grant-type:device_code";
13
- const authClient = createAuthClient({
14
- baseURL: baseURL,
15
- plugins: [deviceAuthorizationClient(), jwtClient()],
16
- });
9
+ function createAuthClientFromState(state) {
10
+ return createAuthClient({
11
+ baseURL: state.cloud.authUrl,
12
+ plugins: [deviceAuthorizationClient(), jwtClient()],
13
+ });
14
+ }
17
15
  async function pollForToken(clientId, deviceCode, interval) {
16
+ const state = await loadState();
17
+ const authClient = createAuthClientFromState(state);
18
18
  const { data, error } = await authClient.device.token({
19
19
  grant_type: oauthGrant,
20
20
  device_code: deviceCode,
@@ -39,16 +39,9 @@ async function pollForToken(clientId, deviceCode, interval) {
39
39
  }
40
40
  }
41
41
  export async function executeLogin() {
42
- let state;
43
- let clientId;
44
- try {
45
- state = await loadState();
46
- clientId = state.hub?.clientId;
47
- }
48
- catch (error) { }
49
- if (!clientId) {
50
- clientId = randomBytes(16).toString("hex");
51
- }
42
+ const state = await loadState();
43
+ const clientId = state.hub?.clientId ?? randomBytes(16).toString("hex");
44
+ const authClient = createAuthClientFromState(state);
52
45
  const { data } = await authClient.device.code({
53
46
  client_id: clientId,
54
47
  });
@@ -69,17 +62,11 @@ export async function executeLogin() {
69
62
  terminal.success("Login successful");
70
63
  terminal.log(` Token saved to user credentials`);
71
64
  }
72
- if (!state) {
73
- const projectId = await getProjectId();
74
- state = createEmptyState(projectId);
75
- }
76
- // Save hub config to project state (without token)
77
65
  await saveState({
78
66
  ...state,
79
67
  hub: {
80
68
  ...state.hub,
81
69
  clientId: clientId,
82
- baseUrl: hubBaseUrl,
83
70
  },
84
71
  });
85
72
  }
@@ -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
+ }
@@ -1,5 +1,5 @@
1
1
  import { loadState, resolveEnvironment } from "../../core/state.js";
2
- import { discoverMonitors, formatDiscoveryErrors } from "../../core/discovery.js";
2
+ import { discoverMonitors, formatDiscoveryErrors, } from "../../core/discovery.js";
3
3
  import { createSdkWithCredentials } from "../../core/sdk.js";
4
4
  import { computeDiff, formatDiff, formatDiffJson } from "../../core/diff.js";
5
5
  import { terminal } from "../../utils/terminal.js";
@@ -38,7 +38,9 @@ export async function executeMonitor(options) {
38
38
  // Create SDK clients with credentials
39
39
  const sdk = await createSdkWithCredentials(state.hub.baseUrl);
40
40
  // Fetch remote monitors for this project + environment
41
- const fetchSpinner = terminal.spinner("Fetching remote monitors...").start();
41
+ const fetchSpinner = terminal
42
+ .spinner("Fetching remote monitors...")
43
+ .start();
42
44
  const response = await withSDKErrorHandling(() => sdk.getMonitor({
43
45
  query: {
44
46
  projectId: state.projectId,
@@ -0,0 +1,17 @@
1
+ export interface NotificationsListOptions {
2
+ monitor?: string;
3
+ enabled?: boolean;
4
+ json?: boolean;
5
+ }
6
+ export interface NotificationsTestOptions {
7
+ webhook: string;
8
+ }
9
+ /**
10
+ * List notification rules (read-only).
11
+ * Rules are defined in monitor DSL and synced via `griffin hub apply`.
12
+ */
13
+ export declare function executeNotificationsList(options: NotificationsListOptions): Promise<void>;
14
+ /**
15
+ * Test a webhook
16
+ */
17
+ export declare function executeNotificationsTest(options: NotificationsTestOptions): Promise<void>;
@@ -0,0 +1,113 @@
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 (read-only).
7
+ * Rules are defined in monitor DSL and synced via `griffin hub apply`.
8
+ */
9
+ export async function executeNotificationsList(options) {
10
+ try {
11
+ const state = await loadState();
12
+ if (!state.hub?.baseUrl) {
13
+ terminal.error("Hub connection not configured.");
14
+ terminal.dim("Connect with:");
15
+ terminal.dim(" griffin hub connect --url <url> --token <token>");
16
+ terminal.exit(1);
17
+ }
18
+ const sdk = await createSdkWithCredentials(state.hub.baseUrl);
19
+ const response = await withSDKErrorHandling(() => sdk.getNotificationsRules({
20
+ query: {
21
+ monitorId: options.monitor,
22
+ enabled: options.enabled,
23
+ },
24
+ }), "Failed to fetch notification rules");
25
+ const rules = response?.data?.data || [];
26
+ if (options.json) {
27
+ terminal.log(JSON.stringify(rules, null, 2));
28
+ return;
29
+ }
30
+ if (rules.length === 0) {
31
+ terminal.info("No notification rules found.");
32
+ terminal.dim("Define notifications in your monitor DSL and run griffin hub apply.");
33
+ return;
34
+ }
35
+ terminal.info("Notification Rules (synced from monitor DSL)");
36
+ terminal.blank();
37
+ const table = terminal.table({
38
+ head: ["ID", "Monitor", "Integration", "Trigger", "Enabled"],
39
+ });
40
+ for (const rule of rules) {
41
+ const triggerDesc = formatTrigger(rule.trigger);
42
+ const enabled = rule.enabled ? "✓" : "✗";
43
+ table.push([
44
+ rule.id.substring(0, 8) + "...",
45
+ rule.monitorId ?? "-",
46
+ rule.integrationName ?? "-",
47
+ triggerDesc,
48
+ enabled,
49
+ ]);
50
+ }
51
+ terminal.log(table.toString());
52
+ }
53
+ catch (error) {
54
+ terminal.error(error.message);
55
+ terminal.exit(1);
56
+ }
57
+ }
58
+ /**
59
+ * Test a webhook
60
+ */
61
+ export async function executeNotificationsTest(options) {
62
+ try {
63
+ const state = await loadState();
64
+ if (!state.hub?.baseUrl) {
65
+ terminal.error("Hub connection not configured.");
66
+ terminal.dim("Connect with:");
67
+ terminal.dim(" griffin hub connect --url <url> --token <token>");
68
+ terminal.exit(1);
69
+ }
70
+ const sdk = await createSdkWithCredentials(state.hub.baseUrl);
71
+ const spinner = terminal.spinner("Sending test notification...").start();
72
+ const response = await withSDKErrorHandling(() => sdk.postNotificationsTest({
73
+ body: {
74
+ channel: {
75
+ type: "webhook",
76
+ url: options.webhook,
77
+ },
78
+ },
79
+ }), "Failed to send test notification");
80
+ spinner.stop();
81
+ const result = response?.data?.data;
82
+ if (result?.success) {
83
+ terminal.success(result.message || "Test notification delivered successfully");
84
+ }
85
+ else {
86
+ terminal.error("Test notification failed");
87
+ terminal.exit(1);
88
+ }
89
+ }
90
+ catch (error) {
91
+ terminal.error(error.message);
92
+ terminal.exit(1);
93
+ }
94
+ }
95
+ /**
96
+ * Format trigger for display
97
+ */
98
+ function formatTrigger(trigger) {
99
+ switch (trigger.type) {
100
+ case "run_failed":
101
+ return "Run failed";
102
+ case "run_recovered":
103
+ return "Run recovered";
104
+ case "consecutive_failures":
105
+ return `${trigger.threshold} consecutive failures`;
106
+ case "success_rate_below":
107
+ return `Success rate < ${trigger.threshold}% (${trigger.window_minutes}m)`;
108
+ case "latency_above":
109
+ return `${trigger.percentile} latency > ${trigger.threshold_ms}ms (${trigger.window_minutes}m)`;
110
+ default:
111
+ return String(trigger.type);
112
+ }
113
+ }
@@ -1,6 +1,6 @@
1
1
  import { loadState, resolveEnvironment } from "../../core/state.js";
2
2
  import { createSdkWithCredentials } from "../../core/sdk.js";
3
- import { discoverMonitors, formatDiscoveryErrors } from "../../core/discovery.js";
3
+ import { discoverMonitors, formatDiscoveryErrors, } from "../../core/discovery.js";
4
4
  import { computeDiff } from "../../core/diff.js";
5
5
  import { terminal } from "../../utils/terminal.js";
6
6
  import { withSDKErrorHandling } from "../../utils/sdk-error.js";
@@ -0,0 +1,35 @@
1
+ export interface SecretsListOptions {
2
+ environment?: string;
3
+ json?: boolean;
4
+ }
5
+ export interface SecretsSetOptions {
6
+ name: string;
7
+ environment?: string;
8
+ value?: string;
9
+ }
10
+ export interface SecretsGetOptions {
11
+ name: string;
12
+ environment?: string;
13
+ json?: boolean;
14
+ }
15
+ export interface SecretsDeleteOptions {
16
+ name: string;
17
+ environment?: string;
18
+ force?: boolean;
19
+ }
20
+ /**
21
+ * List secrets (metadata only)
22
+ */
23
+ export declare function executeSecretsList(options: SecretsListOptions): Promise<void>;
24
+ /**
25
+ * Set a secret (prompts for value if not provided)
26
+ */
27
+ export declare function executeSecretsSet(options: SecretsSetOptions): Promise<void>;
28
+ /**
29
+ * Get secret metadata only
30
+ */
31
+ export declare function executeSecretsGet(options: SecretsGetOptions): Promise<void>;
32
+ /**
33
+ * Delete a secret (with confirmation unless --force)
34
+ */
35
+ export declare function executeSecretsDelete(options: SecretsDeleteOptions): Promise<void>;