mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 06:12:03 +00:00
redesign our approach for api keys to not use claude setup-token
This commit is contained in:
@@ -33,27 +33,24 @@ cd automaker
|
||||
npm install
|
||||
```
|
||||
|
||||
### Windows notes (in-app Claude auth)
|
||||
|
||||
- Node.js 22.x
|
||||
- Prebuilt PTY is bundled; Visual Studio build tools are not required for Claude auth.
|
||||
- If you prefer the external terminal flow, set `CLAUDE_AUTH_DISABLE_PTY=1`.
|
||||
- If you later add native modules beyond the prebuilt PTY, you may still need VS Build Tools + Python to rebuild those.
|
||||
|
||||
**Step 3:** Run the Claude Code setup token command:
|
||||
**Step 3:** Get your Claude subscription token:
|
||||
|
||||
```bash
|
||||
claude setup-token
|
||||
```
|
||||
|
||||
This command will authenticate you via your browser and print a token to your terminal.
|
||||
|
||||
> **⚠️ Warning:** This command will print your token to your terminal. Be careful if you're streaming or sharing your screen, as the token will be visible to anyone watching.
|
||||
|
||||
**Step 4:** Export the Claude Code OAuth token in your shell:
|
||||
**Step 4:** Export the Claude Code OAuth token in your shell (optional - you can also enter it in the app's setup wizard):
|
||||
|
||||
```bash
|
||||
export CLAUDE_CODE_OAUTH_TOKEN="your-token-here"
|
||||
```
|
||||
|
||||
Alternatively, you can enter your token directly in the Automaker setup wizard when you launch the app.
|
||||
|
||||
**Step 5:** Start the development server:
|
||||
|
||||
```bash
|
||||
@@ -62,27 +59,6 @@ npm run dev:electron
|
||||
|
||||
This will start both the Next.js development server and the Electron application.
|
||||
|
||||
### Auth smoke test (Windows)
|
||||
|
||||
1. Ensure dependencies are installed (prebuilt pty is included).
|
||||
2. Run `npm run dev:electron` and open the Setup modal.
|
||||
3. Click Start on Claude auth; watch the embedded terminal stream logs.
|
||||
4. Successful runs show “Token captured automatically.”; otherwise copy/paste the token from the log.
|
||||
5. Optional: `node --test tests/claude-cli-detector.test.js` to verify token parsing.
|
||||
|
||||
**Step 6:** MOST IMPORTANT: Run the Following after all is setup
|
||||
|
||||
```bash
|
||||
echo "W"
|
||||
echo "W"
|
||||
echo "W"
|
||||
echo "W"
|
||||
echo "W"
|
||||
echo "W"
|
||||
echo "W"
|
||||
echo "W"
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- 📋 **Kanban Board** - Visual drag-and-drop board to manage features through backlog, in progress, waiting approval, and verified stages
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@lezer/highlight": "^1.2.3",
|
||||
"@radix-ui/react-accordion": "^1.2.12",
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||
|
||||
@@ -2521,3 +2521,30 @@
|
||||
.xml-editor .xml-highlight {
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
/* Accordion animations */
|
||||
@keyframes accordion-down {
|
||||
from {
|
||||
height: 0;
|
||||
}
|
||||
to {
|
||||
height: var(--radix-accordion-content-height);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes accordion-up {
|
||||
from {
|
||||
height: var(--radix-accordion-content-height);
|
||||
}
|
||||
to {
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-accordion-down {
|
||||
animation: accordion-down 0.2s ease-out;
|
||||
}
|
||||
|
||||
.animate-accordion-up {
|
||||
animation: accordion-up 0.2s ease-out;
|
||||
}
|
||||
|
||||
57
apps/app/src/components/ui/accordion.tsx
Normal file
57
apps/app/src/components/ui/accordion.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as AccordionPrimitive from "@radix-ui/react-accordion";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Accordion = AccordionPrimitive.Root;
|
||||
|
||||
const AccordionItem = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AccordionPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn("border-b border-border", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AccordionItem.displayName = "AccordionItem";
|
||||
|
||||
const AccordionTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
));
|
||||
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
|
||||
|
||||
const AccordionContent = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Content
|
||||
ref={ref}
|
||||
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
||||
{...props}
|
||||
>
|
||||
<div className={cn("pb-4 pt-0", className)}>{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
));
|
||||
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
|
||||
|
||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
|
||||
|
||||
@@ -1,23 +1,61 @@
|
||||
import { useAppStore } from "@/store/app-store";
|
||||
import { useSetupStore } from "@/store/setup-store";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Key, CheckCircle2 } from "lucide-react";
|
||||
import { Key, CheckCircle2, Settings, Trash2, Loader2 } from "lucide-react";
|
||||
import { ApiKeyField } from "./api-key-field";
|
||||
import { buildProviderConfigs } from "@/config/api-providers";
|
||||
import { AuthenticationStatusDisplay } from "./authentication-status-display";
|
||||
import { SecurityNotice } from "./security-notice";
|
||||
import { useApiKeyManagement } from "./hooks/use-api-key-management";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useState, useCallback } from "react";
|
||||
import { getElectronAPI } from "@/lib/electron";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export function ApiKeysSection() {
|
||||
const { apiKeys } = useAppStore();
|
||||
const { claudeAuthStatus } = useSetupStore();
|
||||
const { apiKeys, setApiKeys } = useAppStore();
|
||||
const { claudeAuthStatus, setClaudeAuthStatus, setSetupComplete } = useSetupStore();
|
||||
const [isDeletingAnthropicKey, setIsDeletingAnthropicKey] = useState(false);
|
||||
|
||||
const { providerConfigParams, apiKeyStatus, handleSave, saved } =
|
||||
useApiKeyManagement();
|
||||
|
||||
const providerConfigs = buildProviderConfigs(providerConfigParams);
|
||||
|
||||
// Delete Anthropic API key
|
||||
const deleteAnthropicKey = useCallback(async () => {
|
||||
setIsDeletingAnthropicKey(true);
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
if (!api.setup?.deleteApiKey) {
|
||||
toast.error("Delete API not available");
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await api.setup.deleteApiKey("anthropic");
|
||||
if (result.success) {
|
||||
setApiKeys({ ...apiKeys, anthropic: "" });
|
||||
setClaudeAuthStatus({
|
||||
authenticated: false,
|
||||
method: "none",
|
||||
hasCredentialsFile: claudeAuthStatus?.hasCredentialsFile || false,
|
||||
});
|
||||
toast.success("Anthropic API key deleted");
|
||||
} else {
|
||||
toast.error(result.error || "Failed to delete API key");
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error("Failed to delete API key");
|
||||
} finally {
|
||||
setIsDeletingAnthropicKey(false);
|
||||
}
|
||||
}, [apiKeys, setApiKeys, claudeAuthStatus, setClaudeAuthStatus]);
|
||||
|
||||
// Open setup wizard
|
||||
const openSetupWizard = useCallback(() => {
|
||||
setSetupComplete(false);
|
||||
}, [setSetupComplete]);
|
||||
|
||||
return (
|
||||
<div
|
||||
id="api-keys"
|
||||
@@ -55,8 +93,8 @@ export function ApiKeysSection() {
|
||||
{/* Security Notice */}
|
||||
<SecurityNotice />
|
||||
|
||||
{/* Save Button */}
|
||||
<div className="flex items-center gap-4 pt-2">
|
||||
{/* Action Buttons */}
|
||||
<div className="flex flex-wrap items-center gap-3 pt-2">
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
data-testid="save-settings"
|
||||
@@ -79,6 +117,33 @@ export function ApiKeysSection() {
|
||||
"Save API Keys"
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={openSetupWizard}
|
||||
variant="outline"
|
||||
className="h-10 border-border"
|
||||
data-testid="run-setup-wizard"
|
||||
>
|
||||
<Settings className="w-4 h-4 mr-2" />
|
||||
Run Setup Wizard
|
||||
</Button>
|
||||
|
||||
{apiKeys.anthropic && (
|
||||
<Button
|
||||
onClick={deleteAnthropicKey}
|
||||
disabled={isDeletingAnthropicKey}
|
||||
variant="outline"
|
||||
className="h-10 border-red-500/30 text-red-500 hover:bg-red-500/10 hover:border-red-500/50"
|
||||
data-testid="delete-anthropic-key"
|
||||
>
|
||||
{isDeletingAnthropicKey ? (
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<Trash2 className="w-4 h-4 mr-2" />
|
||||
)}
|
||||
Delete Anthropic Key
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -48,7 +48,9 @@ export function AuthenticationStatusDisplay({
|
||||
<>
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle2 className="w-3 h-3 text-green-500 shrink-0" />
|
||||
<span className="text-green-400 font-medium">Authenticated</span>
|
||||
<span className="text-green-400 font-medium">
|
||||
Authenticated
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-muted-foreground">
|
||||
<Info className="w-3 h-3 shrink-0" />
|
||||
@@ -65,7 +67,9 @@ export function AuthenticationStatusDisplay({
|
||||
? "Using credentials file"
|
||||
: claudeAuthStatus.method === "cli_authenticated"
|
||||
? "Using Claude CLI authentication"
|
||||
: `Using ${claudeAuthStatus.method || "detected"} authentication`}
|
||||
: `Using ${
|
||||
claudeAuthStatus.method || "detected"
|
||||
} authentication`}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
@@ -87,46 +91,6 @@ export function AuthenticationStatusDisplay({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Google/Gemini Authentication Status */}
|
||||
<div className="p-3 rounded-lg bg-card border border-border">
|
||||
<div className="flex items-center gap-2 mb-1.5">
|
||||
<Sparkles className="w-4 h-4 text-purple-500" />
|
||||
<span className="text-sm font-medium text-foreground">
|
||||
Gemini (Google)
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-1.5 text-xs min-h-12">
|
||||
{apiKeyStatus?.hasGoogleKey ? (
|
||||
<>
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle2 className="w-3 h-3 text-green-500 shrink-0" />
|
||||
<span className="text-green-400 font-medium">Authenticated</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-muted-foreground">
|
||||
<Info className="w-3 h-3 shrink-0" />
|
||||
<span>Using GOOGLE_API_KEY</span>
|
||||
</div>
|
||||
</>
|
||||
) : apiKeys.google ? (
|
||||
<>
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle2 className="w-3 h-3 text-green-500 shrink-0" />
|
||||
<span className="text-green-400 font-medium">Authenticated</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-muted-foreground">
|
||||
<Info className="w-3 h-3 shrink-0" />
|
||||
<span>Using stored API key</span>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex items-center gap-1.5 text-yellow-500 py-0.5">
|
||||
<AlertCircle className="w-3 h-3 shrink-0" />
|
||||
<span className="text-xs">Not configured</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CheckCircle2, XCircle, Loader2 } from "lucide-react";
|
||||
import { CheckCircle2, XCircle, Loader2, AlertCircle } from "lucide-react";
|
||||
|
||||
interface StatusBadgeProps {
|
||||
status:
|
||||
@@ -6,7 +6,9 @@ interface StatusBadgeProps {
|
||||
| "not_installed"
|
||||
| "checking"
|
||||
| "authenticated"
|
||||
| "not_authenticated";
|
||||
| "not_authenticated"
|
||||
| "error"
|
||||
| "unverified";
|
||||
label: string;
|
||||
}
|
||||
|
||||
@@ -25,11 +27,21 @@ export function StatusBadge({ status, label }: StatusBadgeProps) {
|
||||
icon: <XCircle className="w-4 h-4" />,
|
||||
className: "bg-red-500/10 text-red-500 border-red-500/20",
|
||||
};
|
||||
case "error":
|
||||
return {
|
||||
icon: <XCircle className="w-4 h-4" />,
|
||||
className: "bg-red-500/10 text-red-500 border-red-500/20",
|
||||
};
|
||||
case "checking":
|
||||
return {
|
||||
icon: <Loader2 className="w-4 h-4 animate-spin" />,
|
||||
className: "bg-yellow-500/10 text-yellow-500 border-yellow-500/20",
|
||||
};
|
||||
case "unverified":
|
||||
return {
|
||||
icon: <AlertCircle className="w-4 h-4" />,
|
||||
className: "bg-yellow-500/10 text-yellow-500 border-yellow-500/20",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
// Re-export all setup dialog components for easier imports
|
||||
export { SetupTokenModal } from "./setup-token-modal";
|
||||
// (SetupTokenModal was removed - setup flow now uses inline API key entry)
|
||||
|
||||
@@ -1,262 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useRef, useCallback } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Loader2,
|
||||
Terminal,
|
||||
CheckCircle2,
|
||||
XCircle,
|
||||
Copy,
|
||||
RotateCcw,
|
||||
} from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { useOAuthAuthentication } from "../hooks";
|
||||
|
||||
interface SetupTokenModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onTokenObtained: (token: string) => void;
|
||||
}
|
||||
|
||||
export function SetupTokenModal({
|
||||
open,
|
||||
onClose,
|
||||
onTokenObtained,
|
||||
}: SetupTokenModalProps) {
|
||||
// Use the OAuth authentication hook
|
||||
const { authState, output, token, error, startAuth, reset } =
|
||||
useOAuthAuthentication({ cliType: "claude" });
|
||||
|
||||
const [manualToken, setManualToken] = useState("");
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Auto-scroll to bottom when output changes
|
||||
useEffect(() => {
|
||||
if (scrollRef.current) {
|
||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||
}
|
||||
}, [output]);
|
||||
|
||||
// Reset state when modal opens/closes
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
reset();
|
||||
setManualToken("");
|
||||
}
|
||||
}, [open, reset]);
|
||||
|
||||
const handleUseToken = useCallback(() => {
|
||||
const tokenToUse = token || manualToken;
|
||||
if (tokenToUse.trim()) {
|
||||
onTokenObtained(tokenToUse.trim());
|
||||
onClose();
|
||||
}
|
||||
}, [token, manualToken, onTokenObtained, onClose]);
|
||||
|
||||
const copyCommand = useCallback(() => {
|
||||
navigator.clipboard.writeText("claude setup-token");
|
||||
toast.success("Command copied to clipboard");
|
||||
}, []);
|
||||
|
||||
const handleRetry = useCallback(() => {
|
||||
reset();
|
||||
setManualToken("");
|
||||
}, [reset]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onClose}>
|
||||
<DialogContent
|
||||
className="max-w-2xl bg-card border-border"
|
||||
data-testid="setup-token-modal"
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2 text-foreground">
|
||||
<Terminal className="w-5 h-5 text-brand-500" />
|
||||
Claude Subscription Authentication
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-muted-foreground">
|
||||
{authState === "idle" &&
|
||||
"Click Start to begin the authentication process."}
|
||||
{authState === "running" &&
|
||||
"Complete the sign-in in your browser..."}
|
||||
{authState === "success" &&
|
||||
"Authentication successful! Your token has been captured."}
|
||||
{authState === "error" &&
|
||||
"Authentication failed. Please try again or enter the token manually."}
|
||||
{authState === "manual" &&
|
||||
"Copy the token from your terminal and paste it below."}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{/* Terminal Output */}
|
||||
<div
|
||||
ref={scrollRef}
|
||||
className="bg-zinc-900 rounded-lg p-4 font-mono text-sm max-h-48 overflow-y-auto border border-border mt-3"
|
||||
>
|
||||
{output.map((line, index) => (
|
||||
<div key={index} className="text-zinc-300 whitespace-pre-wrap">
|
||||
{line.startsWith("Error") || line.startsWith("⚠") ? (
|
||||
<span className="text-yellow-400">{line}</span>
|
||||
) : line.startsWith("✓") ? (
|
||||
<span className="text-green-400">{line}</span>
|
||||
) : (
|
||||
line
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{output.length === 0 && (
|
||||
<div className="text-zinc-500 italic">Waiting to start...</div>
|
||||
)}
|
||||
{authState === "running" && (
|
||||
<div className="flex items-center gap-2 text-brand-400 mt-2">
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
<span>Waiting for authentication...</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Manual Token Input (for fallback) */}
|
||||
{(authState === "manual" || authState === "error") && (
|
||||
<div className="space-y-3 pt-2">
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<span>Run this command in your terminal:</span>
|
||||
<code className="bg-muted px-2 py-1 rounded font-mono text-foreground">
|
||||
claude setup-token
|
||||
</code>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={copyCommand}
|
||||
className="h-7 w-7"
|
||||
>
|
||||
<Copy className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="manual-token" className="text-foreground">
|
||||
Paste your token:
|
||||
</Label>
|
||||
<Input
|
||||
id="manual-token"
|
||||
type="password"
|
||||
placeholder="Paste token here..."
|
||||
value={manualToken}
|
||||
onChange={(e) => setManualToken(e.target.value)}
|
||||
className="bg-input border-border text-foreground"
|
||||
data-testid="manual-token-input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Success State */}
|
||||
{authState === "success" && (
|
||||
<div className="flex items-center gap-3 p-4 rounded-lg bg-green-500/10 border border-green-500/20">
|
||||
<CheckCircle2 className="w-6 h-6 text-green-500 shrink-0" />
|
||||
<div>
|
||||
<p className="font-medium text-foreground">
|
||||
Token captured successfully!
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Click "Use Token" to save and continue.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error State */}
|
||||
{error && authState === "error" && (
|
||||
<div className="flex items-center gap-3 p-4 rounded-lg bg-red-500/10 border border-red-500/20">
|
||||
<XCircle className="w-6 h-6 text-red-500 shrink-0" />
|
||||
<div>
|
||||
<p className="font-medium text-foreground">Error</p>
|
||||
<p className="text-sm text-muted-foreground">{error}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DialogFooter className="mt-5 flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onClose}
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
{authState === "idle" && (
|
||||
<Button
|
||||
onClick={startAuth}
|
||||
className="bg-brand-500 hover:bg-brand-600 text-white"
|
||||
data-testid="start-auth-button"
|
||||
>
|
||||
<Terminal className="w-4 h-4 mr-2" />
|
||||
Start Authentication
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{authState === "running" && (
|
||||
<Button disabled className="bg-brand-500">
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
Authenticating...
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{authState === "success" && (
|
||||
<Button
|
||||
onClick={handleUseToken}
|
||||
className="bg-green-500 hover:bg-green-600 text-white"
|
||||
data-testid="use-token-button"
|
||||
>
|
||||
<CheckCircle2 className="w-4 h-4 mr-2" />
|
||||
Use Token
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{authState === "manual" && (
|
||||
<Button
|
||||
onClick={handleUseToken}
|
||||
disabled={!manualToken.trim()}
|
||||
className="bg-brand-500 hover:bg-brand-600 text-white disabled:opacity-50"
|
||||
data-testid="use-manual-token-button"
|
||||
>
|
||||
<CheckCircle2 className="w-4 h-4 mr-2" />
|
||||
Use Token
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{authState === "error" && (
|
||||
<>
|
||||
{manualToken.trim() && (
|
||||
<Button
|
||||
onClick={handleUseToken}
|
||||
className="bg-green-500 hover:bg-green-600 text-white"
|
||||
>
|
||||
Use Manual Token
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
onClick={handleRetry}
|
||||
className="bg-brand-500 hover:bg-brand-600 text-white"
|
||||
>
|
||||
<RotateCcw className="w-4 h-4 mr-2" />
|
||||
Retry
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
// Re-export all hooks for easier imports
|
||||
export { useCliStatus } from "./use-cli-status";
|
||||
export { useCliInstallation } from "./use-cli-installation";
|
||||
export { useOAuthAuthentication } from "./use-oauth-authentication";
|
||||
export { useTokenSave } from "./use-token-save";
|
||||
|
||||
@@ -1,174 +0,0 @@
|
||||
import { useState, useCallback, useRef, useEffect } from "react";
|
||||
import { getElectronAPI } from "@/lib/electron";
|
||||
|
||||
type AuthState = "idle" | "running" | "success" | "error" | "manual";
|
||||
|
||||
interface UseOAuthAuthenticationOptions {
|
||||
cliType: "claude";
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export function useOAuthAuthentication({
|
||||
cliType,
|
||||
enabled = true,
|
||||
}: UseOAuthAuthenticationOptions) {
|
||||
const [authState, setAuthState] = useState<AuthState>("idle");
|
||||
const [output, setOutput] = useState<string[]>([]);
|
||||
const [token, setToken] = useState("");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const unsubscribeRef = useRef<(() => void) | null>(null);
|
||||
|
||||
// Reset state when disabled
|
||||
useEffect(() => {
|
||||
if (!enabled) {
|
||||
setAuthState("idle");
|
||||
setOutput([]);
|
||||
setToken("");
|
||||
setError(null);
|
||||
|
||||
// Cleanup subscription
|
||||
if (unsubscribeRef.current) {
|
||||
unsubscribeRef.current();
|
||||
unsubscribeRef.current = null;
|
||||
}
|
||||
}
|
||||
}, [enabled]);
|
||||
|
||||
const startAuth = useCallback(async () => {
|
||||
const api = getElectronAPI();
|
||||
if (!api.setup) {
|
||||
setError("Setup API not available");
|
||||
setAuthState("error");
|
||||
return;
|
||||
}
|
||||
|
||||
setAuthState("running");
|
||||
setOutput([
|
||||
"Starting authentication...",
|
||||
`Running ${cliType} CLI in an embedded terminal so you don't need to copy/paste.`,
|
||||
"When your browser opens, complete sign-in and return here.",
|
||||
"",
|
||||
]);
|
||||
setError(null);
|
||||
setToken("");
|
||||
|
||||
// Subscribe to progress events
|
||||
if (api.setup.onAuthProgress) {
|
||||
unsubscribeRef.current = api.setup.onAuthProgress((progress) => {
|
||||
if (progress.cli === cliType && progress.data) {
|
||||
// Split by newlines and add each line
|
||||
const normalized = progress.data.replace(/\r/g, "\n");
|
||||
const lines = normalized
|
||||
.split("\n")
|
||||
.map((line: string) => line.trimEnd())
|
||||
.filter((line: string) => line.length > 0);
|
||||
if (lines.length > 0) {
|
||||
setOutput((prev) => [...prev, ...lines]);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
// Call the auth API
|
||||
const result = await api.setup.authClaude();
|
||||
|
||||
// Cleanup subscription
|
||||
if (unsubscribeRef.current) {
|
||||
unsubscribeRef.current();
|
||||
unsubscribeRef.current = null;
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
setError("Authentication API not available");
|
||||
setAuthState("error");
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for token (only available for Claude)
|
||||
const resultToken =
|
||||
cliType === "claude" && "token" in result ? result.token : undefined;
|
||||
const resultTerminalOpened =
|
||||
cliType === "claude" && "terminalOpened" in result
|
||||
? result.terminalOpened
|
||||
: false;
|
||||
|
||||
if (result.success && resultToken && typeof resultToken === "string") {
|
||||
setToken(resultToken);
|
||||
setAuthState("success");
|
||||
setOutput((prev) => [
|
||||
...prev,
|
||||
"",
|
||||
"✓ Authentication successful!",
|
||||
"✓ Token captured automatically.",
|
||||
]);
|
||||
} else if (result.requiresManualAuth) {
|
||||
// Terminal was opened - user needs to copy token manually
|
||||
setAuthState("manual");
|
||||
// Don't add extra messages if terminalOpened - the progress messages already explain
|
||||
if (!resultTerminalOpened) {
|
||||
const extraMessages = [
|
||||
"",
|
||||
"⚠ Could not capture token automatically.",
|
||||
];
|
||||
if (result.error) {
|
||||
extraMessages.push(result.error);
|
||||
}
|
||||
setOutput((prev) => [
|
||||
...prev,
|
||||
...extraMessages,
|
||||
"Please copy the token from above and paste it below.",
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
setError(result.error || "Authentication failed");
|
||||
setAuthState("error");
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
// Cleanup subscription
|
||||
if (unsubscribeRef.current) {
|
||||
unsubscribeRef.current();
|
||||
unsubscribeRef.current = null;
|
||||
}
|
||||
|
||||
const errorMessage =
|
||||
err instanceof Error
|
||||
? err.message
|
||||
: typeof err === "object" && err !== null && "error" in err
|
||||
? String((err as { error: unknown }).error)
|
||||
: "Authentication failed";
|
||||
|
||||
// Check if we should fall back to manual mode
|
||||
if (
|
||||
typeof err === "object" &&
|
||||
err !== null &&
|
||||
"requiresManualAuth" in err &&
|
||||
(err as { requiresManualAuth: boolean }).requiresManualAuth
|
||||
) {
|
||||
setAuthState("manual");
|
||||
setOutput((prev) => [
|
||||
...prev,
|
||||
"",
|
||||
"⚠ " + errorMessage,
|
||||
"Please copy the token manually and paste it below.",
|
||||
]);
|
||||
} else {
|
||||
setError(errorMessage);
|
||||
setAuthState("error");
|
||||
}
|
||||
}
|
||||
}, [cliType]);
|
||||
|
||||
const reset = useCallback(() => {
|
||||
setAuthState("idle");
|
||||
setOutput([]);
|
||||
setToken("");
|
||||
setError(null);
|
||||
if (unsubscribeRef.current) {
|
||||
unsubscribeRef.current();
|
||||
unsubscribeRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
return { authState, output, token, error, startAuth, reset };
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,6 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import {
|
||||
CheckCircle2,
|
||||
AlertCircle,
|
||||
Shield,
|
||||
Sparkles,
|
||||
} from "lucide-react";
|
||||
import { CheckCircle2, AlertCircle, Shield, Sparkles } from "lucide-react";
|
||||
import { useSetupStore } from "@/store/setup-store";
|
||||
import { useAppStore } from "@/store/app-store";
|
||||
|
||||
@@ -14,8 +9,7 @@ interface CompleteStepProps {
|
||||
}
|
||||
|
||||
export function CompleteStep({ onFinish }: CompleteStepProps) {
|
||||
const { claudeCliStatus, claudeAuthStatus } =
|
||||
useSetupStore();
|
||||
const { claudeCliStatus, claudeAuthStatus } = useSetupStore();
|
||||
const { apiKeys } = useAppStore();
|
||||
|
||||
const claudeReady =
|
||||
@@ -38,44 +32,6 @@ export function CompleteStep({ onFinish }: CompleteStepProps) {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="max-w-md mx-auto">
|
||||
<Card
|
||||
className={`bg-card/50 border ${
|
||||
claudeReady ? "border-green-500/50" : "border-yellow-500/50"
|
||||
}`}
|
||||
>
|
||||
<CardContent className="py-4">
|
||||
<div className="flex items-center gap-3">
|
||||
{claudeReady ? (
|
||||
<CheckCircle2 className="w-6 h-6 text-green-500" />
|
||||
) : (
|
||||
<AlertCircle className="w-6 h-6 text-yellow-500" />
|
||||
)}
|
||||
<div className="text-left">
|
||||
<p className="font-medium text-foreground">Claude</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{claudeReady ? "Ready to use" : "Configure later in settings"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="p-4 rounded-lg bg-muted/50 border border-border max-w-md mx-auto">
|
||||
<div className="flex items-start gap-3">
|
||||
<Shield className="w-5 h-5 text-brand-500 mt-0.5" />
|
||||
<div className="text-left">
|
||||
<p className="text-sm font-medium text-foreground">
|
||||
Your credentials are secure
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
API keys are stored locally and never sent to our servers
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
size="lg"
|
||||
className="bg-gradient-to-r from-brand-500 to-brand-600 hover:from-brand-600 hover:to-brand-700 text-white"
|
||||
|
||||
@@ -19,29 +19,11 @@ export function WelcomeStep({ onNext }: WelcomeStepProps) {
|
||||
Welcome to Automaker
|
||||
</h2>
|
||||
<p className="text-muted-foreground max-w-md mx-auto">
|
||||
Let's set up your development environment. We'll check for
|
||||
required CLI tools and help you configure them.
|
||||
To get started, we'll need to verify either claude code cli is
|
||||
installed or you have Anthropic api keys
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 max-w-md mx-auto place-items-center">
|
||||
<Card className="bg-card/50 border-border hover:border-brand-500/50 transition-colors">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
<Terminal className="w-5 h-5 text-brand-500" />
|
||||
Claude CLI
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Anthropic's powerful AI assistant for code generation and
|
||||
analysis
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
</div>
|
||||
|
||||
<Button
|
||||
size="lg"
|
||||
className="bg-gradient-to-r from-brand-500 to-brand-600 hover:from-brand-600 hover:to-brand-700 text-white"
|
||||
|
||||
@@ -60,7 +60,7 @@ export const buildProviderConfigs = ({
|
||||
}: ProviderConfigParams): ProviderConfig[] => [
|
||||
{
|
||||
key: "anthropic",
|
||||
label: "Anthropic API Key (Claude)",
|
||||
label: "Anthropic API Key",
|
||||
inputId: "anthropic-key",
|
||||
placeholder: "sk-ant-...",
|
||||
value: anthropic.value,
|
||||
@@ -82,33 +82,32 @@ export const buildProviderConfigs = ({
|
||||
descriptionPrefix: "Used for Claude AI features. Get your key at",
|
||||
descriptionLinkHref: "https://console.anthropic.com/account/keys",
|
||||
descriptionLinkText: "console.anthropic.com",
|
||||
descriptionSuffix:
|
||||
". Alternatively, the CLAUDE_CODE_OAUTH_TOKEN environment variable can be used.",
|
||||
},
|
||||
{
|
||||
key: "google",
|
||||
label: "Google API Key (Gemini)",
|
||||
inputId: "google-key",
|
||||
placeholder: "AIza...",
|
||||
value: google.value,
|
||||
setValue: google.setValue,
|
||||
showValue: google.show,
|
||||
setShowValue: google.setShow,
|
||||
hasStoredKey: apiKeys.google,
|
||||
inputTestId: "google-api-key-input",
|
||||
toggleTestId: "toggle-google-visibility",
|
||||
testButton: {
|
||||
onClick: google.onTest,
|
||||
disabled: !google.value || google.testing,
|
||||
loading: google.testing,
|
||||
testId: "test-gemini-connection",
|
||||
},
|
||||
result: google.result,
|
||||
resultTestId: "gemini-test-connection-result",
|
||||
resultMessageTestId: "gemini-test-connection-message",
|
||||
descriptionPrefix:
|
||||
"Used for Gemini AI features (including image/design prompts). Get your key at",
|
||||
descriptionLinkHref: "https://makersuite.google.com/app/apikey",
|
||||
descriptionLinkText: "makersuite.google.com",
|
||||
descriptionSuffix: ".",
|
||||
},
|
||||
// {
|
||||
// key: "google",
|
||||
// label: "Google API Key (Gemini)",
|
||||
// inputId: "google-key",
|
||||
// placeholder: "AIza...",
|
||||
// value: google.value,
|
||||
// setValue: google.setValue,
|
||||
// showValue: google.show,
|
||||
// setShowValue: google.setShow,
|
||||
// hasStoredKey: apiKeys.google,
|
||||
// inputTestId: "google-api-key-input",
|
||||
// toggleTestId: "toggle-google-visibility",
|
||||
// testButton: {
|
||||
// onClick: google.onTest,
|
||||
// disabled: !google.value || google.testing,
|
||||
// loading: google.testing,
|
||||
// testId: "test-gemini-connection",
|
||||
// },
|
||||
// result: google.result,
|
||||
// resultTestId: "gemini-test-connection-result",
|
||||
// resultMessageTestId: "gemini-test-connection-message",
|
||||
// descriptionPrefix:
|
||||
// "Used for Gemini AI features (including image/design prompts). Get your key at",
|
||||
// descriptionLinkHref: "https://makersuite.google.com/app/apikey",
|
||||
// descriptionLinkText: "makersuite.google.com",
|
||||
// },
|
||||
];
|
||||
|
||||
@@ -91,3 +91,4 @@ export const CHAT_TOOLS = [
|
||||
* Default max turns for chat
|
||||
*/
|
||||
export const CHAT_MAX_TURNS = 1000;
|
||||
|
||||
|
||||
@@ -355,6 +355,9 @@ export interface ElectronAPI {
|
||||
provider: string,
|
||||
apiKey: string
|
||||
) => Promise<{ success: boolean; error?: string }>;
|
||||
deleteApiKey: (
|
||||
provider: string
|
||||
) => Promise<{ success: boolean; error?: string; message?: string }>;
|
||||
getApiKeys: () => Promise<{
|
||||
success: boolean;
|
||||
hasAnthropicKey: boolean;
|
||||
@@ -369,6 +372,11 @@ export interface ElectronAPI {
|
||||
isMac: boolean;
|
||||
isLinux: boolean;
|
||||
}>;
|
||||
verifyClaudeAuth: (authMethod?: "cli" | "api_key") => Promise<{
|
||||
success: boolean;
|
||||
authenticated: boolean;
|
||||
error?: string;
|
||||
}>;
|
||||
onInstallProgress?: (callback: (progress: any) => void) => () => void;
|
||||
onAuthProgress?: (callback: (progress: any) => void) => () => void;
|
||||
};
|
||||
@@ -874,6 +882,9 @@ interface SetupAPI {
|
||||
hasAnthropicKey: boolean;
|
||||
hasGoogleKey: boolean;
|
||||
}>;
|
||||
deleteApiKey: (
|
||||
provider: string
|
||||
) => Promise<{ success: boolean; error?: string; message?: string }>;
|
||||
getPlatform: () => Promise<{
|
||||
success: boolean;
|
||||
platform: string;
|
||||
@@ -883,6 +894,11 @@ interface SetupAPI {
|
||||
isMac: boolean;
|
||||
isLinux: boolean;
|
||||
}>;
|
||||
verifyClaudeAuth: (authMethod?: "cli" | "api_key") => Promise<{
|
||||
success: boolean;
|
||||
authenticated: boolean;
|
||||
error?: string;
|
||||
}>;
|
||||
onInstallProgress?: (callback: (progress: any) => void) => () => void;
|
||||
onAuthProgress?: (callback: (progress: any) => void) => () => void;
|
||||
}
|
||||
@@ -942,6 +958,11 @@ function createMockSetupAPI(): SetupAPI {
|
||||
};
|
||||
},
|
||||
|
||||
deleteApiKey: async (provider: string) => {
|
||||
console.log("[Mock] Deleting API key for:", provider);
|
||||
return { success: true, message: `API key for ${provider} deleted` };
|
||||
},
|
||||
|
||||
getPlatform: async () => {
|
||||
return {
|
||||
success: true,
|
||||
@@ -954,6 +975,16 @@ function createMockSetupAPI(): SetupAPI {
|
||||
};
|
||||
},
|
||||
|
||||
verifyClaudeAuth: async (authMethod?: "cli" | "api_key") => {
|
||||
console.log("[Mock] Verifying Claude auth with method:", authMethod);
|
||||
// Mock always returns not authenticated
|
||||
return {
|
||||
success: true,
|
||||
authenticated: false,
|
||||
error: "Mock environment - authentication not available",
|
||||
};
|
||||
},
|
||||
|
||||
onInstallProgress: (callback) => {
|
||||
// Mock progress events
|
||||
return () => {};
|
||||
|
||||
@@ -438,6 +438,14 @@ export class HttpApiClient implements ElectronAPI {
|
||||
error?: string;
|
||||
}> => this.post("/api/setup/store-api-key", { provider, apiKey }),
|
||||
|
||||
deleteApiKey: (
|
||||
provider: string
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
error?: string;
|
||||
message?: string;
|
||||
}> => this.post("/api/setup/delete-api-key", { provider }),
|
||||
|
||||
getApiKeys: (): Promise<{
|
||||
success: boolean;
|
||||
hasAnthropicKey: boolean;
|
||||
@@ -454,6 +462,12 @@ export class HttpApiClient implements ElectronAPI {
|
||||
isLinux: boolean;
|
||||
}> => this.get("/api/setup/platform"),
|
||||
|
||||
verifyClaudeAuth: (authMethod?: "cli" | "api_key"): Promise<{
|
||||
success: boolean;
|
||||
authenticated: boolean;
|
||||
error?: string;
|
||||
}> => this.post("/api/setup/verify-claude-auth", { authMethod }),
|
||||
|
||||
onInstallProgress: (callback: (progress: unknown) => void) => {
|
||||
return this.subscribeToEvent("agent:stream", callback);
|
||||
},
|
||||
|
||||
@@ -12,7 +12,7 @@ export interface CliStatus {
|
||||
|
||||
// Claude Auth Method - all possible authentication sources
|
||||
export type ClaudeAuthMethod =
|
||||
| "oauth_token_env" // CLAUDE_CODE_OAUTH_TOKEN environment variable
|
||||
| "oauth_token_env"
|
||||
| "oauth_token" // Stored OAuth token from claude login
|
||||
| "api_key_env" // ANTHROPIC_API_KEY environment variable
|
||||
| "api_key" // Manually stored API key
|
||||
@@ -65,6 +65,7 @@ export interface SetupState {
|
||||
export interface SetupActions {
|
||||
// Setup flow
|
||||
setCurrentStep: (step: SetupStep) => void;
|
||||
setSetupComplete: (complete: boolean) => void;
|
||||
completeSetup: () => void;
|
||||
resetSetup: () => void;
|
||||
setIsFirstRun: (isFirstRun: boolean) => void;
|
||||
@@ -109,6 +110,12 @@ export const useSetupStore = create<SetupState & SetupActions>()(
|
||||
// Setup flow
|
||||
setCurrentStep: (step) => set({ currentStep: step }),
|
||||
|
||||
setSetupComplete: (complete) =>
|
||||
set({
|
||||
setupComplete: complete,
|
||||
currentStep: complete ? "complete" : "welcome",
|
||||
}),
|
||||
|
||||
completeSetup: () =>
|
||||
set({ setupComplete: true, currentStep: "complete" }),
|
||||
|
||||
|
||||
@@ -72,3 +72,4 @@ export function getLogLevel(): LogLevel {
|
||||
export function setLogLevel(level: LogLevel): void {
|
||||
currentLogLevel = level;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,3 +21,4 @@ export function createSpecRegenerationRoutes(events: EventEmitter): Router {
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
|
||||
@@ -151,7 +151,7 @@ export async function getClaudeStatus() {
|
||||
if (auth.hasEnvOAuthToken) {
|
||||
auth.authenticated = true;
|
||||
auth.oauthTokenValid = true;
|
||||
auth.method = "oauth_token_env"; // OAuth token from CLAUDE_CODE_OAUTH_TOKEN env var
|
||||
auth.method = "oauth_token_env";
|
||||
} else if (auth.hasEnvApiKey) {
|
||||
auth.authenticated = true;
|
||||
auth.apiKeyValid = true;
|
||||
|
||||
@@ -7,8 +7,10 @@ import { createClaudeStatusHandler } from "./routes/claude-status.js";
|
||||
import { createInstallClaudeHandler } from "./routes/install-claude.js";
|
||||
import { createAuthClaudeHandler } from "./routes/auth-claude.js";
|
||||
import { createStoreApiKeyHandler } from "./routes/store-api-key.js";
|
||||
import { createDeleteApiKeyHandler } from "./routes/delete-api-key.js";
|
||||
import { createApiKeysHandler } from "./routes/api-keys.js";
|
||||
import { createPlatformHandler } from "./routes/platform.js";
|
||||
import { createVerifyClaudeAuthHandler } from "./routes/verify-claude-auth.js";
|
||||
|
||||
export function createSetupRoutes(): Router {
|
||||
const router = Router();
|
||||
@@ -17,8 +19,10 @@ export function createSetupRoutes(): Router {
|
||||
router.post("/install-claude", createInstallClaudeHandler());
|
||||
router.post("/auth-claude", createAuthClaudeHandler());
|
||||
router.post("/store-api-key", createStoreApiKeyHandler());
|
||||
router.post("/delete-api-key", createDeleteApiKeyHandler());
|
||||
router.get("/api-keys", createApiKeysHandler());
|
||||
router.get("/platform", createPlatformHandler());
|
||||
router.post("/verify-claude-auth", createVerifyClaudeAuthHandler());
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
104
apps/server/src/routes/setup/routes/delete-api-key.ts
Normal file
104
apps/server/src/routes/setup/routes/delete-api-key.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* POST /delete-api-key endpoint - Delete a stored API key
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import { createLogger } from "../../../lib/logger.js";
|
||||
import path from "path";
|
||||
import fs from "fs/promises";
|
||||
|
||||
const logger = createLogger("Setup");
|
||||
|
||||
// In-memory storage reference (imported from common.ts pattern)
|
||||
// We need to modify common.ts to export a deleteApiKey function
|
||||
import { setApiKey } from "../common.js";
|
||||
|
||||
/**
|
||||
* Remove an API key from the .env file
|
||||
*/
|
||||
async function removeApiKeyFromEnv(key: string): Promise<void> {
|
||||
const envPath = path.join(process.cwd(), ".env");
|
||||
|
||||
try {
|
||||
let envContent = "";
|
||||
try {
|
||||
envContent = await fs.readFile(envPath, "utf-8");
|
||||
} catch {
|
||||
// .env file doesn't exist, nothing to delete
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse existing env content and remove the key
|
||||
const lines = envContent.split("\n");
|
||||
const keyRegex = new RegExp(`^${key}=`);
|
||||
const newLines = lines.filter((line) => !keyRegex.test(line));
|
||||
|
||||
// Remove empty lines at the end
|
||||
while (newLines.length > 0 && newLines[newLines.length - 1].trim() === "") {
|
||||
newLines.pop();
|
||||
}
|
||||
|
||||
await fs.writeFile(envPath, newLines.join("\n") + (newLines.length > 0 ? "\n" : ""));
|
||||
logger.info(`[Setup] Removed ${key} from .env file`);
|
||||
} catch (error) {
|
||||
logger.error(`[Setup] Failed to remove ${key} from .env:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export function createDeleteApiKeyHandler() {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { provider } = req.body as { provider: string };
|
||||
|
||||
if (!provider) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: "Provider is required",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(`[Setup] Deleting API key for provider: ${provider}`);
|
||||
|
||||
// Map provider to env key name
|
||||
const envKeyMap: Record<string, string> = {
|
||||
anthropic: "ANTHROPIC_API_KEY",
|
||||
google: "GOOGLE_GENERATIVE_AI_API_KEY",
|
||||
openai: "OPENAI_API_KEY",
|
||||
};
|
||||
|
||||
const envKey = envKeyMap[provider];
|
||||
if (!envKey) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: `Unknown provider: ${provider}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear from in-memory storage
|
||||
setApiKey(provider, "");
|
||||
|
||||
// Remove from environment
|
||||
delete process.env[envKey];
|
||||
|
||||
// Remove from .env file
|
||||
await removeApiKeyFromEnv(envKey);
|
||||
|
||||
logger.info(`[Setup] Successfully deleted API key for ${provider}`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `API key for ${provider} has been deleted`,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("[Setup] Delete API key error:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Failed to delete API key",
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
330
apps/server/src/routes/setup/routes/verify-claude-auth.ts
Normal file
330
apps/server/src/routes/setup/routes/verify-claude-auth.ts
Normal file
@@ -0,0 +1,330 @@
|
||||
/**
|
||||
* POST /verify-claude-auth endpoint - Verify Claude authentication by running a test query
|
||||
* Supports verifying either CLI auth or API key auth independently
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import { query } from "@anthropic-ai/claude-agent-sdk";
|
||||
import { createLogger } from "../../../lib/logger.js";
|
||||
import { getApiKey } from "../common.js";
|
||||
|
||||
const logger = createLogger("Setup");
|
||||
|
||||
// Known error patterns that indicate auth failure
|
||||
const AUTH_ERROR_PATTERNS = [
|
||||
"OAuth token revoked",
|
||||
"Please run /login",
|
||||
"please run /login",
|
||||
"token revoked",
|
||||
"invalid_api_key",
|
||||
"authentication_error",
|
||||
"unauthorized",
|
||||
"not authenticated",
|
||||
"authentication failed",
|
||||
"invalid api key",
|
||||
"api key is invalid",
|
||||
];
|
||||
|
||||
// Patterns that indicate billing/credit issues - should FAIL verification
|
||||
const BILLING_ERROR_PATTERNS = [
|
||||
"credit balance is too low",
|
||||
"credit balance too low",
|
||||
"insufficient credits",
|
||||
"insufficient balance",
|
||||
"no credits",
|
||||
"out of credits",
|
||||
"billing",
|
||||
"payment required",
|
||||
"add credits",
|
||||
];
|
||||
|
||||
// Patterns that indicate rate/usage limits - should FAIL verification
|
||||
// Users need to wait or upgrade their plan
|
||||
const RATE_LIMIT_PATTERNS = [
|
||||
"limit reached",
|
||||
"rate limit",
|
||||
"rate_limit",
|
||||
"resets", // Only valid if it's a temporary reset, not a billing issue
|
||||
"/upgrade",
|
||||
"extra-usage",
|
||||
];
|
||||
|
||||
function isRateLimitError(text: string): boolean {
|
||||
const lowerText = text.toLowerCase();
|
||||
// First check if it's a billing error - billing errors are NOT rate limits
|
||||
if (isBillingError(text)) {
|
||||
return false;
|
||||
}
|
||||
return RATE_LIMIT_PATTERNS.some((pattern) =>
|
||||
lowerText.includes(pattern.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
function isBillingError(text: string): boolean {
|
||||
const lowerText = text.toLowerCase();
|
||||
return BILLING_ERROR_PATTERNS.some((pattern) =>
|
||||
lowerText.includes(pattern.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
function containsAuthError(text: string): boolean {
|
||||
const lowerText = text.toLowerCase();
|
||||
return AUTH_ERROR_PATTERNS.some((pattern) =>
|
||||
lowerText.includes(pattern.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
export function createVerifyClaudeAuthHandler() {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
// Get the auth method from the request body
|
||||
const { authMethod } = req.body as { authMethod?: "cli" | "api_key" };
|
||||
|
||||
logger.info(
|
||||
`[Setup] Verifying Claude authentication using method: ${
|
||||
authMethod || "auto"
|
||||
}`
|
||||
);
|
||||
|
||||
// Create an AbortController with a 30-second timeout
|
||||
const abortController = new AbortController();
|
||||
const timeoutId = setTimeout(() => abortController.abort(), 30000);
|
||||
|
||||
let authenticated = false;
|
||||
let errorMessage = "";
|
||||
let receivedAnyContent = false;
|
||||
|
||||
// Save original env values
|
||||
const originalAnthropicKey = process.env.ANTHROPIC_API_KEY;
|
||||
|
||||
try {
|
||||
// Configure environment based on auth method
|
||||
if (authMethod === "cli") {
|
||||
// For CLI verification, remove any API key so it uses CLI credentials only
|
||||
delete process.env.ANTHROPIC_API_KEY;
|
||||
logger.info(
|
||||
"[Setup] Cleared API key environment for CLI verification"
|
||||
);
|
||||
} else if (authMethod === "api_key") {
|
||||
// For API key verification, ensure we're using the stored API key
|
||||
const storedApiKey = getApiKey("anthropic");
|
||||
if (storedApiKey) {
|
||||
process.env.ANTHROPIC_API_KEY = storedApiKey;
|
||||
logger.info("[Setup] Using stored API key for verification");
|
||||
} else {
|
||||
// Check env var
|
||||
if (!process.env.ANTHROPIC_API_KEY) {
|
||||
res.json({
|
||||
success: true,
|
||||
authenticated: false,
|
||||
error: "No API key configured. Please enter an API key first.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Run a minimal query to verify authentication
|
||||
const stream = query({
|
||||
prompt: "Reply with only the word 'ok'",
|
||||
options: {
|
||||
model: "claude-sonnet-4-20250514",
|
||||
maxTurns: 1,
|
||||
allowedTools: [],
|
||||
abortController,
|
||||
},
|
||||
});
|
||||
|
||||
// Collect all messages and check for errors
|
||||
const allMessages: string[] = [];
|
||||
|
||||
for await (const msg of stream) {
|
||||
const msgStr = JSON.stringify(msg);
|
||||
allMessages.push(msgStr);
|
||||
logger.info("[Setup] Stream message:", msgStr.substring(0, 500));
|
||||
|
||||
// Check for billing errors FIRST - these should fail verification
|
||||
if (isBillingError(msgStr)) {
|
||||
logger.error("[Setup] Found billing error in message");
|
||||
errorMessage =
|
||||
"Credit balance is too low. Please add credits to your Anthropic account at console.anthropic.com";
|
||||
authenticated = false;
|
||||
break;
|
||||
}
|
||||
|
||||
// Check if any part of the message contains auth errors
|
||||
if (containsAuthError(msgStr)) {
|
||||
logger.error("[Setup] Found auth error in message");
|
||||
if (authMethod === "cli") {
|
||||
errorMessage =
|
||||
"CLI authentication failed. Please run 'claude login' in your terminal to authenticate.";
|
||||
} else {
|
||||
errorMessage = "API key is invalid or has been revoked.";
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Check specifically for assistant messages with text content
|
||||
if (msg.type === "assistant" && (msg as any).message?.content) {
|
||||
const content = (msg as any).message.content;
|
||||
if (Array.isArray(content)) {
|
||||
for (const block of content) {
|
||||
if (block.type === "text" && block.text) {
|
||||
const text = block.text;
|
||||
logger.info("[Setup] Assistant text:", text);
|
||||
|
||||
if (containsAuthError(text)) {
|
||||
if (authMethod === "cli") {
|
||||
errorMessage =
|
||||
"CLI authentication failed. Please run 'claude login' in your terminal to authenticate.";
|
||||
} else {
|
||||
errorMessage = "API key is invalid or has been revoked.";
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Valid text response that's not an error
|
||||
if (text.toLowerCase().includes("ok") || text.length > 0) {
|
||||
receivedAnyContent = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for result messages
|
||||
if (msg.type === "result") {
|
||||
const resultStr = JSON.stringify(msg);
|
||||
|
||||
// First check for billing errors - these should FAIL verification
|
||||
if (isBillingError(resultStr)) {
|
||||
logger.error(
|
||||
"[Setup] Billing error detected - insufficient credits"
|
||||
);
|
||||
errorMessage =
|
||||
"Credit balance is too low. Please add credits to your Anthropic account at console.anthropic.com";
|
||||
authenticated = false;
|
||||
break;
|
||||
}
|
||||
// Check if it's a rate limit error - should FAIL verification
|
||||
else if (isRateLimitError(resultStr)) {
|
||||
logger.warn(
|
||||
"[Setup] Rate limit detected - treating as unverified"
|
||||
);
|
||||
errorMessage =
|
||||
"Rate limit reached. Please wait a while before trying again or upgrade your plan.";
|
||||
authenticated = false;
|
||||
break;
|
||||
} else if (containsAuthError(resultStr)) {
|
||||
if (authMethod === "cli") {
|
||||
errorMessage =
|
||||
"CLI authentication failed. Please run 'claude login' in your terminal to authenticate.";
|
||||
} else {
|
||||
errorMessage = "API key is invalid or has been revoked.";
|
||||
}
|
||||
} else {
|
||||
// Got a result without errors
|
||||
receivedAnyContent = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Determine authentication status
|
||||
if (errorMessage) {
|
||||
authenticated = false;
|
||||
} else if (receivedAnyContent) {
|
||||
authenticated = true;
|
||||
} else {
|
||||
// No content received - might be an issue
|
||||
logger.warn("[Setup] No content received from stream");
|
||||
logger.warn("[Setup] All messages:", allMessages.join("\n"));
|
||||
errorMessage =
|
||||
"No response received from Claude. Please check your authentication.";
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
const errMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
|
||||
logger.error("[Setup] Claude auth verification exception:", errMessage);
|
||||
|
||||
// Check for billing errors FIRST - these always fail
|
||||
if (isBillingError(errMessage)) {
|
||||
authenticated = false;
|
||||
errorMessage =
|
||||
"Credit balance is too low. Please add credits to your Anthropic account at console.anthropic.com";
|
||||
}
|
||||
// Check for rate limit in exception - should FAIL verification
|
||||
else if (isRateLimitError(errMessage)) {
|
||||
authenticated = false;
|
||||
errorMessage =
|
||||
"Rate limit reached. Please wait a while before trying again or upgrade your plan.";
|
||||
logger.warn(
|
||||
"[Setup] Rate limit in exception - treating as unverified"
|
||||
);
|
||||
}
|
||||
// If we already determined auth was successful, keep it
|
||||
else if (authenticated) {
|
||||
logger.info("[Setup] Auth already confirmed, ignoring exception");
|
||||
}
|
||||
// Check for auth-related errors in exception
|
||||
else if (containsAuthError(errMessage)) {
|
||||
if (authMethod === "cli") {
|
||||
errorMessage =
|
||||
"CLI authentication failed. Please run 'claude login' in your terminal to authenticate.";
|
||||
} else {
|
||||
errorMessage = "API key is invalid or has been revoked.";
|
||||
}
|
||||
} else if (
|
||||
errMessage.includes("abort") ||
|
||||
errMessage.includes("timeout")
|
||||
) {
|
||||
errorMessage = "Verification timed out. Please try again.";
|
||||
} else if (
|
||||
errMessage.includes("exit") &&
|
||||
errMessage.includes("code 1")
|
||||
) {
|
||||
// Process exited with code 1 but we might have gotten rate limit info in the stream
|
||||
// Check if we received any content that indicated auth worked
|
||||
if (receivedAnyContent && !errorMessage) {
|
||||
authenticated = true;
|
||||
logger.info(
|
||||
"[Setup] Process exit 1 but content received - auth valid"
|
||||
);
|
||||
} else if (!errorMessage) {
|
||||
errorMessage = errMessage;
|
||||
}
|
||||
} else if (!errorMessage) {
|
||||
errorMessage = errMessage;
|
||||
}
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
// Restore original environment
|
||||
if (originalAnthropicKey !== undefined) {
|
||||
process.env.ANTHROPIC_API_KEY = originalAnthropicKey;
|
||||
} else if (authMethod === "cli") {
|
||||
// If we cleared it and there was no original, keep it cleared
|
||||
delete process.env.ANTHROPIC_API_KEY;
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("[Setup] Verification result:", {
|
||||
authenticated,
|
||||
errorMessage,
|
||||
authMethod,
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
authenticated,
|
||||
error: errorMessage || undefined,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("[Setup] Verify Claude auth endpoint error:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
authenticated: false,
|
||||
error: error instanceof Error ? error.message : "Verification failed",
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -580,3 +580,4 @@ The route organization pattern provides:
|
||||
5. **Testability** - Functions can be tested independently
|
||||
|
||||
Apply this pattern to all route modules for consistency and improved code quality.
|
||||
|
||||
|
||||
478
package-lock.json
generated
478
package-lock.json
generated
@@ -25,6 +25,7 @@
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@lezer/highlight": "^1.2.3",
|
||||
"@radix-ui/react-accordion": "^1.2.12",
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||
@@ -1543,10 +1544,6 @@
|
||||
"version": "1.1.1",
|
||||
"license": "MIT"
|
||||
},
|
||||
"apps/app/node_modules/@radix-ui/primitive": {
|
||||
"version": "1.1.3",
|
||||
"license": "MIT"
|
||||
},
|
||||
"apps/app/node_modules/@radix-ui/react-arrow": {
|
||||
"version": "1.1.7",
|
||||
"license": "MIT",
|
||||
@@ -1596,72 +1593,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"apps/app/node_modules/@radix-ui/react-collection": {
|
||||
"version": "1.1.7",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-slot": "1.2.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"apps/app/node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"apps/app/node_modules/@radix-ui/react-compose-refs": {
|
||||
"version": "1.1.2",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"apps/app/node_modules/@radix-ui/react-context": {
|
||||
"version": "1.1.2",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"apps/app/node_modules/@radix-ui/react-dialog": {
|
||||
"version": "1.1.15",
|
||||
"license": "MIT",
|
||||
@@ -1712,19 +1643,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"apps/app/node_modules/@radix-ui/react-direction": {
|
||||
"version": "1.1.1",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"apps/app/node_modules/@radix-ui/react-dismissable-layer": {
|
||||
"version": "1.1.11",
|
||||
"license": "MIT",
|
||||
@@ -1813,22 +1731,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"apps/app/node_modules/@radix-ui/react-id": {
|
||||
"version": "1.1.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"apps/app/node_modules/@radix-ui/react-label": {
|
||||
"version": "2.1.8",
|
||||
"license": "MIT",
|
||||
@@ -2028,65 +1930,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"apps/app/node_modules/@radix-ui/react-presence": {
|
||||
"version": "1.1.5",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"apps/app/node_modules/@radix-ui/react-primitive": {
|
||||
"version": "2.1.3",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "1.2.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"apps/app/node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"apps/app/node_modules/@radix-ui/react-roving-focus": {
|
||||
"version": "1.1.11",
|
||||
"license": "MIT",
|
||||
@@ -2252,39 +2095,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"apps/app/node_modules/@radix-ui/react-use-controllable-state": {
|
||||
"version": "1.2.2",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-effect-event": "0.0.2",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"apps/app/node_modules/@radix-ui/react-use-effect-event": {
|
||||
"version": "0.0.2",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"apps/app/node_modules/@radix-ui/react-use-escape-keydown": {
|
||||
"version": "1.1.1",
|
||||
"license": "MIT",
|
||||
@@ -2301,19 +2111,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"apps/app/node_modules/@radix-ui/react-use-layout-effect": {
|
||||
"version": "1.1.1",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"apps/app/node_modules/@radix-ui/react-use-previous": {
|
||||
"version": "1.1.1",
|
||||
"license": "MIT",
|
||||
@@ -11192,6 +10989,279 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@radix-ui/primitive": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
|
||||
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@radix-ui/react-accordion": {
|
||||
"version": "1.2.12",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz",
|
||||
"integrity": "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.3",
|
||||
"@radix-ui/react-collapsible": "1.1.12",
|
||||
"@radix-ui/react-collection": "1.1.7",
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-direction": "1.1.1",
|
||||
"@radix-ui/react-id": "1.1.1",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-use-controllable-state": "1.2.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-collapsible": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz",
|
||||
"integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.3",
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-id": "1.1.1",
|
||||
"@radix-ui/react-presence": "1.1.5",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-use-controllable-state": "1.2.2",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-collection": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
|
||||
"integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-slot": "1.2.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-compose-refs": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
||||
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-context": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
|
||||
"integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-direction": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
|
||||
"integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-id": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
|
||||
"integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-presence": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
|
||||
"integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-primitive": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
|
||||
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "1.2.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-controllable-state": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
|
||||
"integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-effect-event": "0.0.2",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-effect-event": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz",
|
||||
"integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-layout-effect": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
|
||||
"integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.53.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz",
|
||||
|
||||
Reference in New Issue
Block a user