From ca183332c54413b2fc47a91c6e77baada231ab34 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 11 Oct 2025 21:25:06 -0500 Subject: [PATCH 1/4] add vercel preview link --- app/api/tasks/[taskId]/deployment/route.ts | 301 +++++++++++++++++++++ components/icons/vercel-icon.tsx | 8 + components/task-details.tsx | 45 +++ 3 files changed, 354 insertions(+) create mode 100644 app/api/tasks/[taskId]/deployment/route.ts create mode 100644 components/icons/vercel-icon.tsx diff --git a/app/api/tasks/[taskId]/deployment/route.ts b/app/api/tasks/[taskId]/deployment/route.ts new file mode 100644 index 0000000..7b2cfd5 --- /dev/null +++ b/app/api/tasks/[taskId]/deployment/route.ts @@ -0,0 +1,301 @@ +import { NextRequest, NextResponse } from 'next/server' +import { getServerSession } from '@/lib/session/get-server-session' +import { db } from '@/lib/db/client' +import { tasks } from '@/lib/db/schema' +import { eq } from 'drizzle-orm' +import { getOctokit } from '@/lib/github/client' + +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ taskId: string }> }, +) { + try { + const session = await getServerSession() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const { taskId } = await params + + // Get task from database + const task = await db.query.tasks.findFirst({ + where: eq(tasks.id, taskId), + }) + + if (!task) { + return NextResponse.json({ error: 'Task not found' }, { status: 404 }) + } + + // Verify task belongs to user + if (task.userId !== session.user.id) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) + } + + // Return early if no branch or repo + if (!task.branchName || !task.repoUrl) { + return NextResponse.json({ + success: true, + data: { + hasDeployment: false, + message: 'Task does not have branch or repository information', + }, + }) + } + + // Parse GitHub repository URL to get owner and repo + const githubMatch = task.repoUrl.match(/github\.com\/([^\/]+)\/([^\/\.]+)/) + if (!githubMatch) { + return NextResponse.json({ + success: true, + data: { + hasDeployment: false, + message: 'Invalid GitHub repository URL', + }, + }) + } + + const [, owner, repo] = githubMatch + + try { + const octokit = await getOctokit() + + // Check if user has GitHub connected + if (!octokit.auth) { + return NextResponse.json({ + success: true, + data: { + hasDeployment: false, + message: 'GitHub account not connected', + }, + }) + } + + // First, get the latest commit on the branch to check for deployment checks + let latestCommitSha: string | null = null + try { + const { data: branch } = await octokit.rest.repos.getBranch({ + owner, + repo, + branch: task.branchName, + }) + latestCommitSha = branch.commit.sha + } catch (branchError: any) { + if (branchError.status === 404) { + return NextResponse.json({ + success: true, + data: { + hasDeployment: false, + message: 'Branch not found', + }, + }) + } + throw branchError + } + + // Check for Vercel deployment via GitHub Checks API (most common) + if (latestCommitSha) { + try { + const { data: checkRuns } = await octokit.rest.checks.listForRef({ + owner, + repo, + ref: latestCommitSha, + per_page: 100, + }) + + // Helper function to extract preview URL from check output + const extractPreviewUrl = (check: any): string | null => { + // Check output summary for deployment URL + if (check.output?.summary) { + const summary = check.output.summary + // Look for URLs in the format https://[deployment]-[hash].vercel.app or other Vercel domains + const urlMatch = summary.match(/https?:\/\/[^\s\)\]<]+\.vercel\.app/i) + if (urlMatch) { + return urlMatch[0] + } + } + + // Check output text as well + if (check.output?.text) { + const text = check.output.text + const urlMatch = text.match(/https?:\/\/[^\s\)\]<]+\.vercel\.app/i) + if (urlMatch) { + return urlMatch[0] + } + } + + return null + } + + // Look for Vercel check runs - try Preview Comments first as it's more likely to have the URL + const vercelPreviewCheck = checkRuns.check_runs.find( + (check) => + check.app?.slug === 'vercel' && + check.name === 'Vercel Preview Comments' && + check.status === 'completed', + ) + + const vercelDeploymentCheck = checkRuns.check_runs.find( + (check) => + check.app?.slug === 'vercel' && + check.name === 'Vercel' && + check.conclusion === 'success' && + check.status === 'completed', + ) + + // Try to get preview URL from either check + let previewUrl: string | null = null + + if (vercelPreviewCheck) { + previewUrl = extractPreviewUrl(vercelPreviewCheck) + } + + if (!previewUrl && vercelDeploymentCheck) { + previewUrl = extractPreviewUrl(vercelDeploymentCheck) + } + + // Fallback to details_url if no preview URL found + if (!previewUrl && vercelDeploymentCheck?.details_url) { + previewUrl = vercelDeploymentCheck.details_url + } + + if (previewUrl) { + return NextResponse.json({ + success: true, + data: { + hasDeployment: true, + previewUrl, + checkId: vercelDeploymentCheck?.id || vercelPreviewCheck?.id, + createdAt: vercelDeploymentCheck?.completed_at || vercelPreviewCheck?.completed_at, + }, + }) + } + } catch (checksError) { + console.error('Error checking GitHub Checks:', checksError) + // Continue to try other methods + } + } + + // Fallback: Check GitHub Deployments API + try { + const { data: deployments } = await octokit.rest.repos.listDeployments({ + owner, + repo, + ref: task.branchName, + per_page: 10, + }) + + if (deployments && deployments.length > 0) { + // Find the most recent Vercel deployment + for (const deployment of deployments) { + // Check if this is a Vercel deployment + if ( + deployment.environment === 'Preview' || + deployment.environment === 'preview' || + deployment.description?.toLowerCase().includes('vercel') + ) { + // Get deployment status + const { data: statuses } = await octokit.rest.repos.listDeploymentStatuses({ + owner, + repo, + deployment_id: deployment.id, + per_page: 1, + }) + + if (statuses && statuses.length > 0) { + const status = statuses[0] + if (status.state === 'success') { + const previewUrl = status.environment_url || status.target_url + if (previewUrl) { + return NextResponse.json({ + success: true, + data: { + hasDeployment: true, + previewUrl, + deploymentId: deployment.id, + createdAt: deployment.created_at, + }, + }) + } + } + } + } + } + } + } catch (deploymentsError) { + console.error('Error checking GitHub Deployments:', deploymentsError) + // Continue to final fallback + } + + // Final fallback: Check commit statuses + if (latestCommitSha) { + try { + const { data: statuses } = await octokit.rest.repos.listCommitStatusesForRef({ + owner, + repo, + ref: latestCommitSha, + per_page: 100, + }) + + const vercelStatus = statuses.find( + (status) => + status.context?.toLowerCase().includes('vercel') && + status.state === 'success' && + status.target_url, + ) + + if (vercelStatus && vercelStatus.target_url) { + return NextResponse.json({ + success: true, + data: { + hasDeployment: true, + previewUrl: vercelStatus.target_url, + createdAt: vercelStatus.created_at, + }, + }) + } + } catch (statusError) { + console.error('Error checking commit statuses:', statusError) + } + } + + // No deployment found + return NextResponse.json({ + success: true, + data: { + hasDeployment: false, + message: 'No successful Vercel deployment found', + }, + }) + } catch (error: any) { + console.error('Error fetching deployment status:', error) + + // Return graceful response for common errors + if (error.status === 404) { + return NextResponse.json({ + success: true, + data: { + hasDeployment: false, + message: 'Branch or repository not found', + }, + }) + } + + return NextResponse.json({ + success: true, + data: { + hasDeployment: false, + message: 'Failed to fetch deployment status', + }, + }) + } + } catch (error) { + console.error('Error in deployment API:', error) + return NextResponse.json( + { + error: error instanceof Error ? error.message : 'Internal server error', + }, + { status: 500 }, + ) + } +} + diff --git a/components/icons/vercel-icon.tsx b/components/icons/vercel-icon.tsx new file mode 100644 index 0000000..8815254 --- /dev/null +++ b/components/icons/vercel-icon.tsx @@ -0,0 +1,8 @@ +export default function VercelIcon({ className = 'h-6 w-6' }: { className?: string }) { + return ( + + + + ) +} + diff --git a/components/task-details.tsx b/components/task-details.tsx index b3c259e..cdb8421 100644 --- a/components/task-details.tsx +++ b/components/task-details.tsx @@ -44,6 +44,7 @@ import LinearIcon from '@/components/icons/linear-icon' import NotionIcon from '@/components/icons/notion-icon' import PlaywrightIcon from '@/components/icons/playwright-icon' import SupabaseIcon from '@/components/icons/supabase-icon' +import VercelIcon from '@/components/icons/vercel-icon' interface TaskDetailsProps { task: Task @@ -124,6 +125,8 @@ export function TaskDetails({ task }: TaskDetailsProps) { const [isTryingAgain, setIsTryingAgain] = useState(false) const [selectedAgent, setSelectedAgent] = useState(task.selectedAgent || 'claude') const [selectedModel, setSelectedModel] = useState(task.selectedModel || DEFAULT_MODELS.claude) + const [deploymentUrl, setDeploymentUrl] = useState(null) + const [loadingDeployment, setLoadingDeployment] = useState(false) const { refreshTasks } = useTasks() const router = useRouter() @@ -285,6 +288,33 @@ export function TaskDetails({ task }: TaskDetailsProps) { // eslint-disable-next-line react-hooks/exhaustive-deps }, [JSON.stringify(task.mcpServerIds)]) + // Fetch deployment info when task is completed and has a branch + useEffect(() => { + async function fetchDeployment() { + if (currentStatus !== 'completed' || !task.branchName) { + return + } + + setLoadingDeployment(true) + + try { + const response = await fetch(`/api/tasks/${task.id}/deployment`) + if (response.ok) { + const result = await response.json() + if (result.success && result.data.hasDeployment && result.data.previewUrl) { + setDeploymentUrl(result.data.previewUrl) + } + } + } catch (error) { + console.error('Failed to fetch deployment info:', error) + } finally { + setLoadingDeployment(false) + } + } + + fetchDeployment() + }, [task.id, task.branchName, currentStatus]) + // Fetch all diffs when files list changes const fetchAllDiffs = async (filesList: string[]) => { if (!filesList.length || loadingDiffsRef.current) return @@ -631,6 +661,21 @@ export function TaskDetails({ task }: TaskDetailsProps) { )} + + {/* Preview Deployment */} + {!loadingDeployment && deploymentUrl && ( +
+ + + Preview + +
+ )} From 90e121f414cbd2c24fa221a8a15572bb4c884600 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 11 Oct 2025 21:25:11 -0500 Subject: [PATCH 2/4] format --- app/api/tasks/[taskId]/deployment/route.ts | 14 +++----------- components/icons/vercel-icon.tsx | 1 - 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/app/api/tasks/[taskId]/deployment/route.ts b/app/api/tasks/[taskId]/deployment/route.ts index 7b2cfd5..9db0618 100644 --- a/app/api/tasks/[taskId]/deployment/route.ts +++ b/app/api/tasks/[taskId]/deployment/route.ts @@ -5,10 +5,7 @@ import { tasks } from '@/lib/db/schema' import { eq } from 'drizzle-orm' import { getOctokit } from '@/lib/github/client' -export async function GET( - request: NextRequest, - { params }: { params: Promise<{ taskId: string }> }, -) { +export async function GET(request: NextRequest, { params }: { params: Promise<{ taskId: string }> }) { try { const session = await getServerSession() if (!session?.user?.id) { @@ -129,9 +126,7 @@ export async function GET( // Look for Vercel check runs - try Preview Comments first as it's more likely to have the URL const vercelPreviewCheck = checkRuns.check_runs.find( (check) => - check.app?.slug === 'vercel' && - check.name === 'Vercel Preview Comments' && - check.status === 'completed', + check.app?.slug === 'vercel' && check.name === 'Vercel Preview Comments' && check.status === 'completed', ) const vercelDeploymentCheck = checkRuns.check_runs.find( @@ -238,9 +233,7 @@ export async function GET( const vercelStatus = statuses.find( (status) => - status.context?.toLowerCase().includes('vercel') && - status.state === 'success' && - status.target_url, + status.context?.toLowerCase().includes('vercel') && status.state === 'success' && status.target_url, ) if (vercelStatus && vercelStatus.target_url) { @@ -298,4 +291,3 @@ export async function GET( ) } } - diff --git a/components/icons/vercel-icon.tsx b/components/icons/vercel-icon.tsx index 8815254..527cf50 100644 --- a/components/icons/vercel-icon.tsx +++ b/components/icons/vercel-icon.tsx @@ -5,4 +5,3 @@ export default function VercelIcon({ className = 'h-6 w-6' }: { className?: stri ) } - From 62b0a841f41ee82ee6ce90f12e488bc999a2b625 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 11 Oct 2025 21:28:56 -0500 Subject: [PATCH 3/4] fix type error --- app/api/tasks/[taskId]/deployment/route.ts | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/app/api/tasks/[taskId]/deployment/route.ts b/app/api/tasks/[taskId]/deployment/route.ts index 9db0618..321c31a 100644 --- a/app/api/tasks/[taskId]/deployment/route.ts +++ b/app/api/tasks/[taskId]/deployment/route.ts @@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from 'next/server' import { getServerSession } from '@/lib/session/get-server-session' import { db } from '@/lib/db/client' import { tasks } from '@/lib/db/schema' -import { eq } from 'drizzle-orm' +import { eq, and, isNull } from 'drizzle-orm' import { getOctokit } from '@/lib/github/client' export async function GET(request: NextRequest, { params }: { params: Promise<{ taskId: string }> }) { @@ -15,19 +15,18 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ const { taskId } = await params // Get task from database - const task = await db.query.tasks.findFirst({ - where: eq(tasks.id, taskId), - }) + const taskResult = await db + .select() + .from(tasks) + .where(and(eq(tasks.id, taskId), eq(tasks.userId, session.user.id), isNull(tasks.deletedAt))) + .limit(1) + + const task = taskResult[0] if (!task) { return NextResponse.json({ error: 'Task not found' }, { status: 404 }) } - // Verify task belongs to user - if (task.userId !== session.user.id) { - return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) - } - // Return early if no branch or repo if (!task.branchName || !task.repoUrl) { return NextResponse.json({ From 3bcd7496cf03883635ee561198277dd759dd1542 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 11 Oct 2025 21:32:55 -0500 Subject: [PATCH 4/4] store previewUrl --- app/api/tasks/[taskId]/deployment/route.ts | 33 +- components/app-layout.tsx | 1 + components/task-details.tsx | 16 +- .../migrations/0012_burly_captain_britain.sql | 1 + lib/db/migrations/meta/0012_snapshot.json | 603 ++++++++++++++++++ lib/db/migrations/meta/_journal.json | 7 + lib/db/schema.ts | 3 + 7 files changed, 655 insertions(+), 9 deletions(-) create mode 100644 lib/db/migrations/0012_burly_captain_britain.sql create mode 100644 lib/db/migrations/meta/0012_snapshot.json diff --git a/app/api/tasks/[taskId]/deployment/route.ts b/app/api/tasks/[taskId]/deployment/route.ts index 321c31a..6d8ecd8 100644 --- a/app/api/tasks/[taskId]/deployment/route.ts +++ b/app/api/tasks/[taskId]/deployment/route.ts @@ -27,6 +27,18 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ return NextResponse.json({ error: 'Task not found' }, { status: 404 }) } + // Return cached preview URL if available + if (task.previewUrl) { + return NextResponse.json({ + success: true, + data: { + hasDeployment: true, + previewUrl: task.previewUrl, + cached: true, + }, + }) + } + // Return early if no branch or repo if (!task.branchName || !task.repoUrl) { return NextResponse.json({ @@ -75,8 +87,8 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ branch: task.branchName, }) latestCommitSha = branch.commit.sha - } catch (branchError: any) { - if (branchError.status === 404) { + } catch (branchError) { + if (branchError && typeof branchError === 'object' && 'status' in branchError && branchError.status === 404) { return NextResponse.json({ success: true, data: { @@ -99,7 +111,9 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ }) // Helper function to extract preview URL from check output - const extractPreviewUrl = (check: any): string | null => { + const extractPreviewUrl = (check: { + output?: { summary?: string | null; text?: string | null } | null + }): string | null => { // Check output summary for deployment URL if (check.output?.summary) { const summary = check.output.summary @@ -153,6 +167,9 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ } if (previewUrl) { + // Store the preview URL in the database + await db.update(tasks).set({ previewUrl }).where(eq(tasks.id, taskId)) + return NextResponse.json({ success: true, data: { @@ -200,6 +217,9 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ if (status.state === 'success') { const previewUrl = status.environment_url || status.target_url if (previewUrl) { + // Store the preview URL in the database + await db.update(tasks).set({ previewUrl }).where(eq(tasks.id, taskId)) + return NextResponse.json({ success: true, data: { @@ -236,6 +256,9 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ ) if (vercelStatus && vercelStatus.target_url) { + // Store the preview URL in the database + await db.update(tasks).set({ previewUrl: vercelStatus.target_url }).where(eq(tasks.id, taskId)) + return NextResponse.json({ success: true, data: { @@ -258,11 +281,11 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ message: 'No successful Vercel deployment found', }, }) - } catch (error: any) { + } catch (error) { console.error('Error fetching deployment status:', error) // Return graceful response for common errors - if (error.status === 404) { + if (error && typeof error === 'object' && 'status' in error && error.status === 404) { return NextResponse.json({ success: true, data: { diff --git a/components/app-layout.tsx b/components/app-layout.tsx index 117feff..e09a167 100644 --- a/components/app-layout.tsx +++ b/components/app-layout.tsx @@ -227,6 +227,7 @@ export function AppLayout({ children, initialSidebarWidth, initialSidebarOpen, i error: null, branchName: null, sandboxUrl: null, + previewUrl: null, mcpServerIds: null, createdAt: new Date(), updatedAt: new Date(), diff --git a/components/task-details.tsx b/components/task-details.tsx index cdb8421..0aa4e28 100644 --- a/components/task-details.tsx +++ b/components/task-details.tsx @@ -125,7 +125,7 @@ export function TaskDetails({ task }: TaskDetailsProps) { const [isTryingAgain, setIsTryingAgain] = useState(false) const [selectedAgent, setSelectedAgent] = useState(task.selectedAgent || 'claude') const [selectedModel, setSelectedModel] = useState(task.selectedModel || DEFAULT_MODELS.claude) - const [deploymentUrl, setDeploymentUrl] = useState(null) + const [deploymentUrl, setDeploymentUrl] = useState(task.previewUrl || null) const [loadingDeployment, setLoadingDeployment] = useState(false) const { refreshTasks } = useTasks() const router = useRouter() @@ -288,10 +288,11 @@ export function TaskDetails({ task }: TaskDetailsProps) { // eslint-disable-next-line react-hooks/exhaustive-deps }, [JSON.stringify(task.mcpServerIds)]) - // Fetch deployment info when task is completed and has a branch + // Fetch deployment info when task is completed and has a branch (only if not already cached) useEffect(() => { async function fetchDeployment() { - if (currentStatus !== 'completed' || !task.branchName) { + // Skip if we already have a preview URL or task isn't ready + if (deploymentUrl || currentStatus !== 'completed' || !task.branchName) { return } @@ -313,7 +314,14 @@ export function TaskDetails({ task }: TaskDetailsProps) { } fetchDeployment() - }, [task.id, task.branchName, currentStatus]) + }, [task.id, task.branchName, currentStatus, deploymentUrl]) + + // Update deploymentUrl when task.previewUrl changes + useEffect(() => { + if (task.previewUrl && task.previewUrl !== deploymentUrl) { + setDeploymentUrl(task.previewUrl) + } + }, [task.previewUrl, deploymentUrl]) // Fetch all diffs when files list changes const fetchAllDiffs = async (filesList: string[]) => { diff --git a/lib/db/migrations/0012_burly_captain_britain.sql b/lib/db/migrations/0012_burly_captain_britain.sql new file mode 100644 index 0000000..e8b87f9 --- /dev/null +++ b/lib/db/migrations/0012_burly_captain_britain.sql @@ -0,0 +1 @@ +ALTER TABLE "tasks" ADD COLUMN "preview_url" text; \ No newline at end of file diff --git a/lib/db/migrations/meta/0012_snapshot.json b/lib/db/migrations/meta/0012_snapshot.json new file mode 100644 index 0000000..6d10d81 --- /dev/null +++ b/lib/db/migrations/meta/0012_snapshot.json @@ -0,0 +1,603 @@ +{ + "id": "b133eb97-a7a9-4ac9-a467-21632a8cf5a1", + "prevId": "9df31f87-de44-4c99-b222-900db0ab4d0a", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.accounts": { + "name": "accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "external_user_id": { + "name": "external_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "accounts_user_id_provider_idx": { + "name": "accounts_user_id_provider_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "accounts_user_id_users_id_fk": { + "name": "accounts_user_id_users_id_fk", + "tableFrom": "accounts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.connectors": { + "name": "connectors", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'remote'" + }, + "base_url": { + "name": "base_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oauth_client_secret": { + "name": "oauth_client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'disconnected'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "connectors_user_id_users_id_fk": { + "name": "connectors_user_id_users_id_fk", + "tableFrom": "connectors", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.keys": { + "name": "keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "keys_user_id_provider_idx": { + "name": "keys_user_id_provider_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "keys_user_id_users_id_fk": { + "name": "keys_user_id_users_id_fk", + "tableFrom": "keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tasks": { + "name": "tasks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repo_url": { + "name": "repo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "selected_agent": { + "name": "selected_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'claude'" + }, + "selected_model": { + "name": "selected_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "install_dependencies": { + "name": "install_dependencies", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "max_duration": { + "name": "max_duration", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 5 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "progress": { + "name": "progress", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "logs": { + "name": "logs", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "branch_name": { + "name": "branch_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sandbox_url": { + "name": "sandbox_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "preview_url": { + "name": "preview_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mcp_server_ids": { + "name": "mcp_server_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "tasks_user_id_users_id_fk": { + "name": "tasks_user_id_users_id_fk", + "tableFrom": "tasks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_login_at": { + "name": "last_login_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "users_provider_external_id_idx": { + "name": "users_provider_external_id_idx", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/lib/db/migrations/meta/_journal.json b/lib/db/migrations/meta/_journal.json index 51decca..c18c687 100644 --- a/lib/db/migrations/meta/_journal.json +++ b/lib/db/migrations/meta/_journal.json @@ -85,6 +85,13 @@ "when": 1760171942092, "tag": "0011_outstanding_punisher", "breakpoints": true + }, + { + "idx": 12, + "version": "7", + "when": 1760236167307, + "tag": "0012_burly_captain_britain", + "breakpoints": true } ] } \ No newline at end of file diff --git a/lib/db/schema.ts b/lib/db/schema.ts index 92e3175..a3c4016 100644 --- a/lib/db/schema.ts +++ b/lib/db/schema.ts @@ -94,6 +94,7 @@ export const tasks = pgTable('tasks', { error: text('error'), branchName: text('branch_name'), sandboxUrl: text('sandbox_url'), + previewUrl: text('preview_url'), mcpServerIds: jsonb('mcp_server_ids').$type(), createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull(), @@ -117,6 +118,7 @@ export const insertTaskSchema = z.object({ error: z.string().optional(), branchName: z.string().optional(), sandboxUrl: z.string().optional(), + previewUrl: z.string().optional(), mcpServerIds: z.array(z.string()).optional(), createdAt: z.date().optional(), updatedAt: z.date().optional(), @@ -139,6 +141,7 @@ export const selectTaskSchema = z.object({ error: z.string().nullable(), branchName: z.string().nullable(), sandboxUrl: z.string().nullable(), + previewUrl: z.string().nullable(), mcpServerIds: z.array(z.string()).nullable(), createdAt: z.date(), updatedAt: z.date(),