@griffin-app/griffin-cli 1.0.4 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -145,7 +145,7 @@ List all available environments.
145
145
  griffin env list
146
146
  ```
147
147
 
148
- Shows all configured environments with an asterisk (*) marking the default environment.
148
+ Shows all configured environments with an asterisk (\*) marking the default environment.
149
149
 
150
150
  ### Local Commands
151
151
 
@@ -166,12 +166,12 @@ Variables are loaded from `variables.yaml` for the specified environment.
166
166
 
167
167
  #### `griffin hub connect`
168
168
 
169
- Configure hub connection settings.
169
+ Configure hub connection settings. When a token is provided, it is stored in the user-level credentials file (`~/.griffin/credentials.json`) for secure, cross-project authentication.
170
170
 
171
171
  **Options:**
172
172
 
173
173
  - `--url <url>` - Hub URL (required)
174
- - `--token <token>` - API authentication token
174
+ - `--token <token>` - API authentication token (optional)
175
175
 
176
176
  **Example:**
177
177
 
@@ -179,9 +179,36 @@ Configure hub connection settings.
179
179
  griffin hub connect --url https://hub.example.com --token abc123
180
180
  ```
181
181
 
182
+ #### `griffin hub login`
183
+
184
+ Authenticate with the hub using device authorization flow. The received token is stored in the user-level credentials file (`~/.griffin/credentials.json`) for secure access across all projects.
185
+
186
+ **Example:**
187
+
188
+ ```bash
189
+ griffin hub login
190
+ ```
191
+
192
+ After running this command, you'll be provided with a URL to complete authentication in your browser. Once authenticated, the token will be automatically saved.
193
+
194
+ #### `griffin hub logout`
195
+
196
+ Remove stored credentials for the currently configured hub or all hubs.
197
+
198
+ **Options:**
199
+
200
+ - `--all` - Remove credentials for all hubs
201
+
202
+ **Example:**
203
+
204
+ ```bash
205
+ griffin hub logout
206
+ griffin hub logout --all
207
+ ```
208
+
182
209
  #### `griffin hub status`
183
210
 
184
- Show hub connection status.
211
+ Show hub connection status, including whether credentials are configured.
185
212
 
186
213
  **Example:**
187
214
 
@@ -264,7 +291,6 @@ griffin hub run staging --plan health-check --wait
264
291
  griffin hub run production --plan api-check --force
265
292
  ```
266
293
 
267
-
268
294
  ## Configuration
269
295
 
270
296
  ### Environment Variables
@@ -273,7 +299,7 @@ griffin hub run production --plan api-check --force
273
299
 
274
300
  ### State File
275
301
 
276
- Griffin stores configuration in `.griffin/state.json`:
302
+ Griffin stores project configuration in `.griffin/state.json`:
277
303
 
278
304
  ```json
279
305
  {
@@ -283,9 +309,9 @@ Griffin stores configuration in `.griffin/state.json`:
283
309
  "local": {}
284
310
  },
285
311
  "defaultEnvironment": "local",
286
- "runner": {
312
+ "hub": {
287
313
  "baseUrl": "https://hub.example.com",
288
- "apiToken": "..."
314
+ "clientId": "..."
289
315
  },
290
316
  "discovery": {
291
317
  "pattern": "**/__griffin__/*.{ts,js}",
@@ -294,7 +320,27 @@ Griffin stores configuration in `.griffin/state.json`:
294
320
  }
295
321
  ```
296
322
 
297
- **Important:** Add `.griffin/` to `.gitignore` as it contains local state and potentially sensitive tokens.
323
+ **Important:** Add `.griffin/` to `.gitignore` as it contains local project state.
324
+
325
+ ### Credentials File
326
+
327
+ Griffin stores user-level authentication credentials in `~/.griffin/credentials.json`:
328
+
329
+ ```json
330
+ {
331
+ "version": 1,
332
+ "hubs": {
333
+ "https://hub.example.com": {
334
+ "token": "...",
335
+ "updatedAt": "2024-01-24T12:00:00.000Z"
336
+ }
337
+ }
338
+ }
339
+ ```
340
+
341
+ This file is automatically created and managed by the CLI when you use `griffin hub login` or `griffin hub connect --token <token>`. Credentials are stored per-hub URL and are available across all projects on your system.
342
+
343
+ **Security:** The credentials file is created with restricted permissions (0600) to ensure only the owner can read/write.
298
344
 
299
345
  ## Environments and Variables
300
346
 
package/dist/cli.js CHANGED
@@ -13,6 +13,8 @@ import { executeRuns } from "./commands/hub/runs.js";
13
13
  import { executePlan } from "./commands/hub/plan.js";
14
14
  import { executeApply } from "./commands/hub/apply.js";
15
15
  import { executeRun } from "./commands/hub/run.js";
16
+ import { executeLogin } from "./commands/hub/login.js";
17
+ import { executeLogout } from "./commands/hub/logout.js";
16
18
  const program = new Command();
17
19
  program
18
20
  .name("griffin")
@@ -106,5 +108,17 @@ hub
106
108
  .action(async (env, options) => {
107
109
  await executeRun({ ...options, env });
108
110
  });
111
+ hub
112
+ .command("login")
113
+ .description("Login to the hub")
114
+ .action(async () => {
115
+ await executeLogin();
116
+ });
117
+ hub
118
+ .command("logout")
119
+ .description("Remove stored credentials")
120
+ .action(async (options) => {
121
+ await executeLogout(options);
122
+ });
109
123
  // Parse arguments
110
124
  program.parse();
@@ -17,7 +17,9 @@ export async function executeEnvList() {
17
17
  terminal.blank();
18
18
  for (const envName of environments) {
19
19
  const isDefault = state.defaultEnvironment === envName;
20
- const marker = isDefault ? terminal.colors.green("●") : terminal.colors.dim("○");
20
+ const marker = isDefault
21
+ ? terminal.colors.green("●")
22
+ : terminal.colors.dim("○");
21
23
  const envDisplay = isDefault
22
24
  ? terminal.colors.cyan(envName) + terminal.colors.dim(" (default)")
23
25
  : envName;
@@ -2,8 +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 { createSdk } from "../../core/sdk.js";
5
+ import { createSdkWithCredentials } from "../../core/sdk.js";
6
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";
7
10
  /**
8
11
  * Apply changes to the hub
9
12
  */
@@ -14,7 +17,7 @@ export async function executeApply(options) {
14
17
  // Resolve environment
15
18
  const envName = await resolveEnvironment(options.env);
16
19
  // Check if runner is configured
17
- if (!state.runner?.baseUrl) {
20
+ if (!state.hub?.baseUrl) {
18
21
  terminal.error("Hub connection not configured.");
19
22
  terminal.dim("Connect with:");
20
23
  terminal.dim(" griffin hub connect --url <url> --token <token>");
@@ -22,11 +25,8 @@ export async function executeApply(options) {
22
25
  }
23
26
  terminal.info(`Applying to ${terminal.colors.cyan(envName)} environment`);
24
27
  terminal.blank();
25
- // Create SDK clients
26
- const sdk = createSdk({
27
- baseUrl: state.runner?.baseUrl || "",
28
- apiToken: state.runner?.apiToken || "",
29
- });
28
+ // Create SDK clients with credentials
29
+ const sdk = await createSdkWithCredentials(state.hub.baseUrl);
30
30
  // Discover local plans
31
31
  const discoveryPattern = state.discovery?.pattern || "**/__griffin__/*.{ts,js}";
32
32
  const discoveryIgnore = state.discovery?.ignore || [
@@ -42,19 +42,22 @@ export async function executeApply(options) {
42
42
  }
43
43
  spinner.succeed(`Found ${plans.length} local plan(s)`);
44
44
  // Fetch remote plans for this project + environment
45
- const fetchSpinner = terminal
46
- .spinner("Fetching remote plans...")
47
- .start();
48
- const response = await sdk.getPlan({
45
+ const fetchSpinner = terminal.spinner("Fetching remote plans...").start();
46
+ const response = await withSDKErrorHandling(() => sdk.getPlan({
49
47
  query: {
50
48
  projectId: state.projectId,
51
49
  environment: envName,
52
50
  },
53
- });
51
+ }), "Failed to fetch remote plans");
54
52
  const remotePlans = response?.data?.data;
55
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));
56
57
  // Compute diff (include deletions if --prune)
57
- 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
+ });
58
61
  // Show plan
59
62
  terminal.blank();
60
63
  terminal.log(formatDiff(diff));
@@ -80,7 +83,7 @@ export async function executeApply(options) {
80
83
  }
81
84
  // Apply changes with environment injection
82
85
  const applySpinner = terminal.spinner("Applying changes...").start();
83
- const result = await applyDiff(diff, sdk, state.projectId, envName, {
86
+ const result = await applyDiff(diff, sdk, {
84
87
  dryRun: options.dryRun,
85
88
  });
86
89
  if (result.success) {
@@ -1,4 +1,6 @@
1
+ import { randomBytes } from "crypto";
1
2
  import { loadState, saveState } from "../../core/state.js";
3
+ import { saveHubCredentials } from "../../core/credentials.js";
2
4
  import { terminal } from "../../utils/terminal.js";
3
5
  /**
4
6
  * Configure hub connection settings
@@ -6,16 +8,20 @@ import { terminal } from "../../utils/terminal.js";
6
8
  export async function executeConnect(options) {
7
9
  try {
8
10
  const state = await loadState();
9
- // Update runner config
10
- 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 = {
11
17
  baseUrl: options.url,
12
- apiToken: options.token,
18
+ clientId: randomBytes(16).toString("hex"),
13
19
  };
14
20
  await saveState(state);
15
21
  terminal.success("Hub connection configured");
16
22
  terminal.log(` URL: ${terminal.colors.cyan(options.url)}`);
17
23
  if (options.token) {
18
- terminal.log(` API Token: ${terminal.colors.dim("***")}`);
24
+ terminal.log(` API Token: ${terminal.colors.dim("***")} (saved to user credentials)`);
19
25
  }
20
26
  terminal.blank();
21
27
  }
@@ -0,0 +1 @@
1
+ export declare function executeLogin(): Promise<void>;
@@ -0,0 +1,89 @@
1
+ // CLI implementation
2
+ import { createAuthClient } from "better-auth/client";
3
+ import { deviceAuthorizationClient, jwtClient, } from "better-auth/client/plugins";
4
+ import { getProjectId, 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
+ import { createEmptyState } from "../../schemas/state.js";
9
+ const baseURL = "http://localhost:4000/api/auth";
10
+ const hubBaseUrl = "http://localhost:3000";
11
+ //const baseURL = "https://cloud.griffin.app"
12
+ const oauthGrant = "urn:ietf:params:oauth:grant-type:device_code";
13
+ const authClient = createAuthClient({
14
+ baseURL: baseURL,
15
+ plugins: [
16
+ deviceAuthorizationClient(),
17
+ jwtClient(),
18
+ ],
19
+ });
20
+ async function pollForToken(clientId, deviceCode, interval) {
21
+ const { data, error } = await authClient.device.token({
22
+ grant_type: oauthGrant,
23
+ device_code: deviceCode,
24
+ client_id: clientId,
25
+ fetchOptions: {
26
+ headers: {
27
+ "user-agent": `griffin-cli`,
28
+ },
29
+ },
30
+ });
31
+ if (data?.access_token)
32
+ return data.access_token;
33
+ switch (error?.error) {
34
+ case "slow_down":
35
+ await new Promise((resolve) => setTimeout(resolve, interval * 2));
36
+ return pollForToken(clientId, deviceCode, interval * 2);
37
+ case "authorization_pending":
38
+ await new Promise((resolve) => setTimeout(resolve, interval));
39
+ return pollForToken(clientId, deviceCode, interval);
40
+ default:
41
+ throw new Error(error?.error_description || "Unknown error");
42
+ }
43
+ }
44
+ export async function executeLogin() {
45
+ let state;
46
+ let clientId;
47
+ try {
48
+ state = await loadState();
49
+ clientId = state.hub?.clientId;
50
+ }
51
+ catch (error) {
52
+ }
53
+ if (!clientId) {
54
+ clientId = randomBytes(16).toString("hex");
55
+ }
56
+ const { data } = await authClient.device.code({
57
+ client_id: clientId,
58
+ });
59
+ terminal.info(`Go to: ${data?.verification_uri_complete}`);
60
+ terminal.info(`Or enter code: ${data?.user_code}`);
61
+ // 2. Poll for authorization
62
+ const sessionToken = await pollForToken(clientId, data?.device_code, (data?.interval ?? 5) * 1000);
63
+ const { data: jwtData } = await authClient.token({
64
+ fetchOptions: {
65
+ headers: {
66
+ Authorization: `Bearer ${sessionToken}`,
67
+ },
68
+ },
69
+ });
70
+ // Save token to user-level credentials file
71
+ if (jwtData?.token) {
72
+ await saveHubCredentials(jwtData.token);
73
+ terminal.success("Login successful");
74
+ terminal.log(` Token saved to user credentials`);
75
+ }
76
+ if (!state) {
77
+ const projectId = await getProjectId();
78
+ state = createEmptyState(projectId);
79
+ }
80
+ // Save hub config to project state (without token)
81
+ await saveState({
82
+ ...state,
83
+ hub: {
84
+ ...state.hub,
85
+ clientId: clientId,
86
+ baseUrl: hubBaseUrl,
87
+ },
88
+ });
89
+ }
@@ -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,8 +1,11 @@
1
1
  import { loadState, resolveEnvironment } from "../../core/state.js";
2
2
  import { discoverPlans, formatDiscoveryErrors } from "../../core/discovery.js";
3
- import { createSdk } from "../../core/sdk.js";
3
+ import { createSdkWithCredentials } from "../../core/sdk.js";
4
4
  import { computeDiff, formatDiff, formatDiffJson } from "../../core/diff.js";
5
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";
6
9
  /**
7
10
  * Show what changes would be applied
8
11
  */
@@ -12,7 +15,7 @@ export async function executePlan(options) {
12
15
  const state = await loadState();
13
16
  // Resolve environment
14
17
  const envName = await resolveEnvironment(options.env);
15
- if (!state.runner?.baseUrl) {
18
+ if (!state.hub?.baseUrl) {
16
19
  terminal.error("Hub connection not configured.");
17
20
  terminal.dim("Connect with:");
18
21
  terminal.dim(" griffin hub connect --url <url> --token <token>");
@@ -32,25 +35,25 @@ export async function executePlan(options) {
32
35
  terminal.exit(1);
33
36
  }
34
37
  spinner.succeed(`Found ${plans.length} local plan(s)`);
35
- // Create SDK clients
36
- const sdk = createSdk({
37
- baseUrl: state.runner?.baseUrl || "",
38
- apiToken: state.runner?.apiToken || "",
39
- });
38
+ // Create SDK clients with credentials
39
+ const sdk = await createSdkWithCredentials(state.hub.baseUrl);
40
40
  // Fetch remote plans for this project + environment
41
- const fetchSpinner = terminal
42
- .spinner("Fetching remote plans...")
43
- .start();
44
- const response = await sdk.getPlan({
41
+ const fetchSpinner = terminal.spinner("Fetching remote plans...").start();
42
+ const response = await withSDKErrorHandling(() => sdk.getPlan({
45
43
  query: {
46
44
  projectId: state.projectId,
47
45
  environment: envName,
48
46
  },
49
- });
47
+ }), "Failed to fetch remote plans");
50
48
  const remotePlans = response?.data?.data;
51
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));
52
53
  // Compute diff (no deletions shown by default)
53
- const diff = computeDiff(plans.map((p) => p.plan), remotePlans, { includeDeletions: false });
54
+ const diff = computeDiff(resolvedPlans, remotePlans, {
55
+ includeDeletions: false,
56
+ });
54
57
  terminal.blank();
55
58
  // Output
56
59
  if (options.json) {
@@ -1,8 +1,11 @@
1
1
  import { loadState, resolveEnvironment } from "../../core/state.js";
2
- import { createSdk } from "../../core/sdk.js";
2
+ import { createSdkWithCredentials } from "../../core/sdk.js";
3
3
  import { discoverPlans, formatDiscoveryErrors } from "../../core/discovery.js";
4
4
  import { computeDiff } from "../../core/diff.js";
5
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";
6
9
  /**
7
10
  * Trigger a plan run on the hub
8
11
  */
@@ -12,17 +15,14 @@ export async function executeRun(options) {
12
15
  const state = await loadState();
13
16
  // Resolve environment
14
17
  const envName = await resolveEnvironment(options.env);
15
- if (!state.runner?.baseUrl) {
18
+ if (!state.hub?.baseUrl) {
16
19
  terminal.error("Hub connection not configured.");
17
20
  terminal.dim("Connect with:");
18
21
  terminal.dim(" griffin hub connect --url <url> --token <token>");
19
22
  terminal.exit(1);
20
23
  }
21
- // Create SDK clients
22
- const sdk = createSdk({
23
- baseUrl: state.runner?.baseUrl || "",
24
- apiToken: state.runner?.apiToken || "",
25
- });
24
+ // Create SDK clients with credentials
25
+ const sdk = await createSdkWithCredentials(state.hub.baseUrl);
26
26
  // Discover local plans
27
27
  const discoveryPattern = state.discovery?.pattern || "**/__griffin__/*.{ts,js}";
28
28
  const discoveryIgnore = state.discovery?.ignore || [
@@ -50,12 +50,12 @@ export async function executeRun(options) {
50
50
  spinner.succeed(`Found local plan: ${terminal.colors.cyan(options.plan)}`);
51
51
  // Fetch remote plans for this project + environment
52
52
  const fetchSpinner = terminal.spinner("Checking hub...").start();
53
- const response = await sdk.getPlan({
53
+ const response = await withSDKErrorHandling(() => sdk.getPlan({
54
54
  query: {
55
55
  projectId: state.projectId,
56
56
  environment: envName,
57
57
  },
58
- });
58
+ }), "Failed to fetch plans from hub");
59
59
  const remotePlans = response?.data?.data;
60
60
  // Find remote plan by name
61
61
  const remotePlan = remotePlans.find((p) => p.name === options.plan);
@@ -65,8 +65,11 @@ export async function executeRun(options) {
65
65
  terminal.exit(1);
66
66
  }
67
67
  fetchSpinner.succeed("Plan found on hub");
68
+ // Load variables and resolve local plan before computing diff
69
+ const variables = await loadVariables(envName);
70
+ const resolvedLocalPlan = resolvePlan(localPlan.plan, state.projectId, envName, variables);
68
71
  // Compute diff to check if local plan differs from remote
69
- const diff = computeDiff([localPlan.plan], [remotePlan], {
72
+ const diff = computeDiff([resolvedLocalPlan], [remotePlan], {
70
73
  includeDeletions: false,
71
74
  });
72
75
  const hasDiff = diff.actions.length > 0 &&
@@ -86,14 +89,14 @@ export async function executeRun(options) {
86
89
  terminal.warn("Running with --force (local changes not applied)");
87
90
  }
88
91
  const triggerSpinner = terminal.spinner("Triggering run...").start();
89
- const runResponse = await sdk.postRunsTriggerByPlanId({
92
+ const runResponse = await withSDKErrorHandling(() => sdk.postRunsTriggerByPlanId({
90
93
  path: {
91
94
  planId: remotePlan.id,
92
95
  },
93
96
  body: {
94
97
  environment: envName,
95
98
  },
96
- });
99
+ }), "Failed to trigger run");
97
100
  const run = runResponse?.data?.data;
98
101
  triggerSpinner.succeed("Run triggered");
99
102
  terminal.blank();
@@ -103,16 +106,18 @@ export async function executeRun(options) {
103
106
  // Wait for completion if requested
104
107
  if (options.wait) {
105
108
  terminal.blank();
106
- const waitSpinner = terminal.spinner("Waiting for run to complete...").start();
109
+ const waitSpinner = terminal
110
+ .spinner("Waiting for run to complete...")
111
+ .start();
107
112
  const runId = run.id;
108
113
  let completed = false;
109
114
  while (!completed) {
110
115
  await new Promise((resolve) => setTimeout(resolve, 2000)); // Poll every 2 seconds
111
- const runStatusResponse = await sdk.getRunsById({
116
+ const runStatusResponse = await withSDKErrorHandling(() => sdk.getRunsById({
112
117
  path: {
113
118
  id: runId,
114
119
  },
115
- });
120
+ }), "Failed to fetch run status");
116
121
  const run = runStatusResponse?.data?.data;
117
122
  if (run.status === "completed" || run.status === "failed") {
118
123
  completed = true;
@@ -127,7 +132,9 @@ export async function executeRun(options) {
127
132
  terminal.log(`Duration: ${terminal.colors.dim((run.duration_ms / 1000).toFixed(2) + "s")}`);
128
133
  }
129
134
  if (run.success !== undefined) {
130
- const successText = run.success ? terminal.colors.green("Yes") : terminal.colors.red("No");
135
+ const successText = run.success
136
+ ? terminal.colors.green("Yes")
137
+ : terminal.colors.red("No");
131
138
  terminal.log(`Success: ${successText}`);
132
139
  }
133
140
  if (run.errors && run.errors.length > 0) {
@@ -1,6 +1,7 @@
1
1
  import { loadState } from "../../core/state.js";
2
- import { createSdk } from "../../core/sdk.js";
2
+ import { createSdkWithCredentials } from "../../core/sdk.js";
3
3
  import { terminal } from "../../utils/terminal.js";
4
+ import { withSDKErrorHandling } from "../../utils/sdk-error.js";
4
5
  /**
5
6
  * Show recent runs from the hub
6
7
  */
@@ -8,29 +9,26 @@ export async function executeRuns(options) {
8
9
  try {
9
10
  // Load state
10
11
  const state = await loadState();
11
- if (!state.runner?.baseUrl) {
12
+ if (!state.hub?.baseUrl) {
12
13
  terminal.error("Hub connection not configured.");
13
14
  terminal.dim("Connect with:");
14
15
  terminal.dim(" griffin hub connect --url <url> --token <token>");
15
16
  terminal.exit(1);
16
17
  }
17
- // Create SDK clients
18
- const sdk = createSdk({
19
- baseUrl: state.runner.baseUrl,
20
- apiToken: state.runner.apiToken || undefined,
21
- });
22
- terminal.info(`Hub: ${terminal.colors.cyan(state.runner.baseUrl)}`);
18
+ // Create SDK clients with credentials
19
+ const sdk = await createSdkWithCredentials(state.hub.baseUrl);
20
+ terminal.info(`Hub: ${terminal.colors.cyan(state.hub.baseUrl)}`);
23
21
  terminal.blank();
24
22
  // Get recent runs
25
23
  const limit = options.limit || 10;
26
24
  const spinner = terminal.spinner("Fetching runs...").start();
27
- const response = await sdk.getRuns({
25
+ const response = await withSDKErrorHandling(() => sdk.getRuns({
28
26
  query: {
29
27
  planId: options.plan,
30
28
  limit: limit,
31
29
  offset: 0,
32
30
  },
33
- });
31
+ }), "Failed to fetch runs");
34
32
  const runsData = response?.data;
35
33
  if (!runsData || runsData.total === 0) {
36
34
  spinner.info("No runs found.");
@@ -54,12 +52,7 @@ export async function executeRuns(options) {
54
52
  ? `${(run.duration_ms / 1000).toFixed(2)}s`
55
53
  : "-";
56
54
  const started = new Date(run.startedAt).toLocaleString();
57
- table.push([
58
- statusIcon,
59
- run.planId || "-",
60
- duration,
61
- started,
62
- ]);
55
+ table.push([statusIcon, run.planId || "-", duration, started]);
63
56
  }
64
57
  catch (error) {
65
58
  terminal.error(`Error processing run ${run.id}: ${error.message}`);
@@ -1,4 +1,5 @@
1
1
  import { loadState } from "../../core/state.js";
2
+ import { getHubCredentials } from "../../core/credentials.js";
2
3
  import { terminal } from "../../utils/terminal.js";
3
4
  /**
4
5
  * Show hub connection status
@@ -6,17 +7,20 @@ import { terminal } from "../../utils/terminal.js";
6
7
  export async function executeStatus() {
7
8
  try {
8
9
  const state = await loadState();
9
- if (!state.runner) {
10
+ if (!state.hub) {
10
11
  terminal.warn("No hub connection configured.");
11
12
  terminal.blank();
12
13
  terminal.dim("Connect with:");
13
14
  terminal.dim(" griffin hub connect --url <url> --token <token>");
14
15
  return;
15
16
  }
17
+ // Read credentials from user-level credentials file
18
+ const credentials = await getHubCredentials();
16
19
  terminal.info("Hub connection:");
17
- terminal.log(` URL: ${terminal.colors.cyan(state.runner.baseUrl)}`);
18
- if (state.runner.apiToken) {
19
- terminal.log(` API Token: ${terminal.colors.dim(state.runner.apiToken.substring(0, 8) + "...")}`);
20
+ terminal.log(` URL: ${terminal.colors.cyan(state.hub.baseUrl)}`);
21
+ if (credentials?.token) {
22
+ terminal.log(` API Token: ${terminal.colors.dim(credentials.token.substring(0, 8) + "...")}`);
23
+ terminal.log(` Updated: ${terminal.colors.dim(new Date(credentials.updatedAt).toLocaleString())}`);
20
24
  }
21
25
  else {
22
26
  terminal.log(` API Token: ${terminal.colors.dim("(not set)")}`);
@@ -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, sdk: GriffinHubSdk, projectId: string, environment: string, options?: {
22
+ export declare function applyDiff(diff: DiffResult, sdk: GriffinHubSdk, options?: {
23
23
  dryRun?: boolean;
24
24
  }): Promise<ApplyResult>;
25
25
  /**