feat: initial CodeBoard monorepo scaffold
Turborepo monorepo with npm workspaces: - apps/web: Next.js 14 frontend with Tailwind v4, SSE progress, doc viewer - apps/worker: BullMQ job processor (clone → parse → LLM generate) - packages/shared: TypeScript types - packages/parser: Babel-based AST parser (JS/TS) + regex (Python) - packages/llm: OpenAI/Anthropic provider abstraction + prompt pipeline - packages/diagrams: Mermaid architecture & dependency graph generators - packages/database: Prisma schema (PostgreSQL) - Docker multi-stage build (web + worker targets) All packages compile successfully with tsc and next build.
This commit is contained in:
14
.dockerignore
Normal file
14
.dockerignore
Normal file
@@ -0,0 +1,14 @@
|
||||
node_modules
|
||||
.next
|
||||
dist
|
||||
.turbo
|
||||
*.tsbuildinfo
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
coverage
|
||||
.DS_Store
|
||||
tmp
|
||||
.vercel
|
||||
*.log
|
||||
.git
|
||||
8
.env.example
Normal file
8
.env.example
Normal file
@@ -0,0 +1,8 @@
|
||||
DATABASE_URL=postgresql://codeboard:codeboard@localhost:5432/codeboard
|
||||
REDIS_URL=redis://localhost:6379
|
||||
OPENAI_API_KEY=
|
||||
ANTHROPIC_API_KEY=
|
||||
GITHUB_CLIENT_ID=
|
||||
GITHUB_CLIENT_SECRET=
|
||||
NEXTAUTH_SECRET=
|
||||
NEXTAUTH_URL=http://localhost:3000
|
||||
144
.gitignore
vendored
144
.gitignore
vendored
@@ -1,138 +1,14 @@
|
||||
# ---> Node
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
.next/
|
||||
dist/
|
||||
.turbo/
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
.stylelintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
.temp
|
||||
.cache
|
||||
|
||||
# vitepress build output
|
||||
**/.vitepress/dist
|
||||
|
||||
# vitepress cache directory
|
||||
**/.vitepress/cache
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
.env.*.local
|
||||
coverage/
|
||||
.DS_Store
|
||||
tmp/
|
||||
.vercel
|
||||
*.log
|
||||
generated/
|
||||
|
||||
48
Dockerfile
Normal file
48
Dockerfile
Normal file
@@ -0,0 +1,48 @@
|
||||
FROM node:20-alpine AS base
|
||||
RUN apk add --no-cache git
|
||||
WORKDIR /app
|
||||
|
||||
FROM base AS deps
|
||||
COPY package.json package-lock.json* ./
|
||||
COPY apps/web/package.json ./apps/web/
|
||||
COPY apps/worker/package.json ./apps/worker/
|
||||
COPY packages/shared/package.json ./packages/shared/
|
||||
COPY packages/parser/package.json ./packages/parser/
|
||||
COPY packages/llm/package.json ./packages/llm/
|
||||
COPY packages/diagrams/package.json ./packages/diagrams/
|
||||
COPY packages/database/package.json ./packages/database/
|
||||
RUN npm install --production=false
|
||||
|
||||
FROM base AS builder
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
RUN npx turbo build
|
||||
|
||||
FROM base AS web
|
||||
RUN addgroup --system --gid 1001 nodejs && \
|
||||
adduser --system --uid 1001 nextjs
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/.next/standalone ./
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/.next/static ./apps/web/.next/static
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/public ./apps/web/public
|
||||
USER nextjs
|
||||
EXPOSE 3000
|
||||
ENV PORT=3000 HOSTNAME="0.0.0.0"
|
||||
CMD ["node", "apps/web/server.js"]
|
||||
|
||||
FROM base AS worker
|
||||
RUN addgroup --system --gid 1001 nodejs && \
|
||||
adduser --system --uid 1001 workeruser
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY --from=builder /app/apps/worker/dist ./apps/worker/dist
|
||||
COPY --from=builder /app/packages/shared/dist ./packages/shared/dist
|
||||
COPY --from=builder /app/packages/parser/dist ./packages/parser/dist
|
||||
COPY --from=builder /app/packages/llm/dist ./packages/llm/dist
|
||||
COPY --from=builder /app/packages/diagrams/dist ./packages/diagrams/dist
|
||||
COPY --from=builder /app/package.json ./
|
||||
COPY --from=builder /app/apps/worker/package.json ./apps/worker/
|
||||
COPY --from=builder /app/packages/shared/package.json ./packages/shared/
|
||||
COPY --from=builder /app/packages/parser/package.json ./packages/parser/
|
||||
COPY --from=builder /app/packages/llm/package.json ./packages/llm/
|
||||
COPY --from=builder /app/packages/diagrams/package.json ./packages/diagrams/
|
||||
USER workeruser
|
||||
CMD ["node", "apps/worker/dist/index.js"]
|
||||
59
README.md
59
README.md
@@ -1,3 +1,60 @@
|
||||
# codeboard
|
||||
# CodeBoard
|
||||
|
||||
Codebase → Onboarding Docs Generator. Paste a GitHub repo URL, get interactive developer onboarding documentation in minutes.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
codeboard/
|
||||
├── apps/
|
||||
│ ├── web/ # Next.js 14 frontend + API routes
|
||||
│ └── worker/ # BullMQ job processor
|
||||
├── packages/
|
||||
│ ├── shared/ # TypeScript types
|
||||
│ ├── parser/ # Babel-based AST parser (JS/TS) + regex (Python)
|
||||
│ ├── llm/ # OpenAI/Anthropic abstraction + prompt pipeline
|
||||
│ ├── diagrams/ # Mermaid diagram generators
|
||||
│ └── database/ # Prisma schema + client
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Build all packages
|
||||
npm run build
|
||||
|
||||
# Start with Docker
|
||||
docker compose up
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
# Run dev server (all workspaces)
|
||||
npm run dev
|
||||
|
||||
# Build
|
||||
npm run build
|
||||
```
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Frontend**: Next.js 14, React 18, Tailwind CSS 4
|
||||
- **Backend**: BullMQ workers, Redis pub/sub for real-time progress
|
||||
- **Parser**: @babel/parser for JS/TS, regex-based for Python
|
||||
- **LLM**: Provider abstraction (OpenAI GPT-4o / Anthropic Claude)
|
||||
- **Diagrams**: Mermaid.js auto-generated architecture & dependency graphs
|
||||
- **Database**: PostgreSQL + Prisma
|
||||
- **Queue**: Redis + BullMQ
|
||||
- **Deployment**: Docker multi-stage builds
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
---
|
||||
|
||||
Built by [Vectry](https://company.repi.fun) — Engineering AI into your workflow.
|
||||
|
||||
5
apps/web/next-env.d.ts
vendored
Normal file
5
apps/web/next-env.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
|
||||
7
apps/web/next.config.mjs
Normal file
7
apps/web/next.config.mjs
Normal file
@@ -0,0 +1,7 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const config = {
|
||||
transpilePackages: ["@codeboard/shared"],
|
||||
output: "standalone",
|
||||
};
|
||||
|
||||
export default config;
|
||||
33
apps/web/package.json
Normal file
33
apps/web/package.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "@codeboard/web",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"db:generate": "prisma generate --schema=../../packages/database/prisma/schema.prisma",
|
||||
"db:push": "prisma db push --schema=../../packages/database/prisma/schema.prisma"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codeboard/shared": "*",
|
||||
"bullmq": "^5.34.0",
|
||||
"ioredis": "^5.4.0",
|
||||
"lucide-react": "^0.563.0",
|
||||
"mermaid": "^11.4.0",
|
||||
"next": "^14.2.0",
|
||||
"react": "^18.3.0",
|
||||
"react-dom": "^18.3.0",
|
||||
"react-markdown": "^9.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.0.0",
|
||||
"@types/node": "^20.0.0",
|
||||
"@types/react": "^18.3.0",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"postcss": "^8.5.0",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"typescript": "^5.7"
|
||||
}
|
||||
}
|
||||
5
apps/web/postcss.config.mjs
Normal file
5
apps/web/postcss.config.mjs
Normal file
@@ -0,0 +1,5 @@
|
||||
export default {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
0
apps/web/public/.gitkeep
Normal file
0
apps/web/public/.gitkeep
Normal file
20
apps/web/src/app/api/docs/[id]/route.ts
Normal file
20
apps/web/src/app/api/docs/[id]/route.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getRedis } from "@/lib/redis";
|
||||
|
||||
export async function GET(
|
||||
_request: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const { id } = await params;
|
||||
const redis = getRedis();
|
||||
|
||||
const result = await redis.get(`codeboard:result:${id}`);
|
||||
if (!result) {
|
||||
return NextResponse.json(
|
||||
{ error: "Documentation not found" },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(JSON.parse(result));
|
||||
}
|
||||
39
apps/web/src/app/api/generate/route.ts
Normal file
39
apps/web/src/app/api/generate/route.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getQueue } from "@/lib/queue";
|
||||
import { getRedis } from "@/lib/redis";
|
||||
|
||||
const GITHUB_URL_RE = /^https?:\/\/(www\.)?github\.com\/[\w.-]+\/[\w.-]+\/?$/;
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const body = await request.json();
|
||||
const repoUrl: string = body.repoUrl?.trim();
|
||||
|
||||
if (!repoUrl || !GITHUB_URL_RE.test(repoUrl)) {
|
||||
return NextResponse.json(
|
||||
{ error: "Invalid GitHub repository URL" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const generationId = `gen_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
|
||||
|
||||
const redis = getRedis();
|
||||
await redis.set(
|
||||
`codeboard:status:${generationId}`,
|
||||
JSON.stringify({ status: "QUEUED", progress: 0, message: "Queued..." }),
|
||||
"EX",
|
||||
3600
|
||||
);
|
||||
|
||||
const queue = getQueue();
|
||||
await queue.add("generate", { repoUrl, generationId }, {
|
||||
jobId: generationId,
|
||||
removeOnComplete: true,
|
||||
removeOnFail: false,
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
{ id: generationId, status: "QUEUED" },
|
||||
{ status: 202 }
|
||||
);
|
||||
}
|
||||
68
apps/web/src/app/api/status/[id]/route.ts
Normal file
68
apps/web/src/app/api/status/[id]/route.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { getRedis } from "@/lib/redis";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET(
|
||||
_request: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const { id } = await params;
|
||||
const redis = getRedis();
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
const send = (event: string, data: unknown) => {
|
||||
controller.enqueue(
|
||||
encoder.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`)
|
||||
);
|
||||
};
|
||||
|
||||
const sub = redis.duplicate();
|
||||
const channel = `codeboard:progress:${id}`;
|
||||
|
||||
const currentStatus = await redis.get(`codeboard:status:${id}`);
|
||||
if (currentStatus) {
|
||||
const parsed = JSON.parse(currentStatus);
|
||||
send("progress", parsed);
|
||||
|
||||
if (parsed.status === "COMPLETED" || parsed.status === "FAILED") {
|
||||
controller.close();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await sub.subscribe(channel);
|
||||
|
||||
sub.on("message", (_ch: string, message: string) => {
|
||||
try {
|
||||
const data = JSON.parse(message);
|
||||
send("progress", data);
|
||||
|
||||
if (data.status === "COMPLETED" || data.status === "FAILED") {
|
||||
sub.unsubscribe(channel);
|
||||
sub.quit();
|
||||
controller.close();
|
||||
}
|
||||
} catch {
|
||||
/* ignore parse errors */
|
||||
}
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
sub.unsubscribe(channel);
|
||||
sub.quit();
|
||||
send("timeout", { message: "Connection timed out" });
|
||||
controller.close();
|
||||
}, 300_000);
|
||||
},
|
||||
});
|
||||
|
||||
return new Response(stream, {
|
||||
headers: {
|
||||
"Content-Type": "text/event-stream",
|
||||
"Cache-Control": "no-cache",
|
||||
Connection: "keep-alive",
|
||||
},
|
||||
});
|
||||
}
|
||||
64
apps/web/src/app/docs/[id]/page.tsx
Normal file
64
apps/web/src/app/docs/[id]/page.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import { DocViewer } from "@/components/doc-viewer";
|
||||
import type { GeneratedDocs } from "@codeboard/shared";
|
||||
import { notFound } from "next/navigation";
|
||||
import { Github, ArrowLeft } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
async function fetchDocs(id: string): Promise<GeneratedDocs | null> {
|
||||
try {
|
||||
const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000";
|
||||
const response = await fetch(`${baseUrl}/api/docs/${id}`, {
|
||||
cache: "no-store",
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return response.json();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default async function DocsPage({
|
||||
params,
|
||||
}: {
|
||||
params: { id: string };
|
||||
}) {
|
||||
const docs = await fetchDocs(params.id);
|
||||
|
||||
if (!docs) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<div className="border-b border-white/10 bg-black/20">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Link
|
||||
href="/"
|
||||
className="flex items-center gap-2 text-sm text-zinc-400 hover:text-white transition-colors"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
Back to Home
|
||||
</Link>
|
||||
|
||||
<a
|
||||
href={docs.repoUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-2 text-sm text-zinc-400 hover:text-white transition-colors"
|
||||
>
|
||||
<Github className="w-4 h-4" />
|
||||
View on GitHub
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DocViewer docs={docs} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
82
apps/web/src/app/generate/page.tsx
Normal file
82
apps/web/src/app/generate/page.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import { Suspense } from "react";
|
||||
import { ProgressTracker } from "@/components/progress-tracker";
|
||||
import { Github, Loader2 } from "lucide-react";
|
||||
|
||||
function GeneratePageContent({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: { repo?: string; id?: string };
|
||||
}) {
|
||||
const repoUrl = searchParams.repo || "";
|
||||
const generationId = searchParams.id || "";
|
||||
|
||||
const repoName = repoUrl
|
||||
? repoUrl.replace("https://github.com/", "").replace(/\/$/, "")
|
||||
: "";
|
||||
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto px-4 sm:px-6 lg:px-8 py-20">
|
||||
<div className="text-center mb-12">
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 rounded-2xl glass mb-6">
|
||||
<Github className="w-8 h-8 text-zinc-400" />
|
||||
</div>
|
||||
|
||||
<h1 className="text-2xl sm:text-3xl font-bold text-white mb-3">
|
||||
Analyzing Repository
|
||||
</h1>
|
||||
|
||||
<p className="text-zinc-400 font-mono text-sm break-all max-w-md mx-auto">
|
||||
{repoName || repoUrl || "Unknown repository"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{generationId ? (
|
||||
<ProgressTracker generationId={generationId} repoUrl={repoUrl} />
|
||||
) : (
|
||||
<div className="text-center p-8 rounded-2xl glass border-red-500/30">
|
||||
<p className="text-red-400">
|
||||
Missing generation ID. Please try again.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function GeneratePageSkeleton() {
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto px-4 sm:px-6 lg:px-8 py-20">
|
||||
<div className="text-center mb-12">
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 rounded-2xl glass mb-6">
|
||||
<Loader2 className="w-8 h-8 text-zinc-400 animate-spin" />
|
||||
</div>
|
||||
<div className="h-8 w-48 bg-zinc-800 rounded animate-pulse mx-auto mb-3" />
|
||||
<div className="h-4 w-64 bg-zinc-800 rounded animate-pulse mx-auto" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{[1, 2, 3, 4, 5].map((i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex items-center gap-4 p-4 rounded-xl bg-zinc-900/50"
|
||||
>
|
||||
<div className="w-8 h-8 rounded-full bg-zinc-800 animate-pulse" />
|
||||
<div className="flex-1 h-4 bg-zinc-800 rounded animate-pulse" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function GeneratePage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: { repo?: string; id?: string };
|
||||
}) {
|
||||
return (
|
||||
<Suspense fallback={<GeneratePageSkeleton />}>
|
||||
<GeneratePageContent searchParams={searchParams} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
365
apps/web/src/app/globals.css
Normal file
365
apps/web/src/app/globals.css
Normal file
@@ -0,0 +1,365 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
--background: #0a0a0f;
|
||||
--surface: rgba(255, 255, 255, 0.03);
|
||||
--surface-hover: rgba(255, 255, 255, 0.06);
|
||||
--border: rgba(255, 255, 255, 0.08);
|
||||
--border-strong: rgba(255, 255, 255, 0.15);
|
||||
|
||||
--text-primary: #ffffff;
|
||||
--text-secondary: #a1a1aa;
|
||||
--text-muted: #71717a;
|
||||
|
||||
--accent-blue: #3b82f6;
|
||||
--accent-indigo: #6366f1;
|
||||
--accent-purple: #9333ea;
|
||||
--accent-cyan: #06b6d4;
|
||||
|
||||
--gradient-primary: linear-gradient(135deg, #3b82f6 0%, #6366f1 50%, #9333ea 100%);
|
||||
--gradient-subtle: linear-gradient(135deg, rgba(59, 130, 246, 0.1) 0%, rgba(147, 51, 234, 0.1) 100%);
|
||||
|
||||
--shadow-glow: 0 0 40px rgba(59, 130, 246, 0.3);
|
||||
--shadow-card: 0 4px 24px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--background);
|
||||
color: var(--text-primary);
|
||||
font-feature-settings: "rlig" 1, "calt" 1;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: rgba(59, 130, 246, 0.3);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.glass {
|
||||
background: var(--surface);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.glass-strong {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
backdrop-filter: blur(24px);
|
||||
-webkit-backdrop-filter: blur(24px);
|
||||
border: 1px solid var(--border-strong);
|
||||
}
|
||||
|
||||
.gradient-text {
|
||||
background: var(--gradient-primary);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.gradient-border {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.gradient-border::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: inherit;
|
||||
padding: 1px;
|
||||
background: var(--gradient-primary);
|
||||
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
||||
mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
||||
-webkit-mask-composite: xor;
|
||||
mask-composite: exclude;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.glow {
|
||||
box-shadow: var(--shadow-glow);
|
||||
}
|
||||
|
||||
.glow-subtle {
|
||||
box-shadow: 0 0 60px rgba(59, 130, 246, 0.15);
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slide-up {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(24px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slide-down {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-12px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes scale-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse-glow {
|
||||
0%, 100% {
|
||||
box-shadow: 0 0 20px rgba(59, 130, 246, 0.4);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 40px rgba(59, 130, 246, 0.6), 0 0 60px rgba(147, 51, 234, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin-slow {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-fade-in {
|
||||
animation: fade-in 0.6s ease-out forwards;
|
||||
}
|
||||
|
||||
.animate-slide-up {
|
||||
animation: slide-up 0.6s ease-out forwards;
|
||||
}
|
||||
|
||||
.animate-slide-down {
|
||||
animation: slide-down 0.4s ease-out forwards;
|
||||
}
|
||||
|
||||
.animate-scale-in {
|
||||
animation: scale-in 0.4s ease-out forwards;
|
||||
}
|
||||
|
||||
.animate-pulse-glow {
|
||||
animation: pulse-glow 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.animate-shimmer {
|
||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.1), transparent);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 2s infinite;
|
||||
}
|
||||
|
||||
.animate-float {
|
||||
animation: float 6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.animate-spin-slow {
|
||||
animation: spin-slow 20s linear infinite;
|
||||
}
|
||||
|
||||
.stagger-1 { animation-delay: 0.1s; }
|
||||
.stagger-2 { animation-delay: 0.2s; }
|
||||
.stagger-3 { animation-delay: 0.3s; }
|
||||
.stagger-4 { animation-delay: 0.4s; }
|
||||
.stagger-5 { animation-delay: 0.5s; }
|
||||
|
||||
.scrollbar-thin {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(255, 255, 255, 0.2) transparent;
|
||||
}
|
||||
|
||||
.scrollbar-thin::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.scrollbar-thin::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.scrollbar-thin::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.scrollbar-thin::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.bg-grid {
|
||||
background-image:
|
||||
linear-gradient(rgba(255, 255, 255, 0.03) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(255, 255, 255, 0.03) 1px, transparent 1px);
|
||||
background-size: 60px 60px;
|
||||
}
|
||||
|
||||
.bg-gradient-radial {
|
||||
background: radial-gradient(ellipse at top, rgba(59, 130, 246, 0.15) 0%, transparent 50%),
|
||||
radial-gradient(ellipse at bottom, rgba(147, 51, 234, 0.1) 0%, transparent 50%);
|
||||
}
|
||||
|
||||
.noise {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.noise::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)'/%3E%3C/svg%3E");
|
||||
opacity: 0.03;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--gradient-primary);
|
||||
color: white;
|
||||
font-weight: 500;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 0.5rem;
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 8px 30px rgba(59, 130, 246, 0.4);
|
||||
}
|
||||
|
||||
.btn-primary:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--surface);
|
||||
color: var(--text-primary);
|
||||
font-weight: 500;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid var(--border);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: var(--surface-hover);
|
||||
border-color: var(--border-strong);
|
||||
}
|
||||
|
||||
.input-field {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.75rem 1rem;
|
||||
color: var(--text-primary);
|
||||
transition: all 0.2s ease;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.input-field:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-blue);
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.input-field::placeholder {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 1rem;
|
||||
padding: 1.5rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
background: var(--surface-hover);
|
||||
border-color: var(--border-strong);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.icon-box {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
border-radius: 0.75rem;
|
||||
background: var(--gradient-subtle);
|
||||
border: 1px solid var(--border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--accent-blue);
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.25rem 0.75rem;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 9999px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.code-block {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.75rem;
|
||||
padding: 1rem;
|
||||
font-family: var(--font-mono), ui-monospace, monospace;
|
||||
font-size: 0.875rem;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, transparent, var(--border), transparent);
|
||||
}
|
||||
58
apps/web/src/app/layout.tsx
Normal file
58
apps/web/src/app/layout.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Inter, JetBrains_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import { Navbar } from "@/components/navbar";
|
||||
import { Footer } from "@/components/footer";
|
||||
|
||||
const inter = Inter({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-sans",
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
const jetbrainsMono = JetBrains_Mono({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-mono",
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "CodeBoard — Understand any codebase in 5 minutes",
|
||||
description:
|
||||
"Paste a GitHub URL and get interactive onboarding documentation with architecture diagrams, module breakdowns, and getting started guides. Built by Vectry AI consultancy.",
|
||||
keywords: ["code analysis", "documentation", "github", "codebase", "AI", "developer tools"],
|
||||
authors: [{ name: "Vectry" }],
|
||||
openGraph: {
|
||||
title: "CodeBoard — Understand any codebase in 5 minutes",
|
||||
description:
|
||||
"Paste a GitHub URL and get interactive onboarding documentation with architecture diagrams, module breakdowns, and getting started guides.",
|
||||
type: "website",
|
||||
},
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="en" className="dark">
|
||||
<body
|
||||
className={`${inter.variable} ${jetbrainsMono.variable} font-sans antialiased bg-[#0a0a0f] text-white min-h-screen`}
|
||||
>
|
||||
<div className="relative min-h-screen flex flex-col">
|
||||
<div className="fixed inset-0 bg-gradient-radial pointer-events-none" />
|
||||
<div className="fixed inset-0 bg-grid pointer-events-none opacity-50" />
|
||||
|
||||
<Navbar />
|
||||
|
||||
<main className="flex-1 relative">
|
||||
{children}
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
280
apps/web/src/app/page.tsx
Normal file
280
apps/web/src/app/page.tsx
Normal file
@@ -0,0 +1,280 @@
|
||||
import { RepoInput } from "@/components/repo-input";
|
||||
import {
|
||||
Link2,
|
||||
Code2,
|
||||
Sparkles,
|
||||
FileText,
|
||||
GitBranch,
|
||||
Boxes,
|
||||
Search,
|
||||
BookOpen,
|
||||
ArrowRight,
|
||||
Github
|
||||
} from "lucide-react";
|
||||
|
||||
export default function HomePage() {
|
||||
const steps = [
|
||||
{
|
||||
number: "01",
|
||||
icon: Link2,
|
||||
title: "Paste URL",
|
||||
description: "Enter any public GitHub repository URL",
|
||||
},
|
||||
{
|
||||
number: "02",
|
||||
icon: Code2,
|
||||
title: "Clone & Analyze",
|
||||
description: "We clone and deeply analyze the codebase structure",
|
||||
},
|
||||
{
|
||||
number: "03",
|
||||
icon: Sparkles,
|
||||
title: "AI Generation",
|
||||
description: "Our AI generates comprehensive documentation",
|
||||
},
|
||||
{
|
||||
number: "04",
|
||||
icon: FileText,
|
||||
title: "Interactive Docs",
|
||||
description: "Explore architecture diagrams and module breakdowns",
|
||||
},
|
||||
];
|
||||
|
||||
const features = [
|
||||
{
|
||||
icon: GitBranch,
|
||||
title: "Architecture Diagrams",
|
||||
description:
|
||||
"Auto-generated Mermaid diagrams visualizing your codebase structure, dependencies, and data flow.",
|
||||
},
|
||||
{
|
||||
icon: Boxes,
|
||||
title: "Module Breakdowns",
|
||||
description:
|
||||
"Understand each part of the codebase with detailed summaries, key files, and public APIs.",
|
||||
},
|
||||
{
|
||||
icon: Search,
|
||||
title: "Pattern Detection",
|
||||
description:
|
||||
"Coding conventions and design patterns automatically identified and documented for you.",
|
||||
},
|
||||
{
|
||||
icon: BookOpen,
|
||||
title: "Getting Started Guide",
|
||||
description:
|
||||
"Actionable onboarding documentation to get new developers productive in minutes, not days.",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
{/* Hero Section */}
|
||||
<section className="relative pt-32 pb-20 lg:pt-48 lg:pb-32">
|
||||
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center">
|
||||
{/* Badge */}
|
||||
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full glass mb-8 animate-fade-in opacity-0">
|
||||
<Sparkles className="w-4 h-4 text-blue-400" />
|
||||
<span className="text-sm text-zinc-300">Powered by AI</span>
|
||||
</div>
|
||||
|
||||
{/* Main Headline */}
|
||||
<h1 className="text-4xl sm:text-5xl lg:text-7xl font-bold tracking-tight mb-6 animate-slide-up opacity-0">
|
||||
<span className="gradient-text">Understand any codebase</span>
|
||||
<br />
|
||||
<span className="text-white">in 5 minutes</span>
|
||||
</h1>
|
||||
|
||||
{/* Subtitle */}
|
||||
<p className="text-lg sm:text-xl text-zinc-400 max-w-2xl mx-auto mb-10 animate-slide-up opacity-0 stagger-1">
|
||||
Paste a GitHub URL. Get interactive onboarding documentation with
|
||||
architecture diagrams, module breakdowns, and getting started guides.
|
||||
</p>
|
||||
|
||||
{/* Repo Input */}
|
||||
<div className="max-w-xl mx-auto mb-16 animate-slide-up opacity-0 stagger-2">
|
||||
<RepoInput />
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="flex flex-wrap justify-center gap-8 sm:gap-12 animate-fade-in opacity-0 stagger-3">
|
||||
<div className="text-center">
|
||||
<div className="text-2xl sm:text-3xl font-bold text-white">5 min</div>
|
||||
<div className="text-sm text-zinc-500">Average generation time</div>
|
||||
</div>
|
||||
<div className="hidden sm:block w-px bg-zinc-800" />
|
||||
<div className="text-center">
|
||||
<div className="text-2xl sm:text-3xl font-bold text-white">100%</div>
|
||||
<div className="text-sm text-zinc-500">Free for public repos</div>
|
||||
</div>
|
||||
<div className="hidden sm:block w-px bg-zinc-800" />
|
||||
<div className="text-center">
|
||||
<div className="text-2xl sm:text-3xl font-bold text-white">AI</div>
|
||||
<div className="text-sm text-zinc-500">Powered insights</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Background Elements */}
|
||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[800px] h-[800px] bg-blue-500/10 rounded-full blur-3xl pointer-events-none" />
|
||||
</section>
|
||||
|
||||
{/* How It Works Section */}
|
||||
<section id="how-it-works" className="py-20 lg:py-32">
|
||||
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="text-3xl sm:text-4xl font-bold text-white mb-4">
|
||||
How It Works
|
||||
</h2>
|
||||
<p className="text-zinc-400 max-w-xl mx-auto">
|
||||
Four simple steps to comprehensive codebase documentation
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
{/* Connection Line - Desktop */}
|
||||
<div className="hidden lg:block absolute top-24 left-[12.5%] right-[12.5%] h-px bg-gradient-to-r from-transparent via-zinc-700 to-transparent" />
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||
{steps.map((step, index) => (
|
||||
<div
|
||||
key={step.number}
|
||||
className="relative group"
|
||||
>
|
||||
<div className="text-center">
|
||||
{/* Step Number */}
|
||||
<div className="text-6xl font-bold text-zinc-800/50 mb-4 group-hover:text-blue-500/20 transition-colors">
|
||||
{step.number}
|
||||
</div>
|
||||
|
||||
{/* Icon */}
|
||||
<div className="relative inline-flex items-center justify-center w-16 h-16 rounded-2xl glass mb-6 group-hover:border-blue-500/30 transition-colors">
|
||||
<step.icon className="w-7 h-7 text-blue-400" />
|
||||
|
||||
{/* Glow effect */}
|
||||
<div className="absolute inset-0 rounded-2xl bg-blue-500/20 blur-xl opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<h3 className="text-lg font-semibold text-white mb-2">
|
||||
{step.title}
|
||||
</h3>
|
||||
|
||||
{/* Description */}
|
||||
<p className="text-sm text-zinc-400 leading-relaxed">
|
||||
{step.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Features Section */}
|
||||
<section id="features" className="py-20 lg:py-32">
|
||||
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="text-3xl sm:text-4xl font-bold text-white mb-4">
|
||||
Everything You Need
|
||||
</h2>
|
||||
<p className="text-zinc-400 max-w-xl mx-auto">
|
||||
Comprehensive documentation generated automatically from your codebase
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{features.map((feature, index) => (
|
||||
<div
|
||||
key={feature.title}
|
||||
className="group relative p-8 rounded-2xl glass hover:bg-white/[0.05] transition-all duration-300 hover:-translate-y-1"
|
||||
>
|
||||
{/* Gradient border on hover */}
|
||||
<div className="absolute inset-0 rounded-2xl bg-gradient-to-r from-blue-500/20 via-indigo-500/20 to-purple-500/20 opacity-0 group-hover:opacity-100 transition-opacity -z-10 blur-xl" />
|
||||
|
||||
<div className="flex items-start gap-5">
|
||||
<div className="flex-shrink-0">
|
||||
<div className="w-12 h-12 rounded-xl bg-gradient-to-br from-blue-500/20 to-purple-500/20 flex items-center justify-center border border-white/10 group-hover:border-blue-500/30 transition-colors">
|
||||
<feature.icon className="w-6 h-6 text-blue-400" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<h3 className="text-xl font-semibold text-white mb-2 group-hover:text-blue-300 transition-colors">
|
||||
{feature.title}
|
||||
</h3>
|
||||
<p className="text-zinc-400 leading-relaxed">
|
||||
{feature.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Built by Vectry Section */}
|
||||
<section className="py-20 lg:py-32">
|
||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="relative rounded-3xl glass-strong p-8 sm:p-12 lg:p-16 overflow-hidden">
|
||||
{/* Background decoration */}
|
||||
<div className="absolute top-0 right-0 w-64 h-64 bg-gradient-to-br from-blue-500/20 to-purple-500/20 rounded-full blur-3xl -translate-y-1/2 translate-x-1/2" />
|
||||
<div className="absolute bottom-0 left-0 w-48 h-48 bg-gradient-to-tr from-indigo-500/10 to-cyan-500/10 rounded-full blur-3xl translate-y-1/2 -translate-x-1/2" />
|
||||
|
||||
<div className="relative text-center">
|
||||
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-white/5 border border-white/10 mb-8">
|
||||
<span className="w-2 h-2 rounded-full bg-green-400 animate-pulse" />
|
||||
<span className="text-sm text-zinc-300">Available for projects</span>
|
||||
</div>
|
||||
|
||||
<h2 className="text-3xl sm:text-4xl font-bold text-white mb-4">
|
||||
Built by{" "}
|
||||
<span className="gradient-text">Vectry</span>
|
||||
</h2>
|
||||
|
||||
<p className="text-lg text-zinc-400 mb-4 max-w-xl mx-auto">
|
||||
We're an AI consultancy that builds tools like this for businesses.
|
||||
</p>
|
||||
|
||||
<p className="text-zinc-500 mb-8">
|
||||
Need AI automation for your workflow?
|
||||
</p>
|
||||
|
||||
<a
|
||||
href="https://company.repi.fun"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-2 btn-primary animate-pulse-glow"
|
||||
>
|
||||
Talk to Us
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</a>
|
||||
|
||||
<div className="mt-12 pt-8 border-t border-white/10">
|
||||
<div className="flex flex-wrap items-center justify-center gap-6 text-sm text-zinc-500">
|
||||
<a
|
||||
href="https://github.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-2 hover:text-white transition-colors"
|
||||
>
|
||||
<Github className="w-4 h-4" />
|
||||
Open Source
|
||||
</a>
|
||||
<span className="hidden sm:inline">•</span>
|
||||
<span>Free for public repositories</span>
|
||||
<span className="hidden sm:inline">•</span>
|
||||
<span>No signup required</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
354
apps/web/src/components/doc-viewer.tsx
Normal file
354
apps/web/src/components/doc-viewer.tsx
Normal file
@@ -0,0 +1,354 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import type { GeneratedDocs, DocsModule } from "@codeboard/shared";
|
||||
import { MermaidDiagram } from "./mermaid-diagram";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import {
|
||||
BookOpen,
|
||||
Boxes,
|
||||
Search,
|
||||
Rocket,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
Folder,
|
||||
FileCode,
|
||||
GitBranch,
|
||||
ExternalLink,
|
||||
} from "lucide-react";
|
||||
|
||||
interface DocViewerProps {
|
||||
docs: GeneratedDocs;
|
||||
}
|
||||
|
||||
export function DocViewer({ docs }: DocViewerProps) {
|
||||
const [activeSection, setActiveSection] = useState("overview");
|
||||
const [expandedModules, setExpandedModules] = useState<Set<string>>(
|
||||
new Set()
|
||||
);
|
||||
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
||||
|
||||
const toggleModule = (moduleName: string) => {
|
||||
const newSet = new Set(expandedModules);
|
||||
if (newSet.has(moduleName)) {
|
||||
newSet.delete(moduleName);
|
||||
} else {
|
||||
newSet.add(moduleName);
|
||||
}
|
||||
setExpandedModules(newSet);
|
||||
};
|
||||
|
||||
const scrollToSection = (sectionId: string) => {
|
||||
setActiveSection(sectionId);
|
||||
const element = document.getElementById(sectionId);
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: "smooth" });
|
||||
}
|
||||
setIsSidebarOpen(false);
|
||||
};
|
||||
|
||||
const sections = [
|
||||
{ id: "overview", label: "Overview", icon: BookOpen },
|
||||
{ id: "modules", label: "Modules", icon: Boxes },
|
||||
{ id: "patterns", label: "Patterns", icon: Search },
|
||||
{ id: "getting-started", label: "Getting Started", icon: Rocket },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="flex flex-col lg:flex-row gap-8">
|
||||
<button
|
||||
onClick={() => setIsSidebarOpen(!isSidebarOpen)}
|
||||
className="lg:hidden flex items-center gap-2 px-4 py-2 glass rounded-lg text-sm"
|
||||
>
|
||||
<BookOpen className="w-4 h-4" />
|
||||
Table of Contents
|
||||
{isSidebarOpen ? (
|
||||
<ChevronDown className="w-4 h-4" />
|
||||
) : (
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
<aside
|
||||
className={`${
|
||||
isSidebarOpen ? "block" : "hidden"
|
||||
} lg:block w-full lg:w-64 flex-shrink-0`}
|
||||
>
|
||||
<div className="sticky top-24 space-y-1">
|
||||
<p className="px-3 py-2 text-xs font-semibold text-zinc-500 uppercase tracking-wider">
|
||||
Contents
|
||||
</p>
|
||||
{sections.map((section) => (
|
||||
<button
|
||||
key={section.id}
|
||||
onClick={() => scrollToSection(section.id)}
|
||||
className={`w-full flex items-center gap-3 px-3 py-2 rounded-lg text-sm transition-colors text-left ${
|
||||
activeSection === section.id
|
||||
? "bg-blue-500/20 text-blue-300"
|
||||
: "text-zinc-400 hover:text-white hover:bg-white/5"
|
||||
}`}
|
||||
>
|
||||
<section.icon className="w-4 h-4" />
|
||||
{section.label}
|
||||
</button>
|
||||
))}
|
||||
|
||||
<div className="pt-4 mt-4 border-t border-white/10">
|
||||
<p className="px-3 py-2 text-xs font-semibold text-zinc-500 uppercase tracking-wider">
|
||||
Repository
|
||||
</p>
|
||||
<a
|
||||
href={docs.repoUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-3 px-3 py-2 rounded-lg text-sm text-zinc-400 hover:text-white hover:bg-white/5 transition-colors"
|
||||
>
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
View on GitHub
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main className="flex-1 min-w-0 space-y-16">
|
||||
<div className="border-b border-white/10 pb-8">
|
||||
<h1 className="text-3xl sm:text-4xl font-bold text-white mb-4">
|
||||
{docs.sections.overview.title}
|
||||
</h1>
|
||||
<p className="text-lg text-zinc-400">
|
||||
{docs.sections.overview.description}
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2 mt-4">
|
||||
{docs.sections.overview.techStack.map((tech: string) => (
|
||||
<span
|
||||
key={tech}
|
||||
className="px-3 py-1 text-sm bg-white/5 border border-white/10 rounded-full text-zinc-300"
|
||||
>
|
||||
{tech}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section id="overview" className="scroll-mt-24">
|
||||
<h2 className="text-2xl font-bold text-white mb-6 flex items-center gap-3">
|
||||
<BookOpen className="w-6 h-6 text-blue-400" />
|
||||
Architecture Overview
|
||||
</h2>
|
||||
|
||||
<div className="glass rounded-xl p-6 mb-6">
|
||||
<MermaidDiagram chart={docs.sections.overview.architectureDiagram} />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="p-4 glass rounded-lg text-center">
|
||||
<div className="text-2xl font-bold text-white">
|
||||
{docs.sections.overview.keyMetrics.files}
|
||||
</div>
|
||||
<div className="text-sm text-zinc-500">Files</div>
|
||||
</div>
|
||||
<div className="p-4 glass rounded-lg text-center">
|
||||
<div className="text-2xl font-bold text-white">
|
||||
{docs.sections.overview.keyMetrics.modules}
|
||||
</div>
|
||||
<div className="text-sm text-zinc-500">Modules</div>
|
||||
</div>
|
||||
<div className="p-4 glass rounded-lg text-center">
|
||||
<div className="text-2xl font-bold text-white">
|
||||
{docs.sections.overview.keyMetrics.languages.length}
|
||||
</div>
|
||||
<div className="text-sm text-zinc-500">Languages</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="modules" className="scroll-mt-24">
|
||||
<h2 className="text-2xl font-bold text-white mb-6 flex items-center gap-3">
|
||||
<Boxes className="w-6 h-6 text-blue-400" />
|
||||
Module Breakdown
|
||||
</h2>
|
||||
|
||||
<div className="space-y-4">
|
||||
{docs.sections.modules.map((module: DocsModule) => (
|
||||
<div
|
||||
key={module.name}
|
||||
className="glass rounded-xl overflow-hidden"
|
||||
>
|
||||
<button
|
||||
onClick={() => toggleModule(module.name)}
|
||||
className="w-full flex items-center justify-between p-5 hover:bg-white/[0.02] transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<Folder className="w-5 h-5 text-blue-400" />
|
||||
<div className="text-left">
|
||||
<h3 className="font-semibold text-white">{module.name}</h3>
|
||||
<p className="text-sm text-zinc-500">{module.path}</p>
|
||||
</div>
|
||||
</div>
|
||||
{expandedModules.has(module.name) ? (
|
||||
<ChevronDown className="w-5 h-5 text-zinc-400" />
|
||||
) : (
|
||||
<ChevronRight className="w-5 h-5 text-zinc-400" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{expandedModules.has(module.name) && (
|
||||
<div className="px-5 pb-5 border-t border-white/10">
|
||||
<p className="text-zinc-300 mt-4 mb-4">{module.summary}</p>
|
||||
|
||||
{module.keyFiles.length > 0 && (
|
||||
<div className="mb-4">
|
||||
<h4 className="text-sm font-medium text-zinc-400 mb-2">
|
||||
Key Files
|
||||
</h4>
|
||||
<div className="space-y-2">
|
||||
{module.keyFiles.map((file: { path: string; purpose: string }) => (
|
||||
<div
|
||||
key={file.path}
|
||||
className="flex items-start gap-2 text-sm"
|
||||
>
|
||||
<FileCode className="w-4 h-4 text-zinc-500 mt-0.5 flex-shrink-0" />
|
||||
<div>
|
||||
<code className="text-blue-300">{file.path}</code>
|
||||
<p className="text-zinc-500">{file.purpose}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{module.publicApi.length > 0 && (
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-zinc-400 mb-2">
|
||||
Public API
|
||||
</h4>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{module.publicApi.map((api: string) => (
|
||||
<code
|
||||
key={api}
|
||||
className="px-2 py-1 text-sm bg-blue-500/10 text-blue-300 rounded"
|
||||
>
|
||||
{api}
|
||||
</code>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="patterns" className="scroll-mt-24">
|
||||
<h2 className="text-2xl font-bold text-white mb-6 flex items-center gap-3">
|
||||
<Search className="w-6 h-6 text-blue-400" />
|
||||
Patterns & Conventions
|
||||
</h2>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<div className="glass rounded-xl p-6">
|
||||
<h3 className="font-semibold text-white mb-4">Coding Conventions</h3>
|
||||
<ul className="space-y-2">
|
||||
{docs.sections.patterns.conventions.map((convention: string, i: number) => (
|
||||
<li key={i} className="flex items-start gap-2 text-sm text-zinc-300">
|
||||
<span className="text-blue-400 mt-1">•</span>
|
||||
{convention}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="glass rounded-xl p-6">
|
||||
<h3 className="font-semibold text-white mb-4">Design Patterns</h3>
|
||||
<ul className="space-y-2">
|
||||
{docs.sections.patterns.designPatterns.map((pattern: string, i: number) => (
|
||||
<li key={i} className="flex items-start gap-2 text-sm text-zinc-300">
|
||||
<span className="text-blue-400 mt-1">•</span>
|
||||
{pattern}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{docs.sections.patterns.architecturalDecisions.length > 0 && (
|
||||
<div className="mt-6 glass rounded-xl p-6">
|
||||
<h3 className="font-semibold text-white mb-4">
|
||||
Architectural Decisions
|
||||
</h3>
|
||||
<ul className="space-y-3">
|
||||
{docs.sections.patterns.architecturalDecisions.map((decision: string, i: number) => (
|
||||
<li key={i} className="flex items-start gap-3 text-zinc-300">
|
||||
<GitBranch className="w-5 h-5 text-blue-400 flex-shrink-0 mt-0.5" />
|
||||
{decision}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section id="getting-started" className="scroll-mt-24">
|
||||
<h2 className="text-2xl font-bold text-white mb-6 flex items-center gap-3">
|
||||
<Rocket className="w-6 h-6 text-blue-400" />
|
||||
Getting Started
|
||||
</h2>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="glass rounded-xl p-6">
|
||||
<h3 className="font-semibold text-white mb-4">Prerequisites</h3>
|
||||
<ul className="space-y-2">
|
||||
{docs.sections.gettingStarted.prerequisites.map((prereq: string, i: number) => (
|
||||
<li key={i} className="flex items-center gap-2 text-sm text-zinc-300">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-green-400" />
|
||||
{prereq}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="glass rounded-xl p-6">
|
||||
<h3 className="font-semibold text-white mb-4">Setup Steps</h3>
|
||||
<ol className="space-y-4">
|
||||
{docs.sections.gettingStarted.setupSteps.map((step: string, i: number) => (
|
||||
<li key={i} className="flex gap-4">
|
||||
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-blue-500/20 text-blue-300 text-sm flex items-center justify-center font-medium">
|
||||
{i + 1}
|
||||
</span>
|
||||
<div className="prose prose-invert prose-sm max-w-none">
|
||||
<ReactMarkdown>{step}</ReactMarkdown>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div className="glass rounded-xl p-6 border-blue-500/20">
|
||||
<h3 className="font-semibold text-white mb-3">First Task</h3>
|
||||
<div className="prose prose-invert prose-sm max-w-none">
|
||||
<ReactMarkdown>{docs.sections.gettingStarted.firstTask}</ReactMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{docs.sections.dependencyGraph && (
|
||||
<section className="scroll-mt-24">
|
||||
<h2 className="text-2xl font-bold text-white mb-6 flex items-center gap-3">
|
||||
<GitBranch className="w-6 h-6 text-blue-400" />
|
||||
Dependency Graph
|
||||
</h2>
|
||||
<div className="glass rounded-xl p-6">
|
||||
<MermaidDiagram chart={docs.sections.dependencyGraph} />
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
58
apps/web/src/components/footer.tsx
Normal file
58
apps/web/src/components/footer.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import Link from "next/link";
|
||||
import { Code2, Github, ArrowUpRight } from "lucide-react";
|
||||
|
||||
export function Footer() {
|
||||
return (
|
||||
<footer className="border-t border-white/10 bg-black/30">
|
||||
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<div className="flex flex-col md:flex-row items-center justify-between gap-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center">
|
||||
<Code2 className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<span className="text-lg font-semibold text-white">
|
||||
CodeBoard
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-6">
|
||||
<a
|
||||
href="https://company.repi.fun"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-1 text-sm text-zinc-400 hover:text-white transition-colors"
|
||||
>
|
||||
Built by Vectry
|
||||
<ArrowUpRight className="w-3 h-3" />
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://github.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-2 text-sm text-zinc-400 hover:text-white transition-colors"
|
||||
>
|
||||
<Github className="w-4 h-4" />
|
||||
Source
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 pt-8 border-t border-white/5 text-center">
|
||||
<p className="text-sm text-zinc-500">
|
||||
© {new Date().getFullYear()} CodeBoard. Built by{" "}
|
||||
<a
|
||||
href="https://company.repi.fun"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-zinc-400 hover:text-white transition-colors"
|
||||
>
|
||||
Vectry
|
||||
</a>
|
||||
. All rights reserved.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
82
apps/web/src/components/mermaid-diagram.tsx
Normal file
82
apps/web/src/components/mermaid-diagram.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import mermaid from "mermaid";
|
||||
|
||||
interface MermaidDiagramProps {
|
||||
chart: string;
|
||||
}
|
||||
|
||||
export function MermaidDiagram({ chart }: MermaidDiagramProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isReady, setIsReady] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
mermaid.initialize({
|
||||
startOnLoad: false,
|
||||
theme: "dark",
|
||||
securityLevel: "loose",
|
||||
themeVariables: {
|
||||
darkMode: true,
|
||||
background: "#0a0a0f",
|
||||
primaryColor: "#1e3a5f",
|
||||
primaryTextColor: "#ffffff",
|
||||
primaryBorderColor: "#3b82f6",
|
||||
lineColor: "#6366f1",
|
||||
secondaryColor: "#1f2937",
|
||||
tertiaryColor: "#374151",
|
||||
fontFamily: "ui-monospace, monospace",
|
||||
},
|
||||
flowchart: {
|
||||
useMaxWidth: true,
|
||||
htmlLabels: true,
|
||||
curve: "basis",
|
||||
},
|
||||
});
|
||||
setIsReady(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isReady || !containerRef.current || !chart) return;
|
||||
|
||||
const renderChart = async () => {
|
||||
try {
|
||||
const id = `mermaid-${Math.random().toString(36).slice(2, 9)}`;
|
||||
const { svg } = await mermaid.render(id, chart);
|
||||
|
||||
if (containerRef.current) {
|
||||
containerRef.current.innerHTML = svg;
|
||||
setError(null);
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Failed to render diagram");
|
||||
}
|
||||
};
|
||||
|
||||
renderChart();
|
||||
}, [chart, isReady]);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="p-4 rounded-lg bg-red-500/10 border border-red-500/20">
|
||||
<p className="text-red-400 text-sm mb-2">Failed to render diagram</p>
|
||||
<pre className="text-xs text-red-300/70 overflow-x-auto">{chart}</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="mermaid-diagram overflow-x-auto"
|
||||
style={{ minHeight: "100px" }}
|
||||
>
|
||||
{!isReady && (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<div className="w-6 h-6 border-2 border-blue-500/30 border-t-blue-500 rounded-full animate-spin" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
94
apps/web/src/components/navbar.tsx
Normal file
94
apps/web/src/components/navbar.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { Code2, Menu, X, Github } from "lucide-react";
|
||||
|
||||
export function Navbar() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const navLinks = [
|
||||
{ href: "/#how-it-works", label: "How it Works" },
|
||||
{ href: "/#features", label: "Features" },
|
||||
];
|
||||
|
||||
return (
|
||||
<header className="fixed top-0 left-0 right-0 z-50">
|
||||
<nav className="glass border-b border-white/5">
|
||||
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex items-center justify-between h-16">
|
||||
<Link href="/" className="flex items-center gap-2 group">
|
||||
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center group-hover:shadow-lg group-hover:shadow-blue-500/25 transition-shadow">
|
||||
<Code2 className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<span className="text-lg font-semibold text-white">
|
||||
CodeBoard
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
<div className="hidden md:flex items-center gap-8">
|
||||
{navLinks.map((link) => (
|
||||
<Link
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className="text-sm text-zinc-400 hover:text-white transition-colors"
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
))}
|
||||
|
||||
<a
|
||||
href="https://github.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-2 text-sm text-zinc-400 hover:text-white transition-colors"
|
||||
>
|
||||
<Github className="w-4 h-4" />
|
||||
GitHub
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="md:hidden p-2 text-zinc-400 hover:text-white"
|
||||
aria-label="Toggle menu"
|
||||
>
|
||||
{isOpen ? (
|
||||
<X className="w-6 h-6" />
|
||||
) : (
|
||||
<Menu className="w-6 h-6" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isOpen && (
|
||||
<div className="md:hidden border-t border-white/5 bg-black/50 backdrop-blur-xl">
|
||||
<div className="px-4 py-4 space-y-3">
|
||||
{navLinks.map((link) => (
|
||||
<Link
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
onClick={() => setIsOpen(false)}
|
||||
className="block text-sm text-zinc-400 hover:text-white transition-colors py-2"
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
))}
|
||||
|
||||
<a
|
||||
href="https://github.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-2 text-sm text-zinc-400 hover:text-white transition-colors py-2"
|
||||
>
|
||||
<Github className="w-4 h-4" />
|
||||
GitHub
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</nav>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
213
apps/web/src/components/progress-tracker.tsx
Normal file
213
apps/web/src/components/progress-tracker.tsx
Normal file
@@ -0,0 +1,213 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import type { GenerationStatus } from "@codeboard/shared";
|
||||
import {
|
||||
Loader2,
|
||||
CheckCircle2,
|
||||
Circle,
|
||||
AlertCircle,
|
||||
FileText,
|
||||
RefreshCw,
|
||||
} from "lucide-react";
|
||||
|
||||
interface ProgressData {
|
||||
status: GenerationStatus;
|
||||
progress: number;
|
||||
message: string;
|
||||
}
|
||||
|
||||
interface ProgressTrackerProps {
|
||||
generationId: string;
|
||||
repoUrl: string;
|
||||
}
|
||||
|
||||
const STEPS: { status: GenerationStatus; label: string }[] = [
|
||||
{ status: "QUEUED", label: "Queued" },
|
||||
{ status: "CLONING", label: "Cloning Repository" },
|
||||
{ status: "PARSING", label: "Analyzing Code" },
|
||||
{ status: "GENERATING", label: "Generating Docs" },
|
||||
{ status: "RENDERING", label: "Finalizing" },
|
||||
];
|
||||
|
||||
export function ProgressTracker({
|
||||
generationId,
|
||||
repoUrl,
|
||||
}: ProgressTrackerProps) {
|
||||
const [data, setData] = useState<ProgressData>({
|
||||
status: "QUEUED",
|
||||
progress: 0,
|
||||
message: "Waiting in queue...",
|
||||
});
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const eventSource = new EventSource(`/api/status/${generationId}`);
|
||||
|
||||
eventSource.addEventListener("progress", (event) => {
|
||||
try {
|
||||
const parsed = JSON.parse(event.data);
|
||||
setData(parsed);
|
||||
|
||||
if (parsed.status === "COMPLETED" || parsed.status === "FAILED") {
|
||||
eventSource.close();
|
||||
}
|
||||
} catch {
|
||||
setError("Failed to parse progress data");
|
||||
}
|
||||
});
|
||||
|
||||
eventSource.addEventListener("timeout", () => {
|
||||
setError("Connection timed out. Please refresh the page.");
|
||||
eventSource.close();
|
||||
});
|
||||
|
||||
eventSource.onerror = () => {
|
||||
setError("Connection error. Please refresh the page.");
|
||||
eventSource.close();
|
||||
};
|
||||
|
||||
return () => {
|
||||
eventSource.close();
|
||||
};
|
||||
}, [generationId]);
|
||||
|
||||
const getStepIndex = (status: GenerationStatus) => {
|
||||
if (status === "COMPLETED") return STEPS.length;
|
||||
if (status === "FAILED") return -1;
|
||||
return STEPS.findIndex((s) => s.status === status);
|
||||
};
|
||||
|
||||
const currentStepIndex = getStepIndex(data.status);
|
||||
const isCompleted = data.status === "COMPLETED";
|
||||
const isFailed = data.status === "FAILED";
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="relative h-2 bg-zinc-800 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="absolute inset-y-0 left-0 bg-gradient-to-r from-blue-500 via-indigo-500 to-purple-500 transition-all duration-500 ease-out"
|
||||
style={{ width: `${data.progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-zinc-400">{data.message}</span>
|
||||
<span className="text-zinc-500 font-mono">{data.progress}%</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{STEPS.map((step, index) => {
|
||||
const isActive = index === currentStepIndex;
|
||||
const isDone = index < currentStepIndex || isCompleted;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={step.status}
|
||||
className={`flex items-center gap-4 p-4 rounded-xl border transition-all duration-300 ${
|
||||
isActive
|
||||
? "bg-blue-500/10 border-blue-500/30"
|
||||
: isDone
|
||||
? "bg-zinc-900/50 border-zinc-800"
|
||||
: "bg-transparent border-zinc-800/50"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center ${
|
||||
isActive
|
||||
? "bg-blue-500/20 text-blue-400"
|
||||
: isDone
|
||||
? "bg-green-500/20 text-green-400"
|
||||
: "bg-zinc-800 text-zinc-500"
|
||||
}`}
|
||||
>
|
||||
{isActive ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
) : isDone ? (
|
||||
<CheckCircle2 className="w-4 h-4" />
|
||||
) : (
|
||||
<Circle className="w-4 h-4" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<p
|
||||
className={`font-medium ${
|
||||
isActive
|
||||
? "text-white"
|
||||
: isDone
|
||||
? "text-zinc-300"
|
||||
: "text-zinc-500"
|
||||
}`}
|
||||
>
|
||||
{step.label}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{isActive && (
|
||||
<div className="flex-shrink-0">
|
||||
<div className="w-2 h-2 rounded-full bg-blue-400 animate-pulse" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{isFailed && (
|
||||
<div className="p-4 rounded-xl bg-red-500/10 border border-red-500/20">
|
||||
<div className="flex items-start gap-3">
|
||||
<AlertCircle className="w-5 h-5 text-red-400 flex-shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<p className="text-red-400 font-medium">Generation Failed</p>
|
||||
<p className="text-red-400/70 text-sm mt-1">
|
||||
Something went wrong. Please try again.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => window.location.reload()}
|
||||
className="mt-4 flex items-center gap-2 px-4 py-2 bg-red-500/20 hover:bg-red-500/30 text-red-400 rounded-lg transition-colors text-sm"
|
||||
>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
Try Again
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isCompleted && (
|
||||
<div className="p-6 rounded-xl bg-green-500/10 border border-green-500/20 text-center">
|
||||
<div className="w-12 h-12 rounded-full bg-green-500/20 flex items-center justify-center mx-auto mb-4">
|
||||
<CheckCircle2 className="w-6 h-6 text-green-400" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-white mb-2">
|
||||
Documentation Ready!
|
||||
</h3>
|
||||
<p className="text-zinc-400 text-sm mb-6">
|
||||
Your interactive documentation has been generated successfully.
|
||||
</p>
|
||||
<Link
|
||||
href={`/docs/${generationId}`}
|
||||
className="inline-flex items-center gap-2 px-6 py-3 bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500 text-white font-medium rounded-xl transition-all"
|
||||
>
|
||||
<FileText className="w-4 h-4" />
|
||||
View Documentation
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && !isFailed && (
|
||||
<div className="p-4 rounded-xl bg-red-500/10 border border-red-500/20">
|
||||
<div className="flex items-start gap-3">
|
||||
<AlertCircle className="w-5 h-5 text-red-400 flex-shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<p className="text-red-400 font-medium">Connection Error</p>
|
||||
<p className="text-red-400/70 text-sm mt-1">{error}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
106
apps/web/src/components/repo-input.tsx
Normal file
106
apps/web/src/components/repo-input.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Github, Loader2, ArrowRight } from "lucide-react";
|
||||
|
||||
const GITHUB_URL_REGEX =
|
||||
/^https?:\/\/(www\.)?github\.com\/[\w.-]+\/[\w.-]+\/?$/;
|
||||
|
||||
export function RepoInput() {
|
||||
const [url, setUrl] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const router = useRouter();
|
||||
|
||||
const isValid = GITHUB_URL_REGEX.test(url);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!isValid) {
|
||||
setError("Please enter a valid GitHub repository URL");
|
||||
return;
|
||||
}
|
||||
|
||||
setError(null);
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/generate", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ repoUrl: url.trim() }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const data = await response.json();
|
||||
throw new Error(data.error || "Failed to start generation");
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
router.push(`/generate?repo=${encodeURIComponent(url)}&id=${data.id}`);
|
||||
} catch (err) {
|
||||
setError(
|
||||
err instanceof Error ? err.message : "An unexpected error occurred"
|
||||
);
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="w-full">
|
||||
<div className="relative flex flex-col sm:flex-row gap-3">
|
||||
<div className="relative flex-1">
|
||||
<div className="absolute left-4 top-1/2 -translate-y-1/2 text-zinc-500">
|
||||
<Github className="w-5 h-5" />
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={url}
|
||||
onChange={(e) => {
|
||||
setUrl(e.target.value);
|
||||
setError(null);
|
||||
}}
|
||||
placeholder="https://github.com/user/repo"
|
||||
className="w-full pl-12 pr-4 py-4 bg-black/40 border border-white/10 rounded-xl text-white placeholder-zinc-500 focus:outline-none focus:border-blue-500/50 focus:ring-2 focus:ring-blue-500/20 transition-all"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading || !url}
|
||||
className="flex items-center justify-center gap-2 px-6 py-4 bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500 text-white font-medium rounded-xl transition-all disabled:opacity-50 disabled:cursor-not-allowed min-w-[160px]"
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Loader2 className="w-5 h-5 animate-spin" />
|
||||
<span>Starting...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span>Generate Docs</span>
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="mt-3 p-3 rounded-lg bg-red-500/10 border border-red-500/20 text-red-400 text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-3 flex items-center gap-2 text-xs text-zinc-500">
|
||||
<div className={`w-2 h-2 rounded-full ${isValid ? "bg-green-500" : "bg-zinc-600"}`} />
|
||||
<span>
|
||||
{isValid
|
||||
? "Valid GitHub URL"
|
||||
: "Enter a public GitHub repository URL"}
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
13
apps/web/src/lib/queue.ts
Normal file
13
apps/web/src/lib/queue.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Queue } from "bullmq";
|
||||
import { getRedis } from "./redis";
|
||||
|
||||
let queue: Queue | null = null;
|
||||
|
||||
export function getQueue(): Queue {
|
||||
if (!queue) {
|
||||
queue = new Queue("codeboard:generate", {
|
||||
connection: getRedis(),
|
||||
});
|
||||
}
|
||||
return queue;
|
||||
}
|
||||
12
apps/web/src/lib/redis.ts
Normal file
12
apps/web/src/lib/redis.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import IORedis from "ioredis";
|
||||
|
||||
let redis: IORedis | null = null;
|
||||
|
||||
export function getRedis(): IORedis {
|
||||
if (!redis) {
|
||||
redis = new IORedis(process.env.REDIS_URL ?? "redis://localhost:6379", {
|
||||
maxRetriesPerRequest: null,
|
||||
});
|
||||
}
|
||||
return redis;
|
||||
}
|
||||
23
apps/web/tsconfig.json
Normal file
23
apps/web/tsconfig.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [{ "name": "next" }],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
25
apps/worker/package.json
Normal file
25
apps/worker/package.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "@codeboard/worker",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"clean": "rm -rf dist",
|
||||
"dev": "tsx watch src/index.ts",
|
||||
"start": "node dist/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codeboard/shared": "*",
|
||||
"@codeboard/parser": "*",
|
||||
"@codeboard/llm": "*",
|
||||
"@codeboard/diagrams": "*",
|
||||
"bullmq": "^5.34.0",
|
||||
"ioredis": "^5.4.0",
|
||||
"simple-git": "^3.27.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"tsx": "^4.19.0",
|
||||
"typescript": "^5.7"
|
||||
}
|
||||
}
|
||||
42
apps/worker/src/index.ts
Normal file
42
apps/worker/src/index.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { Worker } from "bullmq";
|
||||
import IORedis from "ioredis";
|
||||
import { processGenerationJob } from "./processor.js";
|
||||
|
||||
const redisUrl = process.env.REDIS_URL ?? "redis://localhost:6379";
|
||||
const connection = new IORedis(redisUrl, { maxRetriesPerRequest: null });
|
||||
|
||||
const worker = new Worker(
|
||||
"codeboard:generate",
|
||||
async (job) => {
|
||||
console.log(`[worker] Processing job ${job.id}: ${job.data.repoUrl}`);
|
||||
return processGenerationJob(job);
|
||||
},
|
||||
{
|
||||
connection,
|
||||
concurrency: 2,
|
||||
removeOnComplete: { count: 100 },
|
||||
removeOnFail: { count: 50 },
|
||||
}
|
||||
);
|
||||
|
||||
worker.on("completed", (job) => {
|
||||
console.log(`[worker] Job ${job.id} completed`);
|
||||
});
|
||||
|
||||
worker.on("failed", (job, err) => {
|
||||
console.error(`[worker] Job ${job?.id} failed:`, err.message);
|
||||
});
|
||||
|
||||
worker.on("ready", () => {
|
||||
console.log("[worker] Ready and waiting for jobs on codeboard:generate");
|
||||
});
|
||||
|
||||
async function shutdown() {
|
||||
console.log("[worker] Shutting down...");
|
||||
await worker.close();
|
||||
await connection.quit();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
process.on("SIGTERM", shutdown);
|
||||
process.on("SIGINT", shutdown);
|
||||
87
apps/worker/src/jobs/clone.ts
Normal file
87
apps/worker/src/jobs/clone.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { simpleGit } from "simple-git";
|
||||
import { mkdtemp, readdir, stat } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import type { CloneResult } from "@codeboard/shared";
|
||||
|
||||
async function countFiles(dir: string): Promise<{ files: number; lines: number }> {
|
||||
let files = 0;
|
||||
let lines = 0;
|
||||
|
||||
const entries = await readdir(dir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (entry.name === ".git" || entry.name === "node_modules") continue;
|
||||
const fullPath = join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
const sub = await countFiles(fullPath);
|
||||
files += sub.files;
|
||||
lines += sub.lines;
|
||||
} else {
|
||||
files++;
|
||||
const fileStat = await stat(fullPath);
|
||||
lines += Math.ceil(fileStat.size / 40);
|
||||
}
|
||||
}
|
||||
|
||||
return { files, lines };
|
||||
}
|
||||
|
||||
export async function cloneRepository(repoUrl: string): Promise<CloneResult> {
|
||||
const tmpDir = await mkdtemp(join(tmpdir(), "codeboard-"));
|
||||
const git = simpleGit();
|
||||
|
||||
await git.clone(repoUrl, tmpDir, ["--depth", "1", "--single-branch"]);
|
||||
|
||||
const localGit = simpleGit(tmpDir);
|
||||
const log = await localGit.log({ maxCount: 1 });
|
||||
const lastCommit = log.latest?.hash ?? "unknown";
|
||||
|
||||
const repoName = repoUrl
|
||||
.replace(/\.git$/, "")
|
||||
.split("/")
|
||||
.slice(-1)[0] ?? "unknown";
|
||||
|
||||
const { files: totalFiles, lines: totalLines } = await countFiles(tmpDir);
|
||||
|
||||
const languageCounts: Record<string, number> = {};
|
||||
const extMap: Record<string, string> = {
|
||||
".ts": "TypeScript", ".tsx": "TypeScript",
|
||||
".js": "JavaScript", ".jsx": "JavaScript",
|
||||
".py": "Python", ".go": "Go",
|
||||
".rs": "Rust", ".java": "Java",
|
||||
".rb": "Ruby", ".php": "PHP",
|
||||
};
|
||||
|
||||
async function scanLanguages(dir: string) {
|
||||
const entries = await readdir(dir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
||||
const fullPath = join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
await scanLanguages(fullPath);
|
||||
} else {
|
||||
const ext = entry.name.slice(entry.name.lastIndexOf("."));
|
||||
const lang = extMap[ext];
|
||||
if (lang) {
|
||||
const fileStat = await stat(fullPath);
|
||||
languageCounts[lang] = (languageCounts[lang] ?? 0) + fileStat.size;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
await scanLanguages(tmpDir);
|
||||
|
||||
return {
|
||||
localPath: tmpDir,
|
||||
metadata: {
|
||||
name: repoName,
|
||||
description: "",
|
||||
defaultBranch: "main",
|
||||
languages: languageCounts,
|
||||
stars: 0,
|
||||
lastCommit,
|
||||
totalFiles,
|
||||
totalLines,
|
||||
},
|
||||
};
|
||||
}
|
||||
26
apps/worker/src/jobs/generate.ts
Normal file
26
apps/worker/src/jobs/generate.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { CodeStructure, GeneratedDocs } from "@codeboard/shared";
|
||||
import { createProvider, generateDocumentation } from "@codeboard/llm";
|
||||
|
||||
export async function generateDocs(
|
||||
codeStructure: CodeStructure,
|
||||
onProgress?: (stage: string, progress: number) => void
|
||||
): Promise<GeneratedDocs> {
|
||||
const apiKey =
|
||||
process.env.OPENAI_API_KEY ?? process.env.ANTHROPIC_API_KEY ?? "";
|
||||
|
||||
if (!apiKey) {
|
||||
throw new Error(
|
||||
"No LLM API key configured. Set OPENAI_API_KEY or ANTHROPIC_API_KEY."
|
||||
);
|
||||
}
|
||||
|
||||
const providerType = process.env.OPENAI_API_KEY ? "openai" : "anthropic";
|
||||
const provider = createProvider({
|
||||
provider: providerType,
|
||||
apiKey,
|
||||
model: process.env.LLM_MODEL,
|
||||
baseUrl: process.env.LLM_BASE_URL,
|
||||
});
|
||||
|
||||
return generateDocumentation(codeStructure, provider, onProgress);
|
||||
}
|
||||
8
apps/worker/src/jobs/parse.ts
Normal file
8
apps/worker/src/jobs/parse.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import type { CodeStructure } from "@codeboard/shared";
|
||||
import { analyzeRepository } from "@codeboard/parser";
|
||||
|
||||
export async function parseRepository(
|
||||
localPath: string
|
||||
): Promise<CodeStructure> {
|
||||
return analyzeRepository(localPath);
|
||||
}
|
||||
83
apps/worker/src/processor.ts
Normal file
83
apps/worker/src/processor.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import type { Job } from "bullmq";
|
||||
import IORedis from "ioredis";
|
||||
import { cloneRepository } from "./jobs/clone.js";
|
||||
import { parseRepository } from "./jobs/parse.js";
|
||||
import { generateDocs } from "./jobs/generate.js";
|
||||
|
||||
interface GenerateJobData {
|
||||
repoUrl: string;
|
||||
generationId: string;
|
||||
}
|
||||
|
||||
const redis = new IORedis(process.env.REDIS_URL ?? "redis://localhost:6379");
|
||||
|
||||
async function updateProgress(
|
||||
generationId: string,
|
||||
status: string,
|
||||
progress: number,
|
||||
message?: string
|
||||
) {
|
||||
await redis.publish(
|
||||
`codeboard:progress:${generationId}`,
|
||||
JSON.stringify({ status, progress, message })
|
||||
);
|
||||
await redis.set(
|
||||
`codeboard:status:${generationId}`,
|
||||
JSON.stringify({ status, progress, message }),
|
||||
"EX",
|
||||
3600
|
||||
);
|
||||
}
|
||||
|
||||
export async function processGenerationJob(
|
||||
job: Job<GenerateJobData>
|
||||
): Promise<unknown> {
|
||||
const { repoUrl, generationId } = job.data;
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
await updateProgress(generationId, "CLONING", 10, "Cloning repository...");
|
||||
const cloneResult = await cloneRepository(repoUrl);
|
||||
|
||||
await updateProgress(
|
||||
generationId,
|
||||
"PARSING",
|
||||
30,
|
||||
`Analyzing ${cloneResult.metadata.totalFiles} files...`
|
||||
);
|
||||
const codeStructure = await parseRepository(cloneResult.localPath);
|
||||
|
||||
await updateProgress(
|
||||
generationId,
|
||||
"GENERATING",
|
||||
50,
|
||||
`Generating docs for ${codeStructure.modules.length} modules...`
|
||||
);
|
||||
|
||||
const docs = await generateDocs(codeStructure, (stage, progress) => {
|
||||
const mappedProgress = 50 + Math.floor(progress * 0.4);
|
||||
updateProgress(generationId, "GENERATING", mappedProgress, `${stage}...`);
|
||||
});
|
||||
|
||||
docs.id = generationId;
|
||||
docs.repoUrl = repoUrl;
|
||||
docs.repoName = cloneResult.metadata.name;
|
||||
|
||||
const duration = Math.floor((Date.now() - startTime) / 1000);
|
||||
|
||||
await redis.set(
|
||||
`codeboard:result:${generationId}`,
|
||||
JSON.stringify(docs),
|
||||
"EX",
|
||||
86400
|
||||
);
|
||||
|
||||
await updateProgress(generationId, "COMPLETED", 100, "Done!");
|
||||
|
||||
return { generationId, duration, repoName: cloneResult.metadata.name };
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : "Unknown error";
|
||||
await updateProgress(generationId, "FAILED", 0, message);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
8
apps/worker/tsconfig.json
Normal file
8
apps/worker/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
55
docker-compose.yml
Normal file
55
docker-compose.yml
Normal file
@@ -0,0 +1,55 @@
|
||||
services:
|
||||
web:
|
||||
build:
|
||||
context: .
|
||||
target: web
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
- DATABASE_URL=postgresql://codeboard:codeboard@db:5432/codeboard
|
||||
- REDIS_URL=redis://redis:6379
|
||||
- NEXTAUTH_URL=http://localhost:3000
|
||||
depends_on:
|
||||
- db
|
||||
- redis
|
||||
restart: always
|
||||
|
||||
worker:
|
||||
build:
|
||||
context: .
|
||||
target: worker
|
||||
environment:
|
||||
- DATABASE_URL=postgresql://codeboard:codeboard@db:5432/codeboard
|
||||
- REDIS_URL=redis://redis:6379
|
||||
- OPENAI_API_KEY=${OPENAI_API_KEY:-}
|
||||
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-}
|
||||
- LLM_MODEL=${LLM_MODEL:-}
|
||||
- LLM_BASE_URL=${LLM_BASE_URL:-}
|
||||
depends_on:
|
||||
- db
|
||||
- redis
|
||||
restart: always
|
||||
|
||||
db:
|
||||
image: postgres:16-alpine
|
||||
environment:
|
||||
POSTGRES_USER: codeboard
|
||||
POSTGRES_PASSWORD: codeboard
|
||||
POSTGRES_DB: codeboard
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
ports:
|
||||
- "5432:5432"
|
||||
restart: always
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
restart: always
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
redis_data:
|
||||
5920
package-lock.json
generated
Normal file
5920
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
24
package.json
Normal file
24
package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "codeboard",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"apps/*",
|
||||
"packages/*"
|
||||
],
|
||||
"scripts": {
|
||||
"dev": "turbo dev",
|
||||
"build": "turbo build",
|
||||
"lint": "turbo lint",
|
||||
"clean": "turbo clean",
|
||||
"db:generate": "turbo db:generate",
|
||||
"db:push": "turbo db:push"
|
||||
},
|
||||
"devDependencies": {
|
||||
"turbo": "^2",
|
||||
"typescript": "^5.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"packageManager": "npm@10.8.2"
|
||||
}
|
||||
24
packages/database/package.json
Normal file
24
packages/database/package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "@codeboard/database",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"main": "./src/client.ts",
|
||||
"types": "./src/client.ts",
|
||||
"exports": {
|
||||
".": "./src/client.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "echo 'database package uses prisma generate'",
|
||||
"db:generate": "prisma generate",
|
||||
"db:push": "prisma db push",
|
||||
"db:migrate": "prisma migrate dev",
|
||||
"clean": "rm -rf generated"
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/client": "^6.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prisma": "^6.3.0",
|
||||
"typescript": "^5.7"
|
||||
}
|
||||
}
|
||||
50
packages/database/prisma/schema.prisma
Normal file
50
packages/database/prisma/schema.prisma
Normal file
@@ -0,0 +1,50 @@
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
model Generation {
|
||||
id String @id @default(cuid())
|
||||
repoUrl String
|
||||
repoName String
|
||||
commitHash String
|
||||
status Status @default(QUEUED)
|
||||
progress Int @default(0)
|
||||
result Json?
|
||||
error String?
|
||||
costUsd Float?
|
||||
duration Int?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
userId String?
|
||||
user User? @relation(fields: [userId], references: [id])
|
||||
viewCount Int @default(0)
|
||||
|
||||
@@unique([repoUrl, commitHash])
|
||||
@@index([repoUrl])
|
||||
@@index([status])
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
githubId String @unique
|
||||
login String
|
||||
email String?
|
||||
avatarUrl String?
|
||||
createdAt DateTime @default(now())
|
||||
generations Generation[]
|
||||
}
|
||||
|
||||
enum Status {
|
||||
QUEUED
|
||||
CLONING
|
||||
PARSING
|
||||
GENERATING
|
||||
RENDERING
|
||||
COMPLETED
|
||||
FAILED
|
||||
}
|
||||
12
packages/database/src/client.ts
Normal file
12
packages/database/src/client.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient };
|
||||
|
||||
export const prisma =
|
||||
globalForPrisma.prisma ?? new PrismaClient();
|
||||
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
globalForPrisma.prisma = prisma;
|
||||
}
|
||||
|
||||
export { PrismaClient } from "@prisma/client";
|
||||
24
packages/diagrams/package.json
Normal file
24
packages/diagrams/package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "@codeboard/diagrams",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"clean": "rm -rf dist",
|
||||
"dev": "tsc --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codeboard/shared": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.7"
|
||||
}
|
||||
}
|
||||
49
packages/diagrams/src/architecture.ts
Normal file
49
packages/diagrams/src/architecture.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import type { ModuleNode, DependencyEdge } from "@codeboard/shared";
|
||||
|
||||
function sanitizeId(name: string): string {
|
||||
return name.replace(/[^a-zA-Z0-9]/g, "_");
|
||||
}
|
||||
|
||||
function truncateLabel(name: string, max = 20): string {
|
||||
return name.length > max ? name.slice(0, max - 1) + "\u2026" : name;
|
||||
}
|
||||
|
||||
export function generateArchitectureDiagram(
|
||||
modules: ModuleNode[],
|
||||
deps: DependencyEdge[]
|
||||
): string {
|
||||
if (modules.length === 0) {
|
||||
return "flowchart TD\n empty[No modules detected]";
|
||||
}
|
||||
|
||||
const lines: string[] = ["flowchart TD"];
|
||||
|
||||
const moduleIds = new Map<string, string>();
|
||||
for (const mod of modules) {
|
||||
const id = sanitizeId(mod.name);
|
||||
moduleIds.set(mod.path, id);
|
||||
const fileCount = mod.files.length;
|
||||
lines.push(` ${id}["${truncateLabel(mod.name)}\\n${fileCount} files"]`);
|
||||
}
|
||||
|
||||
const edgeSet = new Set<string>();
|
||||
for (const dep of deps) {
|
||||
const sourceModule = modules.find((m) => m.files.includes(dep.source));
|
||||
const targetModule = modules.find((m) => m.files.includes(dep.target));
|
||||
|
||||
if (!sourceModule || !targetModule) continue;
|
||||
if (sourceModule.path === targetModule.path) continue;
|
||||
|
||||
const sourceId = moduleIds.get(sourceModule.path);
|
||||
const targetId = moduleIds.get(targetModule.path);
|
||||
if (!sourceId || !targetId) continue;
|
||||
|
||||
const edgeKey = `${sourceId}-${targetId}`;
|
||||
if (edgeSet.has(edgeKey)) continue;
|
||||
edgeSet.add(edgeKey);
|
||||
|
||||
lines.push(` ${sourceId} --> ${targetId}`);
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
48
packages/diagrams/src/dependency.ts
Normal file
48
packages/diagrams/src/dependency.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import type { FileNode, DependencyEdge } from "@codeboard/shared";
|
||||
|
||||
function sanitizeId(path: string): string {
|
||||
return path.replace(/[^a-zA-Z0-9]/g, "_");
|
||||
}
|
||||
|
||||
function shortenPath(path: string): string {
|
||||
const parts = path.split("/");
|
||||
if (parts.length <= 2) return path;
|
||||
return parts.slice(-2).join("/");
|
||||
}
|
||||
|
||||
export function generateDependencyGraph(
|
||||
files: FileNode[],
|
||||
deps: DependencyEdge[]
|
||||
): string {
|
||||
if (files.length === 0) {
|
||||
return "graph LR\n empty[No files detected]";
|
||||
}
|
||||
|
||||
const maxFiles = 30;
|
||||
const topFiles = files.slice(0, maxFiles);
|
||||
const topPaths = new Set(topFiles.map((f) => f.path));
|
||||
|
||||
const lines: string[] = ["graph LR"];
|
||||
|
||||
for (const file of topFiles) {
|
||||
const id = sanitizeId(file.path);
|
||||
const label = shortenPath(file.path);
|
||||
lines.push(` ${id}["${label}"]`);
|
||||
}
|
||||
|
||||
const edgeSet = new Set<string>();
|
||||
for (const dep of deps) {
|
||||
if (!topPaths.has(dep.source) || !topPaths.has(dep.target)) continue;
|
||||
if (dep.source === dep.target) continue;
|
||||
|
||||
const edgeKey = `${dep.source}-${dep.target}`;
|
||||
if (edgeSet.has(edgeKey)) continue;
|
||||
edgeSet.add(edgeKey);
|
||||
|
||||
const sourceId = sanitizeId(dep.source);
|
||||
const targetId = sanitizeId(dep.target);
|
||||
lines.push(` ${sourceId} --> ${targetId}`);
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
2
packages/diagrams/src/index.ts
Normal file
2
packages/diagrams/src/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { generateArchitectureDiagram } from "./architecture.js";
|
||||
export { generateDependencyGraph } from "./dependency.js";
|
||||
8
packages/diagrams/tsconfig.json
Normal file
8
packages/diagrams/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
26
packages/llm/package.json
Normal file
26
packages/llm/package.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "@codeboard/llm",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"clean": "rm -rf dist",
|
||||
"dev": "tsc --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codeboard/shared": "*",
|
||||
"openai": "^4.77.0",
|
||||
"@anthropic-ai/sdk": "^0.36.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.7"
|
||||
}
|
||||
}
|
||||
72
packages/llm/src/chunker.ts
Normal file
72
packages/llm/src/chunker.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import type { FileNode } from "@codeboard/shared";
|
||||
|
||||
const APPROX_CHARS_PER_TOKEN = 4;
|
||||
|
||||
export function chunkCode(content: string, maxTokens: number): string[] {
|
||||
const maxChars = maxTokens * APPROX_CHARS_PER_TOKEN;
|
||||
if (content.length <= maxChars) return [content];
|
||||
|
||||
const lines = content.split("\n");
|
||||
const chunks: string[] = [];
|
||||
let current: string[] = [];
|
||||
let currentLen = 0;
|
||||
|
||||
for (const line of lines) {
|
||||
if (currentLen + line.length > maxChars && current.length > 0) {
|
||||
chunks.push(current.join("\n"));
|
||||
current = [];
|
||||
currentLen = 0;
|
||||
}
|
||||
current.push(line);
|
||||
currentLen += line.length + 1;
|
||||
}
|
||||
|
||||
if (current.length > 0) {
|
||||
chunks.push(current.join("\n"));
|
||||
}
|
||||
|
||||
return chunks;
|
||||
}
|
||||
|
||||
export function extractSignatures(fileNode: FileNode): string {
|
||||
const parts: string[] = [];
|
||||
|
||||
parts.push(`File: ${fileNode.path} (${fileNode.language})`);
|
||||
|
||||
if (fileNode.imports.length > 0) {
|
||||
parts.push("Imports:");
|
||||
for (const imp of fileNode.imports) {
|
||||
parts.push(` from "${imp.source}" import {${imp.specifiers.join(", ")}}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (fileNode.exports.length > 0) {
|
||||
parts.push("Exports:");
|
||||
for (const exp of fileNode.exports) {
|
||||
parts.push(` ${exp.isDefault ? "default " : ""}${exp.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const fn of fileNode.functions) {
|
||||
const params = fn.params.join(", ");
|
||||
const ret = fn.returnType ? `: ${fn.returnType}` : "";
|
||||
const doc = fn.docstring ? ` — ${fn.docstring.slice(0, 100)}` : "";
|
||||
parts.push(`function ${fn.name}(${params})${ret}${doc}`);
|
||||
}
|
||||
|
||||
for (const cls of fileNode.classes) {
|
||||
parts.push(`class ${cls.name}`);
|
||||
for (const method of cls.methods) {
|
||||
parts.push(` method ${method.name}(${method.params.join(", ")})`);
|
||||
}
|
||||
for (const prop of cls.properties) {
|
||||
parts.push(` property ${prop.name}${prop.type ? `: ${prop.type}` : ""}`);
|
||||
}
|
||||
}
|
||||
|
||||
return parts.join("\n");
|
||||
}
|
||||
|
||||
export function estimateTokens(text: string): number {
|
||||
return Math.ceil(text.length / APPROX_CHARS_PER_TOKEN);
|
||||
}
|
||||
4
packages/llm/src/index.ts
Normal file
4
packages/llm/src/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export { createProvider } from "./providers/factory.js";
|
||||
export { generateDocumentation } from "./pipeline.js";
|
||||
export { chunkCode, extractSignatures } from "./chunker.js";
|
||||
export type { LLMProvider } from "./providers/base.js";
|
||||
153
packages/llm/src/pipeline.ts
Normal file
153
packages/llm/src/pipeline.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
import type { CodeStructure, GeneratedDocs, FileNode } from "@codeboard/shared";
|
||||
import type { LLMProvider } from "./providers/base.js";
|
||||
import { buildArchitecturePrompt } from "./prompts/architecture-overview.js";
|
||||
import { buildModuleSummaryPrompt } from "./prompts/module-summary.js";
|
||||
import { buildPatternsPrompt } from "./prompts/patterns-detection.js";
|
||||
import { buildGettingStartedPrompt } from "./prompts/getting-started.js";
|
||||
|
||||
function parseSection(text: string, header: string): string {
|
||||
const regex = new RegExp(`## ${header}\\s*\\n([\\s\\S]*?)(?=\\n## |$)`);
|
||||
const match = regex.exec(text);
|
||||
return match?.[1]?.trim() ?? "";
|
||||
}
|
||||
|
||||
function parseMermaid(text: string): string {
|
||||
const match = /```mermaid\s*\n([\s\S]*?)```/.exec(text);
|
||||
return match?.[1]?.trim() ?? "flowchart TD\n A[No diagram generated]";
|
||||
}
|
||||
|
||||
function parseList(text: string): string[] {
|
||||
return text
|
||||
.split("\n")
|
||||
.map((l) => l.replace(/^[-*]\s*/, "").trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
export async function generateDocumentation(
|
||||
codeStructure: CodeStructure,
|
||||
provider: LLMProvider,
|
||||
onProgress?: (stage: string, progress: number) => void
|
||||
): Promise<GeneratedDocs> {
|
||||
onProgress?.("architecture", 10);
|
||||
|
||||
const archMessages = buildArchitecturePrompt(codeStructure);
|
||||
const archResponse = await provider.chat(archMessages);
|
||||
|
||||
const architectureOverview = parseSection(archResponse, "Architecture Overview");
|
||||
const techStackRaw = parseSection(archResponse, "Tech Stack");
|
||||
const architectureDiagram = parseMermaid(archResponse);
|
||||
const techStack = techStackRaw.split(",").map((s) => s.trim()).filter(Boolean);
|
||||
|
||||
onProgress?.("modules", 30);
|
||||
|
||||
const moduleLimit = Math.min(codeStructure.modules.length, 10);
|
||||
const moduleSummaries = await Promise.all(
|
||||
codeStructure.modules.slice(0, moduleLimit).map(async (mod) => {
|
||||
const moduleFiles: FileNode[] = codeStructure.files.filter((f) =>
|
||||
mod.files.includes(f.path)
|
||||
);
|
||||
|
||||
if (moduleFiles.length === 0) {
|
||||
return {
|
||||
name: mod.name,
|
||||
path: mod.path,
|
||||
summary: "Empty module — no parseable files found.",
|
||||
keyFiles: [],
|
||||
publicApi: [],
|
||||
dependsOn: [],
|
||||
dependedBy: [],
|
||||
};
|
||||
}
|
||||
|
||||
const messages = buildModuleSummaryPrompt(mod, moduleFiles);
|
||||
const response = await provider.chat(messages, { model: undefined });
|
||||
|
||||
const summary = parseSection(response, "Summary");
|
||||
const keyFilesRaw = parseList(parseSection(response, "Key Files"));
|
||||
const publicApi = parseList(parseSection(response, "Public API"));
|
||||
|
||||
const dependsOn = [
|
||||
...new Set(
|
||||
moduleFiles.flatMap((f) =>
|
||||
f.imports
|
||||
.map((imp) => imp.source)
|
||||
.filter((s) => !s.startsWith("."))
|
||||
)
|
||||
),
|
||||
].slice(0, 10);
|
||||
|
||||
const dependedBy = codeStructure.dependencies
|
||||
.filter((d) => mod.files.includes(d.target))
|
||||
.map((d) => d.source)
|
||||
.filter((s) => !mod.files.includes(s))
|
||||
.slice(0, 10);
|
||||
|
||||
return {
|
||||
name: mod.name,
|
||||
path: mod.path,
|
||||
summary: summary || "Module analyzed but no summary generated.",
|
||||
keyFiles: keyFilesRaw.map((kf) => ({ path: kf, purpose: "" })),
|
||||
publicApi,
|
||||
dependsOn,
|
||||
dependedBy,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
onProgress?.("patterns", 60);
|
||||
|
||||
const patternsMessages = buildPatternsPrompt(codeStructure);
|
||||
const patternsResponse = await provider.chat(patternsMessages);
|
||||
|
||||
const conventions = parseList(parseSection(patternsResponse, "Coding Conventions"));
|
||||
const designPatterns = parseList(parseSection(patternsResponse, "Design Patterns"));
|
||||
const architecturalDecisions = parseList(parseSection(patternsResponse, "Architectural Decisions"));
|
||||
|
||||
onProgress?.("getting-started", 80);
|
||||
|
||||
const gsMessages = buildGettingStartedPrompt(
|
||||
codeStructure,
|
||||
architectureOverview
|
||||
);
|
||||
const gsResponse = await provider.chat(gsMessages);
|
||||
|
||||
const prerequisites = parseList(parseSection(gsResponse, "Prerequisites"));
|
||||
const setupSteps = parseList(parseSection(gsResponse, "Setup Steps"));
|
||||
const firstTask = parseSection(gsResponse, "Your First Task");
|
||||
|
||||
onProgress?.("complete", 100);
|
||||
|
||||
const languages = [...new Set(codeStructure.files.map((f) => f.language))];
|
||||
|
||||
return {
|
||||
id: "",
|
||||
repoUrl: "",
|
||||
repoName: "",
|
||||
generatedAt: new Date().toISOString(),
|
||||
sections: {
|
||||
overview: {
|
||||
title: "Architecture Overview",
|
||||
description: architectureOverview,
|
||||
architectureDiagram,
|
||||
techStack,
|
||||
keyMetrics: {
|
||||
files: codeStructure.files.length,
|
||||
modules: codeStructure.modules.length,
|
||||
languages,
|
||||
},
|
||||
},
|
||||
modules: moduleSummaries,
|
||||
patterns: {
|
||||
conventions,
|
||||
designPatterns,
|
||||
architecturalDecisions,
|
||||
},
|
||||
gettingStarted: {
|
||||
prerequisites,
|
||||
setupSteps,
|
||||
firstTask,
|
||||
},
|
||||
dependencyGraph: architectureDiagram,
|
||||
},
|
||||
};
|
||||
}
|
||||
51
packages/llm/src/prompts/architecture-overview.ts
Normal file
51
packages/llm/src/prompts/architecture-overview.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import type { LLMMessage, CodeStructure } from "@codeboard/shared";
|
||||
|
||||
export function buildArchitecturePrompt(
|
||||
structure: CodeStructure
|
||||
): LLMMessage[] {
|
||||
const fileTree = structure.files
|
||||
.map((f) => ` ${f.path} (${f.language}, ${f.functions.length} functions, ${f.classes.length} classes)`)
|
||||
.join("\n");
|
||||
|
||||
const modules = structure.modules
|
||||
.map((m) => ` ${m.name}/ (${m.files.length} files)`)
|
||||
.join("\n");
|
||||
|
||||
const entryPoints = structure.entryPoints.join(", ") || "none detected";
|
||||
|
||||
return [
|
||||
{
|
||||
role: "system",
|
||||
content: `You are an expert software architect analyzing a codebase. Generate a concise architecture overview and a Mermaid flowchart diagram.
|
||||
|
||||
Output format (use exactly these headers):
|
||||
## Architecture Overview
|
||||
[2-4 paragraphs describing the high-level architecture, key design decisions, and how components interact]
|
||||
|
||||
## Tech Stack
|
||||
[comma-separated list of technologies detected]
|
||||
|
||||
## Mermaid Diagram
|
||||
\`\`\`mermaid
|
||||
[flowchart TD diagram showing modules and their relationships]
|
||||
\`\`\``,
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: `Analyze this codebase structure:
|
||||
|
||||
FILE TREE:
|
||||
${fileTree}
|
||||
|
||||
MODULES:
|
||||
${modules}
|
||||
|
||||
ENTRY POINTS: ${entryPoints}
|
||||
|
||||
DEPENDENCIES (import edges):
|
||||
${structure.dependencies.slice(0, 50).map((d) => ` ${d.source} -> ${d.target}`).join("\n")}
|
||||
|
||||
Generate the architecture overview with a Mermaid diagram.`,
|
||||
},
|
||||
];
|
||||
}
|
||||
43
packages/llm/src/prompts/getting-started.ts
Normal file
43
packages/llm/src/prompts/getting-started.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import type { LLMMessage, CodeStructure } from "@codeboard/shared";
|
||||
|
||||
export function buildGettingStartedPrompt(
|
||||
structure: CodeStructure,
|
||||
architectureOverview: string,
|
||||
readmeContent?: string,
|
||||
packageJsonContent?: string
|
||||
): LLMMessage[] {
|
||||
return [
|
||||
{
|
||||
role: "system",
|
||||
content: `You are writing an onboarding guide for a new developer joining this project. Be specific and actionable.
|
||||
|
||||
Output format:
|
||||
## Prerequisites
|
||||
[list required tools, runtimes, and their versions]
|
||||
|
||||
## Setup Steps
|
||||
[numbered list of concrete commands and actions to get the project running locally]
|
||||
|
||||
## Your First Task
|
||||
[suggest a good first contribution — something small but meaningful that touches multiple parts of the codebase]`,
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: `Create an onboarding guide for this project.
|
||||
|
||||
ARCHITECTURE OVERVIEW:
|
||||
${architectureOverview}
|
||||
|
||||
${readmeContent ? `README:\n${readmeContent.slice(0, 3000)}` : "README: not available"}
|
||||
|
||||
${packageJsonContent ? `PACKAGE.JSON:\n${packageJsonContent.slice(0, 2000)}` : ""}
|
||||
|
||||
LANGUAGES: ${[...new Set(structure.files.map((f) => f.language))].join(", ")}
|
||||
ENTRY POINTS: ${structure.entryPoints.join(", ") || "none detected"}
|
||||
TOTAL FILES: ${structure.files.length}
|
||||
TOTAL MODULES: ${structure.modules.length}
|
||||
|
||||
Write a concrete, actionable onboarding guide.`,
|
||||
},
|
||||
];
|
||||
}
|
||||
42
packages/llm/src/prompts/module-summary.ts
Normal file
42
packages/llm/src/prompts/module-summary.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import type { LLMMessage, ModuleNode, FileNode } from "@codeboard/shared";
|
||||
|
||||
export function buildModuleSummaryPrompt(
|
||||
module: ModuleNode,
|
||||
files: FileNode[]
|
||||
): LLMMessage[] {
|
||||
const fileDetails = files
|
||||
.map((f) => {
|
||||
const fns = f.functions.map((fn) => ` ${fn.name}(${fn.params.join(", ")})`).join("\n");
|
||||
const cls = f.classes.map((c) => ` class ${c.name}`).join("\n");
|
||||
const exps = f.exports.map((e) => ` export ${e.isDefault ? "default " : ""}${e.name}`).join("\n");
|
||||
return ` ${f.path}:\n${fns}\n${cls}\n${exps}`;
|
||||
})
|
||||
.join("\n\n");
|
||||
|
||||
return [
|
||||
{
|
||||
role: "system",
|
||||
content: `You are analyzing a code module. Provide a concise summary.
|
||||
|
||||
Output format:
|
||||
## Summary
|
||||
[1-2 paragraphs explaining what this module does and its role in the project]
|
||||
|
||||
## Key Files
|
||||
[list each important file with a one-line description]
|
||||
|
||||
## Public API
|
||||
[list the main exported functions/classes and what they do]`,
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: `Module: ${module.name} (${module.path})
|
||||
Files: ${module.files.length}
|
||||
|
||||
FILE DETAILS:
|
||||
${fileDetails}
|
||||
|
||||
Summarize this module.`,
|
||||
},
|
||||
];
|
||||
}
|
||||
55
packages/llm/src/prompts/patterns-detection.ts
Normal file
55
packages/llm/src/prompts/patterns-detection.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import type { LLMMessage, CodeStructure } from "@codeboard/shared";
|
||||
|
||||
export function buildPatternsPrompt(structure: CodeStructure): LLMMessage[] {
|
||||
const sampleFunctions = structure.files
|
||||
.flatMap((f) => f.functions.map((fn) => `${f.path}: ${fn.name}(${fn.params.join(", ")})`))
|
||||
.slice(0, 40)
|
||||
.join("\n");
|
||||
|
||||
const sampleClasses = structure.files
|
||||
.flatMap((f) => f.classes.map((c) => `${f.path}: class ${c.name} [${c.methods.map((m) => m.name).join(", ")}]`))
|
||||
.slice(0, 20)
|
||||
.join("\n");
|
||||
|
||||
const importSources = new Set<string>();
|
||||
for (const f of structure.files) {
|
||||
for (const imp of f.imports) {
|
||||
importSources.add(imp.source);
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
role: "system",
|
||||
content: `You are a code reviewer identifying patterns and conventions in a codebase.
|
||||
|
||||
Output format:
|
||||
## Coding Conventions
|
||||
[list conventions like naming patterns, file organization, error handling approach]
|
||||
|
||||
## Design Patterns
|
||||
[list design patterns detected: factory, singleton, observer, repository, etc.]
|
||||
|
||||
## Architectural Decisions
|
||||
[list key architectural decisions: monorepo vs polyrepo, framework choices, state management, etc.]`,
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: `Analyze these code patterns:
|
||||
|
||||
FUNCTION SIGNATURES:
|
||||
${sampleFunctions}
|
||||
|
||||
CLASS DEFINITIONS:
|
||||
${sampleClasses}
|
||||
|
||||
EXTERNAL DEPENDENCIES:
|
||||
${Array.from(importSources).filter((s) => !s.startsWith(".")).slice(0, 30).join(", ")}
|
||||
|
||||
DETECTED PATTERNS FROM AST:
|
||||
${structure.patterns.map((p) => ` ${p.name}: ${p.description}`).join("\n") || " (none pre-detected)"}
|
||||
|
||||
Identify coding conventions, design patterns, and architectural decisions.`,
|
||||
},
|
||||
];
|
||||
}
|
||||
34
packages/llm/src/providers/anthropic.ts
Normal file
34
packages/llm/src/providers/anthropic.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import Anthropic from "@anthropic-ai/sdk";
|
||||
import type { LLMMessage, LLMOptions } from "@codeboard/shared";
|
||||
import type { LLMProvider } from "./base.js";
|
||||
|
||||
export class AnthropicProvider implements LLMProvider {
|
||||
name = "anthropic";
|
||||
private client: Anthropic;
|
||||
private defaultModel: string;
|
||||
|
||||
constructor(apiKey: string, model?: string) {
|
||||
this.client = new Anthropic({ apiKey });
|
||||
this.defaultModel = model ?? "claude-sonnet-4-20250514";
|
||||
}
|
||||
|
||||
async chat(messages: LLMMessage[], options?: LLMOptions): Promise<string> {
|
||||
const systemMessage = messages.find((m) => m.role === "system");
|
||||
const nonSystemMessages = messages
|
||||
.filter((m) => m.role !== "system")
|
||||
.map((m) => ({
|
||||
role: m.role as "user" | "assistant",
|
||||
content: m.content,
|
||||
}));
|
||||
|
||||
const response = await this.client.messages.create({
|
||||
model: options?.model ?? this.defaultModel,
|
||||
max_tokens: options?.maxTokens ?? 4096,
|
||||
system: systemMessage?.content,
|
||||
messages: nonSystemMessages,
|
||||
});
|
||||
|
||||
const textBlock = response.content.find((b) => b.type === "text");
|
||||
return textBlock?.type === "text" ? textBlock.text : "";
|
||||
}
|
||||
}
|
||||
6
packages/llm/src/providers/base.ts
Normal file
6
packages/llm/src/providers/base.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import type { LLMMessage, LLMOptions } from "@codeboard/shared";
|
||||
|
||||
export interface LLMProvider {
|
||||
name: string;
|
||||
chat(messages: LLMMessage[], options?: LLMOptions): Promise<string>;
|
||||
}
|
||||
15
packages/llm/src/providers/factory.ts
Normal file
15
packages/llm/src/providers/factory.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { LLMProviderConfig } from "@codeboard/shared";
|
||||
import type { LLMProvider } from "./base.js";
|
||||
import { OpenAIProvider } from "./openai.js";
|
||||
import { AnthropicProvider } from "./anthropic.js";
|
||||
|
||||
export function createProvider(config: LLMProviderConfig): LLMProvider {
|
||||
switch (config.provider) {
|
||||
case "openai":
|
||||
return new OpenAIProvider(config.apiKey, config.model, config.baseUrl);
|
||||
case "anthropic":
|
||||
return new AnthropicProvider(config.apiKey, config.model);
|
||||
default:
|
||||
throw new Error(`Unknown LLM provider: ${config.provider}`);
|
||||
}
|
||||
}
|
||||
28
packages/llm/src/providers/openai.ts
Normal file
28
packages/llm/src/providers/openai.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import OpenAI from "openai";
|
||||
import type { LLMMessage, LLMOptions } from "@codeboard/shared";
|
||||
import type { LLMProvider } from "./base.js";
|
||||
|
||||
export class OpenAIProvider implements LLMProvider {
|
||||
name = "openai";
|
||||
private client: OpenAI;
|
||||
private defaultModel: string;
|
||||
|
||||
constructor(apiKey: string, model?: string, baseUrl?: string) {
|
||||
this.client = new OpenAI({
|
||||
apiKey,
|
||||
baseURL: baseUrl,
|
||||
});
|
||||
this.defaultModel = model ?? "gpt-4o";
|
||||
}
|
||||
|
||||
async chat(messages: LLMMessage[], options?: LLMOptions): Promise<string> {
|
||||
const response = await this.client.chat.completions.create({
|
||||
model: options?.model ?? this.defaultModel,
|
||||
messages: messages.map((m) => ({ role: m.role, content: m.content })),
|
||||
temperature: options?.temperature ?? 0.3,
|
||||
max_tokens: options?.maxTokens ?? 4096,
|
||||
});
|
||||
|
||||
return response.choices[0]?.message?.content ?? "";
|
||||
}
|
||||
}
|
||||
8
packages/llm/tsconfig.json
Normal file
8
packages/llm/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
29
packages/parser/package.json
Normal file
29
packages/parser/package.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "@codeboard/parser",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"clean": "rm -rf dist",
|
||||
"dev": "tsc --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.26.0",
|
||||
"@babel/traverse": "^7.26.0",
|
||||
"@babel/types": "^7.26.0",
|
||||
"@codeboard/shared": "*",
|
||||
"glob": "^11.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/babel__traverse": "^7.20.0",
|
||||
"typescript": "^5.7"
|
||||
}
|
||||
}
|
||||
150
packages/parser/src/analyzer.ts
Normal file
150
packages/parser/src/analyzer.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { dirname, basename } from "node:path";
|
||||
import type {
|
||||
CodeStructure,
|
||||
FileNode,
|
||||
ModuleNode,
|
||||
DependencyEdge,
|
||||
ExportNode,
|
||||
} from "@codeboard/shared";
|
||||
import { walkFiles } from "./file-walker.js";
|
||||
import { typescriptParser } from "./languages/typescript.js";
|
||||
import { pythonParser } from "./languages/python.js";
|
||||
import type { LanguageParser } from "./languages/base.js";
|
||||
|
||||
const MAX_FILES = 200;
|
||||
|
||||
const parsers: LanguageParser[] = [typescriptParser, pythonParser];
|
||||
|
||||
function getParser(language: string): LanguageParser | null {
|
||||
return (
|
||||
parsers.find((p) =>
|
||||
p.extensions.some((ext) => {
|
||||
const langMap: Record<string, string[]> = {
|
||||
typescript: [".ts", ".tsx"],
|
||||
javascript: [".js", ".jsx", ".mjs", ".cjs"],
|
||||
python: [".py"],
|
||||
};
|
||||
return langMap[language]?.includes(ext);
|
||||
})
|
||||
) ?? null
|
||||
);
|
||||
}
|
||||
|
||||
function buildModules(files: FileNode[]): ModuleNode[] {
|
||||
const dirMap = new Map<string, string[]>();
|
||||
|
||||
for (const file of files) {
|
||||
const dir = dirname(file.path);
|
||||
const existing = dirMap.get(dir);
|
||||
if (existing) {
|
||||
existing.push(file.path);
|
||||
} else {
|
||||
dirMap.set(dir, [file.path]);
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(dirMap.entries()).map(([dirPath, filePaths]) => ({
|
||||
name: basename(dirPath) || "root",
|
||||
path: dirPath,
|
||||
files: filePaths,
|
||||
}));
|
||||
}
|
||||
|
||||
function buildDependencies(files: FileNode[]): DependencyEdge[] {
|
||||
const edges: DependencyEdge[] = [];
|
||||
const filePathSet = new Set(files.map((f) => f.path));
|
||||
|
||||
for (const file of files) {
|
||||
for (const imp of file.imports) {
|
||||
let resolved = imp.source;
|
||||
|
||||
if (resolved.startsWith(".")) {
|
||||
const dir = dirname(file.path);
|
||||
const candidate = `${dir}/${resolved.replace(/^\.\//, "")}`;
|
||||
const extensions = [".ts", ".tsx", ".js", ".jsx", ".py", ""];
|
||||
for (const ext of extensions) {
|
||||
if (filePathSet.has(candidate + ext)) {
|
||||
resolved = candidate + ext;
|
||||
break;
|
||||
}
|
||||
if (filePathSet.has(`${candidate}/index${ext}`)) {
|
||||
resolved = `${candidate}/index${ext}`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
edges.push({
|
||||
source: file.path,
|
||||
target: resolved,
|
||||
type: "import",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return edges;
|
||||
}
|
||||
|
||||
function detectEntryPoints(files: FileNode[]): string[] {
|
||||
const entryNames = new Set([
|
||||
"index",
|
||||
"main",
|
||||
"app",
|
||||
"server",
|
||||
"mod",
|
||||
"lib",
|
||||
"__init__",
|
||||
]);
|
||||
|
||||
return files
|
||||
.filter((f) => {
|
||||
const name = basename(f.path).replace(/\.[^.]+$/, "");
|
||||
return entryNames.has(name);
|
||||
})
|
||||
.map((f) => f.path);
|
||||
}
|
||||
|
||||
function collectExports(files: FileNode[]): ExportNode[] {
|
||||
const allExports: ExportNode[] = [];
|
||||
for (const file of files) {
|
||||
allExports.push(...file.exports);
|
||||
}
|
||||
return allExports;
|
||||
}
|
||||
|
||||
export async function analyzeRepository(
|
||||
repoPath: string
|
||||
): Promise<CodeStructure> {
|
||||
const walkedFiles = await walkFiles(repoPath);
|
||||
const filesToAnalyze = walkedFiles.slice(0, MAX_FILES);
|
||||
|
||||
const parsedFiles: FileNode[] = [];
|
||||
|
||||
for (const walkedFile of filesToAnalyze) {
|
||||
const parser = getParser(walkedFile.language);
|
||||
if (!parser) continue;
|
||||
|
||||
try {
|
||||
const content = await readFile(walkedFile.absolutePath, "utf-8");
|
||||
const fileNode = parser.parse(content, walkedFile.relativePath);
|
||||
parsedFiles.push(fileNode);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const modules = buildModules(parsedFiles);
|
||||
const dependencies = buildDependencies(parsedFiles);
|
||||
const entryPoints = detectEntryPoints(parsedFiles);
|
||||
const exports = collectExports(parsedFiles);
|
||||
|
||||
return {
|
||||
files: parsedFiles,
|
||||
modules,
|
||||
entryPoints,
|
||||
exports,
|
||||
dependencies,
|
||||
patterns: [],
|
||||
};
|
||||
}
|
||||
121
packages/parser/src/file-walker.ts
Normal file
121
packages/parser/src/file-walker.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { readdir, stat, readFile } from "node:fs/promises";
|
||||
import { join, relative, extname, basename } from "node:path";
|
||||
|
||||
const IGNORED_DIRS = new Set([
|
||||
"node_modules",
|
||||
".git",
|
||||
"dist",
|
||||
"build",
|
||||
"vendor",
|
||||
"__pycache__",
|
||||
".next",
|
||||
".turbo",
|
||||
"coverage",
|
||||
".venv",
|
||||
"venv",
|
||||
".tox",
|
||||
"target",
|
||||
".cache",
|
||||
".idea",
|
||||
".vscode",
|
||||
]);
|
||||
|
||||
const LANGUAGE_MAP: Record<string, string> = {
|
||||
".ts": "typescript",
|
||||
".tsx": "typescript",
|
||||
".js": "javascript",
|
||||
".jsx": "javascript",
|
||||
".mjs": "javascript",
|
||||
".cjs": "javascript",
|
||||
".py": "python",
|
||||
".go": "go",
|
||||
".rs": "rust",
|
||||
".java": "java",
|
||||
".rb": "ruby",
|
||||
".php": "php",
|
||||
".cs": "csharp",
|
||||
".cpp": "cpp",
|
||||
".c": "c",
|
||||
".h": "c",
|
||||
".hpp": "cpp",
|
||||
".swift": "swift",
|
||||
".kt": "kotlin",
|
||||
};
|
||||
|
||||
const ENTRY_POINT_NAMES = new Set([
|
||||
"index",
|
||||
"main",
|
||||
"app",
|
||||
"server",
|
||||
"mod",
|
||||
"lib",
|
||||
"__init__",
|
||||
"manage",
|
||||
]);
|
||||
|
||||
export interface WalkedFile {
|
||||
absolutePath: string;
|
||||
relativePath: string;
|
||||
language: string;
|
||||
size: number;
|
||||
isEntryPoint: boolean;
|
||||
}
|
||||
|
||||
async function walkDir(
|
||||
dir: string,
|
||||
rootDir: string,
|
||||
results: WalkedFile[]
|
||||
): Promise<void> {
|
||||
const entries = await readdir(dir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
if (IGNORED_DIRS.has(entry.name)) continue;
|
||||
if (entry.name.startsWith(".")) continue;
|
||||
|
||||
const fullPath = join(dir, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
await walkDir(fullPath, rootDir, results);
|
||||
continue;
|
||||
}
|
||||
|
||||
const ext = extname(entry.name);
|
||||
const language = LANGUAGE_MAP[ext];
|
||||
if (!language) continue;
|
||||
|
||||
const fileStat = await stat(fullPath);
|
||||
if (fileStat.size > 500_000) continue;
|
||||
|
||||
const nameWithoutExt = basename(entry.name, ext);
|
||||
const isEntryPoint = ENTRY_POINT_NAMES.has(nameWithoutExt);
|
||||
|
||||
results.push({
|
||||
absolutePath: fullPath,
|
||||
relativePath: relative(rootDir, fullPath),
|
||||
language,
|
||||
size: fileStat.size,
|
||||
isEntryPoint,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function walkFiles(repoPath: string): Promise<WalkedFile[]> {
|
||||
const results: WalkedFile[] = [];
|
||||
await walkDir(repoPath, repoPath, results);
|
||||
|
||||
results.sort((a, b) => {
|
||||
if (a.isEntryPoint && !b.isEntryPoint) return -1;
|
||||
if (!a.isEntryPoint && b.isEntryPoint) return 1;
|
||||
return a.relativePath.localeCompare(b.relativePath);
|
||||
});
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
export async function readFileContent(filePath: string): Promise<string> {
|
||||
return readFile(filePath, "utf-8");
|
||||
}
|
||||
|
||||
export function detectLanguage(filePath: string): string | null {
|
||||
return LANGUAGE_MAP[extname(filePath)] ?? null;
|
||||
}
|
||||
3
packages/parser/src/index.ts
Normal file
3
packages/parser/src/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { analyzeRepository } from "./analyzer.js";
|
||||
export { walkFiles } from "./file-walker.js";
|
||||
export type { LanguageParser } from "./languages/base.js";
|
||||
6
packages/parser/src/languages/base.ts
Normal file
6
packages/parser/src/languages/base.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import type { FileNode } from "@codeboard/shared";
|
||||
|
||||
export interface LanguageParser {
|
||||
extensions: string[];
|
||||
parse(content: string, filePath: string): FileNode;
|
||||
}
|
||||
157
packages/parser/src/languages/python.ts
Normal file
157
packages/parser/src/languages/python.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import type {
|
||||
FileNode,
|
||||
FunctionNode,
|
||||
ClassNode,
|
||||
ImportNode,
|
||||
ExportNode,
|
||||
} from "@codeboard/shared";
|
||||
import type { LanguageParser } from "./base.js";
|
||||
|
||||
const FUNC_RE = /^(\s*)def\s+(\w+)\s*\(([^)]*)\)(?:\s*->\s*([^:]+))?\s*:/gm;
|
||||
const CLASS_RE = /^(\s*)class\s+(\w+)(?:\(([^)]*)\))?\s*:/gm;
|
||||
const IMPORT_RE = /^(?:from\s+([\w.]+)\s+)?import\s+(.+)$/gm;
|
||||
const DOCSTRING_RE = /^\s*(?:"""([\s\S]*?)"""|'''([\s\S]*?)''')/;
|
||||
|
||||
function parseParams(raw: string): string[] {
|
||||
if (!raw.trim()) return [];
|
||||
return raw
|
||||
.split(",")
|
||||
.map((p) => p.trim().split(":")[0].split("=")[0].trim())
|
||||
.filter((p) => p && p !== "self" && p !== "cls");
|
||||
}
|
||||
|
||||
export const pythonParser: LanguageParser = {
|
||||
extensions: [".py"],
|
||||
|
||||
parse(content: string, filePath: string): FileNode {
|
||||
const lines = content.split("\n");
|
||||
const functions: FunctionNode[] = [];
|
||||
const classes: ClassNode[] = [];
|
||||
const imports: ImportNode[] = [];
|
||||
const exports: ExportNode[] = [];
|
||||
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
FUNC_RE.lastIndex = 0;
|
||||
while ((match = FUNC_RE.exec(content)) !== null) {
|
||||
const indent = match[1].length;
|
||||
const name = match[2];
|
||||
const params = parseParams(match[3]);
|
||||
const returnType = match[4]?.trim();
|
||||
const lineStart =
|
||||
content.substring(0, match.index).split("\n").length;
|
||||
|
||||
let lineEnd = lineStart;
|
||||
for (let i = lineStart; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
if (
|
||||
i > lineStart &&
|
||||
line.trim() &&
|
||||
!line.startsWith(" ".repeat(indent + 1)) &&
|
||||
!line.startsWith("\t".repeat(indent === 0 ? 1 : indent))
|
||||
) {
|
||||
lineEnd = i;
|
||||
break;
|
||||
}
|
||||
lineEnd = i + 1;
|
||||
}
|
||||
|
||||
let docstring: string | undefined;
|
||||
if (lineStart < lines.length) {
|
||||
const bodyStart = lines.slice(lineStart, lineStart + 5).join("\n");
|
||||
const docMatch = DOCSTRING_RE.exec(bodyStart);
|
||||
if (docMatch) {
|
||||
docstring = (docMatch[1] ?? docMatch[2]).trim();
|
||||
}
|
||||
}
|
||||
|
||||
if (indent === 0) {
|
||||
functions.push({
|
||||
name,
|
||||
params,
|
||||
returnType,
|
||||
lineStart,
|
||||
lineEnd,
|
||||
docstring,
|
||||
calls: [],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
CLASS_RE.lastIndex = 0;
|
||||
while ((match = CLASS_RE.exec(content)) !== null) {
|
||||
const name = match[2];
|
||||
const methods: FunctionNode[] = [];
|
||||
const classLineStart =
|
||||
content.substring(0, match.index).split("\n").length;
|
||||
|
||||
const classBody = content.substring(match.index + match[0].length);
|
||||
const methodRe = /^\s{2,}def\s+(\w+)\s*\(([^)]*)\)(?:\s*->\s*([^:]+))?\s*:/gm;
|
||||
let methodMatch: RegExpExecArray | null;
|
||||
while ((methodMatch = methodRe.exec(classBody)) !== null) {
|
||||
const methodLineStart =
|
||||
classLineStart +
|
||||
classBody.substring(0, methodMatch.index).split("\n").length;
|
||||
methods.push({
|
||||
name: methodMatch[1],
|
||||
params: parseParams(methodMatch[2]),
|
||||
returnType: methodMatch[3]?.trim(),
|
||||
lineStart: methodLineStart,
|
||||
lineEnd: methodLineStart + 1,
|
||||
calls: [],
|
||||
});
|
||||
}
|
||||
|
||||
classes.push({ name, methods, properties: [] });
|
||||
}
|
||||
|
||||
IMPORT_RE.lastIndex = 0;
|
||||
while ((match = IMPORT_RE.exec(content)) !== null) {
|
||||
const fromModule = match[1];
|
||||
const importedNames = match[2]
|
||||
.split(",")
|
||||
.map((s) => s.trim().split(" as ")[0].trim())
|
||||
.filter(Boolean);
|
||||
|
||||
if (fromModule) {
|
||||
imports.push({ source: fromModule, specifiers: importedNames });
|
||||
} else {
|
||||
for (const name of importedNames) {
|
||||
imports.push({ source: name, specifiers: [name] });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const allRe = /^__all__\s*=\s*\[([^\]]*)\]/m;
|
||||
const allMatch = allRe.exec(content);
|
||||
if (allMatch) {
|
||||
const names = allMatch[1]
|
||||
.split(",")
|
||||
.map((s) => s.trim().replace(/['"]/g, ""))
|
||||
.filter(Boolean);
|
||||
for (const name of names) {
|
||||
exports.push({ name, isDefault: false });
|
||||
}
|
||||
}
|
||||
|
||||
let complexity = 0;
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (trimmed.startsWith("if ") || trimmed.startsWith("elif ")) complexity++;
|
||||
if (trimmed.startsWith("for ") || trimmed.startsWith("while ")) complexity++;
|
||||
if (trimmed.startsWith("except")) complexity++;
|
||||
if (trimmed.includes(" and ") || trimmed.includes(" or ")) complexity++;
|
||||
}
|
||||
|
||||
return {
|
||||
path: filePath,
|
||||
language: "python",
|
||||
size: content.length,
|
||||
functions,
|
||||
classes,
|
||||
imports,
|
||||
exports,
|
||||
complexity,
|
||||
};
|
||||
},
|
||||
};
|
||||
227
packages/parser/src/languages/typescript.ts
Normal file
227
packages/parser/src/languages/typescript.ts
Normal file
@@ -0,0 +1,227 @@
|
||||
import { parse as babelParse } from "@babel/parser";
|
||||
import _traverse from "@babel/traverse";
|
||||
import type {
|
||||
FileNode,
|
||||
FunctionNode,
|
||||
ClassNode,
|
||||
ImportNode,
|
||||
ExportNode,
|
||||
} from "@codeboard/shared";
|
||||
import type { LanguageParser } from "./base.js";
|
||||
|
||||
const traverse =
|
||||
typeof _traverse === "function"
|
||||
? _traverse
|
||||
: (_traverse as unknown as { default: typeof _traverse }).default;
|
||||
|
||||
function extractFunctionParams(
|
||||
params: Array<{ name?: string; left?: { name?: string }; type?: string }>
|
||||
): string[] {
|
||||
return params.map((p) => {
|
||||
if (p.type === "AssignmentPattern" && p.left?.name) return p.left.name;
|
||||
return p.name ?? "unknown";
|
||||
});
|
||||
}
|
||||
|
||||
export const typescriptParser: LanguageParser = {
|
||||
extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"],
|
||||
|
||||
parse(content: string, filePath: string): FileNode {
|
||||
const functions: FunctionNode[] = [];
|
||||
const classes: ClassNode[] = [];
|
||||
const imports: ImportNode[] = [];
|
||||
const exports: ExportNode[] = [];
|
||||
const calls: Set<string> = new Set();
|
||||
|
||||
let ast;
|
||||
try {
|
||||
ast = babelParse(content, {
|
||||
sourceType: "module",
|
||||
plugins: [
|
||||
"typescript",
|
||||
"jsx",
|
||||
"decorators-legacy",
|
||||
"classProperties",
|
||||
"classPrivateProperties",
|
||||
"classPrivateMethods",
|
||||
"optionalChaining",
|
||||
"nullishCoalescingOperator",
|
||||
"dynamicImport",
|
||||
],
|
||||
errorRecovery: true,
|
||||
});
|
||||
} catch {
|
||||
return {
|
||||
path: filePath,
|
||||
language: filePath.endsWith(".py") ? "python" : "typescript",
|
||||
size: content.length,
|
||||
functions: [],
|
||||
classes: [],
|
||||
imports: [],
|
||||
exports: [],
|
||||
complexity: 0,
|
||||
};
|
||||
}
|
||||
|
||||
traverse(ast, {
|
||||
FunctionDeclaration(path) {
|
||||
const node = path.node;
|
||||
if (!node.id) return;
|
||||
functions.push({
|
||||
name: node.id.name,
|
||||
params: extractFunctionParams(node.params as never[]),
|
||||
returnType: node.returnType
|
||||
? content.slice(node.returnType.start!, node.returnType.end!)
|
||||
: undefined,
|
||||
lineStart: node.loc?.start.line ?? 0,
|
||||
lineEnd: node.loc?.end.line ?? 0,
|
||||
calls: [],
|
||||
});
|
||||
},
|
||||
|
||||
ArrowFunctionExpression(path) {
|
||||
const parent = path.parent;
|
||||
if (
|
||||
parent.type === "VariableDeclarator" &&
|
||||
parent.id.type === "Identifier"
|
||||
) {
|
||||
const node = path.node;
|
||||
functions.push({
|
||||
name: parent.id.name,
|
||||
params: extractFunctionParams(node.params as never[]),
|
||||
returnType: node.returnType
|
||||
? content.slice(node.returnType.start!, node.returnType.end!)
|
||||
: undefined,
|
||||
lineStart: node.loc?.start.line ?? 0,
|
||||
lineEnd: node.loc?.end.line ?? 0,
|
||||
calls: [],
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
ClassDeclaration(path) {
|
||||
const node = path.node;
|
||||
if (!node.id) return;
|
||||
const methods: FunctionNode[] = [];
|
||||
const properties: Array<{ name: string; type?: string }> = [];
|
||||
|
||||
for (const member of node.body.body) {
|
||||
if (
|
||||
member.type === "ClassMethod" &&
|
||||
member.key.type === "Identifier"
|
||||
) {
|
||||
methods.push({
|
||||
name: member.key.name,
|
||||
params: extractFunctionParams(member.params as never[]),
|
||||
lineStart: member.loc?.start.line ?? 0,
|
||||
lineEnd: member.loc?.end.line ?? 0,
|
||||
calls: [],
|
||||
});
|
||||
} else if (
|
||||
member.type === "ClassProperty" &&
|
||||
member.key.type === "Identifier"
|
||||
) {
|
||||
properties.push({
|
||||
name: member.key.name,
|
||||
type: member.typeAnnotation
|
||||
? content.slice(
|
||||
member.typeAnnotation.start!,
|
||||
member.typeAnnotation.end!
|
||||
)
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
classes.push({ name: node.id.name, methods, properties });
|
||||
},
|
||||
|
||||
ImportDeclaration(path) {
|
||||
const node = path.node;
|
||||
const specifiers = node.specifiers.map((s) => s.local.name);
|
||||
imports.push({ source: node.source.value, specifiers });
|
||||
},
|
||||
|
||||
ExportDefaultDeclaration() {
|
||||
exports.push({ name: "default", isDefault: true });
|
||||
},
|
||||
|
||||
ExportNamedDeclaration(path) {
|
||||
const node = path.node;
|
||||
if (node.declaration) {
|
||||
if (
|
||||
node.declaration.type === "FunctionDeclaration" &&
|
||||
node.declaration.id
|
||||
) {
|
||||
exports.push({
|
||||
name: node.declaration.id.name,
|
||||
isDefault: false,
|
||||
});
|
||||
} else if (
|
||||
node.declaration.type === "ClassDeclaration" &&
|
||||
node.declaration.id
|
||||
) {
|
||||
exports.push({
|
||||
name: node.declaration.id.name,
|
||||
isDefault: false,
|
||||
});
|
||||
} else if (node.declaration.type === "VariableDeclaration") {
|
||||
for (const decl of node.declaration.declarations) {
|
||||
if (decl.id.type === "Identifier") {
|
||||
exports.push({ name: decl.id.name, isDefault: false });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (node.specifiers) {
|
||||
for (const spec of node.specifiers) {
|
||||
if (spec.exported.type === "Identifier") {
|
||||
exports.push({ name: spec.exported.name, isDefault: false });
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
CallExpression(path) {
|
||||
const callee = path.node.callee;
|
||||
if (callee.type === "Identifier") {
|
||||
calls.add(callee.name);
|
||||
} else if (
|
||||
callee.type === "MemberExpression" &&
|
||||
callee.property.type === "Identifier"
|
||||
) {
|
||||
calls.add(callee.property.name);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
for (const fn of functions) {
|
||||
fn.calls = Array.from(calls);
|
||||
}
|
||||
|
||||
let complexity = 0;
|
||||
traverse(ast, {
|
||||
IfStatement() { complexity++; },
|
||||
ForStatement() { complexity++; },
|
||||
ForInStatement() { complexity++; },
|
||||
ForOfStatement() { complexity++; },
|
||||
WhileStatement() { complexity++; },
|
||||
DoWhileStatement() { complexity++; },
|
||||
SwitchCase() { complexity++; },
|
||||
ConditionalExpression() { complexity++; },
|
||||
LogicalExpression() { complexity++; },
|
||||
CatchClause() { complexity++; },
|
||||
});
|
||||
|
||||
return {
|
||||
path: filePath,
|
||||
language: filePath.match(/\.tsx?$/) ? "typescript" : "javascript",
|
||||
size: content.length,
|
||||
functions,
|
||||
classes,
|
||||
imports,
|
||||
exports,
|
||||
complexity,
|
||||
};
|
||||
},
|
||||
};
|
||||
8
packages/parser/tsconfig.json
Normal file
8
packages/parser/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
21
packages/shared/package.json
Normal file
21
packages/shared/package.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "@codeboard/shared",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"clean": "rm -rf dist",
|
||||
"dev": "tsc --watch"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.7"
|
||||
}
|
||||
}
|
||||
24
packages/shared/src/index.ts
Normal file
24
packages/shared/src/index.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
export type {
|
||||
CloneResult,
|
||||
CloneMetadata,
|
||||
FileNode,
|
||||
FunctionNode,
|
||||
ClassNode,
|
||||
ClassProperty,
|
||||
ImportNode,
|
||||
ExportNode,
|
||||
CodeStructure,
|
||||
ModuleNode,
|
||||
DetectedPattern,
|
||||
DependencyEdge,
|
||||
GeneratedDocs,
|
||||
DocsOverview,
|
||||
DocsModule,
|
||||
DocsPatterns,
|
||||
DocsGettingStarted,
|
||||
Generation,
|
||||
GenerationStatus,
|
||||
LLMMessage,
|
||||
LLMOptions,
|
||||
LLMProviderConfig,
|
||||
} from "./types.js";
|
||||
185
packages/shared/src/types.ts
Normal file
185
packages/shared/src/types.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
// ── Repository Cloning ──────────────────────────────────────────────
|
||||
|
||||
export interface CloneMetadata {
|
||||
name: string;
|
||||
description: string;
|
||||
defaultBranch: string;
|
||||
languages: Record<string, number>;
|
||||
stars: number;
|
||||
lastCommit: string;
|
||||
totalFiles: number;
|
||||
totalLines: number;
|
||||
}
|
||||
|
||||
export interface CloneResult {
|
||||
localPath: string;
|
||||
metadata: CloneMetadata;
|
||||
}
|
||||
|
||||
// ── AST Parsing ─────────────────────────────────────────────────────
|
||||
|
||||
export interface FunctionNode {
|
||||
name: string;
|
||||
params: string[];
|
||||
returnType?: string;
|
||||
lineStart: number;
|
||||
lineEnd: number;
|
||||
docstring?: string;
|
||||
calls: string[];
|
||||
}
|
||||
|
||||
export interface ClassProperty {
|
||||
name: string;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
export interface ClassNode {
|
||||
name: string;
|
||||
methods: FunctionNode[];
|
||||
properties: ClassProperty[];
|
||||
}
|
||||
|
||||
export interface ImportNode {
|
||||
source: string;
|
||||
specifiers: string[];
|
||||
}
|
||||
|
||||
export interface ExportNode {
|
||||
name: string;
|
||||
isDefault: boolean;
|
||||
}
|
||||
|
||||
export interface FileNode {
|
||||
path: string;
|
||||
language: string;
|
||||
size: number;
|
||||
functions: FunctionNode[];
|
||||
classes: ClassNode[];
|
||||
imports: ImportNode[];
|
||||
exports: ExportNode[];
|
||||
complexity: number;
|
||||
}
|
||||
|
||||
export interface ModuleNode {
|
||||
name: string;
|
||||
path: string;
|
||||
files: string[];
|
||||
summary?: string;
|
||||
}
|
||||
|
||||
export interface DetectedPattern {
|
||||
name: string;
|
||||
description: string;
|
||||
examples: string[];
|
||||
}
|
||||
|
||||
export interface DependencyEdge {
|
||||
source: string;
|
||||
target: string;
|
||||
type: "import" | "call" | "extends";
|
||||
}
|
||||
|
||||
export interface CodeStructure {
|
||||
files: FileNode[];
|
||||
modules: ModuleNode[];
|
||||
entryPoints: string[];
|
||||
exports: ExportNode[];
|
||||
dependencies: DependencyEdge[];
|
||||
patterns: DetectedPattern[];
|
||||
}
|
||||
|
||||
// ── Generated Documentation ─────────────────────────────────────────
|
||||
|
||||
export interface DocsOverview {
|
||||
title: string;
|
||||
description: string;
|
||||
architectureDiagram: string;
|
||||
techStack: string[];
|
||||
keyMetrics: {
|
||||
files: number;
|
||||
modules: number;
|
||||
languages: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface DocsModule {
|
||||
name: string;
|
||||
path: string;
|
||||
summary: string;
|
||||
keyFiles: Array<{ path: string; purpose: string }>;
|
||||
publicApi: string[];
|
||||
dependsOn: string[];
|
||||
dependedBy: string[];
|
||||
}
|
||||
|
||||
export interface DocsPatterns {
|
||||
conventions: string[];
|
||||
designPatterns: string[];
|
||||
architecturalDecisions: string[];
|
||||
}
|
||||
|
||||
export interface DocsGettingStarted {
|
||||
prerequisites: string[];
|
||||
setupSteps: string[];
|
||||
firstTask: string;
|
||||
}
|
||||
|
||||
export interface GeneratedDocs {
|
||||
id: string;
|
||||
repoUrl: string;
|
||||
repoName: string;
|
||||
generatedAt: string;
|
||||
sections: {
|
||||
overview: DocsOverview;
|
||||
modules: DocsModule[];
|
||||
patterns: DocsPatterns;
|
||||
gettingStarted: DocsGettingStarted;
|
||||
dependencyGraph: string;
|
||||
};
|
||||
}
|
||||
|
||||
// ── Generation State ────────────────────────────────────────────────
|
||||
|
||||
export type GenerationStatus =
|
||||
| "QUEUED"
|
||||
| "CLONING"
|
||||
| "PARSING"
|
||||
| "GENERATING"
|
||||
| "RENDERING"
|
||||
| "COMPLETED"
|
||||
| "FAILED";
|
||||
|
||||
export interface Generation {
|
||||
id: string;
|
||||
repoUrl: string;
|
||||
repoName: string;
|
||||
commitHash: string;
|
||||
status: GenerationStatus;
|
||||
progress: number;
|
||||
result: GeneratedDocs | null;
|
||||
error: string | null;
|
||||
costUsd: number | null;
|
||||
duration: number | null;
|
||||
createdAt: string;
|
||||
viewCount: number;
|
||||
}
|
||||
|
||||
// ── LLM Configuration ───────────────────────────────────────────────
|
||||
|
||||
export interface LLMMessage {
|
||||
role: "system" | "user" | "assistant";
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface LLMOptions {
|
||||
temperature?: number;
|
||||
maxTokens?: number;
|
||||
model?: string;
|
||||
}
|
||||
|
||||
export interface LLMProviderConfig {
|
||||
provider: "openai" | "anthropic";
|
||||
apiKey: string;
|
||||
model?: string;
|
||||
baseUrl?: string;
|
||||
}
|
||||
8
packages/shared/tsconfig.json
Normal file
8
packages/shared/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
16
tsconfig.json
Normal file
16
tsconfig.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"lib": ["ES2022"],
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true
|
||||
}
|
||||
}
|
||||
25
turbo.json
Normal file
25
turbo.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"$schema": "https://turbo.build/schema.json",
|
||||
"globalDependencies": ["**/.env.*local"],
|
||||
"tasks": {
|
||||
"build": {
|
||||
"dependsOn": ["^build"],
|
||||
"inputs": ["$TURBO_DEFAULT$", ".env*"],
|
||||
"outputs": [".next/**", "!.next/cache/**", "dist/**"]
|
||||
},
|
||||
"dev": {
|
||||
"cache": false,
|
||||
"persistent": true
|
||||
},
|
||||
"lint": {},
|
||||
"clean": {
|
||||
"cache": false
|
||||
},
|
||||
"db:generate": {
|
||||
"cache": false
|
||||
},
|
||||
"db:push": {
|
||||
"cache": false
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user