@griffin-app/griffin-cli 1.0.2 → 1.0.4
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 +69 -165
- package/dist/cli.js +21 -16
- package/dist/commands/env.d.ts +1 -22
- package/dist/commands/env.js +20 -109
- package/dist/commands/generate-key.js +16 -10
- package/dist/commands/hub/apply.js +51 -30
- package/dist/commands/hub/connect.js +7 -6
- package/dist/commands/hub/plan.js +31 -17
- package/dist/commands/hub/run.js +81 -52
- package/dist/commands/hub/runs.js +77 -37
- package/dist/commands/hub/status.js +12 -11
- package/dist/commands/init.js +32 -24
- package/dist/commands/local/run.js +30 -16
- package/dist/commands/validate.js +18 -17
- package/dist/core/apply.d.ts +2 -2
- package/dist/core/apply.js +33 -34
- package/dist/core/apply.test.js +45 -12
- package/dist/core/diff.d.ts +5 -5
- package/dist/core/diff.test.js +20 -11
- package/dist/core/discovery.d.ts +2 -3
- package/dist/core/discovery.js +3 -10
- package/dist/core/plan-diff.d.ts +4 -4
- package/dist/core/plan-diff.js +4 -3
- package/dist/core/sdk.d.ts +3 -11
- package/dist/core/sdk.js +14 -18
- package/dist/core/variables.js +14 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/resolve.d.ts +3 -0
- package/dist/resolve.js +9 -0
- package/dist/schemas/state.js +1 -3
- package/dist/test-runner.js +18 -22
- package/dist/utils/console.d.ts +5 -0
- package/dist/utils/console.js +5 -0
- package/dist/utils/terminal.d.ts +100 -0
- package/dist/utils/terminal.js +148 -0
- package/package.json +8 -4
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { loadState } from "../../core/state.js";
|
|
2
|
+
import { terminal } from "../../utils/terminal.js";
|
|
2
3
|
/**
|
|
3
4
|
* Show hub connection status
|
|
4
5
|
*/
|
|
@@ -6,24 +7,24 @@ export async function executeStatus() {
|
|
|
6
7
|
try {
|
|
7
8
|
const state = await loadState();
|
|
8
9
|
if (!state.runner) {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
terminal.warn("No hub connection configured.");
|
|
11
|
+
terminal.blank();
|
|
12
|
+
terminal.dim("Connect with:");
|
|
13
|
+
terminal.dim(" griffin hub connect --url <url> --token <token>");
|
|
13
14
|
return;
|
|
14
15
|
}
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
terminal.info("Hub connection:");
|
|
17
|
+
terminal.log(` URL: ${terminal.colors.cyan(state.runner.baseUrl)}`);
|
|
17
18
|
if (state.runner.apiToken) {
|
|
18
|
-
|
|
19
|
+
terminal.log(` API Token: ${terminal.colors.dim(state.runner.apiToken.substring(0, 8) + "...")}`);
|
|
19
20
|
}
|
|
20
21
|
else {
|
|
21
|
-
|
|
22
|
+
terminal.log(` API Token: ${terminal.colors.dim("(not set)")}`);
|
|
22
23
|
}
|
|
23
|
-
|
|
24
|
+
terminal.blank();
|
|
24
25
|
}
|
|
25
26
|
catch (error) {
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
terminal.error(error.message);
|
|
28
|
+
terminal.exit(1);
|
|
28
29
|
}
|
|
29
30
|
}
|
package/dist/commands/init.js
CHANGED
|
@@ -1,41 +1,49 @@
|
|
|
1
1
|
import { initState, stateExists, getStateFilePath, addEnvironment, } from "../core/state.js";
|
|
2
2
|
import { detectProjectId } from "../core/project.js";
|
|
3
|
+
import { terminal } from "../utils/terminal.js";
|
|
3
4
|
/**
|
|
4
5
|
* Initialize griffin in the current directory
|
|
5
6
|
*/
|
|
6
7
|
export async function executeInit(options) {
|
|
7
|
-
|
|
8
|
+
const spinner = terminal.spinner("Initializing griffin...").start();
|
|
8
9
|
// Check if already initialized
|
|
9
10
|
if (await stateExists()) {
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
spinner.fail("Already initialized");
|
|
12
|
+
terminal.dim(`State file exists: ${getStateFilePath()}`);
|
|
13
|
+
terminal.exit(1);
|
|
12
14
|
}
|
|
13
15
|
// Determine project ID
|
|
14
16
|
let projectId = options.project;
|
|
15
17
|
if (!projectId) {
|
|
16
18
|
projectId = await detectProjectId();
|
|
17
19
|
}
|
|
18
|
-
|
|
19
|
-
console.log("");
|
|
20
|
+
spinner.succeed(`Project: ${terminal.colors.cyan(projectId)}`);
|
|
20
21
|
// Initialize state file
|
|
21
22
|
await initState(projectId);
|
|
22
|
-
|
|
23
|
-
// Create
|
|
24
|
-
await addEnvironment("
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
23
|
+
terminal.success(`Created state file: ${terminal.colors.dim(getStateFilePath())}`);
|
|
24
|
+
// Create default environments
|
|
25
|
+
await addEnvironment("dev", {});
|
|
26
|
+
await addEnvironment("staging", {});
|
|
27
|
+
await addEnvironment("production", {});
|
|
28
|
+
terminal.success("Created default environments (dev, staging, production)");
|
|
29
|
+
terminal.blank();
|
|
30
|
+
terminal.success("Initialization complete!");
|
|
31
|
+
terminal.blank();
|
|
32
|
+
terminal.info("Next steps:");
|
|
33
|
+
terminal.dim(" 1. Create a variables.yaml file in the project root:");
|
|
34
|
+
terminal.dim(" environments:");
|
|
35
|
+
terminal.dim(" dev:");
|
|
36
|
+
terminal.dim(" api-service: http://localhost:3000");
|
|
37
|
+
terminal.dim(" staging:");
|
|
38
|
+
terminal.dim(" api-service: https://staging.api.com");
|
|
39
|
+
terminal.dim(" production:");
|
|
40
|
+
terminal.dim(" api-service: https://api.example.com");
|
|
41
|
+
terminal.dim(" 2. Create test plans (*.griffin.ts files in __griffin__/ directories)");
|
|
42
|
+
terminal.dim(" 3. Run tests locally:");
|
|
43
|
+
terminal.dim(" griffin local run");
|
|
44
|
+
terminal.dim(" 4. Connect to hub (optional):");
|
|
45
|
+
terminal.dim(" griffin hub connect --url <url> --token <token>");
|
|
46
|
+
terminal.dim(" 5. Deploy to hub:");
|
|
47
|
+
terminal.dim(" griffin hub apply");
|
|
48
|
+
terminal.blank();
|
|
41
49
|
}
|
|
@@ -1,40 +1,54 @@
|
|
|
1
1
|
import { findTestFiles } from "../../test-discovery.js";
|
|
2
2
|
import { runTestFile } from "../../test-runner.js";
|
|
3
3
|
import { resolveEnvironment } from "../../core/state.js";
|
|
4
|
+
import { terminal } from "../../utils/terminal.js";
|
|
4
5
|
import { basename } from "path";
|
|
5
6
|
export async function executeRunLocal(options = {}) {
|
|
6
7
|
try {
|
|
7
8
|
// Resolve environment
|
|
8
9
|
const envName = await resolveEnvironment(options.env);
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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();
|
|
12
14
|
const testFiles = findTestFiles();
|
|
13
15
|
if (testFiles.length === 0) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
+
spinner.fail("No test files found");
|
|
17
|
+
terminal.dim("Looking for .ts files in __griffin__ directories.");
|
|
18
|
+
terminal.exit(1);
|
|
16
19
|
}
|
|
17
|
-
|
|
18
|
-
testFiles.forEach((file) =>
|
|
19
|
-
|
|
20
|
+
spinner.succeed(`Found ${terminal.colors.bold(testFiles.length.toString())} test file(s)`);
|
|
21
|
+
testFiles.forEach((file) => terminal.dim(` - ${file}`));
|
|
22
|
+
terminal.blank();
|
|
20
23
|
const results = await Promise.all(testFiles.map(async (file) => {
|
|
21
24
|
const fileName = basename(file);
|
|
22
|
-
|
|
25
|
+
const testSpinner = terminal
|
|
26
|
+
.spinner(`Running ${terminal.colors.cyan(fileName)}`)
|
|
27
|
+
.start();
|
|
23
28
|
const result = await runTest(file, envName);
|
|
29
|
+
if (result.success) {
|
|
30
|
+
testSpinner.succeed(`${terminal.colors.cyan(fileName)} passed`);
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
testSpinner.fail(`${terminal.colors.cyan(fileName)} failed`);
|
|
34
|
+
}
|
|
24
35
|
return result;
|
|
25
36
|
}));
|
|
26
37
|
// Print summary
|
|
27
38
|
const successful = results.filter((r) => r.success).length;
|
|
28
39
|
const failed = results.length - successful;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
40
|
+
terminal.blank();
|
|
41
|
+
if (failed === 0) {
|
|
42
|
+
terminal.success(`All tests passed (${terminal.colors.bold(successful.toString())} / ${results.length})`);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
terminal.error(`${terminal.colors.bold(failed.toString())} test(s) failed, ${terminal.colors.bold(successful.toString())} passed`);
|
|
46
|
+
terminal.exit(1);
|
|
33
47
|
}
|
|
34
48
|
}
|
|
35
49
|
catch (error) {
|
|
36
|
-
|
|
37
|
-
|
|
50
|
+
terminal.error(error.message);
|
|
51
|
+
terminal.exit(1);
|
|
38
52
|
}
|
|
39
53
|
}
|
|
40
54
|
async function runTest(file, envName) {
|
|
@@ -43,7 +57,7 @@ async function runTest(file, envName) {
|
|
|
43
57
|
return { success: result.success };
|
|
44
58
|
}
|
|
45
59
|
catch (error) {
|
|
46
|
-
|
|
60
|
+
terminal.error(error.message || String(error));
|
|
47
61
|
return { success: false };
|
|
48
62
|
}
|
|
49
63
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { loadState } from "../core/state.js";
|
|
2
2
|
import { discoverPlans, formatDiscoveryErrors } from "../core/discovery.js";
|
|
3
|
+
import { terminal } from "../utils/terminal.js";
|
|
3
4
|
/**
|
|
4
5
|
* Validate test plan files without syncing
|
|
5
6
|
*/
|
|
6
7
|
export async function executeValidate() {
|
|
7
|
-
|
|
8
|
-
console.log("");
|
|
8
|
+
const spinner = terminal.spinner("Validating test plans...").start();
|
|
9
9
|
try {
|
|
10
10
|
// Load state for discovery settings
|
|
11
11
|
const state = await loadState();
|
|
@@ -18,29 +18,30 @@ export async function executeValidate() {
|
|
|
18
18
|
const { plans, errors } = await discoverPlans(discoveryPattern, discoveryIgnore);
|
|
19
19
|
// Report errors
|
|
20
20
|
if (errors.length > 0) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
spinner.fail(`Validation failed with ${errors.length} error(s)`);
|
|
22
|
+
terminal.blank();
|
|
23
|
+
terminal.error(formatDiscoveryErrors(errors));
|
|
24
|
+
terminal.exit(1);
|
|
25
25
|
}
|
|
26
26
|
// Report success
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
spinner.succeed(`Found ${terminal.colors.bold(plans.length.toString())} valid plan(s)`);
|
|
28
|
+
terminal.blank();
|
|
29
29
|
for (const { plan, filePath, exportName } of plans) {
|
|
30
30
|
const shortPath = filePath.replace(process.cwd(), ".");
|
|
31
|
-
const exportInfo = exportName === "default" ? "" : ` (${exportName})
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
const exportInfo = exportName === "default" ? "" : terminal.colors.dim(` (${exportName})`);
|
|
32
|
+
terminal.log(` ${terminal.colors.green("●")} ${terminal.colors.cyan(plan.name)}${exportInfo}`);
|
|
33
|
+
terminal.dim(` ${shortPath}`);
|
|
34
|
+
terminal.dim(` Nodes: ${plan.nodes.length}, Edges: ${plan.edges.length}`);
|
|
35
35
|
if (plan.frequency) {
|
|
36
|
-
|
|
36
|
+
terminal.dim(` Schedule: Every ${plan.frequency.every} ${plan.frequency.unit}`);
|
|
37
37
|
}
|
|
38
|
-
|
|
38
|
+
terminal.blank();
|
|
39
39
|
}
|
|
40
|
-
|
|
40
|
+
terminal.success("All plans are valid");
|
|
41
41
|
}
|
|
42
42
|
catch (error) {
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
spinner.fail("Validation failed");
|
|
44
|
+
terminal.error(error.message);
|
|
45
|
+
terminal.exit(1);
|
|
45
46
|
}
|
|
46
47
|
}
|
package/dist/core/apply.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { DiffAction, DiffResult } from "./diff.js";
|
|
2
|
-
import type {
|
|
2
|
+
import type { GriffinHubSdk } from "@griffin-app/griffin-hub-sdk";
|
|
3
3
|
export interface ApplyResult {
|
|
4
4
|
success: boolean;
|
|
5
5
|
applied: ApplyAction[];
|
|
@@ -19,7 +19,7 @@ export interface ApplyError {
|
|
|
19
19
|
* Apply diff actions to the hub.
|
|
20
20
|
* CLI injects both project and environment into plan payloads.
|
|
21
21
|
*/
|
|
22
|
-
export declare function applyDiff(diff: DiffResult,
|
|
22
|
+
export declare function applyDiff(diff: DiffResult, sdk: GriffinHubSdk, projectId: string, environment: string, options?: {
|
|
23
23
|
dryRun?: boolean;
|
|
24
24
|
}): Promise<ApplyResult>;
|
|
25
25
|
/**
|
package/dist/core/apply.js
CHANGED
|
@@ -1,38 +1,39 @@
|
|
|
1
|
+
import { loadVariables } from "./variables.js";
|
|
2
|
+
import { resolvePlan } from "../resolve.js";
|
|
3
|
+
import { terminal } from "../utils/terminal.js";
|
|
1
4
|
/**
|
|
2
5
|
* Apply diff actions to the hub.
|
|
3
6
|
* CLI injects both project and environment into plan payloads.
|
|
4
7
|
*/
|
|
5
|
-
export async function applyDiff(diff,
|
|
8
|
+
export async function applyDiff(diff, sdk, projectId, environment, options) {
|
|
6
9
|
const applied = [];
|
|
7
10
|
const errors = [];
|
|
8
11
|
// Filter out noop actions
|
|
9
12
|
const actionsToApply = diff.actions.filter((a) => a.type !== "noop");
|
|
10
13
|
if (actionsToApply.length === 0) {
|
|
11
|
-
console.log("No changes to apply.");
|
|
12
14
|
return { success: true, applied: [], errors: [] };
|
|
13
15
|
}
|
|
14
|
-
console.log(`Applying ${actionsToApply.length} change(s)...`);
|
|
15
16
|
// Process each action
|
|
16
17
|
for (const action of actionsToApply) {
|
|
17
18
|
try {
|
|
18
19
|
if (options?.dryRun) {
|
|
19
|
-
|
|
20
|
+
terminal.dim(`[DRY RUN] Would ${action.type} plan: ${action.plan?.name || action.remotePlan?.name}`);
|
|
20
21
|
continue;
|
|
21
22
|
}
|
|
22
23
|
switch (action.type) {
|
|
23
24
|
case "create":
|
|
24
|
-
await applyCreate(action,
|
|
25
|
+
await applyCreate(action, sdk, projectId, environment, applied);
|
|
25
26
|
break;
|
|
26
27
|
case "update":
|
|
27
|
-
await applyUpdate(action,
|
|
28
|
+
await applyUpdate(action, sdk, projectId, environment, applied);
|
|
28
29
|
break;
|
|
29
30
|
case "delete":
|
|
30
|
-
await applyDelete(action,
|
|
31
|
+
await applyDelete(action, sdk, applied);
|
|
31
32
|
break;
|
|
32
33
|
}
|
|
33
34
|
}
|
|
34
35
|
catch (error) {
|
|
35
|
-
|
|
36
|
+
terminal.error(error.message);
|
|
36
37
|
errors.push({
|
|
37
38
|
action,
|
|
38
39
|
error: error,
|
|
@@ -54,60 +55,58 @@ export async function applyDiff(diff, planApi, projectId, environment, options)
|
|
|
54
55
|
/**
|
|
55
56
|
* Apply a create action
|
|
56
57
|
*/
|
|
57
|
-
async function applyCreate(action,
|
|
58
|
+
async function applyCreate(action, sdk, projectId, environment, applied) {
|
|
58
59
|
const plan = action.plan;
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
project: projectId,
|
|
65
|
-
environment,
|
|
66
|
-
};
|
|
67
|
-
const { data: createdPlan } = await planApi.planPost(payload);
|
|
60
|
+
const variables = await loadVariables(environment);
|
|
61
|
+
const resolvedPlan = resolvePlan(plan, projectId, environment, variables);
|
|
62
|
+
const { data: createdPlan } = await sdk.postPlan({
|
|
63
|
+
body: resolvedPlan,
|
|
64
|
+
});
|
|
68
65
|
applied.push({
|
|
69
66
|
type: "create",
|
|
70
67
|
planName: createdPlan.data.name,
|
|
71
68
|
success: true,
|
|
72
69
|
});
|
|
73
|
-
|
|
70
|
+
terminal.success(`Created: ${terminal.colors.cyan(createdPlan.data.name)}`);
|
|
74
71
|
}
|
|
75
72
|
/**
|
|
76
73
|
* Apply an update action
|
|
77
74
|
*/
|
|
78
|
-
async function applyUpdate(action,
|
|
75
|
+
async function applyUpdate(action, sdk, projectId, environment, applied) {
|
|
79
76
|
const plan = action.plan;
|
|
80
77
|
const remotePlan = action.remotePlan;
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
// PUT body is Omit<TestPlanV1, 'id'> - id comes from URL path
|
|
84
|
-
const payload = {
|
|
85
|
-
...plan,
|
|
86
|
-
project: projectId,
|
|
87
|
-
environment,
|
|
88
|
-
};
|
|
78
|
+
const variables = await loadVariables(environment);
|
|
79
|
+
const resolvedPlan = resolvePlan(plan, projectId, environment, variables);
|
|
89
80
|
// Use the remote plan's ID for the update
|
|
90
|
-
await
|
|
81
|
+
await sdk.putPlanById({
|
|
82
|
+
path: {
|
|
83
|
+
id: remotePlan.id,
|
|
84
|
+
},
|
|
85
|
+
body: resolvedPlan,
|
|
86
|
+
});
|
|
91
87
|
applied.push({
|
|
92
88
|
type: "update",
|
|
93
89
|
planName: plan.name,
|
|
94
90
|
success: true,
|
|
95
91
|
});
|
|
96
|
-
|
|
92
|
+
terminal.success(`Updated: ${terminal.colors.cyan(plan.name)}`);
|
|
97
93
|
}
|
|
98
94
|
/**
|
|
99
95
|
* Apply a delete action
|
|
100
96
|
*/
|
|
101
|
-
async function applyDelete(action,
|
|
97
|
+
async function applyDelete(action, sdk, applied) {
|
|
102
98
|
const remotePlan = action.remotePlan;
|
|
103
|
-
|
|
104
|
-
|
|
99
|
+
await sdk.deletePlanById({
|
|
100
|
+
path: {
|
|
101
|
+
id: remotePlan.id,
|
|
102
|
+
},
|
|
103
|
+
});
|
|
105
104
|
applied.push({
|
|
106
105
|
type: "delete",
|
|
107
106
|
planName: remotePlan.name,
|
|
108
107
|
success: true,
|
|
109
108
|
});
|
|
110
|
-
|
|
109
|
+
terminal.success(`Deleted: ${terminal.colors.cyan(remotePlan.name)}`);
|
|
111
110
|
}
|
|
112
111
|
/**
|
|
113
112
|
* Format apply result for display
|
package/dist/core/apply.test.js
CHANGED
|
@@ -28,7 +28,14 @@ describe("applyDiff", () => {
|
|
|
28
28
|
it("should skip noop actions", async () => {
|
|
29
29
|
const plan = createPlan("health-check");
|
|
30
30
|
const diff = {
|
|
31
|
-
actions: [
|
|
31
|
+
actions: [
|
|
32
|
+
{
|
|
33
|
+
type: "noop",
|
|
34
|
+
plan: plan,
|
|
35
|
+
remotePlan: plan,
|
|
36
|
+
reason: "unchanged",
|
|
37
|
+
},
|
|
38
|
+
],
|
|
32
39
|
summary: { creates: 0, updates: 0, deletes: 0, noops: 1 },
|
|
33
40
|
};
|
|
34
41
|
const mockPlanApi = {};
|
|
@@ -39,7 +46,14 @@ describe("applyDiff", () => {
|
|
|
39
46
|
it("should apply create action", async () => {
|
|
40
47
|
const plan = createPlan("new-plan");
|
|
41
48
|
const diff = {
|
|
42
|
-
actions: [
|
|
49
|
+
actions: [
|
|
50
|
+
{
|
|
51
|
+
type: "create",
|
|
52
|
+
plan: plan,
|
|
53
|
+
remotePlan: null,
|
|
54
|
+
reason: "new",
|
|
55
|
+
},
|
|
56
|
+
],
|
|
43
57
|
summary: { creates: 1, updates: 0, deletes: 0, noops: 0 },
|
|
44
58
|
};
|
|
45
59
|
const mockPlanApi = {
|
|
@@ -54,7 +68,7 @@ describe("applyDiff", () => {
|
|
|
54
68
|
expect(result.applied[0].planName).toBe("new-plan");
|
|
55
69
|
expect(result.applied[0].success).toBe(true);
|
|
56
70
|
// Verify the API was called with injected project and environment
|
|
57
|
-
expect(mockPlanApi.
|
|
71
|
+
expect(mockPlanApi.postPlan).toHaveBeenCalledWith(expect.objectContaining({
|
|
58
72
|
name: "new-plan",
|
|
59
73
|
project: "project-id",
|
|
60
74
|
environment: "staging",
|
|
@@ -65,12 +79,17 @@ describe("applyDiff", () => {
|
|
|
65
79
|
const remotePlan = { ...localPlan, id: "remote-id" };
|
|
66
80
|
const diff = {
|
|
67
81
|
actions: [
|
|
68
|
-
{
|
|
82
|
+
{
|
|
83
|
+
type: "update",
|
|
84
|
+
plan: localPlan,
|
|
85
|
+
remotePlan: remotePlan,
|
|
86
|
+
reason: "changed",
|
|
87
|
+
},
|
|
69
88
|
],
|
|
70
89
|
summary: { creates: 0, updates: 1, deletes: 0, noops: 0 },
|
|
71
90
|
};
|
|
72
91
|
const mockPlanApi = {
|
|
73
|
-
|
|
92
|
+
putPlanById: vi.fn().mockResolvedValue({
|
|
74
93
|
data: { data: remotePlan },
|
|
75
94
|
}),
|
|
76
95
|
};
|
|
@@ -81,7 +100,7 @@ describe("applyDiff", () => {
|
|
|
81
100
|
expect(result.applied[0].planName).toBe("existing-plan");
|
|
82
101
|
expect(result.applied[0].success).toBe(true);
|
|
83
102
|
// Verify the API was called with the remote plan's ID
|
|
84
|
-
expect(mockPlanApi.
|
|
103
|
+
expect(mockPlanApi.putPlanById).toHaveBeenCalledWith("remote-id", expect.objectContaining({
|
|
85
104
|
name: "existing-plan",
|
|
86
105
|
project: "project-id",
|
|
87
106
|
environment: "production",
|
|
@@ -94,7 +113,7 @@ describe("applyDiff", () => {
|
|
|
94
113
|
summary: { creates: 0, updates: 0, deletes: 1, noops: 0 },
|
|
95
114
|
};
|
|
96
115
|
const mockPlanApi = {
|
|
97
|
-
|
|
116
|
+
deletePlanById: vi.fn().mockResolvedValue({}),
|
|
98
117
|
};
|
|
99
118
|
const result = await applyDiff(diff, mockPlanApi, "project-id", "test");
|
|
100
119
|
expect(result.success).toBe(true);
|
|
@@ -103,16 +122,23 @@ describe("applyDiff", () => {
|
|
|
103
122
|
expect(result.applied[0].planName).toBe("old-plan");
|
|
104
123
|
expect(result.applied[0].success).toBe(true);
|
|
105
124
|
// Verify the API was called with the remote plan's ID
|
|
106
|
-
expect(mockPlanApi.
|
|
125
|
+
expect(mockPlanApi.deletePlanById).toHaveBeenCalledWith(remotePlan.id);
|
|
107
126
|
});
|
|
108
127
|
it("should handle errors gracefully", async () => {
|
|
109
128
|
const plan = createPlan("failing-plan");
|
|
110
129
|
const diff = {
|
|
111
|
-
actions: [
|
|
130
|
+
actions: [
|
|
131
|
+
{
|
|
132
|
+
type: "create",
|
|
133
|
+
plan: plan,
|
|
134
|
+
remotePlan: null,
|
|
135
|
+
reason: "new",
|
|
136
|
+
},
|
|
137
|
+
],
|
|
112
138
|
summary: { creates: 1, updates: 0, deletes: 0, noops: 0 },
|
|
113
139
|
};
|
|
114
140
|
const mockPlanApi = {
|
|
115
|
-
|
|
141
|
+
postPlan: vi.fn().mockRejectedValue(new Error("API Error")),
|
|
116
142
|
};
|
|
117
143
|
const result = await applyDiff(diff, mockPlanApi, "project-id", "test");
|
|
118
144
|
expect(result.success).toBe(false);
|
|
@@ -124,7 +150,14 @@ describe("applyDiff", () => {
|
|
|
124
150
|
it("should skip actions in dry-run mode", async () => {
|
|
125
151
|
const plan = createPlan("dry-run-plan");
|
|
126
152
|
const diff = {
|
|
127
|
-
actions: [
|
|
153
|
+
actions: [
|
|
154
|
+
{
|
|
155
|
+
type: "create",
|
|
156
|
+
plan: plan,
|
|
157
|
+
remotePlan: null,
|
|
158
|
+
reason: "new",
|
|
159
|
+
},
|
|
160
|
+
],
|
|
128
161
|
summary: { creates: 1, updates: 0, deletes: 0, noops: 0 },
|
|
129
162
|
};
|
|
130
163
|
const mockPlanApi = {
|
|
@@ -135,6 +168,6 @@ describe("applyDiff", () => {
|
|
|
135
168
|
});
|
|
136
169
|
expect(result.success).toBe(true);
|
|
137
170
|
expect(result.applied).toHaveLength(0);
|
|
138
|
-
expect(mockPlanApi.
|
|
171
|
+
expect(mockPlanApi.postPlan).not.toHaveBeenCalled();
|
|
139
172
|
});
|
|
140
173
|
});
|
package/dist/core/diff.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import {
|
|
1
|
+
import type { PlanV1 } from "@griffin-app/griffin-hub-sdk";
|
|
2
|
+
import type { PlanDSL } from "@griffin-app/griffin-ts/types";
|
|
3
3
|
import { type PlanChanges } from "./plan-diff.js";
|
|
4
4
|
export type DiffActionType = "create" | "update" | "delete" | "noop";
|
|
5
5
|
export interface DiffAction {
|
|
6
6
|
type: DiffActionType;
|
|
7
|
-
plan:
|
|
8
|
-
remotePlan:
|
|
7
|
+
plan: PlanDSL | null;
|
|
8
|
+
remotePlan: PlanV1 | null;
|
|
9
9
|
reason: string;
|
|
10
10
|
changes?: PlanChanges;
|
|
11
11
|
}
|
|
@@ -31,7 +31,7 @@ export interface DiffOptions {
|
|
|
31
31
|
* - DELETE: Plan exists on hub but not locally (only if includeDeletions is true)
|
|
32
32
|
* - NOOP: Plan exists in both with same content
|
|
33
33
|
*/
|
|
34
|
-
export declare function computeDiff(localPlans:
|
|
34
|
+
export declare function computeDiff(localPlans: PlanDSL[], remotePlans: PlanV1[], options: DiffOptions): DiffResult;
|
|
35
35
|
/**
|
|
36
36
|
* Format diff result as human-readable text with granular changes
|
|
37
37
|
*/
|
package/dist/core/diff.test.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
2
|
import { computeDiff } from "./diff.js";
|
|
3
|
-
import { FrequencyUnit } from "@griffin-app/griffin-ts/schema";
|
|
4
3
|
// Helper to create a minimal test plan
|
|
5
4
|
function createPlan(name, overrides) {
|
|
6
5
|
return {
|
|
@@ -9,7 +8,7 @@ function createPlan(name, overrides) {
|
|
|
9
8
|
project: "test-project",
|
|
10
9
|
environment: "test",
|
|
11
10
|
version: "1.0",
|
|
12
|
-
frequency: { every: 5, unit:
|
|
11
|
+
frequency: { every: 5, unit: "MINUTE" },
|
|
13
12
|
nodes: [],
|
|
14
13
|
edges: [],
|
|
15
14
|
...overrides,
|
|
@@ -20,7 +19,9 @@ describe("computeDiff", () => {
|
|
|
20
19
|
it("should create action when plan exists locally but not remotely", () => {
|
|
21
20
|
const local = [createPlan("health-check")];
|
|
22
21
|
const remote = [];
|
|
23
|
-
const result = computeDiff(local, remote, {
|
|
22
|
+
const result = computeDiff(local, remote, {
|
|
23
|
+
includeDeletions: false,
|
|
24
|
+
});
|
|
24
25
|
expect(result.actions).toHaveLength(1);
|
|
25
26
|
expect(result.actions[0].type).toBe("create");
|
|
26
27
|
expect(result.actions[0].plan?.name).toBe("health-check");
|
|
@@ -34,15 +35,17 @@ describe("computeDiff", () => {
|
|
|
34
35
|
it("should create update action when plan content differs", () => {
|
|
35
36
|
const local = [
|
|
36
37
|
createPlan("health-check", {
|
|
37
|
-
frequency: { every: 10, unit:
|
|
38
|
+
frequency: { every: 10, unit: "MINUTE" },
|
|
38
39
|
}),
|
|
39
40
|
];
|
|
40
41
|
const remote = [
|
|
41
42
|
createPlan("health-check", {
|
|
42
|
-
frequency: { every: 5, unit:
|
|
43
|
+
frequency: { every: 5, unit: "MINUTE" },
|
|
43
44
|
}),
|
|
44
45
|
];
|
|
45
|
-
const result = computeDiff(local, remote, {
|
|
46
|
+
const result = computeDiff(local, remote, {
|
|
47
|
+
includeDeletions: false,
|
|
48
|
+
});
|
|
46
49
|
expect(result.actions).toHaveLength(1);
|
|
47
50
|
expect(result.actions[0].type).toBe("update");
|
|
48
51
|
expect(result.actions[0].plan?.name).toBe("health-check");
|
|
@@ -57,7 +60,9 @@ describe("computeDiff", () => {
|
|
|
57
60
|
const plan = createPlan("health-check");
|
|
58
61
|
const local = [plan];
|
|
59
62
|
const remote = [{ ...plan }];
|
|
60
|
-
const result = computeDiff(local, remote, {
|
|
63
|
+
const result = computeDiff(local, remote, {
|
|
64
|
+
includeDeletions: false,
|
|
65
|
+
});
|
|
61
66
|
expect(result.actions).toHaveLength(1);
|
|
62
67
|
expect(result.actions[0].type).toBe("noop");
|
|
63
68
|
expect(result.summary.creates).toBe(0);
|
|
@@ -90,18 +95,20 @@ describe("computeDiff", () => {
|
|
|
90
95
|
const local = [
|
|
91
96
|
createPlan("new-plan"),
|
|
92
97
|
createPlan("updated-plan", {
|
|
93
|
-
frequency: { every: 10, unit:
|
|
98
|
+
frequency: { every: 10, unit: "MINUTE" },
|
|
94
99
|
}),
|
|
95
100
|
createPlan("unchanged-plan"),
|
|
96
101
|
];
|
|
97
102
|
const remote = [
|
|
98
103
|
createPlan("updated-plan", {
|
|
99
|
-
frequency: { every: 5, unit:
|
|
104
|
+
frequency: { every: 5, unit: "MINUTE" },
|
|
100
105
|
}),
|
|
101
106
|
createPlan("unchanged-plan"),
|
|
102
107
|
createPlan("deleted-plan"),
|
|
103
108
|
];
|
|
104
|
-
const result = computeDiff(local, remote, {
|
|
109
|
+
const result = computeDiff(local, remote, {
|
|
110
|
+
includeDeletions: true,
|
|
111
|
+
});
|
|
105
112
|
expect(result.actions).toHaveLength(4);
|
|
106
113
|
expect(result.summary.creates).toBe(1);
|
|
107
114
|
expect(result.summary.updates).toBe(1);
|
|
@@ -113,7 +120,9 @@ describe("computeDiff", () => {
|
|
|
113
120
|
it("should match plans by name, not by ID", () => {
|
|
114
121
|
const local = [createPlan("health-check", { id: "local-id-123" })];
|
|
115
122
|
const remote = [createPlan("health-check", { id: "remote-id-456" })];
|
|
116
|
-
const result = computeDiff(local, remote, {
|
|
123
|
+
const result = computeDiff(local, remote, {
|
|
124
|
+
includeDeletions: false,
|
|
125
|
+
});
|
|
117
126
|
// Should be NOOP because names match (IDs are ignored)
|
|
118
127
|
expect(result.actions).toHaveLength(1);
|
|
119
128
|
expect(result.actions[0].type).toBe("noop");
|