@friggframework/devtools 2.0.0-next.64 → 2.0.0-next.65

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.
@@ -0,0 +1,431 @@
1
+ const http = require('http');
2
+ const url = require('url');
3
+ const chalk = require('chalk');
4
+
5
+ class OAuthCallbackServer {
6
+ constructor(options = {}) {
7
+ this.port = options.port || 3333;
8
+ this.timeout = (options.timeout || 300) * 1000; // Convert to milliseconds
9
+ this.server = null;
10
+ this._resolveCode = null;
11
+ this._rejectCode = null;
12
+ this._timeoutId = null;
13
+ }
14
+
15
+ async start() {
16
+ return new Promise((resolve, reject) => {
17
+ this.server = http.createServer(this.handleRequest.bind(this));
18
+
19
+ this.server.on('error', (err) => {
20
+ if (err.code === 'EADDRINUSE') {
21
+ reject(
22
+ new Error(
23
+ `Port ${this.port} is already in use.\n` +
24
+ `Try using a different port: frigg auth test <module> --port <different-port>`
25
+ )
26
+ );
27
+ } else {
28
+ reject(err);
29
+ }
30
+ });
31
+
32
+ this.server.listen(this.port, () => {
33
+ console.log(
34
+ chalk.gray(
35
+ `Callback server listening on http://localhost:${this.port}`
36
+ )
37
+ );
38
+ resolve();
39
+ });
40
+ });
41
+ }
42
+
43
+ handleRequest(req, res) {
44
+ const parsedUrl = url.parse(req.url, true);
45
+
46
+ // Handle OAuth callback (any path - to support module-specific redirects like /attio, /pipedrive)
47
+ if (parsedUrl.query.code) {
48
+ this.handleOAuthCallback(parsedUrl.query, res);
49
+ } else if (parsedUrl.query.error) {
50
+ this.handleOAuthError(parsedUrl.query, res);
51
+ } else if (parsedUrl.pathname === '/health') {
52
+ // Health check endpoint
53
+ res.writeHead(200, { 'Content-Type': 'application/json' });
54
+ res.end(JSON.stringify({ status: 'ready' }));
55
+ } else {
56
+ res.writeHead(200, { 'Content-Type': 'text/html' });
57
+ res.end(this.getWaitingHtml());
58
+ }
59
+ }
60
+
61
+ handleOAuthCallback(query, res) {
62
+ const { code, state } = query;
63
+
64
+ // Send success page to browser
65
+ res.writeHead(200, { 'Content-Type': 'text/html' });
66
+ res.end(this.getSuccessHtml());
67
+
68
+ // Clear timeout
69
+ if (this._timeoutId) {
70
+ clearTimeout(this._timeoutId);
71
+ this._timeoutId = null;
72
+ }
73
+
74
+ // Resolve the waiting promise
75
+ if (this._resolveCode) {
76
+ this._resolveCode({ code, state });
77
+ this._resolveCode = null;
78
+ this._rejectCode = null;
79
+ }
80
+ }
81
+
82
+ handleOAuthError(query, res) {
83
+ const { error, error_description, error_uri } = query;
84
+
85
+ // Send error page to browser
86
+ res.writeHead(400, { 'Content-Type': 'text/html' });
87
+ res.end(this.getErrorHtml(error, error_description, error_uri));
88
+
89
+ // Clear timeout
90
+ if (this._timeoutId) {
91
+ clearTimeout(this._timeoutId);
92
+ this._timeoutId = null;
93
+ }
94
+
95
+ // Reject the waiting promise
96
+ if (this._rejectCode) {
97
+ const errorMessage = error_description
98
+ ? `OAuth error: ${error} - ${error_description}`
99
+ : `OAuth error: ${error}`;
100
+ this._rejectCode(new Error(errorMessage));
101
+ this._resolveCode = null;
102
+ this._rejectCode = null;
103
+ }
104
+ }
105
+
106
+ waitForCode() {
107
+ return new Promise((resolve, reject) => {
108
+ this._resolveCode = resolve;
109
+ this._rejectCode = reject;
110
+
111
+ // Set timeout
112
+ this._timeoutId = setTimeout(() => {
113
+ this._resolveCode = null;
114
+ this._rejectCode = null;
115
+ reject(
116
+ new Error(
117
+ `OAuth callback timeout after ${
118
+ this.timeout / 1000
119
+ } seconds.\n` +
120
+ `Make sure you completed the authorization in the browser.`
121
+ )
122
+ );
123
+ }, this.timeout);
124
+ });
125
+ }
126
+
127
+ getSuccessHtml() {
128
+ return `
129
+ <!DOCTYPE html>
130
+ <html>
131
+ <head>
132
+ <title>Frigg Auth - Success</title>
133
+ <style>
134
+ :root {
135
+ --primary: hsl(150 45% 30%);
136
+ --primary-foreground: hsl(150 30% 90%);
137
+ --background: hsl(0 0% 100%);
138
+ --foreground: hsl(240 10% 3.9%);
139
+ --muted-foreground: hsl(240 3.8% 46.1%);
140
+ --border: hsl(240 5.9% 90%);
141
+ --radius: 0.5rem;
142
+ }
143
+ body {
144
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
145
+ display: flex;
146
+ justify-content: center;
147
+ align-items: center;
148
+ min-height: 100vh;
149
+ margin: 0;
150
+ background: var(--background);
151
+ }
152
+ .container {
153
+ text-align: center;
154
+ background: var(--background);
155
+ padding: 3rem;
156
+ border-radius: var(--radius);
157
+ border: 1px solid var(--border);
158
+ box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
159
+ max-width: 400px;
160
+ }
161
+ .icon {
162
+ width: 64px;
163
+ height: 64px;
164
+ margin: 0 auto 1.5rem auto;
165
+ background: var(--primary);
166
+ border-radius: 50%;
167
+ display: flex;
168
+ align-items: center;
169
+ justify-content: center;
170
+ }
171
+ .icon svg {
172
+ width: 32px;
173
+ height: 32px;
174
+ color: var(--primary-foreground);
175
+ }
176
+ h1 {
177
+ color: var(--foreground);
178
+ margin: 0 0 0.5rem 0;
179
+ font-size: 1.25rem;
180
+ font-weight: 600;
181
+ }
182
+ p {
183
+ color: var(--muted-foreground);
184
+ margin: 0;
185
+ line-height: 1.6;
186
+ font-size: 0.875rem;
187
+ }
188
+ .frigg-badge {
189
+ margin-top: 2rem;
190
+ padding-top: 1.5rem;
191
+ border-top: 1px solid var(--border);
192
+ font-size: 0.75rem;
193
+ color: var(--muted-foreground);
194
+ }
195
+ </style>
196
+ </head>
197
+ <body>
198
+ <div class="container">
199
+ <div class="icon">
200
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
201
+ <polyline points="20 6 9 17 4 12"></polyline>
202
+ </svg>
203
+ </div>
204
+ <h1>Authentication Successful</h1>
205
+ <p>You can close this window and return to the terminal.</p>
206
+ <div class="frigg-badge">Powered by Frigg</div>
207
+ </div>
208
+ </body>
209
+ </html>`;
210
+ }
211
+
212
+ getErrorHtml(error, description, errorUri) {
213
+ return `
214
+ <!DOCTYPE html>
215
+ <html>
216
+ <head>
217
+ <title>Frigg Auth - Error</title>
218
+ <style>
219
+ :root {
220
+ --destructive: hsl(0 84.2% 60.2%);
221
+ --destructive-foreground: hsl(0 0% 98%);
222
+ --background: hsl(0 0% 100%);
223
+ --foreground: hsl(240 10% 3.9%);
224
+ --muted-foreground: hsl(240 3.8% 46.1%);
225
+ --border: hsl(240 5.9% 90%);
226
+ --primary: hsl(150 45% 30%);
227
+ --radius: 0.5rem;
228
+ }
229
+ body {
230
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
231
+ display: flex;
232
+ justify-content: center;
233
+ align-items: center;
234
+ min-height: 100vh;
235
+ margin: 0;
236
+ background: var(--background);
237
+ }
238
+ .container {
239
+ text-align: center;
240
+ background: var(--background);
241
+ padding: 3rem;
242
+ border-radius: var(--radius);
243
+ border: 1px solid var(--border);
244
+ box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
245
+ max-width: 400px;
246
+ }
247
+ .icon {
248
+ width: 64px;
249
+ height: 64px;
250
+ margin: 0 auto 1.5rem auto;
251
+ background: var(--destructive);
252
+ border-radius: 50%;
253
+ display: flex;
254
+ align-items: center;
255
+ justify-content: center;
256
+ }
257
+ .icon svg {
258
+ width: 32px;
259
+ height: 32px;
260
+ color: var(--destructive-foreground);
261
+ }
262
+ h1 {
263
+ color: var(--foreground);
264
+ margin: 0 0 1rem 0;
265
+ font-size: 1.25rem;
266
+ font-weight: 600;
267
+ }
268
+ .error-code {
269
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
270
+ background: hsl(0 84.2% 97%);
271
+ color: hsl(0 72% 51%);
272
+ padding: 0.5rem 1rem;
273
+ border-radius: calc(var(--radius) - 2px);
274
+ display: inline-block;
275
+ margin-bottom: 1rem;
276
+ font-size: 0.875rem;
277
+ border: 1px solid hsl(0 84.2% 90%);
278
+ }
279
+ p {
280
+ color: var(--muted-foreground);
281
+ margin: 0 0 0.5rem 0;
282
+ line-height: 1.6;
283
+ font-size: 0.875rem;
284
+ }
285
+ a {
286
+ color: var(--primary);
287
+ text-decoration: none;
288
+ }
289
+ a:hover {
290
+ text-decoration: underline;
291
+ }
292
+ .frigg-badge {
293
+ margin-top: 2rem;
294
+ padding-top: 1.5rem;
295
+ border-top: 1px solid var(--border);
296
+ font-size: 0.75rem;
297
+ color: var(--muted-foreground);
298
+ }
299
+ </style>
300
+ </head>
301
+ <body>
302
+ <div class="container">
303
+ <div class="icon">
304
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
305
+ <line x1="18" y1="6" x2="6" y2="18"></line>
306
+ <line x1="6" y1="6" x2="18" y2="18"></line>
307
+ </svg>
308
+ </div>
309
+ <h1>Authentication Failed</h1>
310
+ <div class="error-code">${escapeHtml(error)}</div>
311
+ ${description ? `<p>${escapeHtml(description)}</p>` : ''}
312
+ ${
313
+ errorUri
314
+ ? `<p><a href="${escapeHtml(
315
+ errorUri
316
+ )}" target="_blank">More information →</a></p>`
317
+ : ''
318
+ }
319
+ <p style="margin-top: 1rem;">Check the terminal for details.</p>
320
+ <div class="frigg-badge">Powered by Frigg</div>
321
+ </div>
322
+ </body>
323
+ </html>`;
324
+ }
325
+
326
+ getWaitingHtml() {
327
+ return `
328
+ <!DOCTYPE html>
329
+ <html>
330
+ <head>
331
+ <title>Frigg Auth - Waiting</title>
332
+ <style>
333
+ :root {
334
+ --primary: hsl(150 45% 30%);
335
+ --background: hsl(0 0% 100%);
336
+ --foreground: hsl(240 10% 3.9%);
337
+ --muted-foreground: hsl(240 3.8% 46.1%);
338
+ --border: hsl(240 5.9% 90%);
339
+ --radius: 0.5rem;
340
+ }
341
+ body {
342
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
343
+ display: flex;
344
+ justify-content: center;
345
+ align-items: center;
346
+ min-height: 100vh;
347
+ margin: 0;
348
+ background: var(--background);
349
+ }
350
+ .container {
351
+ text-align: center;
352
+ background: var(--background);
353
+ padding: 3rem;
354
+ border-radius: var(--radius);
355
+ border: 1px solid var(--border);
356
+ box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
357
+ max-width: 400px;
358
+ }
359
+ .spinner {
360
+ width: 48px;
361
+ height: 48px;
362
+ margin: 0 auto 1.5rem auto;
363
+ border: 3px solid var(--border);
364
+ border-top-color: var(--primary);
365
+ border-radius: 50%;
366
+ animation: spin 1s linear infinite;
367
+ }
368
+ @keyframes spin {
369
+ to { transform: rotate(360deg); }
370
+ }
371
+ h1 {
372
+ color: var(--foreground);
373
+ margin: 0 0 0.5rem 0;
374
+ font-size: 1.25rem;
375
+ font-weight: 600;
376
+ }
377
+ p {
378
+ color: var(--muted-foreground);
379
+ margin: 0;
380
+ line-height: 1.6;
381
+ font-size: 0.875rem;
382
+ }
383
+ .frigg-badge {
384
+ margin-top: 2rem;
385
+ padding-top: 1.5rem;
386
+ border-top: 1px solid var(--border);
387
+ font-size: 0.75rem;
388
+ color: var(--muted-foreground);
389
+ }
390
+ </style>
391
+ </head>
392
+ <body>
393
+ <div class="container">
394
+ <div class="spinner"></div>
395
+ <h1>Waiting for Authorization</h1>
396
+ <p>Complete the OAuth flow in your browser to continue.</p>
397
+ <div class="frigg-badge">Powered by Frigg</div>
398
+ </div>
399
+ </body>
400
+ </html>`;
401
+ }
402
+
403
+ async stop() {
404
+ // Clear any pending timeout
405
+ if (this._timeoutId) {
406
+ clearTimeout(this._timeoutId);
407
+ this._timeoutId = null;
408
+ }
409
+
410
+ if (this.server) {
411
+ return new Promise((resolve) => {
412
+ this.server.close(() => {
413
+ this.server = null;
414
+ resolve();
415
+ });
416
+ });
417
+ }
418
+ }
419
+ }
420
+
421
+ function escapeHtml(str) {
422
+ if (!str) return '';
423
+ return String(str)
424
+ .replace(/&/g, '&amp;')
425
+ .replace(/</g, '&lt;')
426
+ .replace(/>/g, '&gt;')
427
+ .replace(/"/g, '&quot;')
428
+ .replace(/'/g, '&#039;');
429
+ }
430
+
431
+ module.exports = { OAuthCallbackServer };
@@ -0,0 +1,195 @@
1
+ const { OAuthCallbackServer } = require('./oauth-callback-server');
2
+ const { openBrowser } = require('./utils/browser');
3
+ const chalk = require('chalk');
4
+ const crypto = require('crypto');
5
+
6
+ async function runOAuthFlow(definition, ApiClass, options) {
7
+ const port = options.port || 3333;
8
+ const defaultRedirectUri = `http://localhost:${port}`;
9
+ const redirectUri =
10
+ definition.env?.redirect_uri ||
11
+ process.env.REDIRECT_URI ||
12
+ defaultRedirectUri;
13
+ const moduleName =
14
+ definition.moduleName || definition.getName?.() || 'unknown';
15
+
16
+ // 1. Generate state for CSRF protection
17
+ const state = crypto.randomBytes(16).toString('hex');
18
+
19
+ // 2. Create API instance with auth params (redirect_uri from definition.env is preserved)
20
+ const apiParams = {
21
+ ...definition.env,
22
+ state,
23
+ };
24
+ if (!definition.env?.redirect_uri) {
25
+ apiParams.redirect_uri = redirectUri;
26
+ }
27
+ const api = new ApiClass(apiParams);
28
+
29
+ // 3. Get authorization URL
30
+ let authUrl;
31
+ if (typeof api.getAuthorizationUri === 'function') {
32
+ authUrl = api.getAuthorizationUri();
33
+ } else if (api.authorizationUri) {
34
+ authUrl = api.authorizationUri;
35
+ }
36
+
37
+ if (!authUrl) {
38
+ throw new Error(
39
+ `Module ${moduleName} did not provide an authorization URL.\n` +
40
+ `Expected api.getAuthorizationUri() or api.authorizationUri property.`
41
+ );
42
+ }
43
+
44
+ if (!authUrl.includes('state=')) {
45
+ const separator = authUrl.includes('?') ? '&' : '?';
46
+ authUrl = `${authUrl}${separator}state=${state}`;
47
+ }
48
+
49
+ console.log(chalk.blue('\n📝 OAuth2 Authorization Flow\n'));
50
+ console.log(chalk.gray(`Module: ${moduleName}`));
51
+ console.log(chalk.gray(`Redirect URI: ${redirectUri}`));
52
+
53
+ // 4. Start callback server
54
+ const server = new OAuthCallbackServer({ port, timeout: options.timeout });
55
+ await server.start();
56
+
57
+ try {
58
+ // 5. Open browser for authorization
59
+ console.log(chalk.gray('\nOpening browser for authorization...'));
60
+ try {
61
+ await openBrowser(authUrl);
62
+ } catch (err) {
63
+ console.log(
64
+ chalk.yellow(
65
+ `Could not open browser automatically: ${err.message}`
66
+ )
67
+ );
68
+ console.log(chalk.yellow('Please open the URL manually:'));
69
+ console.log(chalk.cyan(`\n ${authUrl}\n`));
70
+ }
71
+
72
+ console.log(chalk.gray('Waiting for OAuth callback...'));
73
+ console.log(
74
+ chalk.gray(`(Timeout: ${options.timeout || 300} seconds)\n`)
75
+ );
76
+
77
+ // 6. Wait for callback
78
+ const { code, state: returnedState } = await server.waitForCode();
79
+
80
+ // 7. Verify state (CSRF protection)
81
+ if (returnedState && returnedState !== state) {
82
+ throw new Error(
83
+ 'OAuth state mismatch - possible CSRF attack.\n' +
84
+ `Expected: ${state}\n` +
85
+ `Received: ${returnedState}`
86
+ );
87
+ }
88
+
89
+ console.log(chalk.green('✓ Authorization code received'));
90
+
91
+ // 8. Exchange code for tokens
92
+ console.log(chalk.gray('Exchanging code for tokens...'));
93
+
94
+ let tokenResponse;
95
+ if (definition.requiredAuthMethods?.getToken) {
96
+ tokenResponse = await definition.requiredAuthMethods.getToken(api, {
97
+ code,
98
+ });
99
+ } else {
100
+ // Fallback to direct API call
101
+ tokenResponse = await api.getTokenFromCode(code);
102
+ }
103
+
104
+ console.log(chalk.green('✓ Tokens received'));
105
+
106
+ // 9. Get entity details
107
+ console.log(chalk.gray('Fetching entity details...'));
108
+
109
+ let entityDetails;
110
+ if (definition.requiredAuthMethods?.getEntityDetails) {
111
+ entityDetails =
112
+ await definition.requiredAuthMethods.getEntityDetails(
113
+ api,
114
+ { code, state: returnedState },
115
+ tokenResponse,
116
+ 'cli-test-user'
117
+ );
118
+ } else {
119
+ // Minimal entity details if method not provided
120
+ entityDetails = {
121
+ identifiers: { externalId: 'unknown', user: 'cli-test-user' },
122
+ details: { name: 'Unknown' },
123
+ };
124
+ }
125
+
126
+ console.log(chalk.green('✓ Entity details retrieved'));
127
+
128
+ if (entityDetails?.details?.name) {
129
+ console.log(chalk.gray(` Entity: ${entityDetails.details.name}`));
130
+ }
131
+ if (entityDetails?.identifiers?.externalId) {
132
+ console.log(
133
+ chalk.gray(
134
+ ` External ID: ${entityDetails.identifiers.externalId}`
135
+ )
136
+ );
137
+ }
138
+
139
+ // 10. Collect credential details
140
+ let credentialDetails = {};
141
+ if (definition.requiredAuthMethods?.getCredentialDetails) {
142
+ try {
143
+ credentialDetails =
144
+ await definition.requiredAuthMethods.getCredentialDetails(
145
+ api,
146
+ 'cli-test-user'
147
+ );
148
+ } catch (err) {
149
+ console.log(
150
+ chalk.yellow(
151
+ ` Warning: Could not get credential details: ${err.message}`
152
+ )
153
+ );
154
+ }
155
+ }
156
+
157
+ // 11. Return credentials object
158
+ return {
159
+ tokens: {
160
+ access_token: api.access_token,
161
+ refresh_token: api.refresh_token,
162
+ accessTokenExpire: api.accessTokenExpire,
163
+ refreshTokenExpire: api.refreshTokenExpire,
164
+ },
165
+ entity: entityDetails,
166
+ credential: credentialDetails,
167
+ apiParams: sanitizeApiParams(apiParams),
168
+ tokenResponse: sanitizeTokenResponse(tokenResponse),
169
+ obtainedAt: new Date().toISOString(),
170
+ };
171
+ } finally {
172
+ await server.stop();
173
+ }
174
+ }
175
+
176
+ function sanitizeApiParams(params) {
177
+ // Remove sensitive data that shouldn't be stored
178
+ const sanitized = { ...params };
179
+ delete sanitized.client_secret;
180
+ return sanitized;
181
+ }
182
+
183
+ function sanitizeTokenResponse(response) {
184
+ if (!response) return null;
185
+ // Keep only metadata, not the actual tokens
186
+ return {
187
+ token_type: response.token_type,
188
+ expires_in: response.expires_in,
189
+ scope: response.scope,
190
+ // Include any service-specific metadata (like api_domain for Pipedrive)
191
+ api_domain: response.api_domain,
192
+ };
193
+ }
194
+
195
+ module.exports = { runOAuthFlow };
@@ -0,0 +1,30 @@
1
+ const { exec } = require('child_process');
2
+ const os = require('os');
3
+
4
+ async function openBrowser(url) {
5
+ const platform = os.platform();
6
+ let command;
7
+
8
+ switch (platform) {
9
+ case 'darwin':
10
+ command = `open "${url}"`;
11
+ break;
12
+ case 'win32':
13
+ command = `start "" "${url}"`;
14
+ break;
15
+ default:
16
+ command = `xdg-open "${url}"`;
17
+ }
18
+
19
+ return new Promise((resolve, reject) => {
20
+ exec(command, (error) => {
21
+ if (error) {
22
+ reject(new Error(`Failed to open browser: ${error.message}`));
23
+ } else {
24
+ resolve();
25
+ }
26
+ });
27
+ });
28
+ }
29
+
30
+ module.exports = { openBrowser };
@@ -85,6 +85,7 @@ const { uiCommand } = require('./ui-command');
85
85
  const { dbSetupCommand } = require('./db-setup-command');
86
86
  const { doctorCommand } = require('./doctor-command');
87
87
  const { repairCommand } = require('./repair-command');
88
+ const { authCommand } = require('./auth-command');
88
89
 
89
90
  const program = new Command();
90
91
 
@@ -168,6 +169,40 @@ program
168
169
  .option('-v, --verbose', 'enable verbose output')
169
170
  .action(repairCommand);
170
171
 
172
+ // Auth command group for testing API module authentication
173
+ const authProgram = program
174
+ .command('auth')
175
+ .description('Test API module authentication');
176
+
177
+ authProgram
178
+ .command('test <module>')
179
+ .description('Test authentication for an API module')
180
+ .option('--api-key <key>', 'API key for API-Key authentication')
181
+ .option('--port <port>', 'Callback server port', '3333')
182
+ .option('--timeout <seconds>', 'OAuth callback timeout', '300')
183
+ .option('-v, --verbose', 'Enable verbose output')
184
+ .action(authCommand.test);
185
+
186
+ authProgram
187
+ .command('list')
188
+ .description('List saved credentials')
189
+ .option('--json', 'Output as JSON')
190
+ .action(authCommand.list);
191
+
192
+ authProgram
193
+ .command('get <module>')
194
+ .description('Get credentials for a module')
195
+ .option('--json', 'Output as JSON')
196
+ .option('--export', 'Export as environment variables')
197
+ .action(authCommand.get);
198
+
199
+ authProgram
200
+ .command('delete [module]')
201
+ .description('Delete saved credentials')
202
+ .option('--all', 'Delete all credentials')
203
+ .option('-y, --yes', 'Skip confirmation')
204
+ .action(authCommand.delete);
205
+
171
206
  program.parse(process.argv);
172
207
 
173
- module.exports = { initCommand, installCommand, startCommand, buildCommand, deployCommand, generateIamCommand, uiCommand, dbSetupCommand, doctorCommand, repairCommand };
208
+ module.exports = { initCommand, installCommand, startCommand, buildCommand, deployCommand, generateIamCommand, uiCommand, dbSetupCommand, doctorCommand, repairCommand, authCommand };