@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.
- package/README.md +69 -165
- package/dist/cli.js +21 -16
- package/dist/commands/env.d.ts +1 -22
- package/dist/commands/env.js +20 -109
- package/dist/commands/generate-key.js +16 -10
- package/dist/commands/hub/apply.js +51 -30
- package/dist/commands/hub/connect.js +7 -6
- package/dist/commands/hub/plan.js +31 -17
- package/dist/commands/hub/run.js +81 -52
- package/dist/commands/hub/runs.js +77 -37
- package/dist/commands/hub/status.js +12 -11
- package/dist/commands/init.js +32 -24
- package/dist/commands/local/run.js +30 -16
- package/dist/commands/validate.js +18 -17
- package/dist/core/apply.d.ts +2 -2
- package/dist/core/apply.js +33 -34
- package/dist/core/apply.test.js +45 -12
- package/dist/core/diff.d.ts +5 -5
- package/dist/core/diff.test.js +20 -11
- package/dist/core/discovery.d.ts +2 -3
- package/dist/core/discovery.js +3 -10
- package/dist/core/plan-diff.d.ts +4 -4
- package/dist/core/plan-diff.js +4 -3
- package/dist/core/sdk.d.ts +3 -11
- package/dist/core/sdk.js +14 -18
- package/dist/core/variables.js +14 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/resolve.d.ts +3 -0
- package/dist/resolve.js +9 -0
- package/dist/schemas/state.js +1 -3
- package/dist/test-runner.js +18 -22
- package/dist/utils/console.d.ts +5 -0
- package/dist/utils/console.js +5 -0
- package/dist/utils/terminal.d.ts +100 -0
- package/dist/utils/terminal.js +148 -0
- package/package.json +8 -4
|
@@ -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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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 {
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
23
|
-
|
|
23
|
+
terminal.info(`Applying to ${terminal.colors.cyan(envName)} environment`);
|
|
24
|
+
terminal.blank();
|
|
24
25
|
// Create SDK clients
|
|
25
|
-
const
|
|
26
|
-
baseUrl: state.runner
|
|
27
|
-
apiToken: state.runner
|
|
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
|
-
|
|
38
|
-
|
|
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
|
|
42
|
-
|
|
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
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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
|
-
|
|
57
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
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
|
-
|
|
72
|
-
|
|
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
|
-
|
|
95
|
+
terminal.exit(1);
|
|
75
96
|
}
|
|
76
97
|
}
|
|
77
98
|
catch (error) {
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
15
|
-
|
|
15
|
+
terminal.success("Hub connection configured");
|
|
16
|
+
terminal.log(` URL: ${terminal.colors.cyan(options.url)}`);
|
|
16
17
|
if (options.token) {
|
|
17
|
-
|
|
18
|
+
terminal.log(` API Token: ${terminal.colors.dim("***")}`);
|
|
18
19
|
}
|
|
19
|
-
|
|
20
|
+
terminal.blank();
|
|
20
21
|
}
|
|
21
22
|
catch (error) {
|
|
22
|
-
|
|
23
|
-
|
|
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 {
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
29
|
-
|
|
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
|
|
33
|
-
baseUrl: state.runner
|
|
34
|
-
apiToken: state.runner
|
|
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
|
|
38
|
-
|
|
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
|
-
|
|
57
|
+
terminal.log(formatDiffJson(diff));
|
|
44
58
|
}
|
|
45
59
|
else {
|
|
46
|
-
|
|
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
|
-
|
|
65
|
+
terminal.exit(2); // Exit code 2 indicates changes pending
|
|
52
66
|
}
|
|
53
67
|
}
|
|
54
68
|
catch (error) {
|
|
55
|
-
|
|
56
|
-
|
|
69
|
+
terminal.error(error.message);
|
|
70
|
+
terminal.exit(1);
|
|
57
71
|
}
|
|
58
72
|
}
|
package/dist/commands/hub/run.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { loadState, resolveEnvironment } from "../../core/state.js";
|
|
2
|
-
import {
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
22
|
-
baseUrl: state.runner
|
|
23
|
-
apiToken: state.runner
|
|
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
|
-
|
|
34
|
-
|
|
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
|
-
|
|
40
|
-
|
|
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
|
-
|
|
46
|
+
terminal.dim(` - ${p.plan.name}`);
|
|
43
47
|
}
|
|
44
|
-
|
|
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
|
|
48
|
-
const
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
71
|
-
|
|
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
|
-
|
|
86
|
+
terminal.warn("Running with --force (local changes not applied)");
|
|
74
87
|
}
|
|
75
|
-
const
|
|
76
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
const runId =
|
|
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
|
|
90
|
-
|
|
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
|
-
|
|
94
|
-
|
|
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
|
-
|
|
127
|
+
terminal.log(`Duration: ${terminal.colors.dim((run.duration_ms / 1000).toFixed(2) + "s")}`);
|
|
97
128
|
}
|
|
98
129
|
if (run.success !== undefined) {
|
|
99
|
-
|
|
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
|
-
|
|
103
|
-
|
|
134
|
+
terminal.blank();
|
|
135
|
+
terminal.error("Errors:");
|
|
104
136
|
for (const error of run.errors) {
|
|
105
|
-
|
|
137
|
+
terminal.dim(` - ${error}`);
|
|
106
138
|
}
|
|
107
139
|
}
|
|
108
140
|
if (!run.success) {
|
|
109
|
-
|
|
141
|
+
terminal.exit(1);
|
|
110
142
|
}
|
|
111
143
|
}
|
|
112
|
-
else {
|
|
113
|
-
process.stdout.write(".");
|
|
114
|
-
}
|
|
115
144
|
}
|
|
116
145
|
}
|
|
117
146
|
else {
|
|
118
|
-
|
|
119
|
-
|
|
147
|
+
terminal.blank();
|
|
148
|
+
terminal.dim("Run started. Use 'griffin hub runs' to check progress.");
|
|
120
149
|
}
|
|
121
150
|
}
|
|
122
151
|
catch (error) {
|
|
123
|
-
|
|
124
|
-
|
|
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 {
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
18
|
+
const sdk = createSdk({
|
|
18
19
|
baseUrl: state.runner.baseUrl,
|
|
19
20
|
apiToken: state.runner.apiToken || undefined,
|
|
20
21
|
});
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
59
|
-
|
|
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
|
}
|