@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.
- package/README.md +122 -367
- package/dist/cli.js +48 -29
- package/dist/commands/env.d.ts +14 -3
- package/dist/commands/env.js +24 -22
- package/dist/commands/generate-key.d.ts +5 -6
- package/dist/commands/generate-key.js +20 -26
- package/dist/commands/hub/apply.d.ts +1 -0
- package/dist/commands/hub/apply.js +44 -29
- package/dist/commands/hub/connect.d.ts +2 -1
- package/dist/commands/hub/connect.js +18 -22
- package/dist/commands/hub/destroy.d.ts +2 -1
- package/dist/commands/hub/destroy.js +123 -119
- package/dist/commands/hub/integrations.d.ts +4 -1
- package/dist/commands/hub/integrations.js +160 -141
- package/dist/commands/hub/login.d.ts +13 -1
- package/dist/commands/hub/login.js +81 -15
- package/dist/commands/hub/logout.d.ts +2 -1
- package/dist/commands/hub/logout.js +7 -12
- package/dist/commands/hub/metrics.js +29 -27
- package/dist/commands/hub/monitor.js +16 -16
- package/dist/commands/hub/notifications.d.ts +3 -6
- package/dist/commands/hub/notifications.js +52 -38
- package/dist/commands/hub/run.d.ts +1 -0
- package/dist/commands/hub/run.js +114 -87
- package/dist/commands/hub/runs.d.ts +2 -0
- package/dist/commands/hub/runs.js +47 -45
- package/dist/commands/hub/secrets.d.ts +5 -5
- package/dist/commands/hub/secrets.js +80 -72
- package/dist/commands/hub/status.d.ts +4 -1
- package/dist/commands/hub/status.js +15 -9
- package/dist/commands/init.d.ts +2 -1
- package/dist/commands/init.js +31 -25
- package/dist/commands/local/run.d.ts +1 -0
- package/dist/commands/local/run.js +34 -26
- package/dist/commands/validate.d.ts +4 -1
- package/dist/commands/validate.js +23 -14
- package/dist/commands/variables.d.ts +17 -3
- package/dist/commands/variables.js +29 -28
- package/dist/core/credentials.d.ts +15 -0
- package/dist/core/credentials.js +37 -0
- package/dist/core/variables.js +4 -0
- package/dist/monitor-runner.js +0 -12
- package/dist/utils/command-wrapper.d.ts +9 -0
- package/dist/utils/command-wrapper.js +23 -0
- package/dist/utils/output.d.ts +66 -0
- package/dist/utils/output.js +202 -0
- package/dist/utils/sdk-error.d.ts +6 -1
- package/dist/utils/sdk-error.js +107 -77
- 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 {
|
|
5
|
-
import {
|
|
6
|
-
|
|
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
|
-
|
|
10
|
-
|
|
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
|
-
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
26
|
+
output.info("No integrations found.");
|
|
24
27
|
return;
|
|
25
28
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const 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
|
-
|
|
44
|
+
output.log(table.toString());
|
|
42
45
|
});
|
|
43
|
-
export const executeIntegrationsShow =
|
|
46
|
+
export const executeIntegrationsShow = createCommandHandler("integrations show", async (options, output) => {
|
|
44
47
|
const sdk = await createSdkFromState();
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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 =
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
96
|
+
output.setData({ error: "Failed to add integration." });
|
|
97
|
+
output.error("Failed to add integration.");
|
|
87
98
|
return;
|
|
88
99
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
return;
|
|
100
|
+
output.setData(integration);
|
|
101
|
+
output.success(`Created integration ${integration.name} (${integration.id})`);
|
|
92
102
|
});
|
|
93
|
-
export const executeIntegrationsUpdate =
|
|
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
|
-
|
|
97
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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 =
|
|
131
|
+
export const executeIntegrationsRemove = createCommandHandler("integrations remove", async (options, output) => {
|
|
117
132
|
const sdk = await createSdkFromState();
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
183
|
-
|
|
205
|
+
output.setData({ error: "Initiate failed." });
|
|
206
|
+
output.error("Initiate failed.");
|
|
207
|
+
output.exit(1);
|
|
184
208
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
238
|
-
|
|
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
|
|
276
|
+
const value = await output.input(`${field.label}${field.required ? " (required)" : ""}`, field.default);
|
|
267
277
|
if (field.required && !value?.trim()) {
|
|
268
|
-
|
|
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
|
|
278
|
-
: await
|
|
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
|
-
|
|
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
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
|
|
307
|
-
|
|
308
|
-
|
|
313
|
+
output.setData(data);
|
|
314
|
+
const conn = data;
|
|
315
|
+
output.success(`Integration connected: ${conn.name} (${conn.id})`);
|
|
309
316
|
}
|
|
310
|
-
export
|
|
311
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
339
|
+
output.info(`Available providers for ${options.category}:`);
|
|
322
340
|
for (const p of providersForCategory) {
|
|
323
|
-
|
|
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
|
-
|
|
346
|
+
output.info("Available categories: " + categories.join(", "));
|
|
329
347
|
}
|
|
330
|
-
|
|
348
|
+
output.exit(1);
|
|
331
349
|
}
|
|
332
|
-
|
|
333
|
-
|
|
350
|
+
const provider = providerDef;
|
|
351
|
+
if (provider.authMethod === "oauth") {
|
|
352
|
+
await connectOAuth(options, provider, output);
|
|
334
353
|
}
|
|
335
354
|
else {
|
|
336
|
-
await connectCredentials(options,
|
|
355
|
+
await connectCredentials(options, provider, output);
|
|
337
356
|
}
|
|
338
|
-
}
|
|
357
|
+
});
|
|
@@ -1 +1,13 @@
|
|
|
1
|
-
export
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
//
|
|
54
|
-
|
|
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
|
-
|
|
66
|
-
|
|
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
|
|
138
|
+
clientId,
|
|
73
139
|
},
|
|
74
140
|
});
|
|
75
|
-
}
|
|
141
|
+
});
|
|
@@ -1,16 +1,11 @@
|
|
|
1
1
|
import { removeHubCredentials } from "../../core/credentials.js";
|
|
2
|
-
import {
|
|
2
|
+
import { createCommandHandler } from "../../utils/command-wrapper.js";
|
|
3
3
|
/**
|
|
4
4
|
* Remove stored credentials for hub
|
|
5
5
|
*/
|
|
6
|
-
export async
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
+
});
|