Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 11 additions & 9 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,19 @@ DATABASE_URL=postgresql://user:password@host/dbname?sslmode=require

# GitHub OAuth App
# Create at: https://github.com/settings/developers
GITHUB_CLIENT_ID=0v23li58m3t8TIbfIr8A
GITHUB_CLIENT_ID=your-github-client-id
# Optional fallback for older deployments. Client-side/public only, not a secret.
NEXT_PUBLIC_GITHUB_CLIENT_ID=Ov231iS8m3t8TIbfIr8A
GITHUB_CLIENT_SECRET=8643494973801aaade8e7cd77ee89d3700
2ce897
NEXT_PUBLIC_GITHUB_CLIENT_ID=your-github-client-id
GITHUB_CLIENT_SECRET=your-github-client-secret

# Public URL of your app (used for OAuth callback redirect)
NEXT_PUBLIC_APP_URL=https://repo-app-architect.vercel.app
NEXT_PUBLIC_APP_URL=your-app-url

# OpenAI API Key (used by Vercel AI SDK for analysis)
OPENAI_API_KEY=sk-...

# Anthropic API Key (used for scaffold generation)
# Anthropic API Key (used for analysis and scaffold generation)
ANTHROPIC_API_KEY=sk-ant-...

# Optional Claude model override for analysis
ANTHROPIC_ANALYSIS_MODEL=claude-3-5-sonnet-20241022

# Stripe Checkout
STRIPE_SECRET_KEY=sk_test_...
30 changes: 0 additions & 30 deletions app/api/analyses/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,6 @@
import { NextRequest, NextResponse } from 'next/server'
import { getAnalysisById, getBlueprintsByAnalysis, getRepositoriesForAnalysis } from '@/lib/queries'

export async function GET(
_request: NextRequest,
{ params }: { params: Promise<{ id: string }> },
) {
try {
const { id } = await params
const analysis = await getAnalysisById(id)
if (!analysis) {
return NextResponse.json({ error: 'Analysis not found' }, { status: 404 })
}

const [repositories, blueprints] = await Promise.all([
getRepositoriesForAnalysis(id),
getBlueprintsByAnalysis(id),
])

return NextResponse.json({
...analysis,
repositories,
blueprints,
apps: blueprints,
})
} catch (error) {
console.error('Error fetching analysis details:', error)
return NextResponse.json({ error: 'Failed to fetch analysis details' }, { status: 500 })
}
}
import { NextRequest, NextResponse } from 'next/server'
import { getAnalysisById, getBlueprintsByAnalysis, getRepositoriesForAnalysis } from '@/lib/queries'

export async function GET(
_request: NextRequest,
{ params }: { params: Promise<{ id: string }> },
Expand Down
77 changes: 44 additions & 33 deletions app/api/analyses/[id]/run/route.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { NextRequest } from 'next/server'
import { generateText, Output } from 'ai'
import { z } from 'zod'
import Anthropic from '@anthropic-ai/sdk'
import { getCurrentAccessToken } from '@/lib/auth'
import { getGitHubRepositoryTree } from '@/lib/github'
import {
getAnthropicAnalysisModel,
parseAnalysisBlueprintOutput,
} from '@/lib/analysis-blueprints'
import {
getAnalysisById,
getRepositoriesForAnalysis,
Expand All @@ -13,29 +16,6 @@ import {
getBlueprintsByAnalysis
} from '@/lib/queries'

// Schema for AI-generated app blueprints
const BlueprintSchema = z.object({
name: z.string(),
description: z.string(),
app_type: z.string(),
complexity: z.enum(['simple', 'moderate', 'complex']),
reuse_percentage: z.number().min(0).max(100),
existing_files: z.array(z.object({
path: z.string(),
purpose: z.string(),
})),
missing_files: z.array(z.object({
name: z.string(),
purpose: z.string(),
})),
technologies: z.array(z.string()),
explanation: z.string(),
})

const AnalysisOutputSchema = z.object({
blueprints: z.array(BlueprintSchema),
})

export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
Expand All @@ -57,8 +37,8 @@ export async function POST(
controller.close()
return
}
if (!process.env.OPENAI_API_KEY) {
send({ error: 'AI analysis is not configured. Missing OPENAI_API_KEY.' })
if (!process.env.ANTHROPIC_API_KEY) {
send({ error: 'AI analysis is not configured. Missing ANTHROPIC_API_KEY.' })
controller.close()
return
}
Expand Down Expand Up @@ -132,11 +112,18 @@ export async function POST(
// Build file summary for AI
const fileSummary = allFiles.map(f => `- ${f.repo}: ${f.path}`).join('\n')

const anthropic = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY,
})

// Use AI to analyze and discover app blueprints
const { output } = await generateText({
model: 'openai/gpt-4o-mini',
output: Output.object({ schema: AnalysisOutputSchema }),
prompt: `You are an expert software architect. Analyze these files from GitHub repositories and discover what applications can be built by combining and reusing the existing code.
const response = await anthropic.messages.create({
model: getAnthropicAnalysisModel(),
max_tokens: 4000,
temperature: 0.2,
messages: [{
role: 'user',
content: `You are an expert software architect. Analyze these files from GitHub repositories and discover what applications can be built by combining and reusing the existing code.

REPOSITORIES AND FILES:
${fileSummary}
Expand All @@ -155,8 +142,31 @@ For each app blueprint:
- List technologies detected
- Provide a brief explanation of why this app is possible

Return only valid JSON with this shape:
{
"blueprints": [
{
"name": "string",
"description": "string",
"app_type": "string",
"complexity": "simple" | "moderate" | "complex",
"reuse_percentage": 0-100,
"existing_files": [{ "path": "repo/path.ext", "purpose": "string" }],
"missing_files": [{ "name": "path-or-feature", "purpose": "string" }],
"technologies": ["string"],
"explanation": "string"
}
]
}

Focus on practical, buildable applications based on the actual code patterns you see.`,
}],
})
const text = response.content
.filter((block) => block.type === 'text')
.map((block) => block.text)
.join('\n')
const output = parseAnalysisBlueprintOutput(text)

send({ status: 'analyzing', progress: 80 })

Expand Down Expand Up @@ -190,10 +200,11 @@ Focus on practical, buildable applications based on the actual code patterns you

} catch (error) {
console.error('Analysis error:', error)
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
await updateAnalysisStatus(id, 'failed', {
error_message: error instanceof Error ? error.message : 'Unknown error'
error_message: errorMessage
})
send({ status: 'failed', error: 'Analysis failed' })
send({ status: 'failed', error: errorMessage })
controller.close()
}
},
Expand Down
63 changes: 63 additions & 0 deletions app/api/stripe/checkout/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { NextRequest, NextResponse } from 'next/server'
import Stripe from 'stripe'

import { getPlanById } from '@/lib/billing'

const stripeSecretKey = process.env.STRIPE_SECRET_KEY
const stripe = stripeSecretKey ? new Stripe(stripeSecretKey) : null

function getBaseUrl(request: NextRequest) {
return process.env.NEXT_PUBLIC_APP_URL || request.nextUrl.origin
}

export async function POST(request: NextRequest) {
try {
const { planId } = (await request.json()) as { planId?: string }
const plan = getPlanById(planId)

if (!plan) {
return NextResponse.json({ error: 'Unknown billing plan.' }, { status: 400 })
}

if (!stripe || !process.env.STRIPE_SECRET_KEY) {
return NextResponse.json(
{ error: 'Stripe is not configured. Set STRIPE_SECRET_KEY to enable Checkout.' },
{ status: 503 },
)
}

const baseUrl = getBaseUrl(request)
const session = await stripe.checkout.sessions.create({
mode: 'subscription',
billing_address_collection: 'auto',
allow_promotion_codes: true,
line_items: [
{
price_data: {
currency: plan.currency,
recurring: { interval: plan.interval },
unit_amount: plan.unitAmount,
product_data: {
name: plan.name,
description: plan.description,
metadata: {
plan_id: plan.id,
},
},
},
quantity: 1,
},
],
metadata: {
plan_id: plan.id,
},
success_url: `${baseUrl}/billing/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${baseUrl}/billing/cancel`,
})

return NextResponse.json({ url: session.url })
} catch (error) {
console.error('Error creating Stripe Checkout session:', error)
return NextResponse.json({ error: 'Failed to create Checkout session.' }, { status: 500 })
}
}
43 changes: 43 additions & 0 deletions app/billing/cancel/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import Link from 'next/link'
import { ArrowLeft, CreditCard, RefreshCcw } from 'lucide-react'

import { AppLogo } from '@/components/app-logo'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'

export default function BillingCancelPage() {
return (
<main className="min-h-screen bg-background px-4 py-12">
<div className="mx-auto max-w-2xl space-y-8">
<AppLogo />
<Card className="border-border/80 bg-card/80 shadow-lg">
<CardHeader>
<div className="mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-muted">
<RefreshCcw className="h-6 w-6" />
</div>
<CardTitle className="text-3xl">Checkout canceled</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<p className="text-muted-foreground">
No payment was completed. You can return to pricing and start a new secure Stripe Checkout session whenever you are ready.
</p>
<div className="flex flex-col gap-3 sm:flex-row">
<Button asChild>
<Link href="/#pricing">
<CreditCard className="h-4 w-4" />
Back to pricing
</Link>
</Button>
<Button variant="outline" asChild>
<Link href="/">
<ArrowLeft className="h-4 w-4" />
Return home
</Link>
</Button>
</div>
</CardContent>
</Card>
</div>
</main>
)
}
44 changes: 44 additions & 0 deletions app/billing/success/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import Link from 'next/link'
import { CheckCircle2, ArrowRight } from 'lucide-react'

import { Button } from '@/components/ui/button'
import { Card } from '@/components/ui/card'

export default async function BillingSuccessPage({
searchParams,
}: {
searchParams: Promise<{ session_id?: string }>
}) {
const { session_id } = await searchParams

return (
<main className="min-h-screen bg-background px-4 py-16">
<Card className="mx-auto max-w-2xl p-8 text-center">
<div className="mx-auto mb-6 flex h-14 w-14 items-center justify-center rounded-full bg-primary/10">
<CheckCircle2 className="h-7 w-7 text-primary" />
</div>
<h1 className="text-3xl font-bold tracking-tight">You are on the Pro launch path.</h1>
<p className="mt-4 text-muted-foreground">
Stripe confirmed the checkout redirect. Once webhooks are connected, this is where CodeVault can
activate billing entitlements and show plan details.
</p>
{session_id ? (
<p className="mt-4 rounded-lg bg-muted px-3 py-2 text-xs text-muted-foreground">
Checkout session: <span className="font-mono text-foreground">{session_id}</span>
</p>
) : null}
<div className="mt-8 flex flex-col justify-center gap-3 sm:flex-row">
<Button asChild>
<Link href="/dashboard">
Open dashboard
<ArrowRight className="h-4 w-4" />
</Link>
</Button>
<Button variant="outline" asChild>
<Link href="/">Back to homepage</Link>
</Button>
</div>
</Card>
</main>
)
}
Loading