mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-12 20:15:40 +00:00
fix(copilot): use token file check for instant auth status
- add hasTokenFile() for fast auth check without subprocess - capture device code from copilot-api auth output - export AuthFlowResult type with deviceCode and verificationUrl - echo auth output to terminal while capturing for UI
This commit is contained in:
1 parent
3865747154
commit
4783632958
2 files changed
+96
-15
No files matched your search
+88
-14
@@ -6,12 +6,39 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { spawn } from 'child_process';
|
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 { CopilotAuthStatus, CopilotDebugInfo } from './types';
|
||||||
import {
|
import {
|
||||||
isCopilotApiInstalled as checkInstalled,
|
isCopilotApiInstalled as checkInstalled,
|
||||||
getCopilotApiBinPath,
|
getCopilotApiBinPath,
|
||||||
} from './copilot-package-manager';
|
} 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.
|
* Check if copilot-api is installed locally.
|
||||||
* Uses copilot-package-manager to check ~/.ccs/copilot/node_modules/.bin/copilot-api
|
* Uses copilot-package-manager to check ~/.ccs/copilot/node_modules/.bin/copilot-api
|
||||||
@@ -76,48 +103,95 @@ export async function getCopilotDebugInfo(): Promise<CopilotDebugInfo | null> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Check copilot authentication status.
|
* Check copilot authentication status.
|
||||||
|
* Uses fast token file check first, falls back to copilot-api debug if needed.
|
||||||
*/
|
*/
|
||||||
export async function checkAuthStatus(): Promise<CopilotAuthStatus> {
|
export async function checkAuthStatus(): Promise<CopilotAuthStatus> {
|
||||||
|
// 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();
|
const debugInfo = await getCopilotDebugInfo();
|
||||||
|
|
||||||
if (!debugInfo) {
|
if (debugInfo?.authenticated) {
|
||||||
return {
|
return { authenticated: true };
|
||||||
authenticated: false,
|
|
||||||
error: 'Could not check auth status. Is copilot-api installed?',
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
authenticated: debugInfo.authenticated ?? false,
|
authenticated: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start GitHub OAuth authentication flow.
|
* Auth flow result with optional device code info.
|
||||||
* Opens browser for user to authenticate with GitHub.
|
|
||||||
*
|
|
||||||
* @returns Promise that resolves when auth flow completes
|
|
||||||
*/
|
*/
|
||||||
export function startAuthFlow(): Promise<{ success: boolean; error?: string }> {
|
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<AuthFlowResult> {
|
||||||
const binPath = getCopilotApiBinPath();
|
const binPath = getCopilotApiBinPath();
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
console.log('[i] Starting GitHub authentication for Copilot...');
|
console.log('[i] Starting GitHub authentication for Copilot...');
|
||||||
console.log('[i] A browser window will open for GitHub OAuth.');
|
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|
||||||
const proc = spawn(binPath, ['auth'], {
|
const proc = spawn(binPath, ['auth'], {
|
||||||
stdio: 'inherit',
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
shell: process.platform === 'win32',
|
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) => {
|
proc.on('close', (code) => {
|
||||||
if (code === 0) {
|
if (code === 0) {
|
||||||
resolve({ success: true });
|
resolve({ success: true, deviceCode, verificationUrl });
|
||||||
} else {
|
} else {
|
||||||
resolve({
|
resolve({
|
||||||
success: false,
|
success: false,
|
||||||
error: `Authentication failed with exit code ${code}`,
|
error: `Authentication failed with exit code ${code}`,
|
||||||
|
deviceCode,
|
||||||
|
verificationUrl,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -24,7 +24,14 @@ export {
|
|||||||
} from './copilot-package-manager';
|
} from './copilot-package-manager';
|
||||||
|
|
||||||
// Auth
|
// Auth
|
||||||
export { checkAuthStatus, startAuthFlow, getCopilotDebugInfo } from './copilot-auth';
|
export {
|
||||||
|
checkAuthStatus,
|
||||||
|
startAuthFlow,
|
||||||
|
getCopilotDebugInfo,
|
||||||
|
hasTokenFile,
|
||||||
|
getTokenPath,
|
||||||
|
} from './copilot-auth';
|
||||||
|
export type { AuthFlowResult } from './copilot-auth';
|
||||||
|
|
||||||
// Daemon
|
// Daemon
|
||||||
export { isDaemonRunning, getDaemonStatus, startDaemon, stopDaemon } from './copilot-daemon';
|
export { isDaemonRunning, getDaemonStatus, startDaemon, stopDaemon } from './copilot-daemon';
|
||||||
|
|||||||
Reference in new issue
Block a user