/** * Copilot Auth Handler * * Handles GitHub OAuth authentication for copilot-api. * Uses local installation from ~/.ccs/copilot/ (managed by copilot-package-manager). */ import { spawn } from 'child_process'; import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; import { CopilotAuthStatus, CopilotDebugInfo } from './types'; import { isCopilotApiInstalled as checkInstalled, getCopilotApiBinPath, } from './copilot-package-manager'; /** * Get the path to copilot-api's GitHub token file. * copilot-api stores tokens in ~/.local/share/copilot-api/github_token (Linux/macOS) * or %APPDATA%/copilot-api/github_token (Windows) */ export function getTokenPath(): string { if (process.platform === 'win32') { return path.join(process.env.APPDATA || os.homedir(), 'copilot-api', 'github_token'); } return path.join(os.homedir(), '.local', 'share', 'copilot-api', 'github_token'); } /** * Check if GitHub token file exists. * Fast check that doesn't require spawning copilot-api process. */ export function hasTokenFile(): boolean { try { return fs.existsSync(getTokenPath()); } catch { return false; } } /** * Check if copilot-api is installed locally. * Uses copilot-package-manager to check ~/.ccs/copilot/node_modules/.bin/copilot-api */ export function isCopilotApiInstalled(): boolean { return checkInstalled(); } /** * Get copilot-api debug info. * Returns authentication status and version info. */ export async function getCopilotDebugInfo(): Promise { const binPath = getCopilotApiBinPath(); return new Promise((resolve) => { try { const proc = spawn(binPath, ['debug', '--json'], { stdio: ['ignore', 'pipe', 'pipe'], shell: process.platform === 'win32', timeout: 15000, }); let stdout = ''; let _stderr = ''; proc.stdout.on('data', (data) => { stdout += data.toString(); }); proc.stderr.on('data', (data) => { _stderr += data.toString(); }); proc.on('close', (code) => { if (code === 0 && stdout) { try { const info = JSON.parse(stdout.trim()) as CopilotDebugInfo; resolve(info); } catch { resolve(null); } } else { resolve(null); } }); proc.on('error', () => { resolve(null); }); // Timeout after 15 seconds setTimeout(() => { proc.kill(); resolve(null); }, 15000); } catch { resolve(null); } }); } /** * Check copilot authentication status. * Uses fast token file check first, falls back to copilot-api debug if needed. */ export async function checkAuthStatus(): Promise { // Fast path: check if token file exists (instant, no subprocess) if (hasTokenFile()) { return { authenticated: true }; } // Slow path: try copilot-api debug --json (may timeout) // Only used if token file doesn't exist const debugInfo = await getCopilotDebugInfo(); if (debugInfo?.authenticated) { return { authenticated: true }; } return { authenticated: false, }; } /** * Auth flow result with optional device code info. */ export interface AuthFlowResult { success: boolean; error?: string; /** Device code for user to enter at GitHub */ deviceCode?: string; /** URL where user enters the device code */ verificationUrl?: string; } /** * Start GitHub OAuth authentication flow. * Captures device code from copilot-api output and returns it. * * @returns Promise that resolves with auth result including device code */ export function startAuthFlow(): Promise { const binPath = getCopilotApiBinPath(); return new Promise((resolve) => { console.log('[i] Starting GitHub authentication for Copilot...'); console.log(''); const proc = spawn(binPath, ['auth'], { stdio: ['ignore', 'pipe', 'pipe'], shell: process.platform === 'win32', }); let stdout = ''; let deviceCode: string | undefined; let verificationUrl: string | undefined; proc.stdout.on('data', (data) => { const chunk = data.toString(); stdout += chunk; // Echo to console for terminal users process.stdout.write(chunk); // Parse device code from output like: // "Please enter the code "5653-38A1" in https://github.com/login/device" const codeMatch = stdout.match(/code\s+"([A-Z0-9]{4}-[A-Z0-9]{4})"/i); const urlMatch = stdout.match(/(https:\/\/github\.com\/login\/device)/i); if (codeMatch && !deviceCode) { deviceCode = codeMatch[1]; } if (urlMatch && !verificationUrl) { verificationUrl = urlMatch[1]; } }); proc.stderr.on('data', (data) => { // Echo stderr to console process.stderr.write(data.toString()); }); proc.on('close', (code) => { if (code === 0) { resolve({ success: true, deviceCode, verificationUrl }); } else { resolve({ success: false, error: `Authentication failed with exit code ${code}`, deviceCode, verificationUrl, }); } }); proc.on('error', (err) => { resolve({ success: false, error: `Failed to start auth: ${err.message}`, }); }); }); }