@edgedive/cli 0.2.1 → 0.3.0

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.
@@ -1,278 +0,0 @@
1
- /**
2
- * OAuth 2.0 authorization flow with PKCE
3
- */
4
-
5
- import http from 'http';
6
- import url from 'url';
7
- import axios from 'axios';
8
- import open from 'open';
9
- import { generateCodeChallenge, generateCodeVerifier } from './pkce.js';
10
- import { API_CONFIG, OAUTH_CONFIG, TIMEOUTS } from '../constants.js';
11
- import { ConfigManager } from '../config/config-manager.js';
12
-
13
- export interface OAuthTokenResponse {
14
- access_token: string;
15
- token_type: string;
16
- expires_in: number;
17
- scope: string;
18
- refresh_token: string;
19
- }
20
-
21
- export class OAuthFlow {
22
- private configManager: ConfigManager;
23
-
24
- constructor(configManager: ConfigManager) {
25
- this.configManager = configManager;
26
- }
27
-
28
- /**
29
- * Initiate OAuth flow and wait for user authorization
30
- */
31
- async authorize(): Promise<OAuthTokenResponse> {
32
- // Generate PKCE parameters
33
- const codeVerifier = generateCodeVerifier();
34
- const codeChallenge = generateCodeChallenge(codeVerifier);
35
- const state = generateCodeVerifier(); // Use random string for state
36
-
37
- // Build authorization URL
38
- const authUrl = new URL(API_CONFIG.AUTHORIZE_PATH, API_CONFIG.BASE_URL);
39
- authUrl.searchParams.set('client_id', OAUTH_CONFIG.CLIENT_ID);
40
- authUrl.searchParams.set('redirect_uri', OAUTH_CONFIG.REDIRECT_URI);
41
- authUrl.searchParams.set('code_challenge', codeChallenge);
42
- authUrl.searchParams.set('state', state);
43
- authUrl.searchParams.set('scope', 'read write');
44
-
45
- console.log('\nšŸ” Starting OAuth authorization flow...\n');
46
- console.log('Opening browser for authorization...');
47
- console.log(`If the browser doesn't open, visit: ${authUrl.toString()}\n`);
48
-
49
- // Start local callback server
50
- const authCode = await this.startCallbackServer(state, authUrl.toString());
51
-
52
- // Exchange authorization code for access token
53
- console.log('\nāœ… Authorization successful! Exchanging code for token...\n');
54
- const tokenResponse = await this.exchangeCodeForToken(authCode, codeVerifier);
55
-
56
- // Save token to config
57
- const expiresAt = Date.now() + tokenResponse.expires_in * 1000;
58
- await this.configManager.save({
59
- accessToken: tokenResponse.access_token,
60
- tokenType: tokenResponse.token_type,
61
- expiresAt,
62
- scope: tokenResponse.scope,
63
- refreshToken: tokenResponse.refresh_token,
64
- });
65
-
66
- return tokenResponse;
67
- }
68
-
69
- /**
70
- * Start local HTTP server to receive OAuth callback
71
- */
72
- private async startCallbackServer(expectedState: string, authUrl: string): Promise<string> {
73
- return new Promise((resolve, reject) => {
74
- let resolved = false;
75
- const timeout = setTimeout(() => {
76
- if (!resolved) {
77
- server.close();
78
- reject(new Error('OAuth flow timed out'));
79
- }
80
- }, TIMEOUTS.CALLBACK_SERVER_MS);
81
-
82
- const server = http.createServer((req, res) => {
83
- if (!req.url) {
84
- return;
85
- }
86
-
87
- const parsedUrl = url.parse(req.url, true);
88
-
89
- if (parsedUrl.pathname === '/callback') {
90
- const { code, state, error } = parsedUrl.query;
91
-
92
- // Handle error response
93
- if (error) {
94
- resolved = true;
95
- clearTimeout(timeout);
96
- res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
97
- res.end(`
98
- <html>
99
- <body>
100
- <h1>Authorization Failed</h1>
101
- <p>Error: ${error}</p>
102
- <p>You can close this window.</p>
103
- </body>
104
- </html>
105
- `);
106
- server.close();
107
- reject(new Error(`OAuth error: ${error}`));
108
- return;
109
- }
110
-
111
- // Validate state
112
- if (state !== expectedState) {
113
- resolved = true;
114
- clearTimeout(timeout);
115
- res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
116
- res.end(`
117
- <html>
118
- <body>
119
- <h1>Authorization Failed</h1>
120
- <p>Invalid state parameter. Possible CSRF attack.</p>
121
- <p>You can close this window.</p>
122
- </body>
123
- </html>
124
- `);
125
- server.close();
126
- reject(new Error('Invalid state parameter'));
127
- return;
128
- }
129
-
130
- // Success - got authorization code
131
- if (code && typeof code === 'string') {
132
- resolved = true;
133
- clearTimeout(timeout);
134
- res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
135
- res.end(`
136
- <html>
137
- <head>
138
- <meta charset="utf-8" />
139
- </head>
140
- <body>
141
- <h1>āœ… Authorization Successful!</h1>
142
- <p>You can close this window and return to the terminal.</p>
143
- <script>setTimeout(() => window.close(), 2000);</script>
144
- </body>
145
- </html>
146
- `);
147
- server.close();
148
- resolve(code);
149
- return;
150
- }
151
-
152
- // Missing code
153
- resolved = true;
154
- clearTimeout(timeout);
155
- res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
156
- res.end(`
157
- <html>
158
- <body>
159
- <h1>Authorization Failed</h1>
160
- <p>Missing authorization code.</p>
161
- <p>You can close this window.</p>
162
- </body>
163
- </html>
164
- `);
165
- server.close();
166
- reject(new Error('Missing authorization code'));
167
- }
168
- });
169
-
170
- server.listen(OAUTH_CONFIG.CALLBACK_PORT, () => {
171
- // Open browser for authorization using the provided PKCE parameters
172
- void open(authUrl, { wait: false })
173
- .then((child) => {
174
- if (child && typeof child.unref === 'function') {
175
- child.unref();
176
- }
177
- })
178
- .catch(() => {
179
- // Silently fail if browser can't be opened
180
- });
181
- });
182
-
183
- server.on('error', (err) => {
184
- if (!resolved) {
185
- resolved = true;
186
- clearTimeout(timeout);
187
- reject(err);
188
- }
189
- });
190
- });
191
- }
192
-
193
- /**
194
- * Exchange authorization code for access token
195
- */
196
- private async exchangeCodeForToken(
197
- code: string,
198
- codeVerifier: string
199
- ): Promise<OAuthTokenResponse> {
200
- try {
201
- const tokenUrl = new URL(API_CONFIG.TOKEN_PATH, API_CONFIG.BASE_URL);
202
-
203
- const response = await axios.post<OAuthTokenResponse>(
204
- tokenUrl.toString(),
205
- new URLSearchParams({
206
- grant_type: 'authorization_code',
207
- code,
208
- code_verifier: codeVerifier,
209
- }),
210
- {
211
- headers: {
212
- 'Content-Type': 'application/x-www-form-urlencoded',
213
- },
214
- timeout: TIMEOUTS.DEFAULT_REQUEST_MS,
215
- }
216
- );
217
-
218
- return response.data;
219
- } catch (error: any) {
220
- if (error.response) {
221
- throw new Error(
222
- `Failed to exchange code for token: ${error.response.data?.error || error.message}`
223
- );
224
- }
225
- throw new Error(`Failed to exchange code for token: ${error.message}`);
226
- }
227
- }
228
-
229
- /**
230
- * Refresh access token using refresh token
231
- */
232
- async refreshAccessToken(): Promise<OAuthTokenResponse | null> {
233
- try {
234
- const refreshToken = await this.configManager.getRefreshToken();
235
- if (!refreshToken) {
236
- return null;
237
- }
238
-
239
- const tokenUrl = new URL(API_CONFIG.TOKEN_PATH, API_CONFIG.BASE_URL);
240
-
241
- const response = await axios.post<OAuthTokenResponse>(
242
- tokenUrl.toString(),
243
- new URLSearchParams({
244
- grant_type: 'refresh_token',
245
- refresh_token: refreshToken,
246
- }),
247
- {
248
- headers: {
249
- 'Content-Type': 'application/x-www-form-urlencoded',
250
- },
251
- timeout: TIMEOUTS.DEFAULT_REQUEST_MS,
252
- }
253
- );
254
-
255
- // Save new tokens to config
256
- const expiresAt = Date.now() + response.data.expires_in * 1000;
257
- await this.configManager.save({
258
- accessToken: response.data.access_token,
259
- tokenType: response.data.token_type,
260
- expiresAt,
261
- scope: response.data.scope,
262
- refreshToken: response.data.refresh_token,
263
- });
264
-
265
- return response.data;
266
- } catch (error: any) {
267
- // If refresh fails, clear the config so user needs to login again
268
- await this.configManager.delete();
269
-
270
- if (error.response) {
271
- throw new Error(
272
- `Failed to refresh token: ${error.response.data?.error || error.message}`
273
- );
274
- }
275
- throw new Error(`Failed to refresh token: ${error.message}`);
276
- }
277
- }
278
- }
package/src/auth/pkce.ts DELETED
@@ -1,27 +0,0 @@
1
- /**
2
- * PKCE (Proof Key for Code Exchange) utilities for OAuth 2.0
3
- */
4
-
5
- import crypto from 'crypto';
6
-
7
- /**
8
- * Generate a cryptographically random code verifier
9
- */
10
- export function generateCodeVerifier(): string {
11
- return base64URLEncode(crypto.randomBytes(32));
12
- }
13
-
14
- /**
15
- * Generate a code challenge from a code verifier using SHA256
16
- */
17
- export function generateCodeChallenge(verifier: string): string {
18
- const hash = crypto.createHash('sha256').update(verifier).digest();
19
- return base64URLEncode(hash);
20
- }
21
-
22
- /**
23
- * Base64 URL encode (without padding)
24
- */
25
- function base64URLEncode(buffer: Buffer): string {
26
- return buffer.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
27
- }
@@ -1,286 +0,0 @@
1
- /**
2
- * Local command - download agent session for local development
3
- */
4
-
5
- import chalk from 'chalk';
6
- import { ConfigManager } from '../config/config-manager.js';
7
- import { EdgediveApiClient, type TakeoverResponse } from '../api/client.js';
8
- import { SessionDownloader } from '../utils/session-downloader.js';
9
- import { GitUtils } from '../utils/git-utils.js';
10
- import { launchClaudeSession } from '../utils/claude-launcher.js';
11
-
12
- interface LocalOptions {
13
- prUrl?: string;
14
- issueUrl?: string;
15
- threadUrl?: string;
16
- taskUrl?: string;
17
- sessionUrl?: string;
18
- worktree?: boolean;
19
- }
20
-
21
- export async function localCommand({
22
- prUrl,
23
- issueUrl,
24
- threadUrl,
25
- taskUrl,
26
- sessionUrl,
27
- worktree,
28
- }: LocalOptions): Promise<void> {
29
- const configManager = new ConfigManager();
30
- const apiClient = new EdgediveApiClient(configManager);
31
- const downloader = new SessionDownloader();
32
-
33
- try {
34
- console.log(chalk.bold('\nšŸ”§ Edgedive Local Session Setup\n'));
35
-
36
- // Check for Windows platform
37
- if (process.platform === 'win32') {
38
- console.log(chalk.red('āŒ Windows is not currently supported for the local command.\n'));
39
- console.log(chalk.yellow('Please use macOS or Linux to work on sessions locally.\n'));
40
- process.exit(1);
41
- }
42
-
43
- // Check if authenticated
44
- if (!(await configManager.isAuthenticated())) {
45
- console.log(chalk.red('āŒ You are not logged in.\n'));
46
- console.log(chalk.yellow('Please run: edgedive login\n'));
47
- process.exit(1);
48
- }
49
-
50
- if (!prUrl && !issueUrl && !threadUrl && !taskUrl && !sessionUrl) {
51
- console.log(chalk.red('āŒ Missing session identifier.\n'));
52
- console.log(
53
- chalk.yellow(
54
- 'Provide one of:\n' +
55
- ' --pr-url https://github.com/owner/repo/pull/123\n' +
56
- ' --issue-url https://linear.app/...\n' +
57
- ' --thread-url https://workspace.slack.com/archives/C12345/p1234567890123456\n' +
58
- ' --task-url https://app.asana.com/0/PROJECT_ID/TASK_ID\n' +
59
- ' --session-url https://app.edgedive.com/agent-sessions?session={session_id}\n'
60
- )
61
- );
62
- process.exit(1);
63
- }
64
-
65
- let sessionData;
66
-
67
- if (prUrl) {
68
- // Validate PR URL format
69
- const prUrlPattern = /^https?:\/\/github\.com\/([^\/]+)\/([^\/]+)\/pull\/(\d+)/;
70
- if (!prUrlPattern.test(prUrl)) {
71
- console.log(chalk.red('āŒ Invalid PR URL format.\n'));
72
- console.log(chalk.yellow('Expected format: https://github.com/owner/repo/pull/123\n'));
73
- process.exit(1);
74
- }
75
-
76
- console.log(chalk.dim(`PR URL: ${prUrl}\n`));
77
- console.log('šŸ” Fetching session information...\n');
78
- sessionData = await apiClient.getTakeoverByPrUrl(prUrl);
79
- } else if (issueUrl) {
80
- console.log(chalk.dim(`Linear Issue URL: ${issueUrl}\n`));
81
- console.log('šŸ” Fetching session information...\n');
82
- sessionData = await apiClient.getTakeoverByLinearIssueUrl(issueUrl);
83
- } else if (taskUrl) {
84
- // Validate it's an Asana URL — let the server handle detailed format parsing
85
- const asanaTaskPattern = /^https?:\/\/app\.asana\.com\//;
86
- if (!asanaTaskPattern.test(taskUrl)) {
87
- console.log(
88
- chalk.red(
89
- 'āŒ Invalid Asana task URL. Expected a URL starting with https://app.asana.com/\n'
90
- )
91
- );
92
- process.exit(1);
93
- }
94
-
95
- console.log(chalk.dim(`Asana Task URL: ${taskUrl}\n`));
96
- console.log('šŸ” Fetching session information...\n');
97
- sessionData = await apiClient.getTakeoverByAsanaTaskUrl(taskUrl);
98
- } else if (sessionUrl) {
99
- // Validate session URL format
100
- const sessionUrlPattern = /^https?:\/\/[^\/]+\/agent-sessions\?session=([a-f0-9-]+)/i;
101
- if (!sessionUrlPattern.test(sessionUrl)) {
102
- console.log(chalk.red('āŒ Invalid session URL format.\n'));
103
- console.log(
104
- chalk.yellow(
105
- 'Expected format: https://app.edgedive.com/agent-sessions?session={session_id}\n'
106
- )
107
- );
108
- process.exit(1);
109
- }
110
-
111
- console.log(chalk.dim(`Session URL: ${sessionUrl}\n`));
112
- console.log('šŸ” Fetching session information...\n');
113
- sessionData = await apiClient.getTakeoverBySessionUrl(sessionUrl);
114
- } else {
115
- // Validate Slack thread URL format
116
- const slackThreadPattern = /^https?:\/\/[^\/]+\.slack\.com\/archives\/([A-Z0-9]+)\/p(\d{16})/;
117
- if (!slackThreadPattern.test(threadUrl!)) {
118
- console.log(chalk.red('āŒ Invalid Slack thread URL format.\n'));
119
- console.log(
120
- chalk.yellow(
121
- 'Expected format: https://workspace.slack.com/archives/C12345/p1234567890123456\n'
122
- )
123
- );
124
- process.exit(1);
125
- }
126
-
127
- console.log(chalk.dim(`Slack Thread URL: ${threadUrl}\n`));
128
- console.log('šŸ” Fetching session information...\n');
129
- sessionData = await apiClient.getTakeoverBySlackThreadUrl(threadUrl!);
130
- }
131
-
132
- console.log(chalk.green('āœ… Found agent session!\n'));
133
- if (sessionData.github_pr?.url) {
134
- console.log(chalk.dim(`PR URL: ${sessionData.github_pr.url}`));
135
- }
136
- console.log(chalk.bold('Session Information:'));
137
- console.log(chalk.dim(` Session ID: ${sessionData.session_id}`));
138
- console.log(chalk.dim(` Agent Type: ${sessionData.agent_type}`));
139
- console.log(chalk.dim(` Status: ${sessionData.status}`));
140
- console.log(
141
- chalk.dim(` Repository: ${sessionData.repository.owner}/${sessionData.repository.name}`)
142
- );
143
- console.log(chalk.dim(` Branch: ${sessionData.repository.branch}`));
144
-
145
- if (sessionData.linear_issue) {
146
- console.log(chalk.dim(` Linear Issue: ${sessionData.linear_issue.identifier}`));
147
- console.log(chalk.dim(` Linear URL: ${sessionData.linear_issue.url}`));
148
- }
149
-
150
- if (sessionData.asana_task) {
151
- console.log(chalk.dim(` Asana Task: ${sessionData.asana_task.url}`));
152
- }
153
-
154
- // Verify current repository matches PR source
155
- const cwd = process.cwd();
156
- let repoInfo;
157
-
158
- try {
159
- repoInfo = await GitUtils.verifyRepoMatches(
160
- sessionData.repository.owner,
161
- sessionData.repository.name,
162
- cwd
163
- );
164
- } catch (error: any) {
165
- console.error(chalk.red(`\nāŒ ${error.message}\n`));
166
- console.log(
167
- chalk.yellow('Run this command from within the target repository checked out locally.')
168
- );
169
- process.exit(1);
170
- }
171
-
172
- console.log(chalk.green(`āœ… Using repository at ${repoInfo.rootPath}`));
173
-
174
- // Ensure PR branch is checked out locally or in a worktree
175
- let workingPath = repoInfo.rootPath;
176
- try {
177
- if (worktree) {
178
- workingPath = await GitUtils.ensureBranchInWorktree(
179
- sessionData.repository.branch,
180
- repoInfo.rootPath
181
- );
182
- console.log(
183
- chalk.green(
184
- `āœ… Created worktree for branch ${sessionData.repository.branch} at ${workingPath}`
185
- )
186
- );
187
- } else {
188
- await GitUtils.ensureBranchCheckedOut(sessionData.repository.branch, repoInfo.rootPath);
189
- console.log(chalk.green(`āœ… Checked out branch ${sessionData.repository.branch}`));
190
- }
191
- } catch (error: any) {
192
- console.error(
193
- chalk.red(
194
- `\nāŒ Failed to ${worktree ? 'create worktree for' : 'checkout'} branch ${sessionData.repository.branch}: ${error.message}\n`
195
- )
196
- );
197
- console.log(chalk.yellow('Resolve the git issue above and rerun the local command.'));
198
- process.exit(1);
199
- }
200
-
201
- // Download session files into Claude projects directory
202
- // Use workingPath so the session goes to the correct directory (worktree path if --worktree is used)
203
- const { claudeSessionId, claudeSessionPath } = await downloader.downloadSession(
204
- sessionData,
205
- workingPath
206
- );
207
-
208
- console.log(chalk.blue(`\nšŸš€ Launching Claude session ${claudeSessionId}...\n`));
209
-
210
- try {
211
- const sessionEndInfo = await launchClaudeSession(claudeSessionId, workingPath);
212
- console.log(chalk.green('\nāœ… Claude session closed. Happy debugging!\n'));
213
-
214
- // Upload Claude session file back to server
215
- // Use the session info from the SessionEnd hook if available, otherwise fall back to original path
216
- await uploadClaudeSessionFile(
217
- apiClient,
218
- sessionData,
219
- sessionEndInfo || { claudeSessionPath }
220
- );
221
- } catch (error: any) {
222
- console.error(chalk.red(`\nāŒ Failed to start Claude automatically: ${error.message}\n`));
223
- console.log(
224
- chalk.yellow(
225
- `You can resume manually with: claude -r ${claudeSessionId} (from ${workingPath})`
226
- )
227
- );
228
- process.exit(1);
229
- }
230
- } catch (error: any) {
231
- console.error(chalk.red(`\nāŒ Local command failed: ${error.message}\n`));
232
-
233
- if (error.message.includes('401') || error.message.includes('Unauthorized')) {
234
- console.log(chalk.yellow('Your session may have expired. Please run: edgedive login\n'));
235
- }
236
-
237
- process.exit(1);
238
- }
239
- }
240
-
241
- /**
242
- * Upload Claude session file back to Edgedive after local development
243
- */
244
- async function uploadClaudeSessionFile(
245
- apiClient: EdgediveApiClient,
246
- sessionData: TakeoverResponse,
247
- sessionInfo: { transcript_path?: string; claudeSessionPath?: string }
248
- ): Promise<void> {
249
- try {
250
- const fs = (await import('fs/promises')).default;
251
-
252
- // Use transcript_path from SessionEnd hook if available, otherwise fall back to original path
253
- const sessionFilePath = sessionInfo.transcript_path || sessionInfo.claudeSessionPath;
254
-
255
- if (!sessionFilePath) {
256
- console.log(chalk.yellow('āš ļø No Claude session file path available, skipping upload'));
257
- return;
258
- }
259
-
260
- // Check if file exists
261
- try {
262
- await fs.access(sessionFilePath);
263
- } catch {
264
- console.log(
265
- chalk.yellow(`āš ļø Claude session file not found at ${sessionFilePath}, skipping upload`)
266
- );
267
- return;
268
- }
269
-
270
- console.log(chalk.blue('šŸ“¤ Uploading Claude session...'));
271
-
272
- // Read file content
273
- const fileContent = await fs.readFile(sessionFilePath);
274
-
275
- // Upload to server
276
- await apiClient.uploadClaudeSession(sessionData.session_id, fileContent);
277
-
278
- console.log(chalk.green('āœ… Claude session uploaded successfully\n'));
279
- } catch (error: any) {
280
- // Don't fail the whole command if upload fails
281
- console.error(chalk.yellow(`āš ļø Failed to upload Claude session: ${error.message}`));
282
- console.error(
283
- chalk.yellow(' Your local changes are safe, but were not synced to the server.\n')
284
- );
285
- }
286
- }
@@ -1,48 +0,0 @@
1
- /**
2
- * Login command - initiates OAuth flow
3
- */
4
-
5
- import chalk from 'chalk';
6
- import { ConfigManager } from '../config/config-manager.js';
7
- import { OAuthFlow } from '../auth/oauth-flow.js';
8
-
9
- export async function loginCommand(): Promise<void> {
10
- const configManager = new ConfigManager();
11
- const oauthFlow = new OAuthFlow(configManager);
12
-
13
- try {
14
- console.log(chalk.bold('\nšŸš€ Edgedive CLI Login\n'));
15
-
16
- // Check if already authenticated
17
- if (await configManager.isAuthenticated()) {
18
- console.log(chalk.yellow('āœ“ You are already logged in!'));
19
- console.log(chalk.dim(`Config: ${configManager.getConfigPath()}\n`));
20
-
21
- const { default: inquirer } = await import('inquirer');
22
- const { reauth } = await inquirer.prompt([
23
- {
24
- type: 'confirm',
25
- name: 'reauth',
26
- message: 'Do you want to re-authenticate?',
27
- default: false,
28
- },
29
- ]);
30
-
31
- if (!reauth) {
32
- process.exit(0);
33
- }
34
- }
35
-
36
- // Start OAuth flow
37
- const tokenResponse = await oauthFlow.authorize();
38
-
39
- console.log(chalk.green.bold('āœ… Successfully logged in!\n'));
40
- console.log(chalk.dim(`Token type: ${tokenResponse.token_type}`));
41
- console.log(chalk.dim(`Expires in: ${Math.floor(tokenResponse.expires_in / 86400)} days`));
42
- console.log(chalk.dim(`Config saved to: ${configManager.getConfigPath()}\n`));
43
- process.exit(0);
44
- } catch (error: any) {
45
- console.error(chalk.red(`\nāŒ Login failed: ${error.message}\n`));
46
- process.exit(1);
47
- }
48
- }
@@ -1,29 +0,0 @@
1
- /**
2
- * Logout command - removes stored credentials
3
- */
4
-
5
- import chalk from 'chalk';
6
- import { ConfigManager } from '../config/config-manager.js';
7
-
8
- export async function logoutCommand(): Promise<void> {
9
- const configManager = new ConfigManager();
10
-
11
- try {
12
- console.log(chalk.bold('\nšŸ‘‹ Edgedive CLI Logout\n'));
13
-
14
- // Check if authenticated
15
- if (!(await configManager.isAuthenticated())) {
16
- console.log(chalk.yellow('You are not logged in.\n'));
17
- return;
18
- }
19
-
20
- // Delete config
21
- await configManager.delete();
22
-
23
- console.log(chalk.green('āœ… Successfully logged out!\n'));
24
- console.log(chalk.dim(`Removed config from: ${configManager.getConfigPath()}\n`));
25
- } catch (error: any) {
26
- console.error(chalk.red(`\nāŒ Logout failed: ${error.message}\n`));
27
- process.exit(1);
28
- }
29
- }