@griffin-app/griffin-cli 1.0.4 → 1.0.6
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 +55 -9
- package/dist/cli.js +14 -0
- package/dist/commands/env.js +3 -1
- package/dist/commands/hub/apply.js +17 -14
- package/dist/commands/hub/connect.js +10 -4
- package/dist/commands/hub/login.d.ts +1 -0
- package/dist/commands/hub/login.js +89 -0
- package/dist/commands/hub/logout.d.ts +6 -0
- package/dist/commands/hub/logout.js +16 -0
- package/dist/commands/hub/plan.js +16 -13
- package/dist/commands/hub/run.js +23 -16
- package/dist/commands/hub/runs.js +9 -16
- package/dist/commands/hub/status.js +8 -4
- package/dist/core/apply.d.ts +1 -1
- package/dist/core/apply.js +18 -19
- package/dist/core/apply.test.js +28 -17
- package/dist/core/credentials.d.ts +36 -0
- package/dist/core/credentials.js +98 -0
- package/dist/core/credentials.test.d.ts +1 -0
- package/dist/core/credentials.test.js +137 -0
- package/dist/core/diff.d.ts +5 -4
- package/dist/core/diff.js +2 -1
- package/dist/core/diff.test.js +24 -9
- package/dist/core/plan-diff.d.ts +4 -5
- package/dist/core/plan-diff.js +7 -8
- package/dist/core/sdk.d.ts +4 -0
- package/dist/core/sdk.js +12 -0
- package/dist/core/variables.js +1 -2
- package/dist/index.d.ts +8 -3
- package/dist/index.js +6 -2
- package/dist/resolve.d.ts +1 -1
- package/dist/resolve.js +6 -1
- package/dist/schemas/credentials.d.ts +24 -0
- package/dist/schemas/credentials.js +23 -0
- package/dist/schemas/state.d.ts +5 -5
- package/dist/schemas/state.js +4 -4
- package/dist/utils/sdk-error.d.ts +8 -0
- package/dist/utils/sdk-error.js +114 -0
- package/package.json +5 -4
package/dist/core/apply.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import { loadVariables } from "./variables.js";
|
|
2
|
-
import { resolvePlan } from "../resolve.js";
|
|
3
1
|
import { terminal } from "../utils/terminal.js";
|
|
2
|
+
import { withSDKErrorHandling } from "../utils/sdk-error.js";
|
|
4
3
|
/**
|
|
5
4
|
* Apply diff actions to the hub.
|
|
6
5
|
* CLI injects both project and environment into plan payloads.
|
|
7
6
|
*/
|
|
8
|
-
export async function applyDiff(diff, sdk,
|
|
7
|
+
export async function applyDiff(diff, sdk, options) {
|
|
9
8
|
const applied = [];
|
|
10
9
|
const errors = [];
|
|
11
10
|
// Filter out noop actions
|
|
@@ -22,10 +21,10 @@ export async function applyDiff(diff, sdk, projectId, environment, options) {
|
|
|
22
21
|
}
|
|
23
22
|
switch (action.type) {
|
|
24
23
|
case "create":
|
|
25
|
-
await applyCreate(action, sdk,
|
|
24
|
+
await applyCreate(action, sdk, applied);
|
|
26
25
|
break;
|
|
27
26
|
case "update":
|
|
28
|
-
await applyUpdate(action, sdk,
|
|
27
|
+
await applyUpdate(action, sdk, applied);
|
|
29
28
|
break;
|
|
30
29
|
case "delete":
|
|
31
30
|
await applyDelete(action, sdk, applied);
|
|
@@ -55,10 +54,10 @@ export async function applyDiff(diff, sdk, projectId, environment, options) {
|
|
|
55
54
|
/**
|
|
56
55
|
* Apply a create action
|
|
57
56
|
*/
|
|
58
|
-
async function applyCreate(action, sdk,
|
|
59
|
-
const
|
|
60
|
-
const variables = await loadVariables(environment);
|
|
61
|
-
const resolvedPlan = resolvePlan(plan, projectId, environment, variables);
|
|
57
|
+
async function applyCreate(action, sdk, applied) {
|
|
58
|
+
const resolvedPlan = action.plan;
|
|
59
|
+
//const variables = await loadVariables(environment);
|
|
60
|
+
//const resolvedPlan = resolvePlan(plan, projectId, environment, variables);
|
|
62
61
|
const { data: createdPlan } = await sdk.postPlan({
|
|
63
62
|
body: resolvedPlan,
|
|
64
63
|
});
|
|
@@ -72,35 +71,35 @@ async function applyCreate(action, sdk, projectId, environment, applied) {
|
|
|
72
71
|
/**
|
|
73
72
|
* Apply an update action
|
|
74
73
|
*/
|
|
75
|
-
async function applyUpdate(action, sdk,
|
|
76
|
-
const
|
|
77
|
-
const remotePlan = action.remotePlan
|
|
78
|
-
const variables = await loadVariables(environment);
|
|
79
|
-
const resolvedPlan = resolvePlan(plan, projectId, environment, variables);
|
|
74
|
+
async function applyUpdate(action, sdk, applied) {
|
|
75
|
+
const resolvedPlan = action.plan;
|
|
76
|
+
//const remotePlan = action.remotePlan!;
|
|
77
|
+
//const variables = await loadVariables(environment);
|
|
78
|
+
//const resolvedPlan = resolvePlan(plan, projectId, environment, variables);
|
|
80
79
|
// Use the remote plan's ID for the update
|
|
81
80
|
await sdk.putPlanById({
|
|
82
81
|
path: {
|
|
83
|
-
id: remotePlan.id,
|
|
82
|
+
id: action.remotePlan.id,
|
|
84
83
|
},
|
|
85
84
|
body: resolvedPlan,
|
|
86
85
|
});
|
|
87
86
|
applied.push({
|
|
88
87
|
type: "update",
|
|
89
|
-
planName:
|
|
88
|
+
planName: action.remotePlan.name,
|
|
90
89
|
success: true,
|
|
91
90
|
});
|
|
92
|
-
terminal.success(`Updated: ${terminal.colors.cyan(
|
|
91
|
+
terminal.success(`Updated: ${terminal.colors.cyan(action.remotePlan.name)}`);
|
|
93
92
|
}
|
|
94
93
|
/**
|
|
95
94
|
* Apply a delete action
|
|
96
95
|
*/
|
|
97
96
|
async function applyDelete(action, sdk, applied) {
|
|
98
97
|
const remotePlan = action.remotePlan;
|
|
99
|
-
await sdk.deletePlanById({
|
|
98
|
+
await withSDKErrorHandling(() => sdk.deletePlanById({
|
|
100
99
|
path: {
|
|
101
100
|
id: remotePlan.id,
|
|
102
101
|
},
|
|
103
|
-
});
|
|
102
|
+
}), `Failed to delete plan "${remotePlan.name}"`);
|
|
104
103
|
applied.push({
|
|
105
104
|
type: "delete",
|
|
106
105
|
planName: remotePlan.name,
|
package/dist/core/apply.test.js
CHANGED
|
@@ -20,7 +20,7 @@ describe("applyDiff", () => {
|
|
|
20
20
|
summary: { creates: 0, updates: 0, deletes: 0, noops: 0 },
|
|
21
21
|
};
|
|
22
22
|
const mockPlanApi = {};
|
|
23
|
-
const result = await applyDiff(diff, mockPlanApi
|
|
23
|
+
const result = await applyDiff(diff, mockPlanApi);
|
|
24
24
|
expect(result.success).toBe(true);
|
|
25
25
|
expect(result.applied).toHaveLength(0);
|
|
26
26
|
expect(result.errors).toHaveLength(0);
|
|
@@ -39,7 +39,7 @@ describe("applyDiff", () => {
|
|
|
39
39
|
summary: { creates: 0, updates: 0, deletes: 0, noops: 1 },
|
|
40
40
|
};
|
|
41
41
|
const mockPlanApi = {};
|
|
42
|
-
const result = await applyDiff(diff, mockPlanApi
|
|
42
|
+
const result = await applyDiff(diff, mockPlanApi);
|
|
43
43
|
expect(result.success).toBe(true);
|
|
44
44
|
expect(result.applied).toHaveLength(0);
|
|
45
45
|
});
|
|
@@ -57,11 +57,11 @@ describe("applyDiff", () => {
|
|
|
57
57
|
summary: { creates: 1, updates: 0, deletes: 0, noops: 0 },
|
|
58
58
|
};
|
|
59
59
|
const mockPlanApi = {
|
|
60
|
-
|
|
60
|
+
postPlan: vi.fn().mockResolvedValue({
|
|
61
61
|
data: { data: { ...plan, id: "created-id" } },
|
|
62
62
|
}),
|
|
63
63
|
};
|
|
64
|
-
const result = await applyDiff(diff, mockPlanApi
|
|
64
|
+
const result = await applyDiff(diff, mockPlanApi);
|
|
65
65
|
expect(result.success).toBe(true);
|
|
66
66
|
expect(result.applied).toHaveLength(1);
|
|
67
67
|
expect(result.applied[0].type).toBe("create");
|
|
@@ -69,9 +69,11 @@ describe("applyDiff", () => {
|
|
|
69
69
|
expect(result.applied[0].success).toBe(true);
|
|
70
70
|
// Verify the API was called with injected project and environment
|
|
71
71
|
expect(mockPlanApi.postPlan).toHaveBeenCalledWith(expect.objectContaining({
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
72
|
+
body: expect.objectContaining({
|
|
73
|
+
name: "new-plan",
|
|
74
|
+
project: "test-project",
|
|
75
|
+
environment: "test",
|
|
76
|
+
}),
|
|
75
77
|
}));
|
|
76
78
|
});
|
|
77
79
|
it("should apply update action", async () => {
|
|
@@ -93,17 +95,22 @@ describe("applyDiff", () => {
|
|
|
93
95
|
data: { data: remotePlan },
|
|
94
96
|
}),
|
|
95
97
|
};
|
|
96
|
-
const result = await applyDiff(diff, mockPlanApi
|
|
98
|
+
const result = await applyDiff(diff, mockPlanApi);
|
|
97
99
|
expect(result.success).toBe(true);
|
|
98
100
|
expect(result.applied).toHaveLength(1);
|
|
99
101
|
expect(result.applied[0].type).toBe("update");
|
|
100
102
|
expect(result.applied[0].planName).toBe("existing-plan");
|
|
101
103
|
expect(result.applied[0].success).toBe(true);
|
|
102
104
|
// Verify the API was called with the remote plan's ID
|
|
103
|
-
expect(mockPlanApi.putPlanById).toHaveBeenCalledWith(
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
105
|
+
expect(mockPlanApi.putPlanById).toHaveBeenCalledWith(expect.objectContaining({
|
|
106
|
+
body: expect.objectContaining({
|
|
107
|
+
name: "existing-plan",
|
|
108
|
+
project: "test-project",
|
|
109
|
+
environment: "test",
|
|
110
|
+
}),
|
|
111
|
+
path: {
|
|
112
|
+
id: "remote-id",
|
|
113
|
+
},
|
|
107
114
|
}));
|
|
108
115
|
});
|
|
109
116
|
it("should apply delete action", async () => {
|
|
@@ -115,14 +122,18 @@ describe("applyDiff", () => {
|
|
|
115
122
|
const mockPlanApi = {
|
|
116
123
|
deletePlanById: vi.fn().mockResolvedValue({}),
|
|
117
124
|
};
|
|
118
|
-
const result = await applyDiff(diff, mockPlanApi
|
|
125
|
+
const result = await applyDiff(diff, mockPlanApi);
|
|
119
126
|
expect(result.success).toBe(true);
|
|
120
127
|
expect(result.applied).toHaveLength(1);
|
|
121
128
|
expect(result.applied[0].type).toBe("delete");
|
|
122
129
|
expect(result.applied[0].planName).toBe("old-plan");
|
|
123
130
|
expect(result.applied[0].success).toBe(true);
|
|
124
131
|
// Verify the API was called with the remote plan's ID
|
|
125
|
-
expect(mockPlanApi.deletePlanById).toHaveBeenCalledWith(
|
|
132
|
+
expect(mockPlanApi.deletePlanById).toHaveBeenCalledWith({
|
|
133
|
+
path: {
|
|
134
|
+
id: remotePlan.id,
|
|
135
|
+
},
|
|
136
|
+
});
|
|
126
137
|
});
|
|
127
138
|
it("should handle errors gracefully", async () => {
|
|
128
139
|
const plan = createPlan("failing-plan");
|
|
@@ -140,7 +151,7 @@ describe("applyDiff", () => {
|
|
|
140
151
|
const mockPlanApi = {
|
|
141
152
|
postPlan: vi.fn().mockRejectedValue(new Error("API Error")),
|
|
142
153
|
};
|
|
143
|
-
const result = await applyDiff(diff, mockPlanApi
|
|
154
|
+
const result = await applyDiff(diff, mockPlanApi);
|
|
144
155
|
expect(result.success).toBe(false);
|
|
145
156
|
expect(result.errors).toHaveLength(1);
|
|
146
157
|
expect(result.applied).toHaveLength(1);
|
|
@@ -161,9 +172,9 @@ describe("applyDiff", () => {
|
|
|
161
172
|
summary: { creates: 1, updates: 0, deletes: 0, noops: 0 },
|
|
162
173
|
};
|
|
163
174
|
const mockPlanApi = {
|
|
164
|
-
|
|
175
|
+
postPlan: vi.fn(),
|
|
165
176
|
};
|
|
166
|
-
const result = await applyDiff(diff, mockPlanApi,
|
|
177
|
+
const result = await applyDiff(diff, mockPlanApi, {
|
|
167
178
|
dryRun: true,
|
|
168
179
|
});
|
|
169
180
|
expect(result.success).toBe(true);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type CredentialsFile, HubCredentials } from "../schemas/credentials.js";
|
|
2
|
+
export declare const CREDENTIALS_DIR = ".griffin";
|
|
3
|
+
export declare const CREDENTIALS_FILE = "credentials.json";
|
|
4
|
+
/**
|
|
5
|
+
* Get the user-level credentials directory path (~/.griffin)
|
|
6
|
+
*/
|
|
7
|
+
export declare function getCredentialsDirPath(): string;
|
|
8
|
+
/**
|
|
9
|
+
* Get the credentials file path
|
|
10
|
+
*/
|
|
11
|
+
export declare function getCredentialsFilePath(): string;
|
|
12
|
+
/**
|
|
13
|
+
* Check if credentials file exists
|
|
14
|
+
*/
|
|
15
|
+
export declare function credentialsExist(): Promise<boolean>;
|
|
16
|
+
/**
|
|
17
|
+
* Load credentials file from disk
|
|
18
|
+
* Returns empty credentials if file doesn't exist
|
|
19
|
+
*/
|
|
20
|
+
export declare function loadCredentials(): Promise<CredentialsFile>;
|
|
21
|
+
/**
|
|
22
|
+
* Save credentials file to disk with restricted permissions
|
|
23
|
+
*/
|
|
24
|
+
export declare function saveCredentials(credentials: CredentialsFile): Promise<void>;
|
|
25
|
+
/**
|
|
26
|
+
* Save hub credentials (token from login or API key from connect)
|
|
27
|
+
*/
|
|
28
|
+
export declare function saveHubCredentials(token: string): Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* Get credentials for a specific hub URL
|
|
31
|
+
*/
|
|
32
|
+
export declare function getHubCredentials(): Promise<HubCredentials | undefined>;
|
|
33
|
+
/**
|
|
34
|
+
* Remove credentials for a specific hub URL
|
|
35
|
+
*/
|
|
36
|
+
export declare function removeHubCredentials(): Promise<void>;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import { Value } from "typebox/value";
|
|
5
|
+
import { CredentialsFileSchema, createEmptyCredentials, } from "../schemas/credentials.js";
|
|
6
|
+
export const CREDENTIALS_DIR = ".griffin";
|
|
7
|
+
export const CREDENTIALS_FILE = "credentials.json";
|
|
8
|
+
/**
|
|
9
|
+
* Get the user-level credentials directory path (~/.griffin)
|
|
10
|
+
*/
|
|
11
|
+
export function getCredentialsDirPath() {
|
|
12
|
+
return path.join(os.homedir(), CREDENTIALS_DIR);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Get the credentials file path
|
|
16
|
+
*/
|
|
17
|
+
export function getCredentialsFilePath() {
|
|
18
|
+
return path.join(getCredentialsDirPath(), CREDENTIALS_FILE);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Check if credentials file exists
|
|
22
|
+
*/
|
|
23
|
+
export async function credentialsExist() {
|
|
24
|
+
try {
|
|
25
|
+
await fs.access(getCredentialsFilePath());
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Load credentials file from disk
|
|
34
|
+
* Returns empty credentials if file doesn't exist
|
|
35
|
+
*/
|
|
36
|
+
export async function loadCredentials() {
|
|
37
|
+
const credentialsFilePath = getCredentialsFilePath();
|
|
38
|
+
try {
|
|
39
|
+
const content = await fs.readFile(credentialsFilePath, "utf-8");
|
|
40
|
+
const data = JSON.parse(content);
|
|
41
|
+
// Validate schema
|
|
42
|
+
if (Value.Check(CredentialsFileSchema, data)) {
|
|
43
|
+
return data;
|
|
44
|
+
}
|
|
45
|
+
// Invalid schema
|
|
46
|
+
const errors = [...Value.Errors(CredentialsFileSchema, data)];
|
|
47
|
+
throw new Error(`Invalid credentials file schema:\n${errors.map((e) => ` - ${e.path || "unknown"}: ${e.message}`).join("\n")}`);
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
if (error.code === "ENOENT") {
|
|
51
|
+
// File doesn't exist, return empty credentials
|
|
52
|
+
return createEmptyCredentials();
|
|
53
|
+
}
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Save credentials file to disk with restricted permissions
|
|
59
|
+
*/
|
|
60
|
+
export async function saveCredentials(credentials) {
|
|
61
|
+
const credentialsDirPath = getCredentialsDirPath();
|
|
62
|
+
const credentialsFilePath = getCredentialsFilePath();
|
|
63
|
+
// Ensure directory exists
|
|
64
|
+
await fs.mkdir(credentialsDirPath, { recursive: true });
|
|
65
|
+
// Validate before saving
|
|
66
|
+
if (!Value.Check(CredentialsFileSchema, credentials)) {
|
|
67
|
+
const errors = [...Value.Errors(CredentialsFileSchema, credentials)];
|
|
68
|
+
throw new Error(`Invalid credentials data:\n${errors.map((e) => ` - ${e.path || "unknown"}: ${e.message}`).join("\n")}`);
|
|
69
|
+
}
|
|
70
|
+
// Write with pretty formatting and restricted permissions (0600 - read/write for owner only)
|
|
71
|
+
await fs.writeFile(credentialsFilePath, JSON.stringify(credentials, null, 2), { encoding: "utf-8", mode: 0o600 });
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Save hub credentials (token from login or API key from connect)
|
|
75
|
+
*/
|
|
76
|
+
export async function saveHubCredentials(token) {
|
|
77
|
+
const credentials = await loadCredentials();
|
|
78
|
+
credentials.hub = {
|
|
79
|
+
token,
|
|
80
|
+
updatedAt: new Date().toISOString(),
|
|
81
|
+
};
|
|
82
|
+
await saveCredentials(credentials);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Get credentials for a specific hub URL
|
|
86
|
+
*/
|
|
87
|
+
export async function getHubCredentials() {
|
|
88
|
+
const credentials = await loadCredentials();
|
|
89
|
+
return credentials.hub;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Remove credentials for a specific hub URL
|
|
93
|
+
*/
|
|
94
|
+
export async function removeHubCredentials() {
|
|
95
|
+
const credentials = await loadCredentials();
|
|
96
|
+
delete credentials.hub;
|
|
97
|
+
await saveCredentials(credentials);
|
|
98
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import { loadCredentials, saveCredentials, saveHubCredentials, getHubCredentials, removeHubCredentials, } from "./credentials.js";
|
|
6
|
+
import { createEmptyCredentials } from "../schemas/credentials.js";
|
|
7
|
+
const TEST_DIR = path.join(os.tmpdir(), `griffin-test-${Date.now()}`);
|
|
8
|
+
const TEST_CREDS_FILE = path.join(TEST_DIR, "credentials.json");
|
|
9
|
+
// Mock the credentials directory to use test directory
|
|
10
|
+
const originalHomedir = os.homedir;
|
|
11
|
+
describe("Credentials Management", () => {
|
|
12
|
+
beforeEach(async () => {
|
|
13
|
+
// Create test directory
|
|
14
|
+
await fs.mkdir(TEST_DIR, { recursive: true });
|
|
15
|
+
// Mock homedir to point to test directory
|
|
16
|
+
os.homedir = () => TEST_DIR;
|
|
17
|
+
});
|
|
18
|
+
afterEach(async () => {
|
|
19
|
+
// Restore original homedir
|
|
20
|
+
os.homedir = originalHomedir;
|
|
21
|
+
// Clean up test directory
|
|
22
|
+
await fs.rm(TEST_DIR, { recursive: true, force: true });
|
|
23
|
+
});
|
|
24
|
+
describe("createEmptyCredentials", () => {
|
|
25
|
+
it("should create empty credentials structure", () => {
|
|
26
|
+
const creds = createEmptyCredentials();
|
|
27
|
+
expect(creds).toEqual({
|
|
28
|
+
version: 1,
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
describe("loadCredentials", () => {
|
|
33
|
+
it("should return empty credentials if file doesn't exist", async () => {
|
|
34
|
+
const creds = await loadCredentials();
|
|
35
|
+
expect(creds).toEqual(createEmptyCredentials());
|
|
36
|
+
});
|
|
37
|
+
it("should load existing credentials from file", async () => {
|
|
38
|
+
const testCreds = createEmptyCredentials();
|
|
39
|
+
testCreds.hub = {
|
|
40
|
+
token: "test-token",
|
|
41
|
+
updatedAt: new Date().toISOString(),
|
|
42
|
+
};
|
|
43
|
+
await saveCredentials(testCreds);
|
|
44
|
+
const loaded = await loadCredentials();
|
|
45
|
+
expect(loaded).toEqual(testCreds);
|
|
46
|
+
});
|
|
47
|
+
it("should throw error for invalid schema", async () => {
|
|
48
|
+
const invalidCreds = { invalid: "data" };
|
|
49
|
+
await fs.mkdir(path.join(TEST_DIR, ".griffin"), { recursive: true });
|
|
50
|
+
await fs.writeFile(path.join(TEST_DIR, ".griffin", "credentials.json"), JSON.stringify(invalidCreds));
|
|
51
|
+
await expect(loadCredentials()).rejects.toThrow("Invalid credentials file schema");
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
describe("saveHubCredentials", () => {
|
|
55
|
+
it("should save credentials for a hub", async () => {
|
|
56
|
+
const token = "test-token-123";
|
|
57
|
+
await saveHubCredentials(token);
|
|
58
|
+
const creds = await loadCredentials();
|
|
59
|
+
expect(creds.hub).toBeDefined();
|
|
60
|
+
expect(creds.hub.token).toBe(token);
|
|
61
|
+
expect(creds.hub.updatedAt).toBeDefined();
|
|
62
|
+
});
|
|
63
|
+
it("should normalize hub URL by removing trailing slash", async () => {
|
|
64
|
+
const token = "test-token";
|
|
65
|
+
await saveHubCredentials(token);
|
|
66
|
+
const creds = await loadCredentials();
|
|
67
|
+
expect(creds.hub).toBeDefined();
|
|
68
|
+
expect(creds.hub.token).toBe(token);
|
|
69
|
+
});
|
|
70
|
+
it("should update existing hub credentials", async () => {
|
|
71
|
+
const token1 = "token-1";
|
|
72
|
+
const token2 = "token-2";
|
|
73
|
+
await saveHubCredentials(token1);
|
|
74
|
+
const creds1 = await loadCredentials();
|
|
75
|
+
const updatedAt1 = creds1.hub.updatedAt;
|
|
76
|
+
// Wait a bit to ensure different timestamp
|
|
77
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
78
|
+
await saveHubCredentials(token2);
|
|
79
|
+
const creds2 = await loadCredentials();
|
|
80
|
+
const updatedAt2 = creds2.hub.updatedAt;
|
|
81
|
+
expect(creds2.hub.token).toBe(token2);
|
|
82
|
+
expect(updatedAt2).not.toBe(updatedAt1);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
describe("getHubCredentials", () => {
|
|
86
|
+
it("should return undefined for non-existent hub", async () => {
|
|
87
|
+
const creds = await getHubCredentials();
|
|
88
|
+
expect(creds).toBeUndefined();
|
|
89
|
+
});
|
|
90
|
+
it("should return credentials for existing hub", async () => {
|
|
91
|
+
const token = "test-token";
|
|
92
|
+
await saveHubCredentials(token);
|
|
93
|
+
const creds = await getHubCredentials();
|
|
94
|
+
expect(creds).toBeDefined();
|
|
95
|
+
expect(creds.token).toBe(token);
|
|
96
|
+
});
|
|
97
|
+
it("should normalize hub URL when retrieving", async () => {
|
|
98
|
+
await saveHubCredentials("test-token");
|
|
99
|
+
const creds = await getHubCredentials();
|
|
100
|
+
expect(creds).toBeDefined();
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
describe("removeHubCredentials", () => {
|
|
104
|
+
it("should remove credentials for a hub", async () => {
|
|
105
|
+
const hubUrl = "https://hub.test.com";
|
|
106
|
+
await saveHubCredentials("test-token");
|
|
107
|
+
await removeHubCredentials();
|
|
108
|
+
const creds = await loadCredentials();
|
|
109
|
+
expect(creds.hub).toBeUndefined();
|
|
110
|
+
});
|
|
111
|
+
it("should not affect other hub credentials", async () => {
|
|
112
|
+
await saveHubCredentials("token1");
|
|
113
|
+
await saveHubCredentials("token2");
|
|
114
|
+
await removeHubCredentials();
|
|
115
|
+
const creds = await loadCredentials();
|
|
116
|
+
expect(creds.hub).toBeUndefined();
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
describe("listHubUrls", () => {
|
|
120
|
+
it("should return empty array for no hubs", async () => {
|
|
121
|
+
const creds = await getHubCredentials();
|
|
122
|
+
expect(creds).toBeUndefined();
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
describe("file permissions", () => {
|
|
126
|
+
it("should create credentials file with 0600 permissions", async () => {
|
|
127
|
+
const token = "test-token";
|
|
128
|
+
await saveHubCredentials(token);
|
|
129
|
+
const credsPath = path.join(TEST_DIR, ".griffin", "credentials.json");
|
|
130
|
+
const stats = await fs.stat(credsPath);
|
|
131
|
+
// Check permissions (0600 = owner read/write only)
|
|
132
|
+
// Note: permissions are platform-dependent, so we check that it's at least restrictive
|
|
133
|
+
const mode = stats.mode & 0o777;
|
|
134
|
+
expect(mode).toBe(0o600);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
});
|
package/dist/core/diff.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { PlanV1 } from "@griffin-app/griffin-hub-sdk";
|
|
2
|
-
import type { PlanDSL } from "@griffin-app/griffin-ts/types";
|
|
3
2
|
import { type PlanChanges } from "./plan-diff.js";
|
|
4
3
|
export type DiffActionType = "create" | "update" | "delete" | "noop";
|
|
4
|
+
export type ResolvedPlan = Omit<PlanV1, "id">;
|
|
5
5
|
export interface DiffAction {
|
|
6
6
|
type: DiffActionType;
|
|
7
|
-
plan:
|
|
7
|
+
plan: ResolvedPlan | null;
|
|
8
8
|
remotePlan: PlanV1 | null;
|
|
9
9
|
reason: string;
|
|
10
10
|
changes?: PlanChanges;
|
|
@@ -23,7 +23,8 @@ export interface DiffOptions {
|
|
|
23
23
|
}
|
|
24
24
|
/**
|
|
25
25
|
* Compute diff between local plans and remote plans (hub is source of truth).
|
|
26
|
-
*
|
|
26
|
+
* Local plans should be resolved (variables replaced with actual values) before calling this.
|
|
27
|
+
* Plans are matched by name.
|
|
27
28
|
*
|
|
28
29
|
* Rules:
|
|
29
30
|
* - CREATE: Plan exists locally but not on hub
|
|
@@ -31,7 +32,7 @@ export interface DiffOptions {
|
|
|
31
32
|
* - DELETE: Plan exists on hub but not locally (only if includeDeletions is true)
|
|
32
33
|
* - NOOP: Plan exists in both with same content
|
|
33
34
|
*/
|
|
34
|
-
export declare function computeDiff(localPlans:
|
|
35
|
+
export declare function computeDiff(localPlans: ResolvedPlan[], remotePlans: PlanV1[], options: DiffOptions): DiffResult;
|
|
35
36
|
/**
|
|
36
37
|
* Format diff result as human-readable text with granular changes
|
|
37
38
|
*/
|
package/dist/core/diff.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { comparePlans } from "./plan-diff.js";
|
|
2
2
|
/**
|
|
3
3
|
* Compute diff between local plans and remote plans (hub is source of truth).
|
|
4
|
-
*
|
|
4
|
+
* Local plans should be resolved (variables replaced with actual values) before calling this.
|
|
5
|
+
* Plans are matched by name.
|
|
5
6
|
*
|
|
6
7
|
* Rules:
|
|
7
8
|
* - CREATE: Plan exists locally but not on hub
|
package/dist/core/diff.test.js
CHANGED
|
@@ -14,10 +14,23 @@ function createPlan(name, overrides) {
|
|
|
14
14
|
...overrides,
|
|
15
15
|
};
|
|
16
16
|
}
|
|
17
|
+
// Helper to create a resolved plan (without id)
|
|
18
|
+
function createResolvedPlan(name, overrides) {
|
|
19
|
+
return {
|
|
20
|
+
name,
|
|
21
|
+
project: "test-project",
|
|
22
|
+
environment: "test",
|
|
23
|
+
version: "1.0",
|
|
24
|
+
frequency: { every: 5, unit: "MINUTE" },
|
|
25
|
+
nodes: [],
|
|
26
|
+
edges: [],
|
|
27
|
+
...overrides,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
17
30
|
describe("computeDiff", () => {
|
|
18
31
|
describe("CREATE actions", () => {
|
|
19
32
|
it("should create action when plan exists locally but not remotely", () => {
|
|
20
|
-
const local = [
|
|
33
|
+
const local = [createResolvedPlan("health-check")];
|
|
21
34
|
const remote = [];
|
|
22
35
|
const result = computeDiff(local, remote, {
|
|
23
36
|
includeDeletions: false,
|
|
@@ -34,7 +47,7 @@ describe("computeDiff", () => {
|
|
|
34
47
|
describe("UPDATE actions", () => {
|
|
35
48
|
it("should create update action when plan content differs", () => {
|
|
36
49
|
const local = [
|
|
37
|
-
|
|
50
|
+
createResolvedPlan("health-check", {
|
|
38
51
|
frequency: { every: 10, unit: "MINUTE" },
|
|
39
52
|
}),
|
|
40
53
|
];
|
|
@@ -57,9 +70,10 @@ describe("computeDiff", () => {
|
|
|
57
70
|
});
|
|
58
71
|
describe("NOOP actions", () => {
|
|
59
72
|
it("should create noop action when plan content matches", () => {
|
|
60
|
-
const
|
|
61
|
-
const
|
|
62
|
-
const
|
|
73
|
+
const resolvedPlan = createResolvedPlan("health-check");
|
|
74
|
+
const remotePlan = createPlan("health-check");
|
|
75
|
+
const local = [resolvedPlan];
|
|
76
|
+
const remote = [remotePlan];
|
|
63
77
|
const result = computeDiff(local, remote, {
|
|
64
78
|
includeDeletions: false,
|
|
65
79
|
});
|
|
@@ -93,11 +107,11 @@ describe("computeDiff", () => {
|
|
|
93
107
|
describe("Mixed scenarios", () => {
|
|
94
108
|
it("should handle multiple plans with different actions", () => {
|
|
95
109
|
const local = [
|
|
96
|
-
|
|
97
|
-
|
|
110
|
+
createResolvedPlan("new-plan"),
|
|
111
|
+
createResolvedPlan("updated-plan", {
|
|
98
112
|
frequency: { every: 10, unit: "MINUTE" },
|
|
99
113
|
}),
|
|
100
|
-
|
|
114
|
+
createResolvedPlan("unchanged-plan"),
|
|
101
115
|
];
|
|
102
116
|
const remote = [
|
|
103
117
|
createPlan("updated-plan", {
|
|
@@ -118,7 +132,8 @@ describe("computeDiff", () => {
|
|
|
118
132
|
});
|
|
119
133
|
describe("Matching by name", () => {
|
|
120
134
|
it("should match plans by name, not by ID", () => {
|
|
121
|
-
|
|
135
|
+
// Local plans don't have IDs after resolution
|
|
136
|
+
const local = [createResolvedPlan("health-check")];
|
|
122
137
|
const remote = [createPlan("health-check", { id: "remote-id-456" })];
|
|
123
138
|
const result = computeDiff(local, remote, {
|
|
124
139
|
includeDeletions: false,
|
package/dist/core/plan-diff.d.ts
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import type { PlanDSL } from "@griffin-app/griffin-ts/types";
|
|
2
1
|
import { PlanV1 } from "@griffin-app/griffin-hub-sdk";
|
|
3
|
-
import { NodeType } from "@griffin-app/griffin-ts/schema";
|
|
4
2
|
/**
|
|
5
3
|
* Represents a change to a single field
|
|
6
4
|
*/
|
|
@@ -15,7 +13,7 @@ export interface FieldChange {
|
|
|
15
13
|
export interface NodeChange {
|
|
16
14
|
type: "add" | "remove" | "modify";
|
|
17
15
|
nodeId: string;
|
|
18
|
-
nodeType:
|
|
16
|
+
nodeType: "ASSERTION" | "HTTP_REQUEST" | "WAIT";
|
|
19
17
|
summary: string;
|
|
20
18
|
fieldChanges: FieldChange[];
|
|
21
19
|
}
|
|
@@ -37,6 +35,7 @@ export interface PlanChanges {
|
|
|
37
35
|
topLevel: FieldChange[];
|
|
38
36
|
}
|
|
39
37
|
/**
|
|
40
|
-
* Compare two test plans and return granular changes
|
|
38
|
+
* Compare two test plans and return granular changes.
|
|
39
|
+
* Local plan should be resolved (variables replaced with actual values).
|
|
41
40
|
*/
|
|
42
|
-
export declare function comparePlans(local:
|
|
41
|
+
export declare function comparePlans(local: PlanV1, remote: PlanV1): PlanChanges;
|
package/dist/core/plan-diff.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import objectHash from "object-hash";
|
|
2
2
|
import { NodeType } from "@griffin-app/griffin-ts/schema";
|
|
3
3
|
/**
|
|
4
|
-
* Compare two test plans and return granular changes
|
|
4
|
+
* Compare two test plans and return granular changes.
|
|
5
|
+
* Local plan should be resolved (variables replaced with actual values).
|
|
5
6
|
*/
|
|
6
7
|
export function comparePlans(local, remote) {
|
|
7
8
|
const nodeChanges = compareNodes(local.nodes, remote.nodes);
|
|
@@ -77,14 +78,12 @@ function compareNodes(localNodes, remoteNodes) {
|
|
|
77
78
|
*/
|
|
78
79
|
function getNodeSummary(node) {
|
|
79
80
|
switch (node.type) {
|
|
80
|
-
case "
|
|
81
|
+
case "HTTP_REQUEST":
|
|
81
82
|
return `${node.method} ${formatValue(node.path)}`;
|
|
82
83
|
case "WAIT":
|
|
83
84
|
return `wait ${node.duration_ms}ms`;
|
|
84
85
|
case "ASSERTION":
|
|
85
86
|
return `${node.assertions.length} assertion(s)`;
|
|
86
|
-
default:
|
|
87
|
-
return node.type;
|
|
88
87
|
}
|
|
89
88
|
}
|
|
90
89
|
/**
|
|
@@ -117,8 +116,8 @@ function compareNodeFields(local, remote) {
|
|
|
117
116
|
return changes;
|
|
118
117
|
}
|
|
119
118
|
switch (local.type) {
|
|
120
|
-
case NodeType.
|
|
121
|
-
|
|
119
|
+
case NodeType.HTTP_REQUEST:
|
|
120
|
+
compareHttpRequestFields(local, remote, changes);
|
|
122
121
|
break;
|
|
123
122
|
case NodeType.WAIT:
|
|
124
123
|
compareWaitFields(local, remote, changes);
|
|
@@ -130,9 +129,9 @@ function compareNodeFields(local, remote) {
|
|
|
130
129
|
return changes;
|
|
131
130
|
}
|
|
132
131
|
/**
|
|
133
|
-
* Compare fields specific to
|
|
132
|
+
* Compare fields specific to HttpRequest nodes
|
|
134
133
|
*/
|
|
135
|
-
function
|
|
134
|
+
function compareHttpRequestFields(local, remote, changes) {
|
|
136
135
|
const fields = [
|
|
137
136
|
"method",
|
|
138
137
|
"path",
|