@griffin-app/griffin-cli 1.0.26 → 1.0.28
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 +122 -367
- package/dist/cli.js +48 -29
- package/dist/commands/env.d.ts +14 -3
- package/dist/commands/env.js +24 -22
- package/dist/commands/generate-key.d.ts +5 -6
- package/dist/commands/generate-key.js +20 -26
- package/dist/commands/hub/apply.d.ts +1 -0
- package/dist/commands/hub/apply.js +44 -29
- package/dist/commands/hub/connect.d.ts +2 -1
- package/dist/commands/hub/connect.js +18 -22
- package/dist/commands/hub/destroy.d.ts +2 -1
- package/dist/commands/hub/destroy.js +123 -119
- package/dist/commands/hub/integrations.d.ts +4 -1
- package/dist/commands/hub/integrations.js +160 -141
- package/dist/commands/hub/login.d.ts +13 -1
- package/dist/commands/hub/login.js +81 -15
- package/dist/commands/hub/logout.d.ts +2 -1
- package/dist/commands/hub/logout.js +7 -12
- package/dist/commands/hub/metrics.js +29 -27
- package/dist/commands/hub/monitor.js +16 -16
- package/dist/commands/hub/notifications.d.ts +3 -6
- package/dist/commands/hub/notifications.js +52 -38
- package/dist/commands/hub/run.d.ts +1 -0
- package/dist/commands/hub/run.js +114 -87
- package/dist/commands/hub/runs.d.ts +2 -0
- package/dist/commands/hub/runs.js +47 -45
- package/dist/commands/hub/secrets.d.ts +5 -5
- package/dist/commands/hub/secrets.js +80 -72
- package/dist/commands/hub/status.d.ts +4 -1
- package/dist/commands/hub/status.js +15 -9
- package/dist/commands/init.d.ts +2 -1
- package/dist/commands/init.js +31 -25
- package/dist/commands/local/run.d.ts +1 -0
- package/dist/commands/local/run.js +34 -26
- package/dist/commands/validate.d.ts +4 -1
- package/dist/commands/validate.js +23 -14
- package/dist/commands/variables.d.ts +17 -3
- package/dist/commands/variables.js +29 -28
- package/dist/core/credentials.d.ts +15 -0
- package/dist/core/credentials.js +37 -0
- package/dist/core/variables.js +4 -0
- package/dist/monitor-runner.js +0 -12
- package/dist/utils/command-wrapper.d.ts +9 -0
- package/dist/utils/command-wrapper.js +23 -0
- package/dist/utils/output.d.ts +66 -0
- package/dist/utils/output.js +202 -0
- package/dist/utils/sdk-error.d.ts +6 -1
- package/dist/utils/sdk-error.js +107 -77
- package/package.json +2 -2
|
@@ -1,73 +1,75 @@
|
|
|
1
1
|
import { createSdkFromState } from "../../core/sdk.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { withCommandErrorHandler } from "../../utils/command-wrapper.js";
|
|
2
|
+
import { handleSDKErrorWithOutput } from "../../utils/sdk-error.js";
|
|
3
|
+
import { createCommandHandler } from "../../utils/command-wrapper.js";
|
|
5
4
|
/**
|
|
6
5
|
* Show recent runs from the hub
|
|
7
6
|
*/
|
|
8
|
-
export const executeRuns =
|
|
9
|
-
// Load state
|
|
7
|
+
export const executeRuns = createCommandHandler("runs", async (options, output) => {
|
|
10
8
|
const sdk = await createSdkFromState();
|
|
11
|
-
// Get recent runs
|
|
12
9
|
const limit = options.limit || 10;
|
|
13
|
-
const spinner =
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
10
|
+
const spinner = output.spinner("Fetching runs...").start();
|
|
11
|
+
let response;
|
|
12
|
+
try {
|
|
13
|
+
response = await sdk.getRuns({
|
|
14
|
+
query: {
|
|
15
|
+
monitorId: options.monitor,
|
|
16
|
+
limit,
|
|
17
|
+
offset: 0,
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
handleSDKErrorWithOutput(err, output, "Failed to fetch runs");
|
|
23
|
+
}
|
|
24
|
+
const runsData = response.data;
|
|
22
25
|
if (!runsData || runsData.total === 0) {
|
|
23
26
|
spinner.info("No runs found.");
|
|
27
|
+
output.setData({ runs: [], total: 0 });
|
|
24
28
|
return;
|
|
25
29
|
}
|
|
26
30
|
spinner.succeed(`Found ${runsData.total} run(s)`);
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
31
|
+
output.setData({
|
|
32
|
+
runs: runsData.data.map((run) => ({
|
|
33
|
+
id: run.id,
|
|
34
|
+
status: run.status,
|
|
35
|
+
success: run.success,
|
|
36
|
+
monitorName: run.monitor?.name ?? "-",
|
|
37
|
+
durationMs: run.duration_ms ?? null,
|
|
38
|
+
startedAt: run.startedAt,
|
|
39
|
+
errors: run.errors ?? [],
|
|
40
|
+
})),
|
|
41
|
+
total: runsData.total,
|
|
42
|
+
});
|
|
43
|
+
output.blank();
|
|
44
|
+
const table = output.table({
|
|
30
45
|
head: ["Status", "Monitor", "Duration", "Started"],
|
|
31
46
|
});
|
|
32
47
|
for (const run of runsData.data) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
table.push([statusIcon, run.monitor.name || "-", duration, started]);
|
|
40
|
-
}
|
|
41
|
-
catch (error) {
|
|
42
|
-
terminal.error(`Error processing run ${run.id}: ${error.message}`);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
try {
|
|
46
|
-
terminal.log(table.toString());
|
|
47
|
-
}
|
|
48
|
-
catch (error) {
|
|
49
|
-
terminal.error(`Error rendering table: ${error.message}`);
|
|
50
|
-
terminal.error(error.stack || "");
|
|
51
|
-
terminal.exit(1);
|
|
48
|
+
const statusIcon = getStatusIcon(run.status, run.success);
|
|
49
|
+
const duration = run.duration_ms
|
|
50
|
+
? `${(run.duration_ms / 1000).toFixed(2)}s`
|
|
51
|
+
: "-";
|
|
52
|
+
const started = new Date(run.startedAt).toLocaleString();
|
|
53
|
+
table.push([statusIcon, run.monitor?.name ?? "-", duration, started]);
|
|
52
54
|
}
|
|
53
|
-
|
|
55
|
+
output.log(table.toString());
|
|
54
56
|
const runsWithErrors = runsData.data.filter((run) => Array.isArray(run.errors) && run.errors.length > 0);
|
|
55
57
|
if (runsWithErrors.length > 0) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
58
|
+
output.blank();
|
|
59
|
+
output.warn("Runs with errors:");
|
|
60
|
+
output.blank();
|
|
59
61
|
for (const run of runsWithErrors) {
|
|
60
|
-
|
|
62
|
+
output.log(`${output.colors.red("✗")} ${output.colors.cyan(run.monitor?.name ?? "")}`);
|
|
61
63
|
if (Array.isArray(run.errors) && run.errors.length > 0) {
|
|
62
64
|
const errorsToShow = run.errors.slice(0, 3);
|
|
63
65
|
for (const error of errorsToShow) {
|
|
64
|
-
|
|
66
|
+
output.dim(` - ${String(error)}`);
|
|
65
67
|
}
|
|
66
68
|
if (run.errors.length > 3) {
|
|
67
|
-
|
|
69
|
+
output.dim(` ... and ${run.errors.length - 3} more`);
|
|
68
70
|
}
|
|
69
71
|
}
|
|
70
|
-
|
|
72
|
+
output.blank();
|
|
71
73
|
}
|
|
72
74
|
}
|
|
73
75
|
});
|
|
@@ -6,6 +6,7 @@ export interface SecretsSetOptions {
|
|
|
6
6
|
name: string;
|
|
7
7
|
environment: string;
|
|
8
8
|
value?: string;
|
|
9
|
+
json?: boolean;
|
|
9
10
|
}
|
|
10
11
|
export interface SecretsGetOptions {
|
|
11
12
|
name: string;
|
|
@@ -16,22 +17,21 @@ export interface SecretsDeleteOptions {
|
|
|
16
17
|
name: string;
|
|
17
18
|
environment: string;
|
|
18
19
|
force?: boolean;
|
|
20
|
+
json?: boolean;
|
|
19
21
|
}
|
|
20
22
|
/**
|
|
21
23
|
* List secrets (metadata only).
|
|
22
|
-
* Uses the resolved secrets integration for the organization and environment
|
|
23
|
-
* (from hub integrations API or platform default / legacy config).
|
|
24
24
|
*/
|
|
25
25
|
export declare const executeSecretsList: (options: SecretsListOptions) => Promise<void>;
|
|
26
26
|
/**
|
|
27
|
-
* Set a secret (prompts for value if not provided)
|
|
27
|
+
* Set a secret (prompts for value if not provided; in JSON mode --value is required).
|
|
28
28
|
*/
|
|
29
29
|
export declare const executeSecretsSet: (options: SecretsSetOptions) => Promise<void>;
|
|
30
30
|
/**
|
|
31
|
-
* Get secret metadata only
|
|
31
|
+
* Get secret metadata only.
|
|
32
32
|
*/
|
|
33
33
|
export declare const executeSecretsGet: (options: SecretsGetOptions) => Promise<void>;
|
|
34
34
|
/**
|
|
35
|
-
* Delete a secret (with confirmation unless --force)
|
|
35
|
+
* Delete a secret (with confirmation unless --force; in JSON mode --force is required to avoid prompt).
|
|
36
36
|
*/
|
|
37
37
|
export declare const executeSecretsDelete: (options: SecretsDeleteOptions) => Promise<void>;
|
|
@@ -1,127 +1,135 @@
|
|
|
1
1
|
import { createInterface } from "node:readline";
|
|
2
2
|
import { resolveEnvironment } from "../../core/state.js";
|
|
3
|
-
import { terminal } from "../../utils/terminal.js";
|
|
4
|
-
import { withSDKErrorHandling } from "../../utils/sdk-error.js";
|
|
5
3
|
import { createSdkFromState } from "../../core/sdk.js";
|
|
6
|
-
import {
|
|
4
|
+
import { handleSDKErrorWithOutput } from "../../utils/sdk-error.js";
|
|
5
|
+
import { createCommandHandler } from "../../utils/command-wrapper.js";
|
|
6
|
+
import { OutputError } from "../../utils/output.js";
|
|
7
7
|
const SECRET_NAME_REGEX = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
8
8
|
/**
|
|
9
9
|
* List secrets (metadata only).
|
|
10
|
-
* Uses the resolved secrets integration for the organization and environment
|
|
11
|
-
* (from hub integrations API or platform default / legacy config).
|
|
12
10
|
*/
|
|
13
|
-
export const executeSecretsList =
|
|
11
|
+
export const executeSecretsList = createCommandHandler("secrets list", async (options, output) => {
|
|
14
12
|
const sdk = await createSdkFromState();
|
|
15
13
|
const env = await resolveEnvironment(options.environment);
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
},
|
|
14
|
+
let response;
|
|
15
|
+
try {
|
|
16
|
+
response = await sdk.getSecrets({
|
|
17
|
+
query: { environment: env },
|
|
21
18
|
});
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
19
|
+
}
|
|
20
|
+
catch (err) {
|
|
21
|
+
handleSDKErrorWithOutput(err, output, "Failed to list secrets");
|
|
22
|
+
}
|
|
23
|
+
const secrets = response.data?.data ?? [];
|
|
24
|
+
output.setData({ secrets });
|
|
26
25
|
if (secrets.length === 0) {
|
|
27
|
-
|
|
26
|
+
output.info(`No secrets found for environment "${env}".`);
|
|
28
27
|
return;
|
|
29
28
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const table =
|
|
29
|
+
output.info(`Secrets (environment: ${env})`);
|
|
30
|
+
output.blank();
|
|
31
|
+
const table = output.table({
|
|
33
32
|
head: ["Name", "Created", "Updated"],
|
|
34
33
|
});
|
|
35
34
|
for (const s of secrets) {
|
|
36
35
|
table.push([s.name, s.createdAt ?? "-", s.updatedAt ?? "-"]);
|
|
37
36
|
}
|
|
38
|
-
|
|
37
|
+
output.log(table.toString());
|
|
39
38
|
});
|
|
39
|
+
function promptSecret(promptText) {
|
|
40
|
+
return new Promise((resolve) => {
|
|
41
|
+
const rl = createInterface({
|
|
42
|
+
input: process.stdin,
|
|
43
|
+
output: process.stdout,
|
|
44
|
+
});
|
|
45
|
+
rl.question(promptText, (answer) => {
|
|
46
|
+
rl.close();
|
|
47
|
+
resolve(answer.trim());
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
}
|
|
40
51
|
/**
|
|
41
|
-
* Set a secret (prompts for value if not provided)
|
|
52
|
+
* Set a secret (prompts for value if not provided; in JSON mode --value is required).
|
|
42
53
|
*/
|
|
43
|
-
export const executeSecretsSet =
|
|
54
|
+
export const executeSecretsSet = createCommandHandler("secrets set", async (options, output) => {
|
|
44
55
|
if (!SECRET_NAME_REGEX.test(options.name)) {
|
|
45
|
-
|
|
46
|
-
terminal.exit(1);
|
|
56
|
+
output.flushError("VALIDATION_ERROR", "Secret name must start with a letter or underscore and contain only letters, numbers, and underscores.");
|
|
47
57
|
}
|
|
48
58
|
let value = options.value;
|
|
49
59
|
if (value === undefined) {
|
|
60
|
+
if (options.json) {
|
|
61
|
+
throw new OutputError("INTERACTIVE_REQUIRED", "Secret value required; use --value in non-interactive (JSON) mode", undefined, "Enter secret value");
|
|
62
|
+
}
|
|
50
63
|
value = await promptSecret("Enter secret value:");
|
|
51
64
|
if (!value) {
|
|
52
|
-
|
|
53
|
-
terminal.exit(1);
|
|
65
|
+
output.flushError("VALIDATION_ERROR", "Secret value cannot be empty.");
|
|
54
66
|
}
|
|
55
67
|
}
|
|
56
68
|
const sdk = await createSdkFromState();
|
|
57
69
|
const env = await resolveEnvironment(options.environment);
|
|
58
|
-
|
|
59
|
-
|
|
70
|
+
let response;
|
|
71
|
+
try {
|
|
72
|
+
response = await sdk.putSecretsByName({
|
|
60
73
|
path: { name: options.name },
|
|
61
74
|
body: { value, environment: env },
|
|
62
75
|
});
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
});
|
|
73
|
-
rl.question(promptText, (answer) => {
|
|
74
|
-
rl.close();
|
|
75
|
-
resolve(answer.trim());
|
|
76
|
-
});
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
handleSDKErrorWithOutput(err, output, "Failed to set secret");
|
|
79
|
+
}
|
|
80
|
+
const result = response.data;
|
|
81
|
+
output.setData({
|
|
82
|
+
name: result.data?.name ?? options.name,
|
|
83
|
+
createdAt: result.data?.createdAt,
|
|
84
|
+
updatedAt: result.data?.updatedAt,
|
|
77
85
|
});
|
|
78
|
-
}
|
|
86
|
+
output.success(`Secret "${result.data?.name ?? options.name}" saved.`);
|
|
87
|
+
});
|
|
79
88
|
/**
|
|
80
|
-
* Get secret metadata only
|
|
89
|
+
* Get secret metadata only.
|
|
81
90
|
*/
|
|
82
|
-
export const executeSecretsGet =
|
|
91
|
+
export const executeSecretsGet = createCommandHandler("secrets get", async (options, output) => {
|
|
83
92
|
const sdk = await createSdkFromState();
|
|
84
93
|
const env = await resolveEnvironment(options.environment);
|
|
85
|
-
|
|
86
|
-
|
|
94
|
+
let response;
|
|
95
|
+
try {
|
|
96
|
+
response = await sdk.getSecretsByName({
|
|
87
97
|
path: { name: options.name },
|
|
88
98
|
query: { environment: env },
|
|
89
99
|
});
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
handleSDKErrorWithOutput(err, output, "Failed to get secret");
|
|
103
|
+
}
|
|
104
|
+
const secret = response.data?.data;
|
|
105
|
+
output.setData(secret);
|
|
106
|
+
output.info(`Secret: ${secret.name}`);
|
|
107
|
+
output.dim(`Created: ${secret.createdAt ?? "-"}`);
|
|
108
|
+
output.dim(`Updated: ${secret.updatedAt ?? "-"}`);
|
|
97
109
|
});
|
|
98
110
|
/**
|
|
99
|
-
* Delete a secret (with confirmation unless --force)
|
|
111
|
+
* Delete a secret (with confirmation unless --force; in JSON mode --force is required to avoid prompt).
|
|
100
112
|
*/
|
|
101
|
-
export const executeSecretsDelete =
|
|
113
|
+
export const executeSecretsDelete = createCommandHandler("secrets delete", async (options, output) => {
|
|
102
114
|
const sdk = await createSdkFromState();
|
|
103
115
|
const env = await resolveEnvironment(options.environment);
|
|
104
116
|
if (!options.force) {
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
output:
|
|
108
|
-
|
|
109
|
-
const answer = await new Promise((resolve) => {
|
|
110
|
-
rl.question(`Delete secret "${options.name}" (environment: ${env})? [y/N] `, (a) => {
|
|
111
|
-
rl.close();
|
|
112
|
-
resolve(a.trim().toLowerCase());
|
|
113
|
-
});
|
|
114
|
-
});
|
|
115
|
-
if (answer !== "y" && answer !== "yes") {
|
|
116
|
-
terminal.info("Aborted.");
|
|
117
|
+
const confirmed = await output.confirm(`Delete secret "${options.name}" (environment: ${env})?`, false);
|
|
118
|
+
if (!confirmed) {
|
|
119
|
+
output.setData({ deleted: false, name: options.name });
|
|
120
|
+
output.info("Aborted.");
|
|
117
121
|
return;
|
|
118
122
|
}
|
|
119
123
|
}
|
|
120
|
-
|
|
121
|
-
|
|
124
|
+
try {
|
|
125
|
+
await sdk.deleteSecretsByName({
|
|
122
126
|
path: { name: options.name },
|
|
123
127
|
query: { environment: env },
|
|
124
128
|
});
|
|
125
|
-
}
|
|
126
|
-
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
handleSDKErrorWithOutput(err, output, "Failed to delete secret");
|
|
132
|
+
}
|
|
133
|
+
output.setData({ deleted: true, name: options.name });
|
|
134
|
+
output.success(`Secret deleted.`);
|
|
127
135
|
});
|
|
@@ -1,21 +1,27 @@
|
|
|
1
1
|
import { loadState } from "../../core/state.js";
|
|
2
2
|
import { getHubCredentials } from "../../core/credentials.js";
|
|
3
|
-
import {
|
|
4
|
-
import { withCommandErrorHandler } from "../../utils/command-wrapper.js";
|
|
3
|
+
import { createCommandHandler } from "../../utils/command-wrapper.js";
|
|
5
4
|
/**
|
|
6
5
|
* Show hub connection status
|
|
7
6
|
*/
|
|
8
|
-
export const executeStatus =
|
|
7
|
+
export const executeStatus = createCommandHandler("status", async (options, output) => {
|
|
9
8
|
const state = await loadState();
|
|
10
9
|
const credentials = await getHubCredentials();
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
output.setData({
|
|
11
|
+
hubUrl: state.hub.baseUrl,
|
|
12
|
+
token: credentials?.token
|
|
13
|
+
? credentials.token.substring(0, 8) + "..."
|
|
14
|
+
: null,
|
|
15
|
+
updatedAt: credentials?.updatedAt ?? null,
|
|
16
|
+
});
|
|
17
|
+
output.info("Hub connection:");
|
|
18
|
+
output.log(` URL: ${output.colors.cyan(state.hub.baseUrl)}`);
|
|
13
19
|
if (credentials?.token) {
|
|
14
|
-
|
|
15
|
-
|
|
20
|
+
output.log(` API Token: ${output.colors.dim(credentials.token.substring(0, 8) + "...")}`);
|
|
21
|
+
output.log(` Updated: ${output.colors.dim(new Date(credentials.updatedAt).toLocaleString())}`);
|
|
16
22
|
}
|
|
17
23
|
else {
|
|
18
|
-
|
|
24
|
+
output.log(` API Token: ${output.colors.dim("(not set)")}`);
|
|
19
25
|
}
|
|
20
|
-
|
|
26
|
+
output.blank();
|
|
21
27
|
});
|
package/dist/commands/init.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
export interface InitOptions {
|
|
2
2
|
project?: string;
|
|
3
|
+
json?: boolean;
|
|
3
4
|
}
|
|
4
5
|
/**
|
|
5
6
|
* Initialize griffin in the current directory
|
|
6
7
|
*/
|
|
7
|
-
export declare
|
|
8
|
+
export declare const executeInit: (options: InitOptions) => Promise<void>;
|
package/dist/commands/init.js
CHANGED
|
@@ -1,38 +1,44 @@
|
|
|
1
1
|
import { initState, stateExists, getStateFilePath } from "../core/state.js";
|
|
2
2
|
import { detectProjectId } from "../core/project.js";
|
|
3
|
-
import {
|
|
3
|
+
import { createCommandHandler } from "../utils/command-wrapper.js";
|
|
4
4
|
/**
|
|
5
5
|
* Initialize griffin in the current directory
|
|
6
6
|
*/
|
|
7
|
-
export
|
|
8
|
-
const spinner =
|
|
9
|
-
// Check if already initialized
|
|
7
|
+
export const executeInit = createCommandHandler("init", async (options, output) => {
|
|
8
|
+
const spinner = output.spinner("Initializing griffin...").start();
|
|
10
9
|
if (await stateExists()) {
|
|
11
10
|
spinner.fail("Already initialized");
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
output.setData({
|
|
12
|
+
error: "ALREADY_INITIALIZED",
|
|
13
|
+
message: "State file already exists",
|
|
14
|
+
stateFilePath: getStateFilePath(),
|
|
15
|
+
});
|
|
16
|
+
output.dim(`State file exists: ${getStateFilePath()}`);
|
|
17
|
+
output.exit(1);
|
|
14
18
|
}
|
|
15
|
-
// Determine project ID
|
|
16
19
|
let projectId = options.project;
|
|
17
20
|
if (!projectId) {
|
|
18
21
|
projectId = await detectProjectId();
|
|
19
22
|
}
|
|
20
|
-
spinner.succeed(`Project: ${
|
|
21
|
-
// Initialize state file (includes default environment)
|
|
23
|
+
spinner.succeed(`Project: ${output.colors.cyan(projectId)}`);
|
|
22
24
|
await initState(projectId);
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
25
|
+
output.setData({
|
|
26
|
+
projectId,
|
|
27
|
+
stateFilePath: getStateFilePath(),
|
|
28
|
+
});
|
|
29
|
+
output.success(`Created state file: ${output.colors.dim(getStateFilePath())}`);
|
|
30
|
+
output.blank();
|
|
31
|
+
output.success("Initialization complete!");
|
|
32
|
+
output.blank();
|
|
33
|
+
output.info("Next steps:");
|
|
34
|
+
output.dim(" 1. Add variables for your environment (optional):");
|
|
35
|
+
output.dim(" griffin variables add API_KEY=your-key --env default");
|
|
36
|
+
output.dim(" 2. Create test monitors (*.ts files in __griffin__/ directories)");
|
|
37
|
+
output.dim(" 3. Run tests locally:");
|
|
38
|
+
output.dim(" griffin test");
|
|
39
|
+
output.dim(" 4. Connect to hub (optional):");
|
|
40
|
+
output.dim(" griffin auth connect --url <url> --token <token>");
|
|
41
|
+
output.dim(" 5. Deploy to hub:");
|
|
42
|
+
output.dim(" griffin apply");
|
|
43
|
+
output.blank();
|
|
44
|
+
});
|
|
@@ -1,49 +1,57 @@
|
|
|
1
1
|
import { findTestFiles } from "../../monitor-discovery.js";
|
|
2
2
|
import { runTestFile } from "../../monitor-runner.js";
|
|
3
3
|
import { resolveEnvironment } from "../../core/state.js";
|
|
4
|
-
import { terminal } from "../../utils/terminal.js";
|
|
5
4
|
import { basename } from "path";
|
|
6
|
-
import {
|
|
7
|
-
export const executeRunLocal =
|
|
8
|
-
// Resolve environment
|
|
5
|
+
import { createCommandHandler } from "../../utils/command-wrapper.js";
|
|
6
|
+
export const executeRunLocal = createCommandHandler("test", async (options, output) => {
|
|
9
7
|
const envName = await resolveEnvironment(options.env);
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const spinner =
|
|
8
|
+
output.info(`Running tests locally against ${output.colors.cyan(envName)} environment`);
|
|
9
|
+
output.dim(`Variables will be loaded from variables.yaml for environment: ${envName}`);
|
|
10
|
+
output.blank();
|
|
11
|
+
const spinner = output.spinner("Discovering test files...").start();
|
|
14
12
|
const testFiles = findTestFiles();
|
|
15
13
|
if (testFiles.length === 0) {
|
|
16
14
|
spinner.fail("No test files found");
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
output.setData({
|
|
16
|
+
results: [],
|
|
17
|
+
passed: 0,
|
|
18
|
+
failed: 0,
|
|
19
|
+
error: "NO_TEST_FILES",
|
|
20
|
+
});
|
|
21
|
+
output.dim("Looking for .ts files in __griffin__ directories.");
|
|
22
|
+
output.exit(1);
|
|
19
23
|
}
|
|
20
|
-
spinner.succeed(`Found ${
|
|
21
|
-
testFiles.forEach((file) =>
|
|
22
|
-
|
|
24
|
+
spinner.succeed(`Found ${output.colors.bold(testFiles.length.toString())} test file(s)`);
|
|
25
|
+
testFiles.forEach((file) => output.dim(` - ${file}`));
|
|
26
|
+
output.blank();
|
|
23
27
|
const results = await Promise.all(testFiles.map(async (file) => {
|
|
24
28
|
const fileName = basename(file);
|
|
25
|
-
const testSpinner =
|
|
26
|
-
.spinner(`Running ${
|
|
29
|
+
const testSpinner = output
|
|
30
|
+
.spinner(`Running ${output.colors.cyan(fileName)}`)
|
|
27
31
|
.start();
|
|
28
32
|
const result = await runTest(file, envName);
|
|
29
33
|
if (result.success) {
|
|
30
|
-
testSpinner.succeed(`${
|
|
34
|
+
testSpinner.succeed(`${output.colors.cyan(fileName)} passed`);
|
|
31
35
|
}
|
|
32
36
|
else {
|
|
33
|
-
testSpinner.fail(`${
|
|
37
|
+
testSpinner.fail(`${output.colors.cyan(fileName)} failed`);
|
|
34
38
|
}
|
|
35
|
-
return result;
|
|
39
|
+
return { file: fileName, success: result.success };
|
|
36
40
|
}));
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
+
const passed = results.filter((r) => r.success).length;
|
|
42
|
+
const failed = results.length - passed;
|
|
43
|
+
output.setData({
|
|
44
|
+
results,
|
|
45
|
+
passed,
|
|
46
|
+
failed,
|
|
47
|
+
});
|
|
48
|
+
output.blank();
|
|
41
49
|
if (failed === 0) {
|
|
42
|
-
|
|
50
|
+
output.success(`All tests passed (${output.colors.bold(passed.toString())} / ${results.length})`);
|
|
43
51
|
}
|
|
44
52
|
else {
|
|
45
|
-
|
|
46
|
-
|
|
53
|
+
output.error(`${output.colors.bold(failed.toString())} test(s) failed, ${output.colors.bold(passed.toString())} passed`);
|
|
54
|
+
output.exit(1);
|
|
47
55
|
}
|
|
48
56
|
});
|
|
49
57
|
async function runTest(file, envName) {
|
|
@@ -52,7 +60,7 @@ async function runTest(file, envName) {
|
|
|
52
60
|
return { success: result.success };
|
|
53
61
|
}
|
|
54
62
|
catch (error) {
|
|
55
|
-
|
|
63
|
+
console.error(error.message || String(error));
|
|
56
64
|
return { success: false };
|
|
57
65
|
}
|
|
58
66
|
}
|