@griffin-app/griffin-cli 1.0.39 → 1.0.40
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 +0 -0
- package/dist/commands/apply.d.ts +9 -0
- package/dist/commands/apply.js +76 -0
- package/dist/commands/config.d.ts +36 -0
- package/dist/commands/config.js +144 -0
- package/dist/commands/configure-runner-host.d.ts +2 -0
- package/dist/commands/configure-runner-host.js +41 -0
- package/dist/commands/deploy.d.ts +1 -0
- package/dist/commands/deploy.js +36 -0
- package/dist/commands/execute-remote.d.ts +1 -0
- package/dist/commands/execute-remote.js +33 -0
- package/dist/commands/hub/config.d.ts +27 -0
- package/dist/commands/hub/config.js +102 -0
- package/dist/commands/hub/plan.d.ts +8 -0
- package/dist/commands/hub/plan.js +75 -0
- package/dist/commands/local/config.d.ts +28 -0
- package/dist/commands/local/config.js +82 -0
- package/dist/commands/logs.d.ts +1 -0
- package/dist/commands/logs.js +20 -0
- package/dist/commands/plan.d.ts +8 -0
- package/dist/commands/plan.js +58 -0
- package/dist/commands/run-remote.d.ts +11 -0
- package/dist/commands/run-remote.js +98 -0
- package/dist/commands/run.d.ts +4 -0
- package/dist/commands/run.js +86 -0
- package/dist/commands/runner.d.ts +12 -0
- package/dist/commands/runner.js +53 -0
- package/dist/commands/status.d.ts +8 -0
- package/dist/commands/status.js +75 -0
- package/dist/core/plan-diff.d.ts +41 -0
- package/dist/core/plan-diff.js +257 -0
- package/dist/output/context.d.ts +18 -0
- package/dist/output/context.js +22 -0
- package/dist/output/index.d.ts +3 -0
- package/dist/output/index.js +2 -0
- package/dist/output/renderer.d.ts +6 -0
- package/dist/output/renderer.js +348 -0
- package/dist/output/types.d.ts +153 -0
- package/dist/output/types.js +1 -0
- package/dist/providers/registry.d.ts +24 -0
- package/dist/providers/registry.js +109 -0
- package/dist/schemas/payload.d.ts +6 -0
- package/dist/schemas/payload.js +8 -0
- package/dist/test-discovery.d.ts +4 -0
- package/dist/test-discovery.js +25 -0
- package/dist/test-runner.d.ts +6 -0
- package/dist/test-runner.js +56 -0
- package/dist/utils/console.d.ts +5 -0
- package/dist/utils/console.js +5 -0
- package/package.json +3 -3
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import objectHash from "object-hash";
|
|
2
|
+
import { NodeType } from "@griffin-app/griffin-ts/schema";
|
|
3
|
+
/**
|
|
4
|
+
* Compare two test monitors and return granular changes.
|
|
5
|
+
* Local monitor should be resolved (variables replaced with actual values).
|
|
6
|
+
*/
|
|
7
|
+
export function compareMonitors(local, remote) {
|
|
8
|
+
const nodeChanges = compareNodes(local.nodes, remote.nodes);
|
|
9
|
+
const edgeChanges = compareEdges(local.edges, remote.edges);
|
|
10
|
+
const topLevelChanges = compareTopLevel(local, remote);
|
|
11
|
+
const hasChanges = nodeChanges.length > 0 ||
|
|
12
|
+
edgeChanges.length > 0 ||
|
|
13
|
+
topLevelChanges.length > 0;
|
|
14
|
+
return {
|
|
15
|
+
hasChanges,
|
|
16
|
+
nodes: nodeChanges,
|
|
17
|
+
edges: edgeChanges,
|
|
18
|
+
topLevel: topLevelChanges,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Compare nodes between local and remote monitors
|
|
23
|
+
*/
|
|
24
|
+
function compareNodes(localNodes, remoteNodes) {
|
|
25
|
+
const changes = [];
|
|
26
|
+
// Build map of remote nodes by id
|
|
27
|
+
const remoteByID = new Map();
|
|
28
|
+
for (const node of remoteNodes) {
|
|
29
|
+
remoteByID.set(node.id, node);
|
|
30
|
+
}
|
|
31
|
+
const localIDs = new Set();
|
|
32
|
+
for (const node of localNodes) {
|
|
33
|
+
localIDs.add(node.id);
|
|
34
|
+
}
|
|
35
|
+
// Check local nodes
|
|
36
|
+
for (const local of localNodes) {
|
|
37
|
+
const remote = remoteByID.get(local.id);
|
|
38
|
+
if (!remote) {
|
|
39
|
+
// Node added
|
|
40
|
+
changes.push({
|
|
41
|
+
type: "add",
|
|
42
|
+
nodeId: local.id,
|
|
43
|
+
nodeType: local.type,
|
|
44
|
+
summary: getNodeSummary(local),
|
|
45
|
+
fieldChanges: [],
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
// Node exists - check for modifications
|
|
50
|
+
const fieldChanges = compareNodeFields(local, remote);
|
|
51
|
+
if (fieldChanges.length > 0) {
|
|
52
|
+
changes.push({
|
|
53
|
+
type: "modify",
|
|
54
|
+
nodeId: local.id,
|
|
55
|
+
nodeType: local.type,
|
|
56
|
+
summary: getNodeSummary(local),
|
|
57
|
+
fieldChanges,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Check for removed nodes
|
|
63
|
+
for (const remote of remoteNodes) {
|
|
64
|
+
if (!localIDs.has(remote.id)) {
|
|
65
|
+
changes.push({
|
|
66
|
+
type: "remove",
|
|
67
|
+
nodeId: remote.id,
|
|
68
|
+
nodeType: remote.type,
|
|
69
|
+
summary: getNodeSummary(remote),
|
|
70
|
+
fieldChanges: [],
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return changes;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Get a human-readable summary of a node
|
|
78
|
+
*/
|
|
79
|
+
function getNodeSummary(node) {
|
|
80
|
+
switch (node.type) {
|
|
81
|
+
case "HTTP_REQUEST":
|
|
82
|
+
return `${node.method} ${formatValue(node.path)}`;
|
|
83
|
+
case "WAIT":
|
|
84
|
+
return `wait ${node.duration_ms}ms`;
|
|
85
|
+
case "ASSERTION":
|
|
86
|
+
return `${node.assertions.length} assertion(s)`;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Format a value for display (handle VariableRef objects)
|
|
91
|
+
*/
|
|
92
|
+
function formatValue(value) {
|
|
93
|
+
if (typeof value === "string") {
|
|
94
|
+
return value;
|
|
95
|
+
}
|
|
96
|
+
if (value &&
|
|
97
|
+
typeof value === "object" &&
|
|
98
|
+
"$variable" in value &&
|
|
99
|
+
typeof value.$variable === "object") {
|
|
100
|
+
return `$\{${value.$variable.key}}`;
|
|
101
|
+
}
|
|
102
|
+
return JSON.stringify(value);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Compare fields within two nodes of the same type
|
|
106
|
+
*/
|
|
107
|
+
function compareNodeFields(local, remote) {
|
|
108
|
+
const changes = [];
|
|
109
|
+
// Type should match, but check anyway
|
|
110
|
+
if (local.type !== remote.type) {
|
|
111
|
+
changes.push({
|
|
112
|
+
field: "type",
|
|
113
|
+
oldValue: remote.type,
|
|
114
|
+
newValue: local.type,
|
|
115
|
+
});
|
|
116
|
+
return changes;
|
|
117
|
+
}
|
|
118
|
+
switch (local.type) {
|
|
119
|
+
case NodeType.HTTP_REQUEST:
|
|
120
|
+
compareHttpRequestFields(local, remote, changes);
|
|
121
|
+
break;
|
|
122
|
+
case NodeType.WAIT:
|
|
123
|
+
compareWaitFields(local, remote, changes);
|
|
124
|
+
break;
|
|
125
|
+
case NodeType.ASSERTION:
|
|
126
|
+
compareAssertionFields(local, remote, changes);
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
return changes;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Compare fields specific to HttpRequest nodes
|
|
133
|
+
*/
|
|
134
|
+
function compareHttpRequestFields(local, remote, changes) {
|
|
135
|
+
const fields = [
|
|
136
|
+
"method",
|
|
137
|
+
"path",
|
|
138
|
+
"base",
|
|
139
|
+
"headers",
|
|
140
|
+
"body",
|
|
141
|
+
"response_format",
|
|
142
|
+
];
|
|
143
|
+
for (const field of fields) {
|
|
144
|
+
const localVal = local[field];
|
|
145
|
+
const remoteVal = remote[field];
|
|
146
|
+
if (!deepEqual(localVal, remoteVal)) {
|
|
147
|
+
changes.push({
|
|
148
|
+
field: field,
|
|
149
|
+
oldValue: remoteVal,
|
|
150
|
+
newValue: localVal,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Compare fields specific to Wait nodes
|
|
157
|
+
*/
|
|
158
|
+
function compareWaitFields(local, remote, changes) {
|
|
159
|
+
if (local.duration_ms !== remote.duration_ms) {
|
|
160
|
+
changes.push({
|
|
161
|
+
field: "duration_ms",
|
|
162
|
+
oldValue: remote.duration_ms,
|
|
163
|
+
newValue: local.duration_ms,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Compare fields specific to Assertion nodes
|
|
169
|
+
*/
|
|
170
|
+
function compareAssertionFields(local, remote, changes) {
|
|
171
|
+
if (!deepEqual(local.assertions, remote.assertions)) {
|
|
172
|
+
changes.push({
|
|
173
|
+
field: "assertions",
|
|
174
|
+
oldValue: remote.assertions,
|
|
175
|
+
newValue: local.assertions,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Compare edges between local and remote monitors
|
|
181
|
+
*/
|
|
182
|
+
function compareEdges(localEdges, remoteEdges) {
|
|
183
|
+
const changes = [];
|
|
184
|
+
// Build map of remote edges by "from:to" key
|
|
185
|
+
const remoteByKey = new Map();
|
|
186
|
+
for (const edge of remoteEdges) {
|
|
187
|
+
remoteByKey.set(`${edge.from}:${edge.to}`, edge);
|
|
188
|
+
}
|
|
189
|
+
const localKeys = new Set();
|
|
190
|
+
for (const edge of localEdges) {
|
|
191
|
+
localKeys.add(`${edge.from}:${edge.to}`);
|
|
192
|
+
}
|
|
193
|
+
// Check local edges
|
|
194
|
+
for (const local of localEdges) {
|
|
195
|
+
const key = `${local.from}:${local.to}`;
|
|
196
|
+
if (!remoteByKey.has(key)) {
|
|
197
|
+
changes.push({
|
|
198
|
+
type: "add",
|
|
199
|
+
from: local.from,
|
|
200
|
+
to: local.to,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
// Check for removed edges
|
|
205
|
+
for (const remote of remoteEdges) {
|
|
206
|
+
const key = `${remote.from}:${remote.to}`;
|
|
207
|
+
if (!localKeys.has(key)) {
|
|
208
|
+
changes.push({
|
|
209
|
+
type: "remove",
|
|
210
|
+
from: remote.from,
|
|
211
|
+
to: remote.to,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return changes;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Compare top-level fields: frequency, version, locations
|
|
219
|
+
*/
|
|
220
|
+
function compareTopLevel(local, remote) {
|
|
221
|
+
const changes = [];
|
|
222
|
+
// Compare frequency
|
|
223
|
+
if (!deepEqual(local.frequency, remote.frequency)) {
|
|
224
|
+
changes.push({
|
|
225
|
+
field: "frequency",
|
|
226
|
+
oldValue: remote.frequency,
|
|
227
|
+
newValue: local.frequency,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
// Compare version
|
|
231
|
+
if (local.version !== remote.version) {
|
|
232
|
+
changes.push({
|
|
233
|
+
field: "version",
|
|
234
|
+
oldValue: remote.version,
|
|
235
|
+
newValue: local.version,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
// Compare locations (normalize empty array to undefined)
|
|
239
|
+
const localLocations = local.locations;
|
|
240
|
+
const remoteLocations = remote.locations && remote.locations.length > 0
|
|
241
|
+
? remote.locations
|
|
242
|
+
: undefined;
|
|
243
|
+
if (!deepEqual(localLocations, remoteLocations)) {
|
|
244
|
+
changes.push({
|
|
245
|
+
field: "locations",
|
|
246
|
+
oldValue: remoteLocations,
|
|
247
|
+
newValue: localLocations,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
return changes;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Deep equality check using object-hash
|
|
254
|
+
*/
|
|
255
|
+
function deepEqual(a, b) {
|
|
256
|
+
return objectHash(a ?? null) === objectHash(b ?? null);
|
|
257
|
+
}
|
|
@@ -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,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
|
+
}
|