@griffin-app/griffin-cli 1.0.26 → 1.0.28

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 (49) hide show
  1. package/README.md +122 -367
  2. package/dist/cli.js +48 -29
  3. package/dist/commands/env.d.ts +14 -3
  4. package/dist/commands/env.js +24 -22
  5. package/dist/commands/generate-key.d.ts +5 -6
  6. package/dist/commands/generate-key.js +20 -26
  7. package/dist/commands/hub/apply.d.ts +1 -0
  8. package/dist/commands/hub/apply.js +44 -29
  9. package/dist/commands/hub/connect.d.ts +2 -1
  10. package/dist/commands/hub/connect.js +18 -22
  11. package/dist/commands/hub/destroy.d.ts +2 -1
  12. package/dist/commands/hub/destroy.js +123 -119
  13. package/dist/commands/hub/integrations.d.ts +4 -1
  14. package/dist/commands/hub/integrations.js +160 -141
  15. package/dist/commands/hub/login.d.ts +13 -1
  16. package/dist/commands/hub/login.js +81 -15
  17. package/dist/commands/hub/logout.d.ts +2 -1
  18. package/dist/commands/hub/logout.js +7 -12
  19. package/dist/commands/hub/metrics.js +29 -27
  20. package/dist/commands/hub/monitor.js +16 -16
  21. package/dist/commands/hub/notifications.d.ts +3 -6
  22. package/dist/commands/hub/notifications.js +52 -38
  23. package/dist/commands/hub/run.d.ts +1 -0
  24. package/dist/commands/hub/run.js +114 -87
  25. package/dist/commands/hub/runs.d.ts +2 -0
  26. package/dist/commands/hub/runs.js +47 -45
  27. package/dist/commands/hub/secrets.d.ts +5 -5
  28. package/dist/commands/hub/secrets.js +80 -72
  29. package/dist/commands/hub/status.d.ts +4 -1
  30. package/dist/commands/hub/status.js +15 -9
  31. package/dist/commands/init.d.ts +2 -1
  32. package/dist/commands/init.js +31 -25
  33. package/dist/commands/local/run.d.ts +1 -0
  34. package/dist/commands/local/run.js +34 -26
  35. package/dist/commands/validate.d.ts +4 -1
  36. package/dist/commands/validate.js +23 -14
  37. package/dist/commands/variables.d.ts +17 -3
  38. package/dist/commands/variables.js +29 -28
  39. package/dist/core/credentials.d.ts +15 -0
  40. package/dist/core/credentials.js +37 -0
  41. package/dist/core/variables.js +4 -0
  42. package/dist/monitor-runner.js +0 -12
  43. package/dist/utils/command-wrapper.d.ts +9 -0
  44. package/dist/utils/command-wrapper.js +23 -0
  45. package/dist/utils/output.d.ts +66 -0
  46. package/dist/utils/output.js +202 -0
  47. package/dist/utils/sdk-error.d.ts +6 -1
  48. package/dist/utils/sdk-error.js +107 -77
  49. package/package.json +2 -2
@@ -1,13 +1,14 @@
1
1
  import { loadState, resolveEnvironment } from "../../core/state.js";
2
- import { terminal } from "../../utils/terminal.js";
3
2
  import { createSdkFromState, createSdkWithCredentials, } from "../../core/sdk.js";
4
- import { withSDKErrorHandling } from "../../utils/sdk-error.js";
5
- import { withCommandErrorHandler, outputJsonOrContinue, } from "../../utils/command-wrapper.js";
6
- export const executeIntegrationsList = withCommandErrorHandler(async (options) => {
3
+ import { handleSDKErrorWithOutput } from "../../utils/sdk-error.js";
4
+ import { createCommandHandler } from "../../utils/command-wrapper.js";
5
+ import { terminal } from "../../utils/terminal.js";
6
+ export const executeIntegrationsList = createCommandHandler("integrations list", async (options, output) => {
7
7
  const sdk = await createSdkFromState();
8
8
  const env = await resolveEnvironment(options.environment);
9
- const response = await withSDKErrorHandling(async () => {
10
- return sdk.getIntegrations({
9
+ let response;
10
+ try {
11
+ response = await sdk.getIntegrations({
11
12
  query: {
12
13
  category: options.category,
13
14
  provider: options.provider,
@@ -15,17 +16,19 @@ export const executeIntegrationsList = withCommandErrorHandler(async (options) =
15
16
  enabled: options.enabled,
16
17
  },
17
18
  });
18
- }, "Failed to list integrations");
19
- const list = response?.data?.data ?? [];
20
- if (outputJsonOrContinue(list, options.json))
21
- return;
19
+ }
20
+ catch (err) {
21
+ handleSDKErrorWithOutput(err, output, "Failed to list integrations");
22
+ }
23
+ const list = response.data?.data ?? [];
24
+ output.setData({ integrations: list });
22
25
  if (list.length === 0) {
23
- terminal.info("No integrations found.");
26
+ output.info("No integrations found.");
24
27
  return;
25
28
  }
26
- terminal.info("Integrations");
27
- terminal.blank();
28
- const table = terminal.table({
29
+ output.info("Integrations");
30
+ output.blank();
31
+ const table = output.table({
29
32
  head: ["ID", "Category", "Provider", "Name", "Environment", "Enabled"],
30
33
  });
31
34
  for (const i of list) {
@@ -38,34 +41,39 @@ export const executeIntegrationsList = withCommandErrorHandler(async (options) =
38
41
  i.enabled ? "✓" : "✗",
39
42
  ]);
40
43
  }
41
- terminal.log(table.toString());
44
+ output.log(table.toString());
42
45
  });
43
- export const executeIntegrationsShow = withCommandErrorHandler(async (options) => {
46
+ export const executeIntegrationsShow = createCommandHandler("integrations show", async (options, output) => {
44
47
  const sdk = await createSdkFromState();
45
- const response = await withSDKErrorHandling(async () => {
46
- return sdk.getIntegrationsById({
48
+ let response;
49
+ try {
50
+ response = await sdk.getIntegrationsById({
47
51
  path: { id: options.id },
48
52
  });
49
- }, "Failed to show integration");
50
- const integration = response?.data?.data;
51
- if (!integration) {
52
- terminal.error("Integration not found.");
53
- terminal.exit(1);
54
53
  }
55
- if (outputJsonOrContinue(integration, options.json))
56
- return;
57
- terminal.info(integration.name);
58
- terminal.dim(`ID: ${integration.id}`);
59
- terminal.log(`Category: ${integration.category}`);
60
- terminal.log(`Provider: ${integration.provider}`);
61
- terminal.log(`Environment: ${integration.environment ?? "(all)"}`);
62
- terminal.log(`Enabled: ${integration.enabled ? "Yes" : "No"}`);
63
- terminal.log(`Has credentials: ${integration.hasCredentials ? "Yes" : "No"}`);
64
- if (integration?.config) {
65
- terminal.log("Config: " + JSON.stringify(integration.config));
54
+ catch (err) {
55
+ handleSDKErrorWithOutput(err, output, "Failed to show integration");
56
+ }
57
+ const raw = response.data?.data;
58
+ if (!raw) {
59
+ output.setData({ error: "NOT_FOUND", message: "Integration not found." });
60
+ output.error("Integration not found.");
61
+ output.exit(1);
62
+ }
63
+ const i = raw;
64
+ output.setData(i);
65
+ output.info(i.name);
66
+ output.dim(`ID: ${i.id}`);
67
+ output.log(`Category: ${i.category}`);
68
+ output.log(`Provider: ${i.provider}`);
69
+ output.log(`Environment: ${i.environment ?? "(all)"}`);
70
+ output.log(`Enabled: ${i.enabled ? "Yes" : "No"}`);
71
+ output.log(`Has credentials: ${i.hasCredentials ? "Yes" : "No"}`);
72
+ if (i.config) {
73
+ output.log("Config: " + JSON.stringify(i.config));
66
74
  }
67
75
  });
68
- export const executeIntegrationsAdd = withCommandErrorHandler(async (options) => {
76
+ export const executeIntegrationsAdd = createCommandHandler("integrations add", async (options, output) => {
69
77
  const env = await resolveEnvironment(options.environment);
70
78
  const sdk = await createSdkFromState();
71
79
  const body = {
@@ -76,25 +84,28 @@ export const executeIntegrationsAdd = withCommandErrorHandler(async (options) =>
76
84
  environment: env,
77
85
  enabled: options.enabled ?? true,
78
86
  };
79
- const response = await withSDKErrorHandling(async () => {
80
- return sdk.postIntegrations({
81
- body,
82
- });
83
- }, "Failed to add integration");
84
- const integration = response?.data?.data;
87
+ let response;
88
+ try {
89
+ response = await sdk.postIntegrations({ body });
90
+ }
91
+ catch (err) {
92
+ handleSDKErrorWithOutput(err, output, "Failed to add integration");
93
+ }
94
+ const integration = response.data?.data;
85
95
  if (!integration) {
86
- terminal.error("Failed to add integration.");
96
+ output.setData({ error: "Failed to add integration." });
97
+ output.error("Failed to add integration.");
87
98
  return;
88
99
  }
89
- terminal.success(`Created integration ${integration.name} (${integration.id})`);
90
- if (outputJsonOrContinue(integration, options.json))
91
- return;
100
+ output.setData(integration);
101
+ output.success(`Created integration ${integration.name} (${integration.id})`);
92
102
  });
93
- export const executeIntegrationsUpdate = withCommandErrorHandler(async (options) => {
103
+ export const executeIntegrationsUpdate = createCommandHandler("integrations update", async (options, output) => {
94
104
  const env = await resolveEnvironment(options.environment);
95
105
  const sdk = await createSdkFromState();
96
- const response = await withSDKErrorHandling(async () => {
97
- return sdk.patchIntegrationsById({
106
+ let response;
107
+ try {
108
+ response = await sdk.patchIntegrationsById({
98
109
  path: { id: options.id },
99
110
  body: {
100
111
  name: options.name,
@@ -103,37 +114,47 @@ export const executeIntegrationsUpdate = withCommandErrorHandler(async (options)
103
114
  enabled: options.enabled,
104
115
  },
105
116
  });
106
- }, "Failed to update integration");
107
- const integration = response?.data?.data;
108
- if (!integration) {
109
- terminal.error("Integration not found.");
110
- terminal.exit(1);
111
117
  }
112
- if (outputJsonOrContinue(integration, options.json))
113
- return;
114
- terminal.success(`Updated integration ${integration.name}`);
118
+ catch (err) {
119
+ handleSDKErrorWithOutput(err, output, "Failed to update integration");
120
+ }
121
+ const raw = response.data?.data;
122
+ if (!raw) {
123
+ output.setData({ error: "NOT_FOUND", message: "Integration not found." });
124
+ output.error("Integration not found.");
125
+ output.exit(1);
126
+ }
127
+ const i = raw;
128
+ output.setData(i);
129
+ output.success(`Updated integration ${i.name}`);
115
130
  });
116
- export const executeIntegrationsRemove = withCommandErrorHandler(async (options) => {
131
+ export const executeIntegrationsRemove = createCommandHandler("integrations remove", async (options, output) => {
117
132
  const sdk = await createSdkFromState();
118
- const response = await withSDKErrorHandling(async () => {
119
- return sdk.deleteIntegrationsById({
133
+ let response;
134
+ try {
135
+ response = await sdk.deleteIntegrationsById({
120
136
  path: { id: options.id },
121
137
  });
122
- }, "Failed to remove integration");
123
- const integration = response?.data?.data;
124
- if (!integration) {
125
- terminal.error("Integration not found.");
126
- terminal.exit(1);
127
138
  }
128
- terminal.success(`Integration removed.`);
139
+ catch (err) {
140
+ handleSDKErrorWithOutput(err, output, "Failed to remove integration");
141
+ }
142
+ const raw = response.data?.data;
143
+ if (!raw) {
144
+ output.setData({ error: "NOT_FOUND", message: "Integration not found." });
145
+ output.error("Integration not found.");
146
+ output.exit(1);
147
+ }
148
+ const deleted = raw;
149
+ output.setData({ removed: true, id: options.id, name: deleted.name });
150
+ output.success(`Integration removed.`);
129
151
  });
130
152
  const POLL_INITIAL_MS = 2000;
131
153
  const POLL_MAX_MS = 10000;
132
154
  const POLL_TIMEOUT_MS = 15 * 60 * 1000; // 15 minutes
133
155
  /** Fetch provider catalog from hub. */
134
- async function getProvidersFromHub() {
135
- const sdk = await createSdkFromState();
136
- const response = await withSDKErrorHandling(() => sdk.getIntegrationsProviders(), "Failed to load providers");
156
+ async function getProvidersFromHub(sdk) {
157
+ const response = await sdk.getIntegrationsProviders();
137
158
  const data = response?.data;
138
159
  if (!data?.providers)
139
160
  return [];
@@ -163,7 +184,7 @@ export function printConnectHelp(providers) {
163
184
  terminal.blank();
164
185
  }
165
186
  }
166
- async function connectOAuth(options, _provider) {
187
+ async function connectOAuth(options, _provider, output) {
167
188
  const env = await resolveEnvironment(options.environment);
168
189
  const body = {
169
190
  category: options.category,
@@ -172,26 +193,25 @@ async function connectOAuth(options, _provider) {
172
193
  environment: env,
173
194
  };
174
195
  const sdk = await createSdkFromState();
175
- const response = await withSDKErrorHandling(async () => {
176
- return sdk.postIntegrationsOauthInitiate({
177
- body,
178
- });
179
- }, "Failed to initiate OAuth");
180
- const data = response?.data;
196
+ let response;
197
+ try {
198
+ response = await sdk.postIntegrationsOauthInitiate({ body });
199
+ }
200
+ catch (err) {
201
+ handleSDKErrorWithOutput(err, output, "Failed to initiate OAuth");
202
+ }
203
+ const data = response.data;
181
204
  if (!data) {
182
- terminal.error("Initiate failed.");
183
- return terminal.exit(1);
205
+ output.setData({ error: "Initiate failed." });
206
+ output.error("Initiate failed.");
207
+ output.exit(1);
184
208
  }
185
- terminal.info("Open this URL in your browser to authorize:");
186
- terminal.log(data.authUrl);
187
- terminal.blank();
188
- terminal.openBrowser(data.authUrl);
189
- terminal.info("Waiting for authorization… (polling)");
190
- terminal.info("Open this URL in your browser to authorize:");
191
- terminal.log(data.authUrl);
192
- terminal.blank();
193
- terminal.openBrowser(data.authUrl);
194
- terminal.info("Waiting for authorization… (polling)");
209
+ output.setData({ authUrl: data.authUrl });
210
+ output.info("Open this URL in your browser to authorize:");
211
+ output.log(data.authUrl);
212
+ output.blank();
213
+ output.openBrowser(data.authUrl);
214
+ output.info("Waiting for authorization… (polling)");
195
215
  const start = Date.now();
196
216
  let interval = POLL_INITIAL_MS;
197
217
  const poll = async () => {
@@ -212,32 +232,22 @@ async function connectOAuth(options, _provider) {
212
232
  interval = Math.min(interval + 1000, POLL_MAX_MS);
213
233
  return poll();
214
234
  };
215
- try {
216
- const status = await poll();
217
- if (status.status === "completed" && status.integrationId) {
218
- if (options.json) {
219
- terminal.log(JSON.stringify({ integrationId: status.integrationId }, null, 2));
220
- }
221
- else {
222
- terminal.success(`Integration connected: ${status.integrationId}`);
223
- }
224
- return;
225
- }
226
- if (status.status === "failed") {
227
- terminal.error(status.errorMessage ?? "Authorization failed");
228
- terminal.exit(1);
229
- }
230
- if (status.status === "expired") {
231
- terminal.error(status.errorMessage ?? "Transaction expired");
232
- terminal.exit(1);
233
- }
234
- terminal.error("Unexpected status");
235
- terminal.exit(1);
235
+ const status = await poll();
236
+ if (status.status === "completed" && status.integrationId) {
237
+ output.setData({
238
+ integrationId: status.integrationId,
239
+ authUrl: data.authUrl,
240
+ });
241
+ output.success(`Integration connected: ${status.integrationId}`);
242
+ return;
236
243
  }
237
- catch (err) {
238
- terminal.error(err instanceof Error ? err.message : "Polling failed");
239
- terminal.exit(1);
244
+ if (status.status === "failed") {
245
+ output.flushError("OAUTH_FAILED", status.errorMessage ?? "Authorization failed");
240
246
  }
247
+ if (status.status === "expired") {
248
+ output.flushError("OAUTH_EXPIRED", status.errorMessage ?? "Transaction expired");
249
+ }
250
+ output.flushError("UNKNOWN_ERROR", "Unexpected status");
241
251
  }
242
252
  function buildIntegrationConfig(provider, config) {
243
253
  if (provider.provider === "resend") {
@@ -259,14 +269,13 @@ function buildIntegrationConfig(provider, config) {
259
269
  }
260
270
  return undefined;
261
271
  }
262
- async function connectCredentials(options, provider) {
272
+ async function connectCredentials(options, provider, output) {
263
273
  const env = await resolveEnvironment(options.environment);
264
274
  const config = {};
265
275
  for (const field of provider.configFields ?? []) {
266
- const value = await terminal.input(`${field.label}${field.required ? " (required)" : ""}`, field.default);
276
+ const value = await output.input(`${field.label}${field.required ? " (required)" : ""}`, field.default);
267
277
  if (field.required && !value?.trim()) {
268
- terminal.error(`${field.label} is required`);
269
- terminal.exit(1);
278
+ output.flushError("VALIDATION_ERROR", `${field.label} is required`);
270
279
  }
271
280
  if (value?.trim())
272
281
  config[field.key] = value.trim();
@@ -274,11 +283,10 @@ async function connectCredentials(options, provider) {
274
283
  const credentials = {};
275
284
  for (const field of provider.credentialFields ?? []) {
276
285
  const value = field.secret
277
- ? await terminal.password(`${field.label}${field.required ? " (required)" : ""}`)
278
- : await terminal.input(`${field.label}${field.required ? " (required)" : ""}`, field.default);
286
+ ? await output.password(`${field.label}${field.required ? " (required)" : ""}`)
287
+ : await output.input(`${field.label}${field.required ? " (required)" : ""}`, field.default);
279
288
  if (field.required && !value?.trim()) {
280
- terminal.error(`${field.label} is required`);
281
- terminal.exit(1);
289
+ output.flushError("VALIDATION_ERROR", `${field.label} is required`);
282
290
  }
283
291
  if (value?.trim())
284
292
  credentials[field.key] = value.trim();
@@ -295,44 +303,55 @@ async function connectCredentials(options, provider) {
295
303
  };
296
304
  const state = await loadState();
297
305
  const sdk = await createSdkWithCredentials(state.hub.baseUrl);
298
- const result = await sdk.postIntegrations({
299
- body,
300
- });
301
- const integration = result.data?.data;
302
- if (!integration) {
303
- terminal.error("Integration not found.");
304
- terminal.exit(1);
306
+ const result = await sdk.postIntegrations({ body });
307
+ const data = result.data?.data;
308
+ if (!data) {
309
+ output.setData({ error: "Integration not found." });
310
+ output.error("Integration not found.");
311
+ output.exit(1);
305
312
  }
306
- if (outputJsonOrContinue(integration, options.json))
307
- return;
308
- terminal.success(`Integration connected: ${integration.name} (${integration.id})`);
313
+ output.setData(data);
314
+ const conn = data;
315
+ output.success(`Integration connected: ${conn.name} (${conn.id})`);
309
316
  }
310
- export async function executeIntegrationsConnect(options) {
311
- const providers = await getProvidersFromHub();
317
+ export const executeIntegrationsConnect = createCommandHandler("integrations connect", async (options, output) => {
318
+ const sdk = await createSdkFromState();
319
+ let providers;
320
+ try {
321
+ providers = await getProvidersFromHub(sdk);
322
+ }
323
+ catch (err) {
324
+ handleSDKErrorWithOutput(err, output, "Failed to load providers");
325
+ }
312
326
  if (!options.category || !options.provider) {
313
327
  printConnectHelp(providers);
314
- return terminal.exit(0);
328
+ output.exit(0);
315
329
  }
316
330
  const providerDef = providers.find((p) => p.category === options.category && p.provider === options.provider);
317
331
  if (!providerDef) {
318
- terminal.error(`Unknown provider: ${options.category}/${options.provider}`);
332
+ output.setData({
333
+ error: "NOT_FOUND",
334
+ message: `Unknown provider: ${options.category}/${options.provider}`,
335
+ });
336
+ output.error(`Unknown provider: ${options.category}/${options.provider}`);
319
337
  const providersForCategory = providers.filter((p) => p.category === options.category);
320
338
  if (providersForCategory.length > 0) {
321
- terminal.info(`Available providers for ${options.category}:`);
339
+ output.info(`Available providers for ${options.category}:`);
322
340
  for (const p of providersForCategory) {
323
- terminal.log(` - ${p.provider} (${p.displayName})`);
341
+ output.log(` - ${p.provider} (${p.displayName})`);
324
342
  }
325
343
  }
326
344
  else {
327
345
  const categories = [...new Set(providers.map((p) => p.category))].sort();
328
- terminal.info("Available categories: " + categories.join(", "));
346
+ output.info("Available categories: " + categories.join(", "));
329
347
  }
330
- return terminal.exit(1);
348
+ output.exit(1);
331
349
  }
332
- if (providerDef.authMethod === "oauth") {
333
- await connectOAuth(options, providerDef);
350
+ const provider = providerDef;
351
+ if (provider.authMethod === "oauth") {
352
+ await connectOAuth(options, provider, output);
334
353
  }
335
354
  else {
336
- await connectCredentials(options, providerDef);
355
+ await connectCredentials(options, provider, output);
337
356
  }
338
- }
357
+ });
@@ -1 +1,13 @@
1
- export declare function executeLogin(): Promise<void>;
1
+ export interface LoginOptions {
2
+ json?: boolean;
3
+ /** Only output auth URL and exit; agent then runs with --poll to complete */
4
+ noPoll?: boolean;
5
+ /** Poll for completion after user has authorized in browser (use after --no-poll) */
6
+ poll?: boolean;
7
+ }
8
+ /**
9
+ * Authenticate with Griffin Cloud.
10
+ * Interactive: get URL, open browser, poll until done.
11
+ * Agent (two-step): use --no-poll to get authUrl, then --poll to complete after user authorizes.
12
+ */
13
+ export declare const executeLogin: (options: LoginOptions) => Promise<void>;
@@ -1,9 +1,8 @@
1
- // CLI implementation
2
1
  import { createAuthClient } from "better-auth/client";
3
2
  import { deviceAuthorizationClient, jwtClient, } from "better-auth/client/plugins";
4
3
  import { loadState, saveState } from "../../core/state.js";
5
- import { saveHubCredentials } from "../../core/credentials.js";
6
- import { terminal } from "../../utils/terminal.js";
4
+ import { saveHubCredentials, saveDeviceAuthPending, loadDeviceAuthPending, clearDeviceAuthPending, } from "../../core/credentials.js";
5
+ import { createCommandHandler } from "../../utils/command-wrapper.js";
7
6
  import { randomBytes } from "crypto";
8
7
  const oauthGrant = "urn:ietf:params:oauth:grant-type:device_code";
9
8
  function createAuthClientFromState(state) {
@@ -38,20 +37,87 @@ async function pollForToken(clientId, deviceCode, interval) {
38
37
  throw new Error(error?.error_description || "Unknown error");
39
38
  }
40
39
  }
41
- export async function executeLogin() {
40
+ /**
41
+ * Authenticate with Griffin Cloud.
42
+ * Interactive: get URL, open browser, poll until done.
43
+ * Agent (two-step): use --no-poll to get authUrl, then --poll to complete after user authorizes.
44
+ */
45
+ export const executeLogin = createCommandHandler("auth login", async (options, output) => {
42
46
  const state = await loadState();
43
47
  const clientId = state.hub?.clientId ?? randomBytes(16).toString("hex");
44
48
  const authClient = createAuthClientFromState(state);
49
+ // --- Step 2: Poll only (after user completed flow from --no-poll) ---
50
+ if (options.poll) {
51
+ const pending = await loadDeviceAuthPending();
52
+ if (!pending) {
53
+ output.setData({
54
+ error: "NO_PENDING_AUTH",
55
+ message: "No pending device auth. Run 'griffin auth login --no-poll' first, then complete authorization in the browser, then run 'griffin auth login --poll'.",
56
+ });
57
+ output.flushError("NO_PENDING_AUTH", "No pending device auth. Run 'griffin auth login --no-poll' first, complete authorization in the browser, then run 'griffin auth login --poll'.", undefined, "Run 'griffin auth login --no-poll' to start the flow.");
58
+ }
59
+ const p = pending;
60
+ const sessionToken = await pollForToken(p.clientId, p.deviceCode, p.interval * 1000);
61
+ const { data: jwtData } = await authClient.token({
62
+ fetchOptions: {
63
+ headers: {
64
+ Authorization: `Bearer ${sessionToken}`,
65
+ },
66
+ },
67
+ });
68
+ if (jwtData?.token) {
69
+ await saveHubCredentials(jwtData.token);
70
+ }
71
+ await clearDeviceAuthPending();
72
+ await saveState({
73
+ ...state,
74
+ hub: {
75
+ ...state.hub,
76
+ clientId: p.clientId,
77
+ },
78
+ });
79
+ output.setData({ success: true });
80
+ output.success("Login successful");
81
+ output.log(" Token saved to user credentials");
82
+ return;
83
+ }
84
+ // --- Step 1: Get device code (and optionally exit without polling) ---
45
85
  const { data } = await authClient.device.code({
46
86
  client_id: clientId,
47
87
  });
48
- terminal.info(`Go to: ${data?.verification_uri_complete}`);
49
- terminal.info(`Or enter code: ${data?.user_code}`);
50
- if (data?.verification_uri_complete) {
51
- terminal.openBrowser(data.verification_uri_complete);
88
+ const authUrl = data?.verification_uri_complete ?? "";
89
+ const userCode = data?.user_code ?? "";
90
+ const deviceCode = data?.device_code ?? "";
91
+ const intervalMs = (data?.interval ?? 5) * 1000;
92
+ if (options.noPoll) {
93
+ await saveDeviceAuthPending({
94
+ clientId,
95
+ deviceCode,
96
+ interval: data?.interval ?? 5,
97
+ verificationUriComplete: authUrl,
98
+ userCode,
99
+ });
100
+ output.setData({
101
+ authUrl,
102
+ userCode,
103
+ message: "Complete authorization in the browser, then run: griffin auth login --poll",
104
+ });
105
+ if (!options.json) {
106
+ output.info(`Go to: ${authUrl}`);
107
+ output.info(`Or enter code: ${userCode}`);
108
+ output.blank();
109
+ output.dim("Then run: griffin auth login --poll");
110
+ }
111
+ return;
52
112
  }
53
- // 2. Poll for authorization
54
- const sessionToken = await pollForToken(clientId, data?.device_code, (data?.interval ?? 5) * 1000);
113
+ // --- Interactive: show URL, open browser, poll in same process ---
114
+ output.info(`Go to: ${authUrl}`);
115
+ output.info(`Or enter code: ${userCode}`);
116
+ if (authUrl) {
117
+ output.openBrowser(authUrl);
118
+ }
119
+ output.info("Waiting for authorization… (polling)");
120
+ const sessionToken = await pollForToken(clientId, deviceCode, intervalMs);
55
121
  const { data: jwtData } = await authClient.token({
56
122
  fetchOptions: {
57
123
  headers: {
@@ -59,17 +125,17 @@ export async function executeLogin() {
59
125
  },
60
126
  },
61
127
  });
62
- // Save token to user-level credentials file
63
128
  if (jwtData?.token) {
64
129
  await saveHubCredentials(jwtData.token);
65
- terminal.success("Login successful");
66
- terminal.log(` Token saved to user credentials`);
130
+ output.setData({ success: true });
131
+ output.success("Login successful");
132
+ output.log(" Token saved to user credentials");
67
133
  }
68
134
  await saveState({
69
135
  ...state,
70
136
  hub: {
71
137
  ...state.hub,
72
- clientId: clientId,
138
+ clientId,
73
139
  },
74
140
  });
75
- }
141
+ });
@@ -1,6 +1,7 @@
1
1
  export interface LogoutOptions {
2
+ json?: boolean;
2
3
  }
3
4
  /**
4
5
  * Remove stored credentials for hub
5
6
  */
6
- export declare function executeLogout(options: LogoutOptions): Promise<void>;
7
+ export declare const executeLogout: (options: LogoutOptions) => Promise<void>;
@@ -1,16 +1,11 @@
1
1
  import { removeHubCredentials } from "../../core/credentials.js";
2
- import { terminal } from "../../utils/terminal.js";
2
+ import { createCommandHandler } from "../../utils/command-wrapper.js";
3
3
  /**
4
4
  * Remove stored credentials for hub
5
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
- }
6
+ export const executeLogout = createCommandHandler("auth logout", async (_options, output) => {
7
+ await removeHubCredentials();
8
+ output.setData({ removed: true });
9
+ output.success("Credentials removed.");
10
+ output.blank();
11
+ });