UNPKG

cyrus-ai

Version:

AI-powered Linear issue automation using Claude

163 lines 6.85 kB
import http from "node:http"; import open from "open"; import { CLIPrompts } from "../ui/CLIPrompts.js"; import { BaseCommand } from "./ICommand.js"; /** * Helper function to check Linear token status */ async function checkLinearToken(token) { try { const response = await fetch("https://api.linear.app/graphql", { method: "POST", headers: { "Content-Type": "application/json", Authorization: token, }, body: JSON.stringify({ query: "{ viewer { id email name } }", }), }); const data = (await response.json()); if (data.errors) { return { valid: false, error: data.errors[0]?.message || "Unknown error", }; } return { valid: true }; } catch (error) { return { valid: false, error: error.message }; } } /** * Refresh token command - refresh a specific Linear token */ export class RefreshTokenCommand extends BaseCommand { async execute(_args) { if (!this.app.config.exists()) { this.logError("No edge configuration found. Please run setup first."); process.exit(1); } const config = this.app.config.load(); const workspaceStatuses = []; console.log("Checking current token status...\n"); if (config.linearWorkspaces) { for (const [wsId, wsConfig] of Object.entries(config.linearWorkspaces)) { const name = wsConfig.linearWorkspaceName || wsId; const result = await checkLinearToken(wsConfig.linearToken); workspaceStatuses.push({ id: wsId, name, token: wsConfig.linearToken, valid: result.valid, }); console.log(`${workspaceStatuses.length}. Workspace ${name}: ${result.valid ? "✅ Valid" : "❌ Invalid"}`); } } if (workspaceStatuses.length === 0) { this.logError("No Linear workspaces configured. Run 'cyrus self-auth-linear' first."); process.exit(1); } // Ask which workspace token to refresh const answer = await CLIPrompts.ask('\nWhich workspace token would you like to refresh? (Enter number or "all"): '); const indicesToRefresh = []; if (answer.toLowerCase() === "all") { indicesToRefresh.push(...Array.from({ length: workspaceStatuses.length }, (_, i) => i)); } else { const index = parseInt(answer, 10) - 1; if (Number.isNaN(index) || index < 0 || index >= workspaceStatuses.length) { this.logError("Invalid selection"); process.exit(1); } indicesToRefresh.push(index); } // Refresh tokens for (const index of indicesToRefresh) { const ws = workspaceStatuses[index]; if (!ws) continue; console.log(`\nRefreshing token for workspace ${ws.name} (${ws.id})...`); console.log("Opening Linear OAuth flow in your browser..."); // Use the proxy's OAuth flow with a callback to localhost const serverPort = process.env.CYRUS_SERVER_PORT ? parseInt(process.env.CYRUS_SERVER_PORT, 10) : 3456; const callbackUrl = `http://localhost:${serverPort}/callback`; const proxyUrl = this.app.getProxyUrl(); const oauthUrl = `${proxyUrl}/oauth/authorize?callback=${encodeURIComponent(callbackUrl)}`; console.log(`\nPlease complete the OAuth flow in your browser.`); console.log(`If the browser doesn't open automatically, visit:\n${oauthUrl}\n`); // Start a temporary server to receive the OAuth callback let tokenReceived = null; const server = await new Promise((resolve) => { const s = http.createServer((req, res) => { if (req.url?.startsWith("/callback")) { const url = new URL(req.url, `http://localhost:${serverPort}`); tokenReceived = url.searchParams.get("token"); res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }); res.end(` <html> <head> <meta charset="UTF-8"> </head> <body style="font-family: system-ui; padding: 40px; text-align: center;"> <h2>✅ Authorization successful!</h2> <p>You can close this window and return to your terminal.</p> <script>setTimeout(() => window.close(), 2000);</script> </body> </html> `); } else { res.writeHead(404); res.end("Not found"); } }); s.listen(serverPort, () => { console.log("Waiting for OAuth callback..."); resolve(s); }); }); await open(oauthUrl); // Wait for the token with timeout const startTime = Date.now(); while (!tokenReceived && Date.now() - startTime < 120000) { await new Promise((resolve) => setTimeout(resolve, 100)); } server.close(); const newToken = tokenReceived; if (!newToken || !newToken.startsWith("lin_oauth_")) { this.logError("Invalid token received from OAuth flow"); continue; } // Verify the new token const verifyResult = await checkLinearToken(newToken); if (!verifyResult.valid) { this.logError(`New token is invalid: ${verifyResult.error}`); continue; } // Update the workspace-level token in config this.app.config.update((cfg) => { if (!cfg.linearWorkspaces) { cfg.linearWorkspaces = {}; } cfg.linearWorkspaces[ws.id] = { linearToken: newToken, ...(cfg.linearWorkspaces[ws.id]?.linearRefreshToken ? { linearRefreshToken: cfg.linearWorkspaces[ws.id].linearRefreshToken, } : {}), }; return cfg; }); this.logSuccess(`Updated token for workspace ${ws.name}`); } this.logSuccess("Configuration saved"); } } //# sourceMappingURL=RefreshTokenCommand.js.map