@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.
Files changed (49) hide show
  1. package/README.md +122 -367
  2. package/dist/cli.js +48 -29
  3. package/dist/commands/env.d.ts +14 -3
  4. package/dist/commands/env.js +24 -22
  5. package/dist/commands/generate-key.d.ts +5 -6
  6. package/dist/commands/generate-key.js +20 -26
  7. package/dist/commands/hub/apply.d.ts +1 -0
  8. package/dist/commands/hub/apply.js +44 -29
  9. package/dist/commands/hub/connect.d.ts +2 -1
  10. package/dist/commands/hub/connect.js +18 -22
  11. package/dist/commands/hub/destroy.d.ts +2 -1
  12. package/dist/commands/hub/destroy.js +123 -119
  13. package/dist/commands/hub/integrations.d.ts +4 -1
  14. package/dist/commands/hub/integrations.js +160 -141
  15. package/dist/commands/hub/login.d.ts +13 -1
  16. package/dist/commands/hub/login.js +81 -15
  17. package/dist/commands/hub/logout.d.ts +2 -1
  18. package/dist/commands/hub/logout.js +7 -12
  19. package/dist/commands/hub/metrics.js +29 -27
  20. package/dist/commands/hub/monitor.js +16 -16
  21. package/dist/commands/hub/notifications.d.ts +3 -6
  22. package/dist/commands/hub/notifications.js +52 -38
  23. package/dist/commands/hub/run.d.ts +1 -0
  24. package/dist/commands/hub/run.js +114 -87
  25. package/dist/commands/hub/runs.d.ts +2 -0
  26. package/dist/commands/hub/runs.js +47 -45
  27. package/dist/commands/hub/secrets.d.ts +5 -5
  28. package/dist/commands/hub/secrets.js +80 -72
  29. package/dist/commands/hub/status.d.ts +4 -1
  30. package/dist/commands/hub/status.js +15 -9
  31. package/dist/commands/init.d.ts +2 -1
  32. package/dist/commands/init.js +31 -25
  33. package/dist/commands/local/run.d.ts +1 -0
  34. package/dist/commands/local/run.js +34 -26
  35. package/dist/commands/validate.d.ts +4 -1
  36. package/dist/commands/validate.js +23 -14
  37. package/dist/commands/variables.d.ts +17 -3
  38. package/dist/commands/variables.js +29 -28
  39. package/dist/core/credentials.d.ts +15 -0
  40. package/dist/core/credentials.js +37 -0
  41. package/dist/core/variables.js +4 -0
  42. package/dist/monitor-runner.js +0 -12
  43. package/dist/utils/command-wrapper.d.ts +9 -0
  44. package/dist/utils/command-wrapper.js +23 -0
  45. package/dist/utils/output.d.ts +66 -0
  46. package/dist/utils/output.js +202 -0
  47. package/dist/utils/sdk-error.d.ts +6 -1
  48. package/dist/utils/sdk-error.js +107 -77
  49. package/package.json +2 -2
@@ -1,73 +1,75 @@
1
1
  import { createSdkFromState } from "../../core/sdk.js";
2
- import { terminal } from "../../utils/terminal.js";
3
- import { withSDKErrorHandling } from "../../utils/sdk-error.js";
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 = withCommandErrorHandler(async (options) => {
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 = terminal.spinner("Fetching runs...").start();
14
- const response = await withSDKErrorHandling(() => sdk.getRuns({
15
- query: {
16
- monitorId: options.monitor,
17
- limit: limit,
18
- offset: 0,
19
- },
20
- }), "Failed to fetch runs");
21
- const runsData = response?.data;
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
- terminal.blank();
28
- // Create table
29
- const table = terminal.table({
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
- try {
34
- const statusIcon = getStatusIcon(run.status, run.success);
35
- const duration = run.duration_ms
36
- ? `${(run.duration_ms / 1000).toFixed(2)}s`
37
- : "-";
38
- const started = new Date(run.startedAt).toLocaleString();
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
- // Show detailed errors if any
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
- terminal.blank();
57
- terminal.warn("Runs with errors:");
58
- terminal.blank();
58
+ output.blank();
59
+ output.warn("Runs with errors:");
60
+ output.blank();
59
61
  for (const run of runsWithErrors) {
60
- terminal.log(`${terminal.colors.red("✗")} ${terminal.colors.cyan(run.monitor.name)}`);
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
- terminal.dim(` - ${String(error)}`);
66
+ output.dim(` - ${String(error)}`);
65
67
  }
66
68
  if (run.errors.length > 3) {
67
- terminal.dim(` ... and ${run.errors.length - 3} more`);
69
+ output.dim(` ... and ${run.errors.length - 3} more`);
68
70
  }
69
71
  }
70
- terminal.blank();
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 { withCommandErrorHandler, outputJsonOrContinue, } from "../../utils/command-wrapper.js";
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 = withCommandErrorHandler(async (options) => {
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
- const response = await withSDKErrorHandling(async () => {
17
- return sdk.getSecrets({
18
- query: {
19
- environment: env,
20
- },
14
+ let response;
15
+ try {
16
+ response = await sdk.getSecrets({
17
+ query: { environment: env },
21
18
  });
22
- }, "Failed to list secrets");
23
- const secrets = response?.data?.data ?? [];
24
- if (outputJsonOrContinue(secrets, options.json))
25
- return;
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
- terminal.info(`No secrets found for environment "${env}".`);
26
+ output.info(`No secrets found for environment "${env}".`);
28
27
  return;
29
28
  }
30
- terminal.info(`Secrets (environment: ${env})`);
31
- terminal.blank();
32
- const table = terminal.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
- terminal.log(table.toString());
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 = withCommandErrorHandler(async (options) => {
54
+ export const executeSecretsSet = createCommandHandler("secrets set", async (options, output) => {
44
55
  if (!SECRET_NAME_REGEX.test(options.name)) {
45
- terminal.error("Secret name must start with a letter or underscore and contain only letters, numbers, and underscores.");
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
- terminal.error("Secret value cannot be empty.");
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
- const response = await withSDKErrorHandling(async () => {
59
- return sdk.putSecretsByName({
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
- }, "Failed to set secret");
64
- const result = response?.data;
65
- terminal.success(`Secret "${result.data.name}" saved.`);
66
- });
67
- function promptSecret(promptText) {
68
- return new Promise((resolve) => {
69
- const rl = createInterface({
70
- input: process.stdin,
71
- output: process.stdout,
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 = withCommandErrorHandler(async (options) => {
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
- const response = await withSDKErrorHandling(async () => {
86
- return sdk.getSecretsByName({
94
+ let response;
95
+ try {
96
+ response = await sdk.getSecretsByName({
87
97
  path: { name: options.name },
88
98
  query: { environment: env },
89
99
  });
90
- }, "Failed to get secret");
91
- const secret = response?.data?.data;
92
- if (outputJsonOrContinue(secret, options.json))
93
- return;
94
- terminal.info(`Secret: ${secret.name}`);
95
- terminal.dim(`Created: ${secret.createdAt ?? "-"}`);
96
- terminal.dim(`Updated: ${secret.updatedAt ?? "-"}`);
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 = withCommandErrorHandler(async (options) => {
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 rl = createInterface({
106
- input: process.stdin,
107
- output: process.stdout,
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
- await withSDKErrorHandling(async () => {
121
- return sdk.deleteSecretsByName({
124
+ try {
125
+ await sdk.deleteSecretsByName({
122
126
  path: { name: options.name },
123
127
  query: { environment: env },
124
128
  });
125
- }, "Failed to delete secret");
126
- terminal.success(`Secret deleted.`);
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,4 +1,7 @@
1
+ export interface StatusOptions {
2
+ json?: boolean;
3
+ }
1
4
  /**
2
5
  * Show hub connection status
3
6
  */
4
- export declare const executeStatus: () => Promise<void>;
7
+ export declare const executeStatus: (options: StatusOptions) => Promise<void>;
@@ -1,21 +1,27 @@
1
1
  import { loadState } from "../../core/state.js";
2
2
  import { getHubCredentials } from "../../core/credentials.js";
3
- import { terminal } from "../../utils/terminal.js";
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 = withCommandErrorHandler(async () => {
7
+ export const executeStatus = createCommandHandler("status", async (options, output) => {
9
8
  const state = await loadState();
10
9
  const credentials = await getHubCredentials();
11
- terminal.info("Hub connection:");
12
- terminal.log(` URL: ${terminal.colors.cyan(state.hub.baseUrl)}`);
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
- terminal.log(` API Token: ${terminal.colors.dim(credentials.token.substring(0, 8) + "...")}`);
15
- terminal.log(` Updated: ${terminal.colors.dim(new Date(credentials.updatedAt).toLocaleString())}`);
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
- terminal.log(` API Token: ${terminal.colors.dim("(not set)")}`);
24
+ output.log(` API Token: ${output.colors.dim("(not set)")}`);
19
25
  }
20
- terminal.blank();
26
+ output.blank();
21
27
  });
@@ -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 function executeInit(options: InitOptions): Promise<void>;
8
+ export declare const executeInit: (options: InitOptions) => Promise<void>;
@@ -1,38 +1,44 @@
1
1
  import { initState, stateExists, getStateFilePath } from "../core/state.js";
2
2
  import { detectProjectId } from "../core/project.js";
3
- import { terminal } from "../utils/terminal.js";
3
+ import { createCommandHandler } from "../utils/command-wrapper.js";
4
4
  /**
5
5
  * Initialize griffin in the current directory
6
6
  */
7
- export async function executeInit(options) {
8
- const spinner = terminal.spinner("Initializing griffin...").start();
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
- terminal.dim(`State file exists: ${getStateFilePath()}`);
13
- terminal.exit(1);
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: ${terminal.colors.cyan(projectId)}`);
21
- // Initialize state file (includes default environment)
23
+ spinner.succeed(`Project: ${output.colors.cyan(projectId)}`);
22
24
  await initState(projectId);
23
- terminal.success(`Created state file: ${terminal.colors.dim(getStateFilePath())}`);
24
- terminal.blank();
25
- terminal.success("Initialization complete!");
26
- terminal.blank();
27
- terminal.info("Next steps:");
28
- terminal.dim(" 1. Add variables for your environment (optional):");
29
- terminal.dim(" griffin variables add API_KEY=your-key --env default");
30
- terminal.dim(" 2. Create test monitors (*.ts files in __griffin__/ directories)");
31
- terminal.dim(" 3. Run tests locally:");
32
- terminal.dim(" griffin test");
33
- terminal.dim(" 4. Connect to hub (optional):");
34
- terminal.dim(" griffin auth connect --url <url> --token <token>");
35
- terminal.dim(" 5. Deploy to hub:");
36
- terminal.dim(" griffin apply");
37
- terminal.blank();
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,4 +1,5 @@
1
1
  export interface RunLocalOptions {
2
2
  env: string;
3
+ json?: boolean;
3
4
  }
4
5
  export declare const executeRunLocal: (options: RunLocalOptions) => Promise<void>;
@@ -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 { withCommandErrorHandler } from "../../utils/command-wrapper.js";
7
- export const executeRunLocal = withCommandErrorHandler(async (options) => {
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
- terminal.info(`Running tests locally against ${terminal.colors.cyan(envName)} environment`);
11
- terminal.dim(`Variables will be loaded from variables.yaml for environment: ${envName}`);
12
- terminal.blank();
13
- const spinner = terminal.spinner("Discovering test files...").start();
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
- terminal.dim("Looking for .ts files in __griffin__ directories.");
18
- terminal.exit(1);
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 ${terminal.colors.bold(testFiles.length.toString())} test file(s)`);
21
- testFiles.forEach((file) => terminal.dim(` - ${file}`));
22
- terminal.blank();
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 = terminal
26
- .spinner(`Running ${terminal.colors.cyan(fileName)}`)
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(`${terminal.colors.cyan(fileName)} passed`);
34
+ testSpinner.succeed(`${output.colors.cyan(fileName)} passed`);
31
35
  }
32
36
  else {
33
- testSpinner.fail(`${terminal.colors.cyan(fileName)} failed`);
37
+ testSpinner.fail(`${output.colors.cyan(fileName)} failed`);
34
38
  }
35
- return result;
39
+ return { file: fileName, success: result.success };
36
40
  }));
37
- // Print summary
38
- const successful = results.filter((r) => r.success).length;
39
- const failed = results.length - successful;
40
- terminal.blank();
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
- terminal.success(`All tests passed (${terminal.colors.bold(successful.toString())} / ${results.length})`);
50
+ output.success(`All tests passed (${output.colors.bold(passed.toString())} / ${results.length})`);
43
51
  }
44
52
  else {
45
- terminal.error(`${terminal.colors.bold(failed.toString())} test(s) failed, ${terminal.colors.bold(successful.toString())} passed`);
46
- terminal.exit(1);
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
- terminal.error(error.message || String(error));
63
+ console.error(error.message || String(error));
56
64
  return { success: false };
57
65
  }
58
66
  }
@@ -1,4 +1,7 @@
1
+ export interface ValidateOptions {
2
+ json?: boolean;
3
+ }
1
4
  /**
2
5
  * Validate test monitor files without syncing
3
6
  */
4
- export declare const executeValidate: () => Promise<void>;
7
+ export declare const executeValidate: (options: ValidateOptions) => Promise<void>;