@griffin-app/griffin-cli 1.0.0 → 1.0.2

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 (81) hide show
  1. package/dist/cli.d.ts +2 -0
  2. package/dist/cli.js +105 -0
  3. package/dist/commands/apply.d.ts +9 -0
  4. package/dist/commands/apply.js +76 -0
  5. package/dist/commands/config.d.ts +36 -0
  6. package/dist/commands/config.js +144 -0
  7. package/dist/commands/configure-runner-host.d.ts +2 -0
  8. package/dist/commands/configure-runner-host.js +41 -0
  9. package/dist/commands/deploy.d.ts +1 -0
  10. package/dist/commands/deploy.js +36 -0
  11. package/dist/commands/env.d.ts +25 -0
  12. package/dist/commands/env.js +121 -0
  13. package/dist/commands/execute-remote.d.ts +1 -0
  14. package/dist/commands/execute-remote.js +33 -0
  15. package/dist/commands/generate-key.d.ts +9 -0
  16. package/dist/commands/generate-key.js +27 -0
  17. package/dist/commands/hub/apply.d.ts +10 -0
  18. package/dist/commands/hub/apply.js +81 -0
  19. package/dist/commands/hub/config.d.ts +27 -0
  20. package/dist/commands/hub/config.js +102 -0
  21. package/dist/commands/hub/connect.d.ts +8 -0
  22. package/dist/commands/hub/connect.js +25 -0
  23. package/dist/commands/hub/plan.d.ts +8 -0
  24. package/dist/commands/hub/plan.js +58 -0
  25. package/dist/commands/hub/run.d.ts +10 -0
  26. package/dist/commands/hub/run.js +126 -0
  27. package/dist/commands/hub/runs.d.ts +8 -0
  28. package/dist/commands/hub/runs.js +75 -0
  29. package/dist/commands/hub/status.d.ts +4 -0
  30. package/dist/commands/hub/status.js +29 -0
  31. package/dist/commands/init.d.ts +7 -0
  32. package/dist/commands/init.js +41 -0
  33. package/dist/commands/local/config.d.ts +28 -0
  34. package/dist/commands/local/config.js +82 -0
  35. package/dist/commands/local/run.d.ts +4 -0
  36. package/dist/commands/local/run.js +49 -0
  37. package/dist/commands/logs.d.ts +1 -0
  38. package/dist/commands/logs.js +20 -0
  39. package/dist/commands/plan.d.ts +8 -0
  40. package/dist/commands/plan.js +58 -0
  41. package/dist/commands/run-remote.d.ts +11 -0
  42. package/dist/commands/run-remote.js +98 -0
  43. package/dist/commands/run.d.ts +4 -0
  44. package/dist/commands/run.js +86 -0
  45. package/dist/commands/runner.d.ts +12 -0
  46. package/dist/commands/runner.js +53 -0
  47. package/dist/commands/status.d.ts +8 -0
  48. package/dist/commands/status.js +75 -0
  49. package/dist/commands/validate.d.ts +4 -0
  50. package/dist/commands/validate.js +46 -0
  51. package/dist/core/apply.d.ts +28 -0
  52. package/dist/core/apply.js +135 -0
  53. package/dist/core/apply.test.d.ts +1 -0
  54. package/dist/core/apply.test.js +140 -0
  55. package/dist/core/diff.d.ts +42 -0
  56. package/dist/core/diff.js +212 -0
  57. package/dist/core/diff.test.d.ts +1 -0
  58. package/dist/core/diff.test.js +122 -0
  59. package/dist/core/discovery.d.ts +23 -0
  60. package/dist/core/discovery.js +89 -0
  61. package/dist/core/plan-diff.d.ts +42 -0
  62. package/dist/core/plan-diff.js +257 -0
  63. package/dist/core/project.d.ts +8 -0
  64. package/dist/core/project.js +46 -0
  65. package/dist/core/sdk.d.ts +16 -0
  66. package/dist/core/sdk.js +23 -0
  67. package/dist/core/state.d.ts +52 -0
  68. package/dist/core/state.js +151 -0
  69. package/dist/core/variables.d.ts +19 -0
  70. package/dist/core/variables.js +100 -0
  71. package/dist/index.d.ts +21 -0
  72. package/dist/index.js +21 -0
  73. package/dist/schemas/payload.d.ts +6 -0
  74. package/dist/schemas/payload.js +8 -0
  75. package/dist/schemas/state.d.ts +43 -0
  76. package/dist/schemas/state.js +54 -0
  77. package/dist/test-discovery.d.ts +4 -0
  78. package/dist/test-discovery.js +25 -0
  79. package/dist/test-runner.d.ts +6 -0
  80. package/dist/test-runner.js +59 -0
  81. package/package.json +6 -5
@@ -0,0 +1,23 @@
1
+ import type { TestPlanV1 } from "@griffin-app/griffin-ts/types";
2
+ export type RawTestPlan = Omit<TestPlanV1, "id" | "environment" | "project">;
3
+ export interface DiscoveredPlan {
4
+ plan: RawTestPlan;
5
+ filePath: string;
6
+ exportName: string;
7
+ }
8
+ export interface DiscoveryResult {
9
+ plans: DiscoveredPlan[];
10
+ errors: DiscoveryError[];
11
+ }
12
+ export interface DiscoveryError {
13
+ filePath: string;
14
+ error: Error;
15
+ }
16
+ /**
17
+ * Discover and load test plan files from the filesystem
18
+ */
19
+ export declare function discoverPlans(pattern: string, ignore: string[]): Promise<DiscoveryResult>;
20
+ /**
21
+ * Format discovery errors for display
22
+ */
23
+ export declare function formatDiscoveryErrors(errors: DiscoveryError[]): string;
@@ -0,0 +1,89 @@
1
+ import { glob } from "glob";
2
+ import path from "node:path";
3
+ import { pathToFileURL } from "node:url";
4
+ import { TestPlanV1Schema } from "@griffin-app/griffin-ts/schema";
5
+ import { Value } from "typebox/value";
6
+ import { Type } from "typebox";
7
+ const RawTestPlanSchema = Type.Omit(TestPlanV1Schema, [
8
+ "id",
9
+ "environment",
10
+ "project",
11
+ ]);
12
+ /**
13
+ * Discover and load test plan files from the filesystem
14
+ */
15
+ export async function discoverPlans(pattern, ignore) {
16
+ const plans = [];
17
+ const errors = [];
18
+ // Find all matching files
19
+ const files = await glob(pattern, {
20
+ ignore,
21
+ absolute: true,
22
+ cwd: process.cwd(),
23
+ });
24
+ console.log(`Found ${files.length} test file(s)`);
25
+ // Load each file
26
+ for (const filePath of files) {
27
+ try {
28
+ const loaded = await loadPlansFromFile(filePath);
29
+ plans.push(...loaded);
30
+ }
31
+ catch (error) {
32
+ errors.push({
33
+ filePath,
34
+ error: error,
35
+ });
36
+ }
37
+ }
38
+ return { plans, errors };
39
+ }
40
+ function isPlan(value) {
41
+ return Value.Check(RawTestPlanSchema, value);
42
+ }
43
+ /**
44
+ * Load plans from a single file
45
+ * Supports both default and named exports
46
+ */
47
+ async function loadPlansFromFile(filePath) {
48
+ const plans = [];
49
+ // Convert to file URL for dynamic import (works with both ESM and CJS)
50
+ const fileUrl = pathToFileURL(filePath).href;
51
+ try {
52
+ const module = await import(fileUrl);
53
+ // Check default export
54
+ if (module.default) {
55
+ if (isPlan(module.default)) {
56
+ plans.push({
57
+ plan: module.default,
58
+ filePath,
59
+ exportName: "default",
60
+ });
61
+ }
62
+ else {
63
+ const errors = Value.Errors(RawTestPlanSchema, module.default);
64
+ throw new Error(`Default export is not a valid TestPlan. Got: ${JSON.stringify(errors, null, 2)}`);
65
+ }
66
+ }
67
+ if (plans.length === 0) {
68
+ throw new Error("No valid TestPlan exports found in file");
69
+ }
70
+ }
71
+ catch (error) {
72
+ throw new Error(`Failed to load ${filePath}: ${error.message}`);
73
+ }
74
+ return plans;
75
+ }
76
+ /**
77
+ * Format discovery errors for display
78
+ */
79
+ export function formatDiscoveryErrors(errors) {
80
+ if (errors.length === 0)
81
+ return "";
82
+ const lines = ["Errors during discovery:"];
83
+ for (const { filePath, error } of errors) {
84
+ const relativePath = path.relative(process.cwd(), filePath);
85
+ lines.push(` ❌ ${relativePath}`);
86
+ lines.push(` ${error.message}`);
87
+ }
88
+ return lines.join("\n");
89
+ }
@@ -0,0 +1,42 @@
1
+ import type { RawTestPlan } from "./discovery.js";
2
+ import type { TestPlanV1 } from "@griffin-app/griffin-ts/types";
3
+ import { NodeType } from "@griffin-app/griffin-ts/schema";
4
+ /**
5
+ * Represents a change to a single field
6
+ */
7
+ export interface FieldChange {
8
+ field: string;
9
+ oldValue: unknown;
10
+ newValue: unknown;
11
+ }
12
+ /**
13
+ * Represents a change to a node (add/remove/modify)
14
+ */
15
+ export interface NodeChange {
16
+ type: "add" | "remove" | "modify";
17
+ nodeId: string;
18
+ nodeType: NodeType;
19
+ summary: string;
20
+ fieldChanges: FieldChange[];
21
+ }
22
+ /**
23
+ * Represents a change to an edge (add/remove)
24
+ */
25
+ export interface EdgeChange {
26
+ type: "add" | "remove";
27
+ from: string;
28
+ to: string;
29
+ }
30
+ /**
31
+ * Complete set of changes between local and remote plans
32
+ */
33
+ export interface PlanChanges {
34
+ hasChanges: boolean;
35
+ nodes: NodeChange[];
36
+ edges: EdgeChange[];
37
+ topLevel: FieldChange[];
38
+ }
39
+ /**
40
+ * Compare two test plans and return granular changes
41
+ */
42
+ export declare function comparePlans(local: RawTestPlan, remote: TestPlanV1): PlanChanges;
@@ -0,0 +1,257 @@
1
+ import objectHash from "object-hash";
2
+ /**
3
+ * Compare two test plans and return granular changes
4
+ */
5
+ export function comparePlans(local, remote) {
6
+ const nodeChanges = compareNodes(local.nodes, remote.nodes);
7
+ const edgeChanges = compareEdges(local.edges, remote.edges);
8
+ const topLevelChanges = compareTopLevel(local, remote);
9
+ const hasChanges = nodeChanges.length > 0 ||
10
+ edgeChanges.length > 0 ||
11
+ topLevelChanges.length > 0;
12
+ return {
13
+ hasChanges,
14
+ nodes: nodeChanges,
15
+ edges: edgeChanges,
16
+ topLevel: topLevelChanges,
17
+ };
18
+ }
19
+ /**
20
+ * Compare nodes between local and remote plans
21
+ */
22
+ function compareNodes(localNodes, remoteNodes) {
23
+ const changes = [];
24
+ // Build map of remote nodes by id
25
+ const remoteByID = new Map();
26
+ for (const node of remoteNodes) {
27
+ remoteByID.set(node.id, node);
28
+ }
29
+ const localIDs = new Set();
30
+ for (const node of localNodes) {
31
+ localIDs.add(node.id);
32
+ }
33
+ // Check local nodes
34
+ for (const local of localNodes) {
35
+ const remote = remoteByID.get(local.id);
36
+ if (!remote) {
37
+ // Node added
38
+ changes.push({
39
+ type: "add",
40
+ nodeId: local.id,
41
+ nodeType: local.type,
42
+ summary: getNodeSummary(local),
43
+ fieldChanges: [],
44
+ });
45
+ }
46
+ else {
47
+ // Node exists - check for modifications
48
+ const fieldChanges = compareNodeFields(local, remote);
49
+ if (fieldChanges.length > 0) {
50
+ changes.push({
51
+ type: "modify",
52
+ nodeId: local.id,
53
+ nodeType: local.type,
54
+ summary: getNodeSummary(local),
55
+ fieldChanges,
56
+ });
57
+ }
58
+ }
59
+ }
60
+ // Check for removed nodes
61
+ for (const remote of remoteNodes) {
62
+ if (!localIDs.has(remote.id)) {
63
+ changes.push({
64
+ type: "remove",
65
+ nodeId: remote.id,
66
+ nodeType: remote.type,
67
+ summary: getNodeSummary(remote),
68
+ fieldChanges: [],
69
+ });
70
+ }
71
+ }
72
+ return changes;
73
+ }
74
+ /**
75
+ * Get a human-readable summary of a node
76
+ */
77
+ function getNodeSummary(node) {
78
+ switch (node.type) {
79
+ case "ENDPOINT":
80
+ return `${node.method} ${formatValue(node.path)}`;
81
+ case "WAIT":
82
+ return `wait ${node.duration_ms}ms`;
83
+ case "ASSERTION":
84
+ return `${node.assertions.length} assertion(s)`;
85
+ default:
86
+ return node.type;
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 "ENDPOINT":
120
+ compareEndpointFields(local, remote, changes);
121
+ break;
122
+ case "WAIT":
123
+ compareWaitFields(local, remote, changes);
124
+ break;
125
+ case "ASSERTION":
126
+ compareAssertionFields(local, remote, changes);
127
+ break;
128
+ }
129
+ return changes;
130
+ }
131
+ /**
132
+ * Compare fields specific to Endpoint nodes
133
+ */
134
+ function compareEndpointFields(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 plans
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,8 @@
1
+ /**
2
+ * Auto-detect project ID from the environment
3
+ *
4
+ * Priority:
5
+ * 1. package.json name field
6
+ * 2. Directory name
7
+ */
8
+ export declare function detectProjectId(): Promise<string>;
@@ -0,0 +1,46 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ /**
4
+ * Auto-detect project ID from the environment
5
+ *
6
+ * Priority:
7
+ * 1. package.json name field
8
+ * 2. Directory name
9
+ */
10
+ export async function detectProjectId() {
11
+ try {
12
+ // Try to find package.json walking up from current directory
13
+ const packageJsonPath = await findPackageJson(process.cwd());
14
+ if (packageJsonPath) {
15
+ const content = await fs.readFile(packageJsonPath, "utf-8");
16
+ const pkg = JSON.parse(content);
17
+ if (pkg.name && typeof pkg.name === "string") {
18
+ return pkg.name;
19
+ }
20
+ }
21
+ }
22
+ catch (error) {
23
+ // Fall through to directory name
24
+ }
25
+ // Fallback to directory name
26
+ return path.basename(process.cwd());
27
+ }
28
+ /**
29
+ * Find package.json by walking up the directory tree
30
+ */
31
+ async function findPackageJson(startDir) {
32
+ let currentDir = startDir;
33
+ const root = path.parse(currentDir).root;
34
+ while (currentDir !== root) {
35
+ const packageJsonPath = path.join(currentDir, "package.json");
36
+ try {
37
+ await fs.access(packageJsonPath);
38
+ return packageJsonPath;
39
+ }
40
+ catch {
41
+ // Not found, go up one level
42
+ currentDir = path.dirname(currentDir);
43
+ }
44
+ }
45
+ return null;
46
+ }
@@ -0,0 +1,16 @@
1
+ import { PlanApi, RunsApi } from "@griffin-app/griffin-hub-sdk";
2
+ import type { TestPlanV1 } from "@griffin-app/griffin-ts/types";
3
+ /**
4
+ * Create configured SDK API instances
5
+ */
6
+ export declare function createSdkClients(config: {
7
+ baseUrl: string;
8
+ apiToken?: string;
9
+ }): {
10
+ planApi: PlanApi;
11
+ runsApi: RunsApi;
12
+ };
13
+ /**
14
+ * Inject projectId into a plan payload before sending to runner
15
+ */
16
+ export declare function injectProjectId(plan: Omit<TestPlanV1, "project">, projectId: string): TestPlanV1;
@@ -0,0 +1,23 @@
1
+ import { PlanApi, RunsApi, Configuration } from "@griffin-app/griffin-hub-sdk";
2
+ /**
3
+ * Create configured SDK API instances
4
+ */
5
+ export function createSdkClients(config) {
6
+ const configuration = new Configuration({
7
+ basePath: config.baseUrl.replace(/\/$/, ""), // Remove trailing slash
8
+ accessToken: config.apiToken,
9
+ });
10
+ return {
11
+ planApi: new PlanApi(configuration),
12
+ runsApi: new RunsApi(configuration),
13
+ };
14
+ }
15
+ /**
16
+ * Inject projectId into a plan payload before sending to runner
17
+ */
18
+ export function injectProjectId(plan, projectId) {
19
+ return {
20
+ ...plan,
21
+ project: projectId,
22
+ };
23
+ }
@@ -0,0 +1,52 @@
1
+ import { type StateFile, type EnvironmentConfig } from "../schemas/state.js";
2
+ export declare const STATE_DIR = ".griffin";
3
+ export declare const STATE_FILE = "state.json";
4
+ /**
5
+ * Get the state directory path (in current working directory)
6
+ */
7
+ export declare function getStateDirPath(): string;
8
+ /**
9
+ * Get the state file path
10
+ */
11
+ export declare function getStateFilePath(): string;
12
+ /**
13
+ * Check if state file exists
14
+ */
15
+ export declare function stateExists(): Promise<boolean>;
16
+ /**
17
+ * Load state file from disk
18
+ * Throws if file doesn't exist or is invalid
19
+ */
20
+ export declare function loadState(): Promise<StateFile>;
21
+ /**
22
+ * Save state file to disk
23
+ */
24
+ export declare function saveState(state: StateFile): Promise<void>;
25
+ /**
26
+ * Initialize a new state file
27
+ */
28
+ export declare function initState(projectId: string): Promise<void>;
29
+ /**
30
+ * Add or update an environment
31
+ */
32
+ export declare function addEnvironment(name: string, config: EnvironmentConfig): Promise<void>;
33
+ /**
34
+ * Remove an environment
35
+ */
36
+ export declare function removeEnvironment(name: string): Promise<void>;
37
+ /**
38
+ * Set the default environment
39
+ */
40
+ export declare function setDefaultEnvironment(name: string): Promise<void>;
41
+ /**
42
+ * Get the current environment name (from flag, env var, or default)
43
+ */
44
+ export declare function resolveEnvironment(envFlag?: string): Promise<string>;
45
+ /**
46
+ * Get environment configuration
47
+ */
48
+ export declare function getEnvironment(name: string): Promise<EnvironmentConfig>;
49
+ /**
50
+ * Get the project ID
51
+ */
52
+ export declare function getProjectId(): Promise<string>;