@griffin-app/griffin-cli 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,5 @@
1
1
  import { randomBytes } from "node:crypto";
2
+ import { terminal } from "../utils/terminal.js";
2
3
  /**
3
4
  * Generate a cryptographically secure API key for griffin-runner authentication.
4
5
  *
@@ -14,14 +15,19 @@ export async function executeGenerateKey() {
14
15
  const keySecret = keyBytes.toString("hex");
15
16
  // Add prefix following the pattern: grfn_sk_<secret>
16
17
  const apiKey = `grfn_sk_${keySecret}`;
17
- console.log("\n✓ Generated API key:\n");
18
- console.log(` ${apiKey}\n`);
19
- console.log("⚠️ Store this key securely - it cannot be retrieved later.\n");
20
- console.log("To use this key:");
21
- console.log(" 1. Add it to your runner's AUTH_API_KEYS environment variable:");
22
- console.log(` AUTH_API_KEYS=${apiKey}`);
23
- console.log(" 2. Or add it to your .griffinrc.json:");
24
- console.log(` { "runner": { "apiToken": "${apiKey}" } }`);
25
- console.log(" 3. Or pass it via environment variable:");
26
- console.log(` GRIFFIN_API_TOKEN=${apiKey}\n`);
18
+ terminal.blank();
19
+ terminal.success("Generated API key:");
20
+ terminal.blank();
21
+ terminal.log(` ${terminal.colors.cyan(apiKey)}`);
22
+ terminal.blank();
23
+ terminal.warn("Store this key securely - it cannot be retrieved later.");
24
+ terminal.blank();
25
+ terminal.info("To use this key:");
26
+ terminal.dim(" 1. Add it to your runner's AUTH_API_KEYS environment variable:");
27
+ terminal.dim(` AUTH_API_KEYS=${apiKey}`);
28
+ terminal.dim(" 2. Or add it to your .griffinrc.json:");
29
+ terminal.dim(` { "runner": { "apiToken": "${apiKey}" } }`);
30
+ terminal.dim(" 3. Or pass it via environment variable:");
31
+ terminal.dim(` GRIFFIN_API_TOKEN=${apiKey}`);
32
+ terminal.blank();
27
33
  }
@@ -2,7 +2,8 @@ import { loadState, resolveEnvironment } from "../../core/state.js";
2
2
  import { discoverPlans, formatDiscoveryErrors } from "../../core/discovery.js";
3
3
  import { computeDiff, formatDiff } from "../../core/diff.js";
4
4
  import { applyDiff, formatApplyResult } from "../../core/apply.js";
5
- import { createSdkClients } from "../../core/sdk.js";
5
+ import { createSdk } from "../../core/sdk.js";
6
+ import { terminal } from "../../utils/terminal.js";
6
7
  /**
7
8
  * Apply changes to the hub
8
9
  */
@@ -14,17 +15,17 @@ export async function executeApply(options) {
14
15
  const envName = await resolveEnvironment(options.env);
15
16
  // Check if runner is configured
16
17
  if (!state.runner?.baseUrl) {
17
- console.error("Error: Hub connection not configured.");
18
- console.log("Connect with:");
19
- console.log(" griffin hub connect --url <url> --token <token>");
20
- process.exit(1);
18
+ terminal.error("Hub connection not configured.");
19
+ terminal.dim("Connect with:");
20
+ terminal.dim(" griffin hub connect --url <url> --token <token>");
21
+ terminal.exit(1);
21
22
  }
22
- console.log(`Applying to '${envName}' environment`);
23
- console.log("");
23
+ terminal.info(`Applying to ${terminal.colors.cyan(envName)} environment`);
24
+ terminal.blank();
24
25
  // Create SDK clients
25
- const { planApi } = createSdkClients({
26
- baseUrl: state.runner.baseUrl,
27
- apiToken: state.runner.apiToken || undefined,
26
+ const sdk = createSdk({
27
+ baseUrl: state.runner?.baseUrl || "",
28
+ apiToken: state.runner?.apiToken || "",
28
29
  });
29
30
  // Discover local plans
30
31
  const discoveryPattern = state.discovery?.pattern || "**/__griffin__/*.{ts,js}";
@@ -32,50 +33,70 @@ export async function executeApply(options) {
32
33
  "node_modules/**",
33
34
  "dist/**",
34
35
  ];
36
+ const spinner = terminal.spinner("Discovering local plans...").start();
35
37
  const { plans, errors } = await discoverPlans(discoveryPattern, discoveryIgnore);
36
38
  if (errors.length > 0) {
37
- console.error(formatDiscoveryErrors(errors));
38
- process.exit(1);
39
+ spinner.fail("Discovery failed");
40
+ terminal.error(formatDiscoveryErrors(errors));
41
+ terminal.exit(1);
39
42
  }
43
+ spinner.succeed(`Found ${plans.length} local plan(s)`);
40
44
  // Fetch remote plans for this project + environment
41
- const response = await planApi.planGet(state.projectId, envName);
42
- const remotePlans = response.data.data;
45
+ const fetchSpinner = terminal
46
+ .spinner("Fetching remote plans...")
47
+ .start();
48
+ const response = await sdk.getPlan({
49
+ query: {
50
+ projectId: state.projectId,
51
+ environment: envName,
52
+ },
53
+ });
54
+ const remotePlans = response?.data?.data;
55
+ fetchSpinner.succeed(`Found ${remotePlans.length} remote plan(s)`);
43
56
  // Compute diff (include deletions if --prune)
44
57
  const diff = computeDiff(plans.map((p) => p.plan), remotePlans, { includeDeletions: options.prune || false });
45
58
  // Show plan
46
- console.log(formatDiff(diff));
47
- console.log("");
59
+ terminal.blank();
60
+ terminal.log(formatDiff(diff));
61
+ terminal.blank();
48
62
  // Check if there are changes
49
63
  if (diff.summary.creates + diff.summary.updates + diff.summary.deletes ===
50
64
  0) {
51
- console.log("No changes to apply.");
65
+ terminal.success("No changes to apply.");
52
66
  return;
53
67
  }
54
68
  // Show deletions warning if --prune
55
69
  if (options.prune && diff.summary.deletes > 0) {
56
- console.warn(`⚠️ --prune will DELETE ${diff.summary.deletes} plan(s) from the hub`);
57
- console.log("");
70
+ terminal.warn(`--prune will DELETE ${diff.summary.deletes} plan(s) from the hub`);
71
+ terminal.blank();
58
72
  }
59
73
  // Ask for confirmation unless auto-approved
60
74
  if (!options.autoApprove && !options.dryRun) {
61
- console.log("Do you want to perform these actions? (yes/no)");
62
- // For now, just proceed - in a real implementation, we'd use readline
63
- // to get user input
64
- console.log("Note: Use --auto-approve flag to skip confirmation");
65
- console.log("");
75
+ const confirmed = await terminal.confirm("Do you want to perform these actions?");
76
+ if (!confirmed) {
77
+ terminal.warn("Apply cancelled.");
78
+ return;
79
+ }
66
80
  }
67
81
  // Apply changes with environment injection
68
- const result = await applyDiff(diff, planApi, state.projectId, envName, {
82
+ const applySpinner = terminal.spinner("Applying changes...").start();
83
+ const result = await applyDiff(diff, sdk, state.projectId, envName, {
69
84
  dryRun: options.dryRun,
70
85
  });
71
- console.log("");
72
- console.log(formatApplyResult(result));
86
+ if (result.success) {
87
+ applySpinner.succeed("Changes applied successfully");
88
+ }
89
+ else {
90
+ applySpinner.fail("Apply failed");
91
+ }
92
+ terminal.blank();
93
+ terminal.log(formatApplyResult(result));
73
94
  if (!result.success) {
74
- process.exit(1);
95
+ terminal.exit(1);
75
96
  }
76
97
  }
77
98
  catch (error) {
78
- console.error(`Error: ${error.message}`);
79
- process.exit(1);
99
+ terminal.error(error.message);
100
+ terminal.exit(1);
80
101
  }
81
102
  }
@@ -1,4 +1,5 @@
1
1
  import { loadState, saveState } from "../../core/state.js";
2
+ import { terminal } from "../../utils/terminal.js";
2
3
  /**
3
4
  * Configure hub connection settings
4
5
  */
@@ -11,15 +12,15 @@ export async function executeConnect(options) {
11
12
  apiToken: options.token,
12
13
  };
13
14
  await saveState(state);
14
- console.log("Hub connection configured");
15
- console.log(` URL: ${options.url}`);
15
+ terminal.success("Hub connection configured");
16
+ terminal.log(` URL: ${terminal.colors.cyan(options.url)}`);
16
17
  if (options.token) {
17
- console.log(" API Token: ***");
18
+ terminal.log(` API Token: ${terminal.colors.dim("***")}`);
18
19
  }
19
- console.log("");
20
+ terminal.blank();
20
21
  }
21
22
  catch (error) {
22
- console.error(`Error: ${error.message}`);
23
- process.exit(1);
23
+ terminal.error(error.message);
24
+ terminal.exit(1);
24
25
  }
25
26
  }
@@ -1,7 +1,8 @@
1
1
  import { loadState, resolveEnvironment } from "../../core/state.js";
2
2
  import { discoverPlans, formatDiscoveryErrors } from "../../core/discovery.js";
3
- import { createSdkClients } from "../../core/sdk.js";
3
+ import { createSdk } from "../../core/sdk.js";
4
4
  import { computeDiff, formatDiff, formatDiffJson } from "../../core/diff.js";
5
+ import { terminal } from "../../utils/terminal.js";
5
6
  /**
6
7
  * Show what changes would be applied
7
8
  */
@@ -12,10 +13,10 @@ export async function executePlan(options) {
12
13
  // Resolve environment
13
14
  const envName = await resolveEnvironment(options.env);
14
15
  if (!state.runner?.baseUrl) {
15
- console.error("Error: Hub connection not configured.");
16
- console.log("Connect with:");
17
- console.log(" griffin hub connect --url <url> --token <token>");
18
- process.exit(1);
16
+ terminal.error("Hub connection not configured.");
17
+ terminal.dim("Connect with:");
18
+ terminal.dim(" griffin hub connect --url <url> --token <token>");
19
+ terminal.exit(1);
19
20
  }
20
21
  // Discover local plans
21
22
  const discoveryPattern = state.discovery?.pattern || "**/__griffin__/*.{ts,js}";
@@ -23,36 +24,49 @@ export async function executePlan(options) {
23
24
  "node_modules/**",
24
25
  "dist/**",
25
26
  ];
27
+ const spinner = terminal.spinner("Discovering local plans...").start();
26
28
  const { plans, errors } = await discoverPlans(discoveryPattern, discoveryIgnore);
27
29
  if (errors.length > 0) {
28
- console.error(formatDiscoveryErrors(errors));
29
- process.exit(1);
30
+ spinner.fail("Discovery failed");
31
+ terminal.error(formatDiscoveryErrors(errors));
32
+ terminal.exit(1);
30
33
  }
34
+ spinner.succeed(`Found ${plans.length} local plan(s)`);
31
35
  // Create SDK clients
32
- const { planApi } = createSdkClients({
33
- baseUrl: state.runner.baseUrl,
34
- apiToken: state.runner.apiToken || undefined,
36
+ const sdk = createSdk({
37
+ baseUrl: state.runner?.baseUrl || "",
38
+ apiToken: state.runner?.apiToken || "",
35
39
  });
36
40
  // Fetch remote plans for this project + environment
37
- const response = await planApi.planGet(state.projectId, envName);
38
- const remotePlans = response.data.data.map((p) => p);
41
+ const fetchSpinner = terminal
42
+ .spinner("Fetching remote plans...")
43
+ .start();
44
+ const response = await sdk.getPlan({
45
+ query: {
46
+ projectId: state.projectId,
47
+ environment: envName,
48
+ },
49
+ });
50
+ const remotePlans = response?.data?.data;
51
+ fetchSpinner.succeed(`Found ${remotePlans.length} remote plan(s)`);
39
52
  // Compute diff (no deletions shown by default)
40
53
  const diff = computeDiff(plans.map((p) => p.plan), remotePlans, { includeDeletions: false });
54
+ terminal.blank();
41
55
  // Output
42
56
  if (options.json) {
43
- console.log(formatDiffJson(diff));
57
+ terminal.log(formatDiffJson(diff));
44
58
  }
45
59
  else {
46
- console.log(formatDiff(diff));
60
+ terminal.log(formatDiff(diff));
47
61
  }
48
62
  // Exit with error code if there are changes
49
63
  if (diff.summary.creates + diff.summary.updates + diff.summary.deletes >
50
64
  0) {
51
- process.exit(2); // Exit code 2 indicates changes pending
65
+ terminal.exit(2); // Exit code 2 indicates changes pending
52
66
  }
53
67
  }
54
68
  catch (error) {
55
- console.error(`Error: ${error.message}`);
56
- process.exit(1);
69
+ terminal.error(error.message);
70
+ terminal.exit(1);
57
71
  }
58
72
  }
@@ -1,7 +1,8 @@
1
1
  import { loadState, resolveEnvironment } from "../../core/state.js";
2
- import { createSdkClients } from "../../core/sdk.js";
2
+ import { createSdk } from "../../core/sdk.js";
3
3
  import { discoverPlans, formatDiscoveryErrors } from "../../core/discovery.js";
4
4
  import { computeDiff } from "../../core/diff.js";
5
+ import { terminal } from "../../utils/terminal.js";
5
6
  /**
6
7
  * Trigger a plan run on the hub
7
8
  */
@@ -12,15 +13,15 @@ export async function executeRun(options) {
12
13
  // Resolve environment
13
14
  const envName = await resolveEnvironment(options.env);
14
15
  if (!state.runner?.baseUrl) {
15
- console.error("Error: Hub connection not configured.");
16
- console.log("Connect with:");
17
- console.log(" griffin hub connect --url <url> --token <token>");
18
- process.exit(1);
16
+ terminal.error("Hub connection not configured.");
17
+ terminal.dim("Connect with:");
18
+ terminal.dim(" griffin hub connect --url <url> --token <token>");
19
+ terminal.exit(1);
19
20
  }
20
21
  // Create SDK clients
21
- const { planApi, runsApi } = createSdkClients({
22
- baseUrl: state.runner.baseUrl,
23
- apiToken: state.runner.apiToken || undefined,
22
+ const sdk = createSdk({
23
+ baseUrl: state.runner?.baseUrl || "",
24
+ apiToken: state.runner?.apiToken || "",
24
25
  });
25
26
  // Discover local plans
26
27
  const discoveryPattern = state.discovery?.pattern || "**/__griffin__/*.{ts,js}";
@@ -28,31 +29,42 @@ export async function executeRun(options) {
28
29
  "node_modules/**",
29
30
  "dist/**",
30
31
  ];
32
+ const spinner = terminal.spinner("Discovering local plans...").start();
31
33
  const { plans: discoveredPlans, errors } = await discoverPlans(discoveryPattern, discoveryIgnore);
32
34
  if (errors.length > 0) {
33
- console.error(formatDiscoveryErrors(errors));
34
- process.exit(1);
35
+ spinner.fail("Discovery failed");
36
+ terminal.error(formatDiscoveryErrors(errors));
37
+ terminal.exit(1);
35
38
  }
36
39
  // Find local plan by name
37
40
  const localPlan = discoveredPlans.find((p) => p.plan.name === options.plan);
38
41
  if (!localPlan) {
39
- console.error(`Error: Plan "${options.plan}" not found locally`);
40
- console.error("Available plans:");
42
+ spinner.fail(`Plan "${options.plan}" not found locally`);
43
+ terminal.blank();
44
+ terminal.info("Available plans:");
41
45
  for (const p of discoveredPlans) {
42
- console.error(` - ${p.plan.name}`);
46
+ terminal.dim(` - ${p.plan.name}`);
43
47
  }
44
- process.exit(1);
48
+ terminal.exit(1);
45
49
  }
50
+ spinner.succeed(`Found local plan: ${terminal.colors.cyan(options.plan)}`);
46
51
  // Fetch remote plans for this project + environment
47
- const response = await planApi.planGet(state.projectId, envName);
48
- const remotePlans = response.data.data;
52
+ const fetchSpinner = terminal.spinner("Checking hub...").start();
53
+ const response = await sdk.getPlan({
54
+ query: {
55
+ projectId: state.projectId,
56
+ environment: envName,
57
+ },
58
+ });
59
+ const remotePlans = response?.data?.data;
49
60
  // Find remote plan by name
50
61
  const remotePlan = remotePlans.find((p) => p.name === options.plan);
51
62
  if (!remotePlan) {
52
- console.error(`Error: Plan "${options.plan}" not found on hub`);
53
- console.error("Run 'griffin hub apply' to sync your plans first");
54
- process.exit(1);
63
+ fetchSpinner.fail(`Plan "${options.plan}" not found on hub`);
64
+ terminal.dim("Run 'griffin hub apply' to sync your plans first");
65
+ terminal.exit(1);
55
66
  }
67
+ fetchSpinner.succeed("Plan found on hub");
56
68
  // Compute diff to check if local plan differs from remote
57
69
  const diff = computeDiff([localPlan.plan], [remotePlan], {
58
70
  includeDeletions: false,
@@ -60,67 +72,84 @@ export async function executeRun(options) {
60
72
  const hasDiff = diff.actions.length > 0 &&
61
73
  diff.actions.some((a) => a.type === "update" || a.type === "create");
62
74
  if (hasDiff && !options.force) {
63
- console.error(`Error: Local plan "${options.plan}" differs from hub`);
64
- console.error("");
65
- console.error("The plan on the hub is different from your local version.");
66
- console.error("Run 'griffin hub apply' to sync, or use --force to run anyway.");
67
- process.exit(1);
75
+ terminal.error(`Local plan "${options.plan}" differs from hub`);
76
+ terminal.blank();
77
+ terminal.warn("The plan on the hub is different from your local version.");
78
+ terminal.dim("Run 'griffin hub apply' to sync, or use --force to run anyway.");
79
+ terminal.exit(1);
68
80
  }
69
81
  // Trigger the run
70
- console.log(`Triggering run for plan: ${options.plan}`);
71
- console.log(`Target environment: ${envName}`);
82
+ terminal.blank();
83
+ terminal.info(`Triggering run for plan: ${terminal.colors.cyan(options.plan)}`);
84
+ terminal.log(`Target environment: ${terminal.colors.cyan(envName)}`);
72
85
  if (hasDiff && options.force) {
73
- console.log("⚠️ Running with --force (local changes not applied)");
86
+ terminal.warn("Running with --force (local changes not applied)");
74
87
  }
75
- const runResponse = await runsApi.runsTriggerPlanIdPost(remotePlan.id, {
76
- environment: envName,
88
+ const triggerSpinner = terminal.spinner("Triggering run...").start();
89
+ const runResponse = await sdk.postRunsTriggerByPlanId({
90
+ path: {
91
+ planId: remotePlan.id,
92
+ },
93
+ body: {
94
+ environment: envName,
95
+ },
77
96
  });
78
- console.log(`Run ID: ${runResponse.data.data.id}`);
79
- console.log(`Status: ${runResponse.data.data.status}`);
80
- console.log(`Started: ${new Date(runResponse.data.data.startedAt).toLocaleString()}`);
97
+ const run = runResponse?.data?.data;
98
+ triggerSpinner.succeed("Run triggered");
99
+ terminal.blank();
100
+ terminal.log(`Run ID: ${terminal.colors.dim(run.id)}`);
101
+ terminal.log(`Status: ${terminal.colors.cyan(run.status)}`);
102
+ terminal.log(`Started: ${terminal.colors.dim(new Date(run.startedAt).toLocaleString())}`);
81
103
  // Wait for completion if requested
82
104
  if (options.wait) {
83
- console.log("");
84
- console.log("Waiting for run to complete...");
85
- const runId = runResponse.data.data.id;
105
+ terminal.blank();
106
+ const waitSpinner = terminal.spinner("Waiting for run to complete...").start();
107
+ const runId = run.id;
86
108
  let completed = false;
87
109
  while (!completed) {
88
110
  await new Promise((resolve) => setTimeout(resolve, 2000)); // Poll every 2 seconds
89
- const { data: runStatusResponse } = await runsApi.runsIdGet(runId);
90
- const run = runStatusResponse.data;
111
+ const runStatusResponse = await sdk.getRunsById({
112
+ path: {
113
+ id: runId,
114
+ },
115
+ });
116
+ const run = runStatusResponse?.data?.data;
91
117
  if (run.status === "completed" || run.status === "failed") {
92
118
  completed = true;
93
- console.log("");
94
- console.log(`✓ Run ${run.status}`);
119
+ if (run.success) {
120
+ waitSpinner.succeed(`Run ${run.status}`);
121
+ }
122
+ else {
123
+ waitSpinner.fail(`Run ${run.status}`);
124
+ }
125
+ terminal.blank();
95
126
  if (run.duration_ms) {
96
- console.log(`Duration: ${(run.duration_ms / 1000).toFixed(2)}s`);
127
+ terminal.log(`Duration: ${terminal.colors.dim((run.duration_ms / 1000).toFixed(2) + "s")}`);
97
128
  }
98
129
  if (run.success !== undefined) {
99
- console.log(`Success: ${run.success ? "Yes" : "No"}`);
130
+ const successText = run.success ? terminal.colors.green("Yes") : terminal.colors.red("No");
131
+ terminal.log(`Success: ${successText}`);
100
132
  }
101
133
  if (run.errors && run.errors.length > 0) {
102
- console.log("");
103
- console.log("Errors:");
134
+ terminal.blank();
135
+ terminal.error("Errors:");
104
136
  for (const error of run.errors) {
105
- console.log(` - ${error}`);
137
+ terminal.dim(` - ${error}`);
106
138
  }
107
139
  }
108
140
  if (!run.success) {
109
- process.exit(1);
141
+ terminal.exit(1);
110
142
  }
111
143
  }
112
- else {
113
- process.stdout.write(".");
114
- }
115
144
  }
116
145
  }
117
146
  else {
118
- console.log("");
119
- console.log("Run started. Use 'griffin hub runs' to check progress.");
147
+ terminal.blank();
148
+ terminal.dim("Run started. Use 'griffin hub runs' to check progress.");
120
149
  }
121
150
  }
122
151
  catch (error) {
123
- console.error(`Error: ${error.message}`);
124
- process.exit(1);
152
+ terminal.error(error.message);
153
+ terminal.exit(1);
125
154
  }
126
155
  }
@@ -1,5 +1,6 @@
1
1
  import { loadState } from "../../core/state.js";
2
- import { createSdkClients } from "../../core/sdk.js";
2
+ import { createSdk } from "../../core/sdk.js";
3
+ import { terminal } from "../../utils/terminal.js";
3
4
  /**
4
5
  * Show recent runs from the hub
5
6
  */
@@ -8,55 +9,94 @@ export async function executeRuns(options) {
8
9
  // Load state
9
10
  const state = await loadState();
10
11
  if (!state.runner?.baseUrl) {
11
- console.error("Error: Hub connection not configured.");
12
- console.log("Connect with:");
13
- console.log(" griffin hub connect --url <url> --token <token>");
14
- process.exit(1);
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);
15
16
  }
16
17
  // Create SDK clients
17
- const { runsApi } = createSdkClients({
18
+ const sdk = createSdk({
18
19
  baseUrl: state.runner.baseUrl,
19
20
  apiToken: state.runner.apiToken || undefined,
20
21
  });
21
- console.log(`Hub: ${state.runner.baseUrl}`);
22
- console.log("");
22
+ terminal.info(`Hub: ${terminal.colors.cyan(state.runner.baseUrl)}`);
23
+ terminal.blank();
23
24
  // Get recent runs
24
25
  const limit = options.limit || 10;
25
- const response = await runsApi.runsGet(options.plan, undefined, limit, 0);
26
- const runs = response.data;
27
- if (runs.total === 0) {
28
- console.log("No runs found.");
26
+ const spinner = terminal.spinner("Fetching runs...").start();
27
+ const response = await sdk.getRuns({
28
+ query: {
29
+ planId: options.plan,
30
+ limit: limit,
31
+ offset: 0,
32
+ },
33
+ });
34
+ const runsData = response?.data;
35
+ if (!runsData || runsData.total === 0) {
36
+ spinner.info("No runs found.");
29
37
  return;
30
38
  }
31
- console.log(`Recent runs (${runs.total} total):`);
32
- console.log("");
33
- for (const run of runs.runs) {
34
- const statusIcon = getStatusIcon(run.status, run.success);
35
- const duration = run.duration_ms
36
- ? ` (${(run.duration_ms / 1000).toFixed(2)}s)`
37
- : "";
38
- console.log(`${statusIcon} ${run.planName}`);
39
- console.log(` ID: ${run.id}`);
40
- console.log(` Status: ${run.status}${duration}`);
41
- console.log(` Started: ${new Date(run.startedAt).toLocaleString()}`);
42
- if (run.completedAt) {
43
- console.log(` Completed: ${new Date(run.completedAt).toLocaleString()}`);
39
+ if (!Array.isArray(runsData.data)) {
40
+ spinner.fail("Invalid response format");
41
+ terminal.error(`Expected data array but got: ${typeof runsData.data}`);
42
+ terminal.exit(1);
43
+ }
44
+ spinner.succeed(`Found ${runsData.total} run(s)`);
45
+ terminal.blank();
46
+ // Create table
47
+ const table = terminal.table({
48
+ head: ["Status", "Plan", "Duration", "Started"],
49
+ });
50
+ for (const run of runsData.data) {
51
+ try {
52
+ const statusIcon = getStatusIcon(run.status, run.success);
53
+ const duration = run.duration_ms
54
+ ? `${(run.duration_ms / 1000).toFixed(2)}s`
55
+ : "-";
56
+ const started = new Date(run.startedAt).toLocaleString();
57
+ table.push([
58
+ statusIcon,
59
+ run.planId || "-",
60
+ duration,
61
+ started,
62
+ ]);
44
63
  }
45
- if (run.errors && run.errors.length > 0) {
46
- console.log(` Errors:`);
47
- for (const error of run.errors.slice(0, 3)) {
48
- console.log(` - ${error}`);
49
- }
50
- if (run.errors.length > 3) {
51
- console.log(` ... and ${run.errors.length - 3} more`);
64
+ catch (error) {
65
+ terminal.error(`Error processing run ${run.id}: ${error.message}`);
66
+ }
67
+ }
68
+ try {
69
+ terminal.log(table.toString());
70
+ }
71
+ catch (error) {
72
+ terminal.error(`Error rendering table: ${error.message}`);
73
+ terminal.error(error.stack || "");
74
+ terminal.exit(1);
75
+ }
76
+ // Show detailed errors if any
77
+ const runsWithErrors = runsData.data.filter((run) => Array.isArray(run.errors) && run.errors.length > 0);
78
+ if (runsWithErrors.length > 0) {
79
+ terminal.blank();
80
+ terminal.warn("Runs with errors:");
81
+ terminal.blank();
82
+ for (const run of runsWithErrors) {
83
+ terminal.log(`${terminal.colors.red("✗")} ${terminal.colors.cyan(run.planId || run.id)}`);
84
+ if (Array.isArray(run.errors) && run.errors.length > 0) {
85
+ const errorsToShow = run.errors.slice(0, 3);
86
+ for (const error of errorsToShow) {
87
+ terminal.dim(` - ${String(error)}`);
88
+ }
89
+ if (run.errors.length > 3) {
90
+ terminal.dim(` ... and ${run.errors.length - 3} more`);
91
+ }
52
92
  }
93
+ terminal.blank();
53
94
  }
54
- console.log("");
55
95
  }
56
96
  }
57
97
  catch (error) {
58
- console.error(`Error: ${error.message}`);
59
- process.exit(1);
98
+ terminal.error(error.message);
99
+ terminal.exit(1);
60
100
  }
61
101
  }
62
102
  function getStatusIcon(status, success) {
@@ -66,9 +106,9 @@ function getStatusIcon(status, success) {
66
106
  case "running":
67
107
  return "🏃";
68
108
  case "completed":
69
- return success ? "" : "";
109
+ return success ? "" : "";
70
110
  case "failed":
71
- return "";
111
+ return "";
72
112
  default:
73
113
  return "•";
74
114
  }