diff --git a/src/cliproxy/auth/auth-types.ts b/src/cliproxy/auth/auth-types.ts index 4bcc476c..927c2e2a 100644 --- a/src/cliproxy/auth/auth-types.ts +++ b/src/cliproxy/auth/auth-types.ts @@ -173,4 +173,6 @@ export interface OAuthOptions { fromUI?: boolean; /** If true, use --no-incognito flag (Kiro only - use normal browser instead of incognito) */ noIncognito?: boolean; + /** If true, skip OAuth and import token from Kiro IDE directly (Kiro only) */ + import?: boolean; } diff --git a/src/cliproxy/auth/kiro-import.ts b/src/cliproxy/auth/kiro-import.ts new file mode 100644 index 00000000..cc507110 --- /dev/null +++ b/src/cliproxy/auth/kiro-import.ts @@ -0,0 +1,159 @@ +/** + * Kiro Import Helper + * + * Imports Kiro token from Kiro IDE when OAuth callback redirects to IDE instead of CLI. + * Spawns cli-proxy-api-plus --kiro-import to import token from Kiro IDE's storage. + */ + +import { spawn } from 'child_process'; +import { info, ok, fail } from '../../utils/ui'; +import { ensureCLIProxyBinary } from '../binary-manager'; +import { generateConfig } from '../config-generator'; +import { getProviderTokenDir } from './token-manager'; + +export interface KiroImportResult { + success: boolean; + provider?: string; + email?: string; + error?: string; +} + +/** + * Try to import Kiro token from Kiro IDE + * Uses cli-proxy-api-plus --kiro-import flag + */ +export async function tryKiroImport(tokenDir: string, verbose = false): Promise { + const log = (msg: string) => { + if (verbose) console.error(`[kiro-import] ${msg}`); + }; + + try { + log('Ensuring CLIProxy binary is available...'); + const binaryPath = await ensureCLIProxyBinary(verbose); + const configPath = generateConfig('kiro'); + + log(`Binary: ${binaryPath}`); + log(`Config: ${configPath}`); + log(`Token dir: ${tokenDir}`); + + return new Promise((resolve) => { + const args = ['--config', configPath, '--kiro-import']; + + log(`Running: ${binaryPath} ${args.join(' ')}`); + + const proc = spawn(binaryPath, args, { + stdio: ['ignore', 'pipe', 'pipe'], + env: { ...process.env, CLI_PROXY_AUTH_DIR: tokenDir }, + }); + + let stdout = ''; + let stderr = ''; + let resolved = false; + + const safeResolve = (result: KiroImportResult) => { + if (resolved) return; + resolved = true; + clearTimeout(timeoutId); + resolve(result); + }; + + proc.stdout?.on('data', (data: Buffer) => { + const output = data.toString(); + stdout += output; + log(`stdout: ${output.trim()}`); + }); + + proc.stderr?.on('data', (data: Buffer) => { + const output = data.toString(); + stderr += output; + log(`stderr: ${output.trim()}`); + }); + + proc.on('exit', (code) => { + log(`Exit code: ${code}`); + + if (code === 0) { + // Parse output for provider info + const providerMatch = stdout.match(/Provider:\s*(\w+)/i); + const emailMatch = stdout.match(/email[:\s]+([^\s,)]+)/i); + const successMatch = + stdout.includes('Kiro token import successful') || + stdout.includes('Imported Kiro token') || + stdout.includes('Authentication saved'); + + if (successMatch) { + safeResolve({ + success: true, + provider: providerMatch?.[1], + email: emailMatch?.[1], + }); + } else { + safeResolve({ + success: false, + error: 'Import completed but token not confirmed', + }); + } + } else { + const errorLine = stderr.trim().split('\n')[0] || stdout.trim().split('\n')[0]; + safeResolve({ + success: false, + error: errorLine || `Exit code ${code}`, + }); + } + }); + + proc.on('error', (error) => { + log(`Process error: ${error.message}`); + safeResolve({ + success: false, + error: error.message, + }); + }); + + // Timeout after 30 seconds + const timeoutId = setTimeout(() => { + if (!resolved && !proc.killed) { + proc.kill(); + safeResolve({ + success: false, + error: 'Import timed out after 30 seconds', + }); + } + }, 30000); + }); + } catch (error) { + log(`Error: ${(error as Error).message}`); + return { + success: false, + error: (error as Error).message, + }; + } +} + +/** + * Import Kiro token with user-facing output + * Shows progress and result to user + */ +export async function importKiroToken(verbose = false): Promise { + const tokenDir = getProviderTokenDir('kiro'); + + console.log(''); + console.log(info('Importing token from Kiro IDE...')); + + const result = await tryKiroImport(tokenDir, verbose); + + if (result.success) { + const providerInfo = result.provider ? ` (Provider: ${result.provider})` : ''; + console.log(ok(`Imported Kiro token from IDE${providerInfo}`)); + return true; + } + + console.log(fail(`Import failed: ${result.error}`)); + console.log(''); + console.log('Make sure you are logged into Kiro IDE first:'); + console.log(' 1. Open Kiro IDE'); + console.log(' 2. Sign in with your AWS/Google account'); + console.log(' 3. Run: ccs kiro --import'); + + return false; +} diff --git a/src/cliproxy/auth/oauth-handler.ts b/src/cliproxy/auth/oauth-handler.ts index caba977c..34ecabbc 100644 --- a/src/cliproxy/auth/oauth-handler.ts +++ b/src/cliproxy/auth/oauth-handler.ts @@ -27,8 +27,9 @@ import { } from '../../management/oauth-port-diagnostics'; import { OAuthOptions, OAUTH_CALLBACK_PORTS, getOAuthConfig } from './auth-types'; import { isHeadlessEnvironment, killProcessOnPort, showStep } from './environment-detector'; -import { getProviderTokenDir, isAuthenticated } from './token-manager'; +import { getProviderTokenDir, isAuthenticated, registerAccountFromToken } from './token-manager'; import { executeOAuthProcess } from './oauth-process'; +import { importKiroToken } from './kiro-import'; /** * Prompt user to add another account @@ -127,6 +128,17 @@ export async function triggerOAuth( ): Promise { const oauthConfig = getOAuthConfig(provider); const { verbose = false, add = false, nickname, fromUI = false, noIncognito = true } = options; + + // Handle --import flag: skip OAuth and import from Kiro IDE directly + if (options.import && provider === 'kiro') { + const tokenDir = getProviderTokenDir(provider); + const success = await importKiroToken(verbose); + if (success) { + return registerAccountFromToken(provider, tokenDir, nickname); + } + return null; + } + const callbackPort = OAUTH_PORTS[provider]; const isCLI = !fromUI; const headless = options.headless ?? isHeadlessEnvironment(); diff --git a/src/cliproxy/auth/oauth-process.ts b/src/cliproxy/auth/oauth-process.ts index 71d58eab..309df84d 100644 --- a/src/cliproxy/auth/oauth-process.ts +++ b/src/cliproxy/auth/oauth-process.ts @@ -6,7 +6,8 @@ */ import { spawn, ChildProcess } from 'child_process'; -import { ok, fail, info } from '../../utils/ui'; +import { ok, fail, info, warn } from '../../utils/ui'; +import { tryKiroImport } from './kiro-import'; import { CLIProxyProvider } from '../types'; import { AccountInfo } from '../account-manager'; import { @@ -205,7 +206,35 @@ function displayUrlFromStderr( } /** Handle token not found after successful process exit */ -function handleTokenNotFound(provider: CLIProxyProvider, callbackPort: number | null): void { +async function handleTokenNotFound( + provider: CLIProxyProvider, + callbackPort: number | null, + tokenDir: string, + nickname: string | undefined, + verbose: boolean +): Promise { + // Kiro-specific: Try auto-import from Kiro IDE + if (provider === 'kiro') { + console.log(''); + console.log(warn('Callback redirected to Kiro IDE. Attempting to import token...')); + + const result = await tryKiroImport(tokenDir, verbose); + + if (result.success) { + const providerInfo = result.provider ? ` (Provider: ${result.provider})` : ''; + console.log(ok(`Imported Kiro token from IDE${providerInfo}`)); + return registerAccountFromToken(provider, tokenDir, nickname); + } + + console.log(fail(`Auto-import failed: ${result.error}`)); + console.log(''); + console.log('To manually import from Kiro IDE:'); + console.log(' 1. Ensure you are logged into Kiro IDE'); + console.log(' 2. Run: ccs kiro --import'); + return null; + } + + // Default behavior for other providers console.log(''); console.log(fail('Token not found after authentication')); console.log(''); @@ -225,6 +254,7 @@ function handleTokenNotFound(provider: CLIProxyProvider, callbackPort: number | console.log(''); console.log(`Try: ccs ${provider} --auth --verbose`); + return null; } /** Handle process exit with error */ @@ -356,7 +386,7 @@ export function executeOAuthProcess(options: OAuthProcessOptions): Promise { + authProcess.on('exit', async (code) => { clearTimeout(timeout); // H5: Remove signal handlers to prevent memory leaks process.removeListener('SIGINT', cleanup); @@ -383,8 +413,15 @@ export function executeOAuthProcess(options: OAuthProcessOptions): Promise --config', 'Change model (agy, gemini)'], ['ccs --logout', 'Clear authentication'], ['ccs --headless', 'Headless auth (for SSH)'], + ['ccs kiro --import', 'Import token from Kiro IDE'], ['ccs kiro --incognito', 'Use incognito browser (default: normal)'], ['ccs codex "explain code"', 'Use with prompt'], ]