diff --git a/app/components/@settings/tabs/connections/ConnectionsTab.tsx b/app/components/@settings/tabs/connections/ConnectionsTab.tsx
index 72ff6434..defd7aeb 100644
--- a/app/components/@settings/tabs/connections/ConnectionsTab.tsx
+++ b/app/components/@settings/tabs/connections/ConnectionsTab.tsx
@@ -1,5 +1,6 @@
import { motion } from 'framer-motion';
import React, { Suspense } from 'react';
+import VercelConnection from './VercelConnection';
// Use React.lazy for dynamic imports
const GithubConnection = React.lazy(() => import('./GithubConnection'));
@@ -39,6 +40,9 @@ export default function ConnectionsTab() {
}>
+ }>
+
+
);
diff --git a/app/components/@settings/tabs/connections/VercelConnection.tsx b/app/components/@settings/tabs/connections/VercelConnection.tsx
new file mode 100644
index 00000000..4a442a08
--- /dev/null
+++ b/app/components/@settings/tabs/connections/VercelConnection.tsx
@@ -0,0 +1,289 @@
+import React, { useEffect, useState } from 'react';
+import { motion } from 'framer-motion';
+import { toast } from 'react-toastify';
+import { useStore } from '@nanostores/react';
+import { logStore } from '~/lib/stores/logs';
+import { classNames } from '~/utils/classNames';
+import {
+ vercelConnection,
+ isConnecting,
+ isFetchingStats,
+ updateVercelConnection,
+ fetchVercelStats,
+} from '~/lib/stores/vercel';
+
+export default function VercelConnection() {
+ const connection = useStore(vercelConnection);
+ const connecting = useStore(isConnecting);
+ const fetchingStats = useStore(isFetchingStats);
+ const [isProjectsExpanded, setIsProjectsExpanded] = useState(false);
+
+ useEffect(() => {
+ const fetchProjects = async () => {
+ if (connection.user && connection.token) {
+ await fetchVercelStats(connection.token);
+ }
+ };
+ fetchProjects();
+ }, [connection.user, connection.token]);
+
+ const handleConnect = async (event: React.FormEvent) => {
+ event.preventDefault();
+ isConnecting.set(true);
+
+ try {
+ const response = await fetch('https://api.vercel.com/v2/user', {
+ headers: {
+ Authorization: `Bearer ${connection.token}`,
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error('Invalid token or unauthorized');
+ }
+
+ const userData = (await response.json()) as any;
+ updateVercelConnection({
+ user: userData.user || userData, // Handle both possible structures
+ token: connection.token,
+ });
+
+ await fetchVercelStats(connection.token);
+ toast.success('Successfully connected to Vercel');
+ } catch (error) {
+ console.error('Auth error:', error);
+ logStore.logError('Failed to authenticate with Vercel', { error });
+ toast.error('Failed to connect to Vercel');
+ updateVercelConnection({ user: null, token: '' });
+ } finally {
+ isConnecting.set(false);
+ }
+ };
+
+ const handleDisconnect = () => {
+ updateVercelConnection({ user: null, token: '' });
+ toast.success('Disconnected from Vercel');
+ };
+
+ console.log('connection', connection);
+
+ return (
+
+
+
+
+

+
Vercel Connection
+
+
+
+ {!connection.user ? (
+
+
+
+
updateVercelConnection({ ...connection, token: e.target.value })}
+ disabled={connecting}
+ placeholder="Enter your Vercel personal access token"
+ className={classNames(
+ 'w-full px-3 py-2 rounded-lg text-sm',
+ 'bg-[#F8F8F8] dark:bg-[#1A1A1A]',
+ 'border border-[#E5E5E5] dark:border-[#333333]',
+ 'text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary',
+ 'focus:outline-none focus:ring-1 focus:ring-bolt-elements-borderColorActive',
+ 'disabled:opacity-50',
+ )}
+ />
+
+
+
+
+
+ ) : (
+
+
+
+
+
+
+ Connected to Vercel
+
+
+
+
+
+ {/* Debug output */}
+
{JSON.stringify(connection.user, null, 2)}
+
+

+
+
+ {connection.user?.username || connection.user?.user?.username || 'Vercel User'}
+
+
+ {connection.user?.email || connection.user?.user?.email || 'No email available'}
+
+
+
+
+ {fetchingStats ? (
+
+
+ Fetching Vercel projects...
+
+ ) : (
+
+
+ {isProjectsExpanded && connection.stats?.projects?.length ? (
+
+ ) : isProjectsExpanded ? (
+
+
+ No projects found in your Vercel account
+
+ ) : null}
+
+ )}
+
+ )}
+
+
+ );
+}
diff --git a/app/components/chat/NetlifyDeploymentLink.client.tsx b/app/components/chat/NetlifyDeploymentLink.client.tsx
index da8e0b41..4e60793f 100644
--- a/app/components/chat/NetlifyDeploymentLink.client.tsx
+++ b/app/components/chat/NetlifyDeploymentLink.client.tsx
@@ -30,10 +30,10 @@ export function NetlifyDeploymentLink() {
rel="noopener noreferrer"
className="inline-flex items-center justify-center w-8 h-8 rounded hover:bg-bolt-elements-item-backgroundActive text-bolt-elements-textSecondary hover:text-[#00AD9F] z-50"
onClick={(e) => {
- e.stopPropagation(); // Add this to prevent click from bubbling up
+ e.stopPropagation(); // This is to prevent click from bubbling up
}}
>
-
+
diff --git a/app/components/chat/VercelDeploymentLink.client.tsx b/app/components/chat/VercelDeploymentLink.client.tsx
new file mode 100644
index 00000000..ecb5a587
--- /dev/null
+++ b/app/components/chat/VercelDeploymentLink.client.tsx
@@ -0,0 +1,158 @@
+import { useStore } from '@nanostores/react';
+import { vercelConnection } from '~/lib/stores/vercel';
+import { chatId } from '~/lib/persistence/useChatHistory';
+import * as Tooltip from '@radix-ui/react-tooltip';
+import { useEffect, useState } from 'react';
+
+export function VercelDeploymentLink() {
+ const connection = useStore(vercelConnection);
+ const currentChatId = useStore(chatId);
+ const [deploymentUrl, setDeploymentUrl] = useState(null);
+ const [isLoading, setIsLoading] = useState(false);
+
+ useEffect(() => {
+ async function fetchProjectData() {
+ if (!connection.token || !currentChatId) {
+ return;
+ }
+
+ // Check if we have a stored project ID for this chat
+ const projectId = localStorage.getItem(`vercel-project-${currentChatId}`);
+
+ if (!projectId) {
+ return;
+ }
+
+ setIsLoading(true);
+
+ try {
+ // Fetch projects directly from the API
+ const projectsResponse = await fetch('https://api.vercel.com/v9/projects', {
+ headers: {
+ Authorization: `Bearer ${connection.token}`,
+ 'Content-Type': 'application/json',
+ },
+ cache: 'no-store',
+ });
+
+ if (!projectsResponse.ok) {
+ throw new Error(`Failed to fetch projects: ${projectsResponse.status}`);
+ }
+
+ const projectsData = (await projectsResponse.json()) as any;
+ const projects = projectsData.projects || [];
+
+ // Extract the chat number from currentChatId
+ const chatNumber = currentChatId.split('-')[0];
+
+ // Find project by matching the chat number in the name
+ const project = projects.find((p: { name: string | string[] }) => p.name.includes(`bolt-diy-${chatNumber}`));
+
+ if (project) {
+ // Fetch project details including deployments
+ const projectDetailsResponse = await fetch(`https://api.vercel.com/v9/projects/${project.id}`, {
+ headers: {
+ Authorization: `Bearer ${connection.token}`,
+ 'Content-Type': 'application/json',
+ },
+ cache: 'no-store',
+ });
+
+ if (projectDetailsResponse.ok) {
+ const projectDetails = (await projectDetailsResponse.json()) as any;
+
+ // Try to get URL from production aliases first
+ if (projectDetails.targets?.production?.alias && projectDetails.targets.production.alias.length > 0) {
+ // Find the clean URL (without -projects.vercel.app)
+ const cleanUrl = projectDetails.targets.production.alias.find(
+ (a: string) => a.endsWith('.vercel.app') && !a.includes('-projects.vercel.app'),
+ );
+
+ if (cleanUrl) {
+ setDeploymentUrl(`https://${cleanUrl}`);
+ return;
+ } else {
+ // If no clean URL found, use the first alias
+ setDeploymentUrl(`https://${projectDetails.targets.production.alias[0]}`);
+ return;
+ }
+ }
+ }
+
+ // If no aliases or project details failed, try fetching deployments
+ const deploymentsResponse = await fetch(
+ `https://api.vercel.com/v6/deployments?projectId=${project.id}&limit=1`,
+ {
+ headers: {
+ Authorization: `Bearer ${connection.token}`,
+ 'Content-Type': 'application/json',
+ },
+ cache: 'no-store',
+ },
+ );
+
+ if (deploymentsResponse.ok) {
+ const deploymentsData = (await deploymentsResponse.json()) as any;
+
+ if (deploymentsData.deployments && deploymentsData.deployments.length > 0) {
+ setDeploymentUrl(`https://${deploymentsData.deployments[0].url}`);
+ return;
+ }
+ }
+ }
+
+ // Fallback to API call if not found in fetched projects
+ const fallbackResponse = await fetch(`/api/vercel-deploy?projectId=${projectId}&token=${connection.token}`, {
+ method: 'GET',
+ });
+
+ const data = await fallbackResponse.json();
+
+ if ((data as { deploy?: { url?: string } }).deploy?.url) {
+ setDeploymentUrl((data as { deploy: { url: string } }).deploy.url);
+ } else if ((data as { project?: { url?: string } }).project?.url) {
+ setDeploymentUrl((data as { project: { url: string } }).project.url);
+ }
+ } catch (err) {
+ console.error('Error fetching Vercel deployment:', err);
+ } finally {
+ setIsLoading(false);
+ }
+ }
+
+ fetchProjectData();
+ }, [connection.token, currentChatId]);
+
+ if (!deploymentUrl) {
+ return null;
+ }
+
+ return (
+
+
+
+ {
+ e.stopPropagation();
+ }}
+ >
+
+
+
+
+
+ {deploymentUrl}
+
+
+
+
+
+ );
+}
diff --git a/app/components/header/HeaderActionButtons.client.tsx b/app/components/header/HeaderActionButtons.client.tsx
index d6273f57..56945c9e 100644
--- a/app/components/header/HeaderActionButtons.client.tsx
+++ b/app/components/header/HeaderActionButtons.client.tsx
@@ -3,26 +3,30 @@ import { toast } from 'react-toastify';
import useViewport from '~/lib/hooks';
import { chatStore } from '~/lib/stores/chat';
import { netlifyConnection } from '~/lib/stores/netlify';
+import { vercelConnection } from '~/lib/stores/vercel';
import { workbenchStore } from '~/lib/stores/workbench';
import { webcontainer } from '~/lib/webcontainer';
import { classNames } from '~/utils/classNames';
import { path } from '~/utils/path';
import { useEffect, useRef, useState } from 'react';
import type { ActionCallbackData } from '~/lib/runtime/message-parser';
-import { chatId } from '~/lib/persistence/useChatHistory'; // Add this import
+import { chatId } from '~/lib/persistence/useChatHistory';
import { streamingState } from '~/lib/stores/streaming';
import { NetlifyDeploymentLink } from '~/components/chat/NetlifyDeploymentLink.client';
+import { VercelDeploymentLink } from '~/components/chat/VercelDeploymentLink.client';
interface HeaderActionButtonsProps {}
export function HeaderActionButtons({}: HeaderActionButtonsProps) {
const showWorkbench = useStore(workbenchStore.showWorkbench);
const { showChat } = useStore(chatStore);
- const connection = useStore(netlifyConnection);
+ const netlifyConn = useStore(netlifyConnection);
+ const vercelConn = useStore(vercelConnection);
const [activePreviewIndex] = useState(0);
const previews = useStore(workbenchStore.previews);
const activePreview = previews[activePreviewIndex];
const [isDeploying, setIsDeploying] = useState(false);
+ const [deployingTo, setDeployingTo] = useState<'netlify' | 'vercel' | null>(null);
const isSmallViewport = useViewport(1024);
const canHideChat = showWorkbench || !showChat;
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
@@ -42,8 +46,8 @@ export function HeaderActionButtons({}: HeaderActionButtonsProps) {
const currentChatId = useStore(chatId);
- const handleDeploy = async () => {
- if (!connection.user || !connection.token) {
+ const handleNetlifyDeploy = async () => {
+ if (!netlifyConn.user || !netlifyConn.token) {
toast.error('Please connect to Netlify first in the settings tab!');
return;
}
@@ -118,7 +122,7 @@ export function HeaderActionButtons({}: HeaderActionButtonsProps) {
const existingSiteId = localStorage.getItem(`netlify-site-${currentChatId}`);
// Deploy using the API route with file contents
- const response = await fetch('/api/deploy', {
+ const response = await fetch('/api/netlify-deploy', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -126,7 +130,7 @@ export function HeaderActionButtons({}: HeaderActionButtonsProps) {
body: JSON.stringify({
siteId: existingSiteId || undefined,
files: fileContents,
- token: connection.token,
+ token: netlifyConn.token,
chatId: currentChatId, // Use chatId instead of artifact.id
}),
});
@@ -149,7 +153,7 @@ export function HeaderActionButtons({}: HeaderActionButtonsProps) {
`https://api.netlify.com/api/v1/sites/${data.site.id}/deploys/${data.deploy.id}`,
{
headers: {
- Authorization: `Bearer ${connection.token}`,
+ Authorization: `Bearer ${netlifyConn.token}`,
},
},
);
@@ -203,6 +207,125 @@ export function HeaderActionButtons({}: HeaderActionButtonsProps) {
}
};
+ const handleVercelDeploy = async () => {
+ if (!vercelConn.user || !vercelConn.token) {
+ toast.error('Please connect to Vercel first in the settings tab!');
+ return;
+ }
+
+ if (!currentChatId) {
+ toast.error('No active chat found');
+ return;
+ }
+
+ try {
+ setIsDeploying(true);
+ setDeployingTo('vercel');
+
+ const artifact = workbenchStore.firstArtifact;
+
+ if (!artifact) {
+ throw new Error('No active project found');
+ }
+
+ const actionId = 'build-' + Date.now();
+ const actionData: ActionCallbackData = {
+ messageId: 'vercel build',
+ artifactId: artifact.id,
+ actionId,
+ action: {
+ type: 'build' as const,
+ content: 'npm run build',
+ },
+ };
+
+ // Add the action first
+ artifact.runner.addAction(actionData);
+
+ // Then run it
+ await artifact.runner.runAction(actionData);
+
+ if (!artifact.runner.buildOutput) {
+ throw new Error('Build failed');
+ }
+
+ // Get the build files
+ const container = await webcontainer;
+
+ // Remove /home/project from buildPath if it exists
+ const buildPath = artifact.runner.buildOutput.path.replace('/home/project', '');
+
+ // Get all files recursively
+ async function getAllFiles(dirPath: string): Promise> {
+ const files: Record = {};
+ const entries = await container.fs.readdir(dirPath, { withFileTypes: true });
+
+ for (const entry of entries) {
+ const fullPath = path.join(dirPath, entry.name);
+
+ if (entry.isFile()) {
+ const content = await container.fs.readFile(fullPath, 'utf-8');
+
+ // Remove /dist prefix from the path
+ const deployPath = fullPath.replace(buildPath, '');
+ files[deployPath] = content;
+ } else if (entry.isDirectory()) {
+ const subFiles = await getAllFiles(fullPath);
+ Object.assign(files, subFiles);
+ }
+ }
+
+ return files;
+ }
+
+ const fileContents = await getAllFiles(buildPath);
+
+ // Use chatId instead of artifact.id
+ const existingProjectId = localStorage.getItem(`vercel-project-${currentChatId}`);
+
+ // Deploy using the API route with file contents
+ const response = await fetch('/api/vercel-deploy', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ projectId: existingProjectId || undefined,
+ files: fileContents,
+ token: vercelConn.token,
+ chatId: currentChatId,
+ }),
+ });
+
+ const data = (await response.json()) as any;
+
+ if (!response.ok || !data.deploy || !data.project) {
+ console.error('Invalid deploy response:', data);
+ throw new Error(data.error || 'Invalid deployment response');
+ }
+
+ // Store the project ID if it's a new project
+ if (data.project) {
+ localStorage.setItem(`vercel-project-${currentChatId}`, data.project.id);
+ }
+
+ toast.success(
+ ,
+ );
+ } catch (error) {
+ console.error('Vercel deploy error:', error);
+ toast.error(error instanceof Error ? error.message : 'Vercel deployment failed');
+ } finally {
+ setIsDeploying(false);
+ setDeployingTo(null);
+ }
+ };
+
return (
@@ -213,7 +336,7 @@ export function HeaderActionButtons({}: HeaderActionButtonsProps) {
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
className="px-4 hover:bg-bolt-elements-item-backgroundActive flex items-center gap-2"
>
- {isDeploying ? 'Deploying...' : 'Deploy'}
+ {isDeploying ? `Deploying to ${deployingTo}...` : 'Deploy'}
@@ -225,10 +348,10 @@ export function HeaderActionButtons({}: HeaderActionButtonsProps) {
Deploy to Cloudflare (Coming Soon)
diff --git a/app/lib/stores/vercel.ts b/app/lib/stores/vercel.ts
new file mode 100644
index 00000000..3258642c
--- /dev/null
+++ b/app/lib/stores/vercel.ts
@@ -0,0 +1,94 @@
+import { atom } from 'nanostores';
+import type { VercelConnection } from '~/types/vercel';
+import { logStore } from './logs';
+import { toast } from 'react-toastify';
+
+// Initialize with stored connection or defaults
+const storedConnection = typeof window !== 'undefined' ? localStorage.getItem('vercel_connection') : null;
+const initialConnection: VercelConnection = storedConnection
+ ? JSON.parse(storedConnection)
+ : {
+ user: null,
+ token: '',
+ stats: undefined,
+ };
+
+export const vercelConnection = atom
(initialConnection);
+export const isConnecting = atom(false);
+export const isFetchingStats = atom(false);
+
+export const updateVercelConnection = (updates: Partial) => {
+ const currentState = vercelConnection.get();
+ const newState = { ...currentState, ...updates };
+ vercelConnection.set(newState);
+
+ // Persist to localStorage
+ if (typeof window !== 'undefined') {
+ localStorage.setItem('vercel_connection', JSON.stringify(newState));
+ }
+};
+
+export async function fetchVercelStats(token: string) {
+ try {
+ isFetchingStats.set(true);
+
+ const projectsResponse = await fetch('https://api.vercel.com/v9/projects', {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ if (!projectsResponse.ok) {
+ throw new Error(`Failed to fetch projects: ${projectsResponse.status}`);
+ }
+
+ const projectsData = (await projectsResponse.json()) as any;
+ const projects = projectsData.projects || [];
+
+ // Fetch latest deployment for each project
+ const projectsWithDeployments = await Promise.all(
+ projects.map(async (project: any) => {
+ try {
+ const deploymentsResponse = await fetch(
+ `https://api.vercel.com/v6/deployments?projectId=${project.id}&limit=1`,
+ {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ 'Content-Type': 'application/json',
+ },
+ },
+ );
+
+ if (deploymentsResponse.ok) {
+ const deploymentsData = (await deploymentsResponse.json()) as any;
+ return {
+ ...project,
+ latestDeployments: deploymentsData.deployments || [],
+ };
+ }
+
+ return project;
+ } catch (error) {
+ console.error(`Error fetching deployments for project ${project.id}:`, error);
+ return project;
+ }
+ }),
+ );
+
+ const currentState = vercelConnection.get();
+ updateVercelConnection({
+ ...currentState,
+ stats: {
+ projects: projectsWithDeployments,
+ totalProjects: projectsWithDeployments.length,
+ },
+ });
+ } catch (error) {
+ console.error('Vercel API Error:', error);
+ logStore.logError('Failed to fetch Vercel stats', { error });
+ toast.error('Failed to fetch Vercel statistics');
+ } finally {
+ isFetchingStats.set(false);
+ }
+}
diff --git a/app/routes/api.deploy.ts b/app/routes/api.netlify-deploy.ts
similarity index 100%
rename from app/routes/api.deploy.ts
rename to app/routes/api.netlify-deploy.ts
diff --git a/app/routes/api.vercel-deploy.ts b/app/routes/api.vercel-deploy.ts
new file mode 100644
index 00000000..c9f6909d
--- /dev/null
+++ b/app/routes/api.vercel-deploy.ts
@@ -0,0 +1,248 @@
+import { type ActionFunctionArgs, type LoaderFunctionArgs, json } from '@remix-run/cloudflare';
+import type { VercelProjectInfo } from '~/types/vercel';
+
+// Add loader function to handle GET requests
+export async function loader({ request }: LoaderFunctionArgs) {
+ const url = new URL(request.url);
+ const projectId = url.searchParams.get('projectId');
+ const token = url.searchParams.get('token');
+
+ if (!projectId || !token) {
+ return json({ error: 'Missing projectId or token' }, { status: 400 });
+ }
+
+ try {
+ // Get project info
+ const projectResponse = await fetch(`https://api.vercel.com/v9/projects/${projectId}`, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ });
+
+ if (!projectResponse.ok) {
+ return json({ error: 'Failed to fetch project' }, { status: 400 });
+ }
+
+ const projectData = (await projectResponse.json()) as any;
+
+ // Get latest deployment
+ const deploymentsResponse = await fetch(`https://api.vercel.com/v6/deployments?projectId=${projectId}&limit=1`, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ });
+
+ if (!deploymentsResponse.ok) {
+ return json({ error: 'Failed to fetch deployments' }, { status: 400 });
+ }
+
+ const deploymentsData = (await deploymentsResponse.json()) as any;
+
+ const latestDeployment = deploymentsData.deployments?.[0];
+
+ return json({
+ project: {
+ id: projectData.id,
+ name: projectData.name,
+ url: `https://${projectData.name}.vercel.app`,
+ },
+ deploy: latestDeployment
+ ? {
+ id: latestDeployment.id,
+ state: latestDeployment.state,
+ url: latestDeployment.url ? `https://${latestDeployment.url}` : `https://${projectData.name}.vercel.app`,
+ }
+ : null,
+ });
+ } catch (error) {
+ console.error('Error fetching Vercel deployment:', error);
+ return json({ error: 'Failed to fetch deployment' }, { status: 500 });
+ }
+}
+
+interface DeployRequestBody {
+ projectId?: string;
+ files: Record;
+ chatId: string;
+}
+
+// Existing action function for POST requests
+export async function action({ request }: ActionFunctionArgs) {
+ try {
+ const { projectId, files, token, chatId } = (await request.json()) as DeployRequestBody & { token: string };
+
+ if (!token) {
+ return json({ error: 'Not connected to Vercel' }, { status: 401 });
+ }
+
+ let targetProjectId = projectId;
+ let projectInfo: VercelProjectInfo | undefined;
+
+ // If no projectId provided, create a new project
+ if (!targetProjectId) {
+ const projectName = `bolt-diy-${chatId}-${Date.now()}`;
+ const createProjectResponse = await fetch('https://api.vercel.com/v9/projects', {
+ method: 'POST',
+ headers: {
+ Authorization: `Bearer ${token}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ name: projectName,
+ framework: null,
+ }),
+ });
+
+ if (!createProjectResponse.ok) {
+ const errorData = (await createProjectResponse.json()) as any;
+ return json(
+ { error: `Failed to create project: ${errorData.error?.message || 'Unknown error'}` },
+ { status: 400 },
+ );
+ }
+
+ const newProject = (await createProjectResponse.json()) as any;
+ targetProjectId = newProject.id;
+ projectInfo = {
+ id: newProject.id,
+ name: newProject.name,
+ url: `https://${newProject.name}.vercel.app`,
+ chatId,
+ };
+ } else {
+ // Get existing project info
+ const projectResponse = await fetch(`https://api.vercel.com/v9/projects/${targetProjectId}`, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ });
+
+ if (projectResponse.ok) {
+ const existingProject = (await projectResponse.json()) as any;
+ projectInfo = {
+ id: existingProject.id,
+ name: existingProject.name,
+ url: `https://${existingProject.name}.vercel.app`,
+ chatId,
+ };
+ } else {
+ // If project doesn't exist, create a new one
+ const projectName = `bolt-diy-${chatId}-${Date.now()}`;
+ const createProjectResponse = await fetch('https://api.vercel.com/v9/projects', {
+ method: 'POST',
+ headers: {
+ Authorization: `Bearer ${token}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ name: projectName,
+ framework: null,
+ }),
+ });
+
+ if (!createProjectResponse.ok) {
+ const errorData = (await createProjectResponse.json()) as any;
+ return json(
+ { error: `Failed to create project: ${errorData.error?.message || 'Unknown error'}` },
+ { status: 400 },
+ );
+ }
+
+ const newProject = (await createProjectResponse.json()) as any;
+ targetProjectId = newProject.id;
+ projectInfo = {
+ id: newProject.id,
+ name: newProject.name,
+ url: `https://${newProject.name}.vercel.app`,
+ chatId,
+ };
+ }
+ }
+
+ // Prepare files for deployment
+ const deploymentFiles = [];
+
+ for (const [filePath, content] of Object.entries(files)) {
+ // Ensure file path doesn't start with a slash for Vercel
+ const normalizedPath = filePath.startsWith('/') ? filePath.substring(1) : filePath;
+ deploymentFiles.push({
+ file: normalizedPath,
+ data: content,
+ });
+ }
+
+ // Create a new deployment
+ const deployResponse = await fetch(`https://api.vercel.com/v13/deployments`, {
+ method: 'POST',
+ headers: {
+ Authorization: `Bearer ${token}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ name: projectInfo.name,
+ project: targetProjectId,
+ target: 'production',
+ files: deploymentFiles,
+ routes: [{ src: '/(.*)', dest: '/$1' }],
+ }),
+ });
+
+ if (!deployResponse.ok) {
+ const errorData = (await deployResponse.json()) as any;
+ return json(
+ { error: `Failed to create deployment: ${errorData.error?.message || 'Unknown error'}` },
+ { status: 400 },
+ );
+ }
+
+ const deployData = (await deployResponse.json()) as any;
+
+ // Poll for deployment status
+ let retryCount = 0;
+ const maxRetries = 60;
+ let deploymentUrl = '';
+ let deploymentState = '';
+
+ while (retryCount < maxRetries) {
+ const statusResponse = await fetch(`https://api.vercel.com/v13/deployments/${deployData.id}`, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ });
+
+ if (statusResponse.ok) {
+ const status = (await statusResponse.json()) as any;
+ deploymentState = status.readyState;
+ deploymentUrl = status.url ? `https://${status.url}` : '';
+
+ if (status.readyState === 'READY' || status.readyState === 'ERROR') {
+ break;
+ }
+ }
+
+ retryCount++;
+ await new Promise((resolve) => setTimeout(resolve, 2000));
+ }
+
+ if (deploymentState === 'ERROR') {
+ return json({ error: 'Deployment failed' }, { status: 500 });
+ }
+
+ if (retryCount >= maxRetries) {
+ return json({ error: 'Deployment timed out' }, { status: 500 });
+ }
+
+ return json({
+ success: true,
+ deploy: {
+ id: deployData.id,
+ state: deploymentState,
+ url: deploymentUrl || projectInfo.url,
+ },
+ project: projectInfo,
+ });
+ } catch (error) {
+ console.error('Vercel deploy error:', error);
+ return json({ error: 'Deployment failed' }, { status: 500 });
+ }
+}
diff --git a/app/types/vercel.ts b/app/types/vercel.ts
new file mode 100644
index 00000000..5a7d8db8
--- /dev/null
+++ b/app/types/vercel.ts
@@ -0,0 +1,40 @@
+export interface VercelUser {
+ user: any;
+ id: string;
+ username: string;
+ email: string;
+ name: string;
+ avatar?: string;
+}
+
+export interface VercelProject {
+ createdAt: string | number | Date;
+ targets: any;
+ id: string;
+ name: string;
+ framework?: string;
+ latestDeployments?: Array<{
+ id: string;
+ url: string;
+ created: number;
+ state: string;
+ }>;
+}
+
+export interface VercelStats {
+ projects: VercelProject[];
+ totalProjects: number;
+}
+
+export interface VercelConnection {
+ user: VercelUser | null;
+ token: string;
+ stats?: VercelStats;
+}
+
+export interface VercelProjectInfo {
+ id: string;
+ name: string;
+ url: string;
+ chatId: string;
+}