@griffin-app/griffin-cli 1.0.3 → 1.0.5

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 (51) hide show
  1. package/README.md +111 -161
  2. package/dist/cli.js +34 -15
  3. package/dist/commands/env.d.ts +1 -22
  4. package/dist/commands/env.js +22 -109
  5. package/dist/commands/generate-key.js +16 -10
  6. package/dist/commands/hub/apply.js +58 -34
  7. package/dist/commands/hub/connect.js +16 -9
  8. package/dist/commands/hub/login.d.ts +1 -0
  9. package/dist/commands/hub/login.js +79 -0
  10. package/dist/commands/hub/logout.d.ts +6 -0
  11. package/dist/commands/hub/logout.js +16 -0
  12. package/dist/commands/hub/plan.js +38 -21
  13. package/dist/commands/hub/run.js +93 -57
  14. package/dist/commands/hub/runs.js +75 -42
  15. package/dist/commands/hub/status.js +18 -13
  16. package/dist/commands/init.js +32 -24
  17. package/dist/commands/local/run.js +30 -16
  18. package/dist/commands/validate.js +18 -17
  19. package/dist/core/apply.d.ts +2 -2
  20. package/dist/core/apply.js +36 -38
  21. package/dist/core/apply.test.js +71 -27
  22. package/dist/core/credentials.d.ts +36 -0
  23. package/dist/core/credentials.js +98 -0
  24. package/dist/core/credentials.test.d.ts +1 -0
  25. package/dist/core/credentials.test.js +137 -0
  26. package/dist/core/diff.d.ts +7 -6
  27. package/dist/core/diff.js +2 -1
  28. package/dist/core/diff.test.js +44 -20
  29. package/dist/core/discovery.d.ts +2 -3
  30. package/dist/core/discovery.js +3 -10
  31. package/dist/core/plan-diff.d.ts +5 -6
  32. package/dist/core/plan-diff.js +6 -6
  33. package/dist/core/sdk.d.ts +5 -9
  34. package/dist/core/sdk.js +23 -15
  35. package/dist/core/variables.js +13 -0
  36. package/dist/index.d.ts +8 -3
  37. package/dist/index.js +6 -2
  38. package/dist/resolve.d.ts +3 -0
  39. package/dist/resolve.js +9 -0
  40. package/dist/schemas/credentials.d.ts +24 -0
  41. package/dist/schemas/credentials.js +23 -0
  42. package/dist/schemas/state.d.ts +5 -5
  43. package/dist/schemas/state.js +5 -7
  44. package/dist/test-runner.js +18 -22
  45. package/dist/utils/console.d.ts +5 -0
  46. package/dist/utils/console.js +5 -0
  47. package/dist/utils/sdk-error.d.ts +8 -0
  48. package/dist/utils/sdk-error.js +114 -0
  49. package/dist/utils/terminal.d.ts +100 -0
  50. package/dist/utils/terminal.js +148 -0
  51. package/package.json +9 -4
@@ -1,121 +1,34 @@
1
- import { loadState, addEnvironment, removeEnvironment, setDefaultEnvironment, } from "../core/state.js";
1
+ import { loadState } from "../core/state.js";
2
+ import { terminal } from "../utils/terminal.js";
2
3
  /**
3
- * List all environments
4
+ * List all available environments
4
5
  */
5
6
  export async function executeEnvList() {
6
7
  try {
7
8
  const state = await loadState();
8
- const envNames = Object.keys(state.environments);
9
- if (envNames.length === 0) {
10
- console.log("No environments configured.");
11
- console.log("");
12
- console.log("Add an environment with:");
13
- console.log(" griffin env add <name> --base-url <url>");
9
+ const environments = Object.keys(state.environments);
10
+ if (environments.length === 0) {
11
+ terminal.warn("No environments configured.");
12
+ terminal.blank();
13
+ terminal.dim("Run 'griffin init' to set up your project.");
14
14
  return;
15
15
  }
16
- console.log("Environments:");
17
- console.log("");
18
- envNames.forEach((name) => {
19
- const config = state.environments[name];
20
- const isDefault = name === state.defaultEnvironment;
21
- const marker = isDefault ? " (default)" : "";
22
- console.log(` ${name}${marker}`);
23
- console.log(` URL: ${config.baseUrl}`);
24
- });
25
- console.log("");
26
- }
27
- catch (error) {
28
- console.error(`Error: ${error.message}`);
29
- process.exit(1);
30
- }
31
- }
32
- /**
33
- * Add or update an environment
34
- */
35
- export async function executeEnvAdd(name, options) {
36
- try {
37
- const state = await loadState();
38
- const isUpdate = name in state.environments;
39
- await addEnvironment(name, { baseUrl: options.baseUrl });
40
- if (isUpdate) {
41
- console.log(`✓ Updated environment '${name}'`);
42
- }
43
- else {
44
- console.log(`✓ Added environment '${name}'`);
45
- }
46
- console.log(` URL: ${options.baseUrl}`);
47
- // Show if this was set as default
48
- const updatedState = await loadState();
49
- if (updatedState.defaultEnvironment === name && !isUpdate) {
50
- console.log(" (set as default - first environment)");
51
- }
52
- console.log("");
53
- }
54
- catch (error) {
55
- console.error(`Error: ${error.message}`);
56
- process.exit(1);
57
- }
58
- }
59
- /**
60
- * Remove an environment
61
- */
62
- export async function executeEnvRemove(name) {
63
- try {
64
- const state = await loadState();
65
- // Check if environment exists
66
- if (!(name in state.environments)) {
67
- console.error(`Error: Environment '${name}' does not exist`);
68
- console.log("");
69
- console.log("Available environments:");
70
- Object.keys(state.environments).forEach((env) => {
71
- console.log(` - ${env}`);
72
- });
73
- process.exit(1);
74
- }
75
- // Warn if there are plans in this environment
76
- const planCount = state.plans[name]?.length || 0;
77
- if (planCount > 0) {
78
- console.log(`Warning: Environment '${name}' has ${planCount} synced plan(s).`);
79
- console.log("This will remove all plan state for this environment.");
80
- console.log("");
81
- }
82
- await removeEnvironment(name);
83
- console.log(`✓ Removed environment '${name}'`);
84
- // Show new default if it changed
85
- const updatedState = await loadState();
86
- if (updatedState.defaultEnvironment &&
87
- updatedState.defaultEnvironment !== name) {
88
- console.log(` New default: ${updatedState.defaultEnvironment}`);
89
- }
90
- console.log("");
91
- }
92
- catch (error) {
93
- console.error(`Error: ${error.message}`);
94
- process.exit(1);
95
- }
96
- }
97
- /**
98
- * Set default environment
99
- */
100
- export async function executeEnvDefault(name) {
101
- try {
102
- const state = await loadState();
103
- // Check if environment exists
104
- if (!(name in state.environments)) {
105
- console.error(`Error: Environment '${name}' does not exist`);
106
- console.log("");
107
- console.log("Available environments:");
108
- Object.keys(state.environments).forEach((env) => {
109
- console.log(` - ${env}`);
110
- });
111
- process.exit(1);
16
+ terminal.info("Available environments:");
17
+ terminal.blank();
18
+ for (const envName of environments) {
19
+ const isDefault = state.defaultEnvironment === envName;
20
+ const marker = isDefault
21
+ ? terminal.colors.green("")
22
+ : terminal.colors.dim("○");
23
+ const envDisplay = isDefault
24
+ ? terminal.colors.cyan(envName) + terminal.colors.dim(" (default)")
25
+ : envName;
26
+ terminal.log(` ${marker} ${envDisplay}`);
112
27
  }
113
- await setDefaultEnvironment(name);
114
- console.log(`✓ Set '${name}' as default environment`);
115
- console.log("");
28
+ terminal.blank();
116
29
  }
117
30
  catch (error) {
118
- console.error(`Error: ${error.message}`);
119
- process.exit(1);
31
+ terminal.error(error.message);
32
+ terminal.exit(1);
120
33
  }
121
34
  }
@@ -1,4 +1,5 @@
1
1
  import { randomBytes } from "node:crypto";
2
+ import { terminal } from "../utils/terminal.js";
2
3
  /**
3
4
  * Generate a cryptographically secure API key for griffin-runner authentication.
4
5
  *
@@ -14,14 +15,19 @@ export async function executeGenerateKey() {
14
15
  const keySecret = keyBytes.toString("hex");
15
16
  // Add prefix following the pattern: grfn_sk_<secret>
16
17
  const apiKey = `grfn_sk_${keySecret}`;
17
- console.log("\n✓ Generated API key:\n");
18
- console.log(` ${apiKey}\n`);
19
- console.log("⚠️ Store this key securely - it cannot be retrieved later.\n");
20
- console.log("To use this key:");
21
- console.log(" 1. Add it to your runner's AUTH_API_KEYS environment variable:");
22
- console.log(` AUTH_API_KEYS=${apiKey}`);
23
- console.log(" 2. Or add it to your .griffinrc.json:");
24
- console.log(` { "runner": { "apiToken": "${apiKey}" } }`);
25
- console.log(" 3. Or pass it via environment variable:");
26
- console.log(` GRIFFIN_API_TOKEN=${apiKey}\n`);
18
+ terminal.blank();
19
+ terminal.success("Generated API key:");
20
+ terminal.blank();
21
+ terminal.log(` ${terminal.colors.cyan(apiKey)}`);
22
+ terminal.blank();
23
+ terminal.warn("Store this key securely - it cannot be retrieved later.");
24
+ terminal.blank();
25
+ terminal.info("To use this key:");
26
+ terminal.dim(" 1. Add it to your runner's AUTH_API_KEYS environment variable:");
27
+ terminal.dim(` AUTH_API_KEYS=${apiKey}`);
28
+ terminal.dim(" 2. Or add it to your .griffinrc.json:");
29
+ terminal.dim(` { "runner": { "apiToken": "${apiKey}" } }`);
30
+ terminal.dim(" 3. Or pass it via environment variable:");
31
+ terminal.dim(` GRIFFIN_API_TOKEN=${apiKey}`);
32
+ terminal.blank();
27
33
  }
@@ -2,7 +2,11 @@ import { loadState, resolveEnvironment } from "../../core/state.js";
2
2
  import { discoverPlans, formatDiscoveryErrors } from "../../core/discovery.js";
3
3
  import { computeDiff, formatDiff } from "../../core/diff.js";
4
4
  import { applyDiff, formatApplyResult } from "../../core/apply.js";
5
- import { createSdkClients } from "../../core/sdk.js";
5
+ import { createSdkWithCredentials } from "../../core/sdk.js";
6
+ import { terminal } from "../../utils/terminal.js";
7
+ import { withSDKErrorHandling } from "../../utils/sdk-error.js";
8
+ import { loadVariables } from "../../core/variables.js";
9
+ import { resolvePlan } from "../../resolve.js";
6
10
  /**
7
11
  * Apply changes to the hub
8
12
  */
@@ -13,69 +17,89 @@ export async function executeApply(options) {
13
17
  // Resolve environment
14
18
  const envName = await resolveEnvironment(options.env);
15
19
  // Check if runner is configured
16
- if (!state.runner?.baseUrl) {
17
- console.error("Error: Hub connection not configured.");
18
- console.log("Connect with:");
19
- console.log(" griffin hub connect --url <url> --token <token>");
20
- process.exit(1);
20
+ if (!state.hub?.baseUrl) {
21
+ terminal.error("Hub connection not configured.");
22
+ terminal.dim("Connect with:");
23
+ terminal.dim(" griffin hub connect --url <url> --token <token>");
24
+ terminal.exit(1);
21
25
  }
22
- console.log(`Applying to '${envName}' environment`);
23
- console.log("");
24
- // Create SDK clients
25
- const { planApi } = createSdkClients({
26
- baseUrl: state.runner.baseUrl,
27
- apiToken: state.runner.apiToken || undefined,
28
- });
26
+ terminal.info(`Applying to ${terminal.colors.cyan(envName)} environment`);
27
+ terminal.blank();
28
+ // Create SDK clients with credentials
29
+ const sdk = await createSdkWithCredentials(state.hub.baseUrl);
29
30
  // Discover local plans
30
31
  const discoveryPattern = state.discovery?.pattern || "**/__griffin__/*.{ts,js}";
31
32
  const discoveryIgnore = state.discovery?.ignore || [
32
33
  "node_modules/**",
33
34
  "dist/**",
34
35
  ];
36
+ const spinner = terminal.spinner("Discovering local plans...").start();
35
37
  const { plans, errors } = await discoverPlans(discoveryPattern, discoveryIgnore);
36
38
  if (errors.length > 0) {
37
- console.error(formatDiscoveryErrors(errors));
38
- process.exit(1);
39
+ spinner.fail("Discovery failed");
40
+ terminal.error(formatDiscoveryErrors(errors));
41
+ terminal.exit(1);
39
42
  }
43
+ spinner.succeed(`Found ${plans.length} local plan(s)`);
40
44
  // Fetch remote plans for this project + environment
41
- const response = await planApi.planGet(state.projectId, envName);
42
- const remotePlans = response.data.data;
45
+ const fetchSpinner = terminal.spinner("Fetching remote plans...").start();
46
+ const response = await withSDKErrorHandling(() => sdk.getPlan({
47
+ query: {
48
+ projectId: state.projectId,
49
+ environment: envName,
50
+ },
51
+ }), "Failed to fetch remote plans");
52
+ const remotePlans = response?.data?.data;
53
+ fetchSpinner.succeed(`Found ${remotePlans.length} remote plan(s)`);
54
+ // Load variables and resolve local plans before computing diff
55
+ const variables = await loadVariables(envName);
56
+ const resolvedPlans = plans.map((p) => resolvePlan(p.plan, state.projectId, envName, variables));
43
57
  // Compute diff (include deletions if --prune)
44
- const diff = computeDiff(plans.map((p) => p.plan), remotePlans, { includeDeletions: options.prune || false });
58
+ const diff = computeDiff(resolvedPlans, remotePlans, {
59
+ includeDeletions: options.prune || false,
60
+ });
45
61
  // Show plan
46
- console.log(formatDiff(diff));
47
- console.log("");
62
+ terminal.blank();
63
+ terminal.log(formatDiff(diff));
64
+ terminal.blank();
48
65
  // Check if there are changes
49
66
  if (diff.summary.creates + diff.summary.updates + diff.summary.deletes ===
50
67
  0) {
51
- console.log("No changes to apply.");
68
+ terminal.success("No changes to apply.");
52
69
  return;
53
70
  }
54
71
  // Show deletions warning if --prune
55
72
  if (options.prune && diff.summary.deletes > 0) {
56
- console.warn(`⚠️ --prune will DELETE ${diff.summary.deletes} plan(s) from the hub`);
57
- console.log("");
73
+ terminal.warn(`--prune will DELETE ${diff.summary.deletes} plan(s) from the hub`);
74
+ terminal.blank();
58
75
  }
59
76
  // Ask for confirmation unless auto-approved
60
77
  if (!options.autoApprove && !options.dryRun) {
61
- console.log("Do you want to perform these actions? (yes/no)");
62
- // For now, just proceed - in a real implementation, we'd use readline
63
- // to get user input
64
- console.log("Note: Use --auto-approve flag to skip confirmation");
65
- console.log("");
78
+ const confirmed = await terminal.confirm("Do you want to perform these actions?");
79
+ if (!confirmed) {
80
+ terminal.warn("Apply cancelled.");
81
+ return;
82
+ }
66
83
  }
67
84
  // Apply changes with environment injection
68
- const result = await applyDiff(diff, planApi, state.projectId, envName, {
85
+ const applySpinner = terminal.spinner("Applying changes...").start();
86
+ const result = await applyDiff(diff, sdk, {
69
87
  dryRun: options.dryRun,
70
88
  });
71
- console.log("");
72
- console.log(formatApplyResult(result));
89
+ if (result.success) {
90
+ applySpinner.succeed("Changes applied successfully");
91
+ }
92
+ else {
93
+ applySpinner.fail("Apply failed");
94
+ }
95
+ terminal.blank();
96
+ terminal.log(formatApplyResult(result));
73
97
  if (!result.success) {
74
- process.exit(1);
98
+ terminal.exit(1);
75
99
  }
76
100
  }
77
101
  catch (error) {
78
- console.error(`Error: ${error.message}`);
79
- process.exit(1);
102
+ terminal.error(error.message);
103
+ terminal.exit(1);
80
104
  }
81
105
  }
@@ -1,25 +1,32 @@
1
+ import { randomBytes } from "crypto";
1
2
  import { loadState, saveState } from "../../core/state.js";
3
+ import { saveHubCredentials } from "../../core/credentials.js";
4
+ import { terminal } from "../../utils/terminal.js";
2
5
  /**
3
6
  * Configure hub connection settings
4
7
  */
5
8
  export async function executeConnect(options) {
6
9
  try {
7
10
  const state = await loadState();
8
- // Update runner config
9
- state.runner = {
11
+ // Save token to user-level credentials file if provided
12
+ if (options.token) {
13
+ await saveHubCredentials(options.token);
14
+ }
15
+ // Update hub config in project state (without token)
16
+ state.hub = {
10
17
  baseUrl: options.url,
11
- apiToken: options.token,
18
+ clientId: randomBytes(16).toString("hex"),
12
19
  };
13
20
  await saveState(state);
14
- console.log("Hub connection configured");
15
- console.log(` URL: ${options.url}`);
21
+ terminal.success("Hub connection configured");
22
+ terminal.log(` URL: ${terminal.colors.cyan(options.url)}`);
16
23
  if (options.token) {
17
- console.log(" API Token: ***");
24
+ terminal.log(` API Token: ${terminal.colors.dim("***")} (saved to user credentials)`);
18
25
  }
19
- console.log("");
26
+ terminal.blank();
20
27
  }
21
28
  catch (error) {
22
- console.error(`Error: ${error.message}`);
23
- process.exit(1);
29
+ terminal.error(error.message);
30
+ terminal.exit(1);
24
31
  }
25
32
  }
@@ -0,0 +1 @@
1
+ export declare function executeLogin(): Promise<void>;
@@ -0,0 +1,79 @@
1
+ // CLI implementation
2
+ import { createAuthClient } from "better-auth/client";
3
+ import { deviceAuthorizationClient, jwtClient, } from "better-auth/client/plugins";
4
+ import { loadState, saveState } from "../../core/state.js";
5
+ import { saveHubCredentials } from "../../core/credentials.js";
6
+ import { terminal } from "../../utils/terminal.js";
7
+ import { randomBytes } from "crypto";
8
+ const baseURL = "http://localhost:4000/api/auth";
9
+ const hubBaseUrl = "http://localhost:3000";
10
+ //const baseURL = "https://cloud.griffin.app"
11
+ const oauthGrant = "urn:ietf:params:oauth:grant-type:device_code";
12
+ const authClient = createAuthClient({
13
+ baseURL: baseURL,
14
+ plugins: [
15
+ deviceAuthorizationClient(),
16
+ jwtClient(),
17
+ ],
18
+ });
19
+ async function pollForToken(clientId, deviceCode, interval) {
20
+ const { data, error } = await authClient.device.token({
21
+ grant_type: oauthGrant,
22
+ device_code: deviceCode,
23
+ client_id: clientId,
24
+ fetchOptions: {
25
+ headers: {
26
+ "user-agent": `griffin-cli`,
27
+ },
28
+ },
29
+ });
30
+ if (data?.access_token)
31
+ return data.access_token;
32
+ switch (error?.error) {
33
+ case "slow_down":
34
+ await new Promise((resolve) => setTimeout(resolve, interval * 2));
35
+ return pollForToken(clientId, deviceCode, interval * 2);
36
+ case "authorization_pending":
37
+ await new Promise((resolve) => setTimeout(resolve, interval));
38
+ return pollForToken(clientId, deviceCode, interval);
39
+ default:
40
+ throw new Error(error?.error_description || "Unknown error");
41
+ }
42
+ }
43
+ export async function executeLogin() {
44
+ const state = await loadState();
45
+ let clientId = state.hub?.clientId;
46
+ if (!clientId) {
47
+ clientId = randomBytes(16).toString("hex");
48
+ }
49
+ //const clientId = state.hub?.clientId;
50
+ const { data } = await authClient.device.code({
51
+ client_id: clientId,
52
+ });
53
+ terminal.info(`Go to: ${data?.verification_uri_complete}`);
54
+ terminal.info(`Or enter code: ${data?.user_code}`);
55
+ // 2. Poll for authorization
56
+ const sessionToken = await pollForToken(clientId, data?.device_code, (data?.interval ?? 5) * 1000);
57
+ const { data: jwtData } = await authClient.token({
58
+ fetchOptions: {
59
+ headers: {
60
+ Authorization: `Bearer ${sessionToken}`,
61
+ },
62
+ },
63
+ });
64
+ // Save token to user-level credentials file
65
+ if (jwtData?.token) {
66
+ await saveHubCredentials(jwtData.token);
67
+ terminal.success("Login successful");
68
+ terminal.log(` Token saved to user credentials`);
69
+ }
70
+ // Save hub config to project state (without token)
71
+ await saveState({
72
+ ...state,
73
+ hub: {
74
+ ...state.hub,
75
+ clientId: clientId,
76
+ baseUrl: hubBaseUrl,
77
+ },
78
+ });
79
+ }
@@ -0,0 +1,6 @@
1
+ export interface LogoutOptions {
2
+ }
3
+ /**
4
+ * Remove stored credentials for hub
5
+ */
6
+ export declare function executeLogout(options: LogoutOptions): Promise<void>;
@@ -0,0 +1,16 @@
1
+ import { removeHubCredentials } from "../../core/credentials.js";
2
+ import { terminal } from "../../utils/terminal.js";
3
+ /**
4
+ * Remove stored credentials for hub
5
+ */
6
+ export async function executeLogout(options) {
7
+ try {
8
+ await removeHubCredentials();
9
+ terminal.success("Credentials removed.");
10
+ terminal.blank();
11
+ }
12
+ catch (error) {
13
+ terminal.error(error.message);
14
+ terminal.exit(1);
15
+ }
16
+ }
@@ -1,7 +1,11 @@
1
1
  import { loadState, resolveEnvironment } from "../../core/state.js";
2
2
  import { discoverPlans, formatDiscoveryErrors } from "../../core/discovery.js";
3
- import { createSdkClients } from "../../core/sdk.js";
3
+ import { createSdkWithCredentials } from "../../core/sdk.js";
4
4
  import { computeDiff, formatDiff, formatDiffJson } from "../../core/diff.js";
5
+ import { terminal } from "../../utils/terminal.js";
6
+ import { withSDKErrorHandling } from "../../utils/sdk-error.js";
7
+ import { loadVariables } from "../../core/variables.js";
8
+ import { resolvePlan } from "../../resolve.js";
5
9
  /**
6
10
  * Show what changes would be applied
7
11
  */
@@ -11,11 +15,11 @@ export async function executePlan(options) {
11
15
  const state = await loadState();
12
16
  // Resolve environment
13
17
  const envName = await resolveEnvironment(options.env);
14
- if (!state.runner?.baseUrl) {
15
- console.error("Error: Hub connection not configured.");
16
- console.log("Connect with:");
17
- console.log(" griffin hub connect --url <url> --token <token>");
18
- process.exit(1);
18
+ if (!state.hub?.baseUrl) {
19
+ terminal.error("Hub connection not configured.");
20
+ terminal.dim("Connect with:");
21
+ terminal.dim(" griffin hub connect --url <url> --token <token>");
22
+ terminal.exit(1);
19
23
  }
20
24
  // Discover local plans
21
25
  const discoveryPattern = state.discovery?.pattern || "**/__griffin__/*.{ts,js}";
@@ -23,36 +27,49 @@ export async function executePlan(options) {
23
27
  "node_modules/**",
24
28
  "dist/**",
25
29
  ];
30
+ const spinner = terminal.spinner("Discovering local plans...").start();
26
31
  const { plans, errors } = await discoverPlans(discoveryPattern, discoveryIgnore);
27
32
  if (errors.length > 0) {
28
- console.error(formatDiscoveryErrors(errors));
29
- process.exit(1);
33
+ spinner.fail("Discovery failed");
34
+ terminal.error(formatDiscoveryErrors(errors));
35
+ terminal.exit(1);
30
36
  }
31
- // Create SDK clients
32
- const { planApi } = createSdkClients({
33
- baseUrl: state.runner.baseUrl,
34
- apiToken: state.runner.apiToken || undefined,
35
- });
37
+ spinner.succeed(`Found ${plans.length} local plan(s)`);
38
+ // Create SDK clients with credentials
39
+ const sdk = await createSdkWithCredentials(state.hub.baseUrl);
36
40
  // Fetch remote plans for this project + environment
37
- const response = await planApi.planGet(state.projectId, envName);
38
- const remotePlans = response.data.data.map((p) => p);
41
+ const fetchSpinner = terminal.spinner("Fetching remote plans...").start();
42
+ const response = await withSDKErrorHandling(() => sdk.getPlan({
43
+ query: {
44
+ projectId: state.projectId,
45
+ environment: envName,
46
+ },
47
+ }), "Failed to fetch remote plans");
48
+ const remotePlans = response?.data?.data;
49
+ fetchSpinner.succeed(`Found ${remotePlans.length} remote plan(s)`);
50
+ // Load variables and resolve local plans before computing diff
51
+ const variables = await loadVariables(envName);
52
+ const resolvedPlans = plans.map((p) => resolvePlan(p.plan, state.projectId, envName, variables));
39
53
  // Compute diff (no deletions shown by default)
40
- const diff = computeDiff(plans.map((p) => p.plan), remotePlans, { includeDeletions: false });
54
+ const diff = computeDiff(resolvedPlans, remotePlans, {
55
+ includeDeletions: false,
56
+ });
57
+ terminal.blank();
41
58
  // Output
42
59
  if (options.json) {
43
- console.log(formatDiffJson(diff));
60
+ terminal.log(formatDiffJson(diff));
44
61
  }
45
62
  else {
46
- console.log(formatDiff(diff));
63
+ terminal.log(formatDiff(diff));
47
64
  }
48
65
  // Exit with error code if there are changes
49
66
  if (diff.summary.creates + diff.summary.updates + diff.summary.deletes >
50
67
  0) {
51
- process.exit(2); // Exit code 2 indicates changes pending
68
+ terminal.exit(2); // Exit code 2 indicates changes pending
52
69
  }
53
70
  }
54
71
  catch (error) {
55
- console.error(`Error: ${error.message}`);
56
- process.exit(1);
72
+ terminal.error(error.message);
73
+ terminal.exit(1);
57
74
  }
58
75
  }