mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-02 18:18:43 +00:00
feat(dashboard): add Import from Kiro IDE button
Add "Import from IDE" button to Dashboard AddAccountDialog for Kiro provider: - POST /api/cliproxy/auth/kiro/import endpoint using tryKiroImport() - useKiroImport() React Query hook with cache invalidation - UI button shown alongside OAuth authenticate for Kiro only - Applies default preset when importing first account - Fix UI typecheck script (remove incompatible --build flag)
This commit is contained in:
@@ -24,6 +24,8 @@ import {
|
|||||||
import { getProxyTarget } from '../../cliproxy/proxy-target-resolver';
|
import { getProxyTarget } from '../../cliproxy/proxy-target-resolver';
|
||||||
import { fetchRemoteAuthStatus } from '../../cliproxy/remote-auth-fetcher';
|
import { fetchRemoteAuthStatus } from '../../cliproxy/remote-auth-fetcher';
|
||||||
import { loadOrCreateUnifiedConfig } from '../../config/unified-config-loader';
|
import { loadOrCreateUnifiedConfig } from '../../config/unified-config-loader';
|
||||||
|
import { tryKiroImport } from '../../cliproxy/auth/kiro-import';
|
||||||
|
import { getProviderTokenDir } from '../../cliproxy/auth/token-manager';
|
||||||
import type { CLIProxyProvider } from '../../cliproxy/types';
|
import type { CLIProxyProvider } from '../../cliproxy/types';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
@@ -350,4 +352,52 @@ router.post('/project-selection/:sessionId', (req: Request, res: Response): void
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/cliproxy/auth/kiro/import - Import Kiro token from Kiro IDE
|
||||||
|
* Alternative auth path when OAuth callback fails to redirect properly
|
||||||
|
*/
|
||||||
|
router.post('/kiro/import', async (_req: Request, res: Response): Promise<void> => {
|
||||||
|
// Check if remote mode is enabled - import not available remotely
|
||||||
|
const target = getProxyTarget();
|
||||||
|
if (target.isRemote) {
|
||||||
|
res.status(501).json({
|
||||||
|
error: 'Kiro import not available in remote mode',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tokenDir = getProviderTokenDir('kiro');
|
||||||
|
const result = await tryKiroImport(tokenDir, false);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
// Re-initialize accounts to pick up new token
|
||||||
|
initializeAccounts();
|
||||||
|
|
||||||
|
// Get the newly added account
|
||||||
|
const accounts = getProviderAccounts('kiro');
|
||||||
|
const newAccount = accounts.find((a) => a.isDefault) || accounts[0];
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
account: newAccount
|
||||||
|
? {
|
||||||
|
id: newAccount.id,
|
||||||
|
email: newAccount.email,
|
||||||
|
provider: 'kiro',
|
||||||
|
isDefault: newAccount.isDefault,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: result.error || 'Failed to import Kiro token',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: (error as Error).message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc -b && vite build",
|
"build": "tsc -b && vite build",
|
||||||
"typecheck": "tsc -b --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"lint:fix": "eslint . --fix",
|
"lint:fix": "eslint . --fix",
|
||||||
"format": "prettier --write src/ tests/",
|
"format": "prettier --write src/ tests/",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
* Add Account Dialog Component
|
* Add Account Dialog Component
|
||||||
* Triggers OAuth flow server-side to add another account to a provider
|
* Triggers OAuth flow server-side to add another account to a provider
|
||||||
* Applies default preset when adding first account
|
* Applies default preset when adding first account
|
||||||
|
* For Kiro: Also shows "Import from IDE" option as fallback
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
@@ -15,8 +16,8 @@ import {
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Loader2, ExternalLink, User } from 'lucide-react';
|
import { Loader2, ExternalLink, User, Download } from 'lucide-react';
|
||||||
import { useStartAuth } from '@/hooks/use-cliproxy';
|
import { useStartAuth, useKiroImport } from '@/hooks/use-cliproxy';
|
||||||
import { applyDefaultPreset } from '@/lib/preset-utils';
|
import { applyDefaultPreset } from '@/lib/preset-utils';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
@@ -38,6 +39,10 @@ export function AddAccountDialog({
|
|||||||
}: AddAccountDialogProps) {
|
}: AddAccountDialogProps) {
|
||||||
const [nickname, setNickname] = useState('');
|
const [nickname, setNickname] = useState('');
|
||||||
const startAuthMutation = useStartAuth();
|
const startAuthMutation = useStartAuth();
|
||||||
|
const kiroImportMutation = useKiroImport();
|
||||||
|
|
||||||
|
const isKiro = provider === 'kiro';
|
||||||
|
const isPending = startAuthMutation.isPending || kiroImportMutation.isPending;
|
||||||
|
|
||||||
const handleStartAuth = () => {
|
const handleStartAuth = () => {
|
||||||
startAuthMutation.mutate(
|
startAuthMutation.mutate(
|
||||||
@@ -60,8 +65,24 @@ export function AddAccountDialog({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleKiroImport = () => {
|
||||||
|
kiroImportMutation.mutate(undefined, {
|
||||||
|
onSuccess: async () => {
|
||||||
|
// Apply default preset if this is the first account
|
||||||
|
if (isFirstAccount) {
|
||||||
|
const result = await applyDefaultPreset('kiro');
|
||||||
|
if (result.success && result.presetName) {
|
||||||
|
toast.success(`Applied "${result.presetName}" preset`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setNickname('');
|
||||||
|
onClose();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const handleOpenChange = (isOpen: boolean) => {
|
const handleOpenChange = (isOpen: boolean) => {
|
||||||
if (!isOpen && !startAuthMutation.isPending) {
|
if (!isOpen && !isPending) {
|
||||||
setNickname('');
|
setNickname('');
|
||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
@@ -73,8 +94,9 @@ export function AddAccountDialog({
|
|||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Add {displayName} Account</DialogTitle>
|
<DialogTitle>Add {displayName} Account</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
Click the button below to authenticate a new account. A browser window will open for
|
{isKiro
|
||||||
OAuth.
|
? 'Authenticate via browser or import an existing token from Kiro IDE.'
|
||||||
|
: 'Click the button below to authenticate a new account. A browser window will open for OAuth.'}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
@@ -88,7 +110,7 @@ export function AddAccountDialog({
|
|||||||
value={nickname}
|
value={nickname}
|
||||||
onChange={(e) => setNickname(e.target.value)}
|
onChange={(e) => setNickname(e.target.value)}
|
||||||
placeholder="e.g., work, personal"
|
placeholder="e.g., work, personal"
|
||||||
disabled={startAuthMutation.isPending}
|
disabled={isPending}
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -98,10 +120,25 @@ export function AddAccountDialog({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-end gap-2 pt-2">
|
<div className="flex items-center justify-end gap-2 pt-2">
|
||||||
<Button variant="ghost" onClick={onClose} disabled={startAuthMutation.isPending}>
|
<Button variant="ghost" onClick={onClose} disabled={isPending}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleStartAuth} disabled={startAuthMutation.isPending}>
|
{isKiro && (
|
||||||
|
<Button variant="outline" onClick={handleKiroImport} disabled={isPending}>
|
||||||
|
{kiroImportMutation.isPending ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||||
|
Importing...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Download className="w-4 h-4 mr-2" />
|
||||||
|
Import from IDE
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button onClick={handleStartAuth} disabled={isPending}>
|
||||||
{startAuthMutation.isPending ? (
|
{startAuthMutation.isPending ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||||
@@ -121,6 +158,11 @@ export function AddAccountDialog({
|
|||||||
Complete the OAuth flow in your browser...
|
Complete the OAuth flow in your browser...
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
{kiroImportMutation.isPending && (
|
||||||
|
<p className="text-sm text-center text-muted-foreground">
|
||||||
|
Importing token from Kiro IDE...
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -136,6 +136,27 @@ export function useStartAuth() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Kiro IDE import hook (alternative auth path when OAuth callback fails)
|
||||||
|
export function useKiroImport() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: () => api.cliproxy.auth.kiroImport(),
|
||||||
|
onSuccess: (data) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['cliproxy-accounts'] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['cliproxy-auth'] });
|
||||||
|
if (data.account) {
|
||||||
|
toast.success(`Imported Kiro account: ${data.account.email || data.account.id}`);
|
||||||
|
} else {
|
||||||
|
toast.success('Kiro token imported');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: (error: Error) => {
|
||||||
|
toast.error(error.message);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Stats and models hooks for Overview tab
|
// Stats and models hooks for Overview tab
|
||||||
export function useCliproxyStats() {
|
export function useCliproxyStats() {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
|
|||||||
@@ -331,6 +331,12 @@ export const api = {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ nickname }),
|
body: JSON.stringify({ nickname }),
|
||||||
}),
|
}),
|
||||||
|
/** Import Kiro token from Kiro IDE (Kiro only) */
|
||||||
|
kiroImport: () =>
|
||||||
|
request<{ success: boolean; account: OAuthAccount | null; error?: string }>(
|
||||||
|
'/cliproxy/auth/kiro/import',
|
||||||
|
{ method: 'POST' }
|
||||||
|
),
|
||||||
},
|
},
|
||||||
// Error logs
|
// Error logs
|
||||||
errorLogs: {
|
errorLogs: {
|
||||||
|
|||||||
Reference in New Issue
Block a user