// // auth.js — account status and sign-in, delegated to the Claude Code CLI. // // The extension never implements the OAuth flow itself: `claude auth login` // runs Anthropic's own flow and stores the credentials where Claude Code // expects them. We drive that command, surface the URL, and relay the code. // const { log, warn, runCommand, processWriter } = require("./util.js"); const cli = require("./cli.js"); const URL_PATTERN = new RegExp("https://[^\\s\"'<>]+", "g"); const LOGIN_URL_TIMEOUT = 20000; /** * Current account status. * Returns { available, loggedIn, authMethod, email, subscriptionType, orgName, error }. */ async function status() { let binary; try { binary = await cli.resolveBinary(); } catch (err) { return { available: false, loggedIn: false, error: String(err.message || err) }; } const result = await runCommand(binary, ["auth", "status", "--json"], { env: cli.environment(), timeout: 15000, }); if (result.status !== 0 && !result.stdout.trim()) { return { available: true, loggedIn: false, error: result.stderr.trim() || `\`claude auth status\` exited with ${result.status}`, }; } try { const parsed = JSON.parse(result.stdout.trim()); return Object.assign({ available: true, usingStoredKey: !!cli.storedApiKey() }, parsed); } catch (err) { return { available: true, loggedIn: false, error: "Could not read the account status returned by Claude Code.", }; } } /** A short line for the sidebar, e.g. "james@example.com · Pro". */ function describe(state) { if (!state) return "Checking…"; if (!state.available) return "Claude Code not found"; if (state.error && !state.loggedIn) return "Signed out"; if (!state.loggedIn) return "Signed out"; const parts = []; if (state.email) parts.push(state.email); else if (state.authMethod === "apiKey") parts.push("API key"); if (state.subscriptionType) { parts.push(state.subscriptionType.charAt(0).toUpperCase() + state.subscriptionType.slice(1)); } else if (state.authMethod) { parts.push(state.authMethod); } return parts.join(" · ") || "Signed in"; } /** * Run `claude auth login`, opening the authorization URL in the browser and * relaying the pasted code back to the CLI. * * Resolves true when the CLI exits successfully. */ async function signIn(options = {}) { const binary = await cli.resolveBinary(); const args = ["auth", "login", options.console ? "--console" : "--claudeai"]; const process = new Process(binary, { args: args, cwd: nova.workspace.path || nova.environment["HOME"], env: cli.environment(), stdio: "pipe", }); return new Promise((resolve, reject) => { let writer = null; let sawURL = false; let finished = false; let output = ""; const finish = (fn, value) => { if (finished) return; finished = true; clearTimeout(urlTimer); fn(value); }; const handleLine = (line) => { output += line; log("auth login:", line.trim()); if (sawURL) return; const matches = String(line).match(URL_PATTERN); if (!matches || matches.length === 0) return; const url = matches[0].replace(/[.,)\]]+$/, ""); if (!/oauth|authorize|login/i.test(url)) return; sawURL = true; nova.openURL(url); promptForCode(url); }; const promptForCode = (url) => { nova.workspace.showInputPanel( "Finish signing in to Claude in your browser, then paste the code it gives you.", { label: "Authorization code", placeholder: "Paste code here", prompt: "Sign In", }, (value) => { if (value === null) { try { process.terminate(); } catch (_) { /* already gone */ } finish(resolve, false); return; } try { if (!writer) writer = processWriter(process); writer.write(value.trim() + "\n"); } catch (err) { finish(reject, new Error("Could not send the code to Claude Code: " + err)); } } ); }; const urlTimer = setTimeout(() => { if (sawURL || finished) return; try { process.terminate(); } catch (_) { /* already gone */ } finish( reject, new Error( "Claude Code did not offer a sign-in link. Run `claude auth login` in a terminal to sign in." ) ); }, LOGIN_URL_TIMEOUT); process.onStdout(handleLine); process.onStderr(handleLine); process.onDidExit((code) => { if (code === 0) { finish(resolve, true); } else { finish( reject, new Error( (output.trim().split("\n").pop() || "Sign-in failed.") + ` (claude auth login exited with ${code})` ) ); } }); try { process.start(); } catch (err) { finish(reject, err); } }); } async function signOut() { const binary = await cli.resolveBinary(); const result = await runCommand(binary, ["auth", "logout"], { env: cli.environment(), timeout: 15000, }); if (result.status !== 0) { throw new Error(result.stderr.trim() || `\`claude auth logout\` exited with ${result.status}`); } return true; } /** * Fallback path: open a terminal window running `claude auth login`, for * environments where the piped flow cannot complete (SSO, device prompts). */ async function signInViaTerminal() { const binary = await cli.resolveBinary(); const dir = nova.extension.globalStoragePath; try { nova.fs.mkdir(dir); } catch (_) { /* already exists */ } const scriptPath = nova.path.join(dir, "sign-in-to-claude.command"); const file = nova.fs.open(scriptPath, "w"); file.write("#!/bin/sh\n"); file.write("# Opened by the Claude extension for Nova.\n"); file.write(`exec ${JSON.stringify(binary)} auth login\n`); file.close(); await runCommand("/bin/chmod", ["+x", scriptPath], { timeout: 5000 }); const result = await runCommand("/usr/bin/open", [scriptPath], { timeout: 5000 }); if (result.status !== 0) { warn("could not open a terminal:", result.stderr); throw new Error("Could not open a terminal window. Run `claude auth login` yourself to sign in."); } return true; } module.exports = { status, describe, signIn, signOut, signInViaTerminal, };