fix(kiro): add fallback import from Kiro IDE when OAuth callback redirects

When Kiro OAuth callback redirects to Kiro IDE instead of CLI, this fix:
- Auto-attempts token import from Kiro IDE storage
- Adds --import flag for manual import (ccs kiro --import)
- Shows clear instructions if import fails

Fixes #212
This commit is contained in:
kaitranntt
2025-12-27 00:01:40 -05:00
parent 8833a5a77f
commit add4aa55c7
6 changed files with 252 additions and 6 deletions
+2
View File
@@ -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;
}
+159
View File
@@ -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<KiroImportResult> {
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<KiroImportResult>((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<boolean> {
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;
}
+13 -1
View File
@@ -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<AccountInfo | null> {
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();
+42 -5
View File
@@ -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<AccountInfo | null> {
// 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<Accou
resolve(null);
}, timeoutMs);
authProcess.on('exit', (code) => {
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<Accou
});
}
handleTokenNotFound(provider, callbackPort);
resolve(null);
// Try auto-import for Kiro, show error for others
const account = await handleTokenNotFound(
provider,
callbackPort,
tokenDir,
nickname,
verbose
);
resolve(account);
}
} else {
// Emit device code failure event for UI
+35
View File
@@ -255,6 +255,8 @@ export async function execClaudeWithCLIProxy(
const forceConfig = argsWithoutProxy.includes('--config');
const addAccount = argsWithoutProxy.includes('--add');
const showAccounts = argsWithoutProxy.includes('--accounts');
// Kiro-specific: --import to import token from Kiro IDE directly
const forceImport = argsWithoutProxy.includes('--import');
// Kiro-specific: browser mode for OAuth
// Default to normal browser (noIncognito=true) for reliability - incognito often fails on Linux
// --incognito flag opts into incognito mode, --no-incognito is legacy (now default)
@@ -367,6 +369,38 @@ export async function execClaudeWithCLIProxy(
process.exit(0);
}
// Handle --import: import token from Kiro IDE directly (Kiro only)
if (forceImport) {
if (provider !== 'kiro') {
console.error(fail('--import is only available for Kiro'));
console.error(` Run "ccs ${provider} --auth" to authenticate`);
process.exit(1);
}
// Validate flag conflicts
if (forceAuth) {
console.error(fail('Cannot use --import with --auth'));
console.error(' --import: Import existing token from Kiro IDE');
console.error(' --auth: Trigger new OAuth flow in browser');
process.exit(1);
}
if (forceLogout) {
console.error(fail('Cannot use --import with --logout'));
process.exit(1);
}
const { triggerOAuth } = await import('./auth-handler');
const authSuccess = await triggerOAuth(provider, {
verbose,
import: true,
...(setNickname ? { nickname: setNickname } : {}),
});
if (!authSuccess) {
console.error(fail('Failed to import Kiro token from IDE'));
console.error(' Make sure you are logged into Kiro IDE first');
process.exit(1);
}
process.exit(0);
}
// 3. Ensure OAuth completed (if provider requires it)
if (providerConfig.requiresOAuth) {
log(`Checking authentication for ${provider}`);
@@ -636,6 +670,7 @@ export async function execClaudeWithCLIProxy(
'--nickname',
'--incognito',
'--no-incognito',
'--import',
// Proxy flags are handled by resolveProxyConfig, but list for documentation
...PROXY_CLI_FLAGS,
];
+1
View File
@@ -173,6 +173,7 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim();
['ccs <provider> --config', 'Change model (agy, gemini)'],
['ccs <provider> --logout', 'Clear authentication'],
['ccs <provider> --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'],
]