@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.
- package/README.md +111 -161
- package/dist/cli.js +34 -15
- package/dist/commands/env.d.ts +1 -22
- package/dist/commands/env.js +22 -109
- package/dist/commands/generate-key.js +16 -10
- package/dist/commands/hub/apply.js +58 -34
- package/dist/commands/hub/connect.js +16 -9
- package/dist/commands/hub/login.d.ts +1 -0
- package/dist/commands/hub/login.js +79 -0
- package/dist/commands/hub/logout.d.ts +6 -0
- package/dist/commands/hub/logout.js +16 -0
- package/dist/commands/hub/plan.js +38 -21
- package/dist/commands/hub/run.js +93 -57
- package/dist/commands/hub/runs.js +75 -42
- package/dist/commands/hub/status.js +18 -13
- 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 +36 -38
- package/dist/core/apply.test.js +71 -27
- 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 +7 -6
- package/dist/core/diff.js +2 -1
- package/dist/core/diff.test.js +44 -20
- package/dist/core/discovery.d.ts +2 -3
- package/dist/core/discovery.js +3 -10
- package/dist/core/plan-diff.d.ts +5 -6
- package/dist/core/plan-diff.js +6 -6
- package/dist/core/sdk.d.ts +5 -9
- package/dist/core/sdk.js +23 -15
- package/dist/core/variables.js +13 -0
- package/dist/index.d.ts +8 -3
- package/dist/index.js +6 -2
- package/dist/resolve.d.ts +3 -0
- package/dist/resolve.js +9 -0
- 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 +5 -7
- 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/sdk-error.d.ts +8 -0
- package/dist/utils/sdk-error.js +114 -0
- package/dist/utils/terminal.d.ts +100 -0
- package/dist/utils/terminal.js +148 -0
- package/package.json +9 -4
package/dist/commands/env.js
CHANGED
|
@@ -1,121 +1,34 @@
|
|
|
1
|
-
import { loadState
|
|
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
|
|
9
|
-
if (
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
114
|
-
console.log(`✓ Set '${name}' as default environment`);
|
|
115
|
-
console.log("");
|
|
28
|
+
terminal.blank();
|
|
116
29
|
}
|
|
117
30
|
catch (error) {
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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 {
|
|
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.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
// Create SDK clients
|
|
25
|
-
const
|
|
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
|
-
|
|
38
|
-
|
|
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
|
|
42
|
-
const
|
|
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(
|
|
58
|
+
const diff = computeDiff(resolvedPlans, remotePlans, {
|
|
59
|
+
includeDeletions: options.prune || false,
|
|
60
|
+
});
|
|
45
61
|
// Show plan
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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
|
-
|
|
57
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
85
|
+
const applySpinner = terminal.spinner("Applying changes...").start();
|
|
86
|
+
const result = await applyDiff(diff, sdk, {
|
|
69
87
|
dryRun: options.dryRun,
|
|
70
88
|
});
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
98
|
+
terminal.exit(1);
|
|
75
99
|
}
|
|
76
100
|
}
|
|
77
101
|
catch (error) {
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
//
|
|
9
|
-
|
|
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
|
-
|
|
18
|
+
clientId: randomBytes(16).toString("hex"),
|
|
12
19
|
};
|
|
13
20
|
await saveState(state);
|
|
14
|
-
|
|
15
|
-
|
|
21
|
+
terminal.success("Hub connection configured");
|
|
22
|
+
terminal.log(` URL: ${terminal.colors.cyan(options.url)}`);
|
|
16
23
|
if (options.token) {
|
|
17
|
-
|
|
24
|
+
terminal.log(` API Token: ${terminal.colors.dim("***")} (saved to user credentials)`);
|
|
18
25
|
}
|
|
19
|
-
|
|
26
|
+
terminal.blank();
|
|
20
27
|
}
|
|
21
28
|
catch (error) {
|
|
22
|
-
|
|
23
|
-
|
|
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,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 {
|
|
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.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
29
|
-
|
|
33
|
+
spinner.fail("Discovery failed");
|
|
34
|
+
terminal.error(formatDiscoveryErrors(errors));
|
|
35
|
+
terminal.exit(1);
|
|
30
36
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
38
|
-
const
|
|
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(
|
|
54
|
+
const diff = computeDiff(resolvedPlans, remotePlans, {
|
|
55
|
+
includeDeletions: false,
|
|
56
|
+
});
|
|
57
|
+
terminal.blank();
|
|
41
58
|
// Output
|
|
42
59
|
if (options.json) {
|
|
43
|
-
|
|
60
|
+
terminal.log(formatDiffJson(diff));
|
|
44
61
|
}
|
|
45
62
|
else {
|
|
46
|
-
|
|
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
|
-
|
|
68
|
+
terminal.exit(2); // Exit code 2 indicates changes pending
|
|
52
69
|
}
|
|
53
70
|
}
|
|
54
71
|
catch (error) {
|
|
55
|
-
|
|
56
|
-
|
|
72
|
+
terminal.error(error.message);
|
|
73
|
+
terminal.exit(1);
|
|
57
74
|
}
|
|
58
75
|
}
|