@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.
@@ -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
- console.log("No hub connection configured.");
10
- console.log("");
11
- console.log("Connect with:");
12
- console.log(" griffin hub connect --url <url> --token <token>");
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
- console.log("Hub connection:");
16
- console.log(` URL: ${state.runner.baseUrl}`);
16
+ terminal.info("Hub connection:");
17
+ terminal.log(` URL: ${terminal.colors.cyan(state.runner.baseUrl)}`);
17
18
  if (state.runner.apiToken) {
18
- console.log(` API Token: ${state.runner.apiToken.substring(0, 8)}...`);
19
+ terminal.log(` API Token: ${terminal.colors.dim(state.runner.apiToken.substring(0, 8) + "...")}`);
19
20
  }
20
21
  else {
21
- console.log(" API Token: (not set)");
22
+ terminal.log(` API Token: ${terminal.colors.dim("(not set)")}`);
22
23
  }
23
- console.log("");
24
+ terminal.blank();
24
25
  }
25
26
  catch (error) {
26
- console.error(`Error: ${error.message}`);
27
- process.exit(1);
27
+ terminal.error(error.message);
28
+ terminal.exit(1);
28
29
  }
29
30
  }
@@ -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
- console.log("Initializing griffin...");
8
+ const spinner = terminal.spinner("Initializing griffin...").start();
8
9
  // Check if already initialized
9
10
  if (await stateExists()) {
10
- console.error(`Error: Already initialized (state file exists: ${getStateFilePath()})`);
11
- process.exit(1);
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
- console.log(`Project: ${projectId}`);
19
- console.log("");
20
+ spinner.succeed(`Project: ${terminal.colors.cyan(projectId)}`);
20
21
  // Initialize state file
21
22
  await initState(projectId);
22
- console.log(`✓ Created state file: ${getStateFilePath()}`);
23
- // Create a default local environment
24
- await addEnvironment("local", {});
25
- console.log(`✓ Created default 'local' environment`);
26
- console.log("");
27
- console.log("Initialization complete!");
28
- console.log("");
29
- console.log("Next steps:");
30
- console.log(" 1. Create a variables.yaml file in the project root:");
31
- console.log(" environments:");
32
- console.log(" local:");
33
- console.log(" api-service: http://localhost:3000");
34
- console.log(" 2. Create test plans (*.griffin.ts files in __griffin__/ directories)");
35
- console.log(" 3. Run tests locally:");
36
- console.log(" griffin local run");
37
- console.log(" 4. Connect to hub (optional):");
38
- console.log(" griffin hub connect --url <url> --token <token>");
39
- console.log(" 5. Deploy to hub:");
40
- console.log(" griffin hub apply");
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
- console.log(`Running tests locally against '${envName}' environment`);
10
- console.log(`Variables will be loaded from variables.yaml for environment: ${envName}`);
11
- console.log("");
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
- console.error("No test files found. Looking for .ts files in __griffin__ directories.");
15
- process.exit(1);
16
+ spinner.fail("No test files found");
17
+ terminal.dim("Looking for .ts files in __griffin__ directories.");
18
+ terminal.exit(1);
16
19
  }
17
- console.log(`Found ${testFiles.length} test file(s):`);
18
- testFiles.forEach((file) => console.log(` - ${file}`));
19
- console.log("");
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
- console.log(`Running ${fileName}`);
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
- console.log("");
30
- console.log(`Summary: ${successful} passed, ${failed} failed`);
31
- if (failed > 0) {
32
- process.exit(1);
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
- console.error(`Error: ${error.message}`);
37
- process.exit(1);
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
- console.error(error.message || String(error));
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
- console.log("Validating test plans...");
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
- console.error(formatDiscoveryErrors(errors));
22
- console.error("");
23
- console.error(`✗ Validation failed with ${errors.length} error(s)`);
24
- process.exit(1);
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
- console.log(`✓ Found ${plans.length} valid plan(s):`);
28
- console.log("");
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
- console.log(` ${plan.name}${exportInfo}`);
33
- console.log(` ${shortPath}`);
34
- console.log(` Nodes: ${plan.nodes.length}, Edges: ${plan.edges.length}`);
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
- console.log(` Schedule: Every ${plan.frequency.every} ${plan.frequency.unit}`);
36
+ terminal.dim(` Schedule: Every ${plan.frequency.every} ${plan.frequency.unit}`);
37
37
  }
38
- console.log("");
38
+ terminal.blank();
39
39
  }
40
- console.log("All plans are valid");
40
+ terminal.success("All plans are valid");
41
41
  }
42
42
  catch (error) {
43
- console.error(`Error: ${error.message}`);
44
- process.exit(1);
43
+ spinner.fail("Validation failed");
44
+ terminal.error(error.message);
45
+ terminal.exit(1);
45
46
  }
46
47
  }
@@ -1,5 +1,5 @@
1
1
  import type { DiffAction, DiffResult } from "./diff.js";
2
- import type { PlanApi } from "@griffin-app/griffin-hub-sdk";
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, planApi: PlanApi, projectId: string, environment: string, options?: {
22
+ export declare function applyDiff(diff: DiffResult, sdk: GriffinHubSdk, projectId: string, environment: string, options?: {
23
23
  dryRun?: boolean;
24
24
  }): Promise<ApplyResult>;
25
25
  /**
@@ -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, planApi, projectId, environment, options) {
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
- console.log(`[DRY RUN] Would ${action.type} plan: ${action.plan?.name || action.remotePlan?.name}`);
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, planApi, projectId, environment, applied);
25
+ await applyCreate(action, sdk, projectId, environment, applied);
25
26
  break;
26
27
  case "update":
27
- await applyUpdate(action, planApi, projectId, environment, applied);
28
+ await applyUpdate(action, sdk, projectId, environment, applied);
28
29
  break;
29
30
  case "delete":
30
- await applyDelete(action, planApi, applied);
31
+ await applyDelete(action, sdk, applied);
31
32
  break;
32
33
  }
33
34
  }
34
35
  catch (error) {
35
- console.error(error);
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, planApi, projectId, environment, applied) {
58
+ async function applyCreate(action, sdk, projectId, environment, applied) {
58
59
  const plan = action.plan;
59
- console.log(`Creating plan: ${plan.name}`);
60
- // Inject project AND environment (plan itself is env-agnostic)
61
- // POST body is Omit<TestPlanV1, 'id'> - hub assigns the id
62
- const payload = {
63
- ...plan,
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
- console.log(`✓ Created: ${createdPlan.data.name}`);
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, planApi, projectId, environment, applied) {
75
+ async function applyUpdate(action, sdk, projectId, environment, applied) {
79
76
  const plan = action.plan;
80
77
  const remotePlan = action.remotePlan;
81
- console.log(`Updating plan: ${plan.name}`);
82
- // Inject project AND environment
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 planApi.planIdPut(remotePlan.id, payload);
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
- console.log(`✓ Updated: ${plan.name}`);
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, planApi, applied) {
97
+ async function applyDelete(action, sdk, applied) {
102
98
  const remotePlan = action.remotePlan;
103
- console.log(`Deleting plan: ${remotePlan.name}`);
104
- await planApi.planIdDelete(remotePlan.id);
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
- console.log(`✓ Deleted: ${remotePlan.name}`);
109
+ terminal.success(`Deleted: ${terminal.colors.cyan(remotePlan.name)}`);
111
110
  }
112
111
  /**
113
112
  * Format apply result for display
@@ -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: [{ type: "noop", plan, remotePlan: plan, reason: "unchanged" }],
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: [{ type: "create", plan, remotePlan: null, reason: "new" }],
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.planPost).toHaveBeenCalledWith(expect.objectContaining({
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
- { type: "update", plan: localPlan, remotePlan, reason: "changed" },
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
- planIdPut: vi.fn().mockResolvedValue({
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.planIdPut).toHaveBeenCalledWith("remote-id", expect.objectContaining({
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
- planIdDelete: vi.fn().mockResolvedValue({}),
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.planIdDelete).toHaveBeenCalledWith(remotePlan.id);
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: [{ type: "create", plan, remotePlan: null, reason: "new" }],
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
- planPost: vi.fn().mockRejectedValue(new Error("API Error")),
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: [{ type: "create", plan, remotePlan: null, reason: "new" }],
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.planPost).not.toHaveBeenCalled();
171
+ expect(mockPlanApi.postPlan).not.toHaveBeenCalled();
139
172
  });
140
173
  });
@@ -1,11 +1,11 @@
1
- import type { TestPlanV1 } from "@griffin-app/griffin-ts/types";
2
- import { RawTestPlan } from "./discovery.js";
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: RawTestPlan | null;
8
- remotePlan: TestPlanV1 | null;
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: RawTestPlan[], remotePlans: TestPlanV1[], options: DiffOptions): DiffResult;
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
  */
@@ -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: FrequencyUnit.MINUTE },
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, { includeDeletions: false });
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: FrequencyUnit.MINUTE },
38
+ frequency: { every: 10, unit: "MINUTE" },
38
39
  }),
39
40
  ];
40
41
  const remote = [
41
42
  createPlan("health-check", {
42
- frequency: { every: 5, unit: FrequencyUnit.MINUTE },
43
+ frequency: { every: 5, unit: "MINUTE" },
43
44
  }),
44
45
  ];
45
- const result = computeDiff(local, remote, { includeDeletions: false });
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, { includeDeletions: false });
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: FrequencyUnit.MINUTE },
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: FrequencyUnit.MINUTE },
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, { includeDeletions: true });
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, { includeDeletions: false });
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");