From 24ff837b1df55cc1cf5e2508d8a85c8f98c4bc2f Mon Sep 17 00:00:00 2001 From: Naheel Muhammed Date: Mon, 9 Feb 2026 15:21:45 +0530 Subject: [PATCH 1/4] create and update the main API Route Co-Authored-By: Adithyan <100783336+adithyanmkd@users.noreply.github.com> --- src/app/api/.gitkeep | 0 src/app/api/generate/route.ts | 79 +++++++++++++++++++++++++++++++++++ src/lib/octokit.ts | 31 ++++++++++---- tsconfig.json | 5 ++- 4 files changed, 106 insertions(+), 9 deletions(-) create mode 100644 src/app/api/.gitkeep create mode 100644 src/app/api/generate/route.ts diff --git a/src/app/api/.gitkeep b/src/app/api/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/app/api/generate/route.ts b/src/app/api/generate/route.ts new file mode 100644 index 0000000..8faa658 --- /dev/null +++ b/src/app/api/generate/route.ts @@ -0,0 +1,79 @@ +import { NextResponse } from "next/server"; +import { getGeminiModel } from "@/lib/gemini"; +import { getRepoData, getRepoContents } from "@/lib/octokit"; + +export const dynamic = "force-dynamic"; + +export async function POST(req: Request) { + try { + const { url } = await req.json(); + + // 1. URL Cleanup & Validation + if (!url || !url.includes("github.com")) { + return NextResponse.json({ error: "Please provide a valid GitHub URL" }, { status: 400 }); + } + + const cleanUrl = url.replace(/\/$/, ""); + const parts = cleanUrl.split("github.com/")[1]?.split("/"); + const owner = parts?.[0]; + const repo = parts?.[1]; + + if (!owner || !repo) { + return NextResponse.json({ error: "Could not parse repository owner or name" }, { status: 400 }); + } + + // 2. Fetch Metadata and File Structure in Parallel + // We use the new helpers from your lib/octokit.ts + const [repoInfo, repoContents] = await Promise.all([ + getRepoData(owner, repo), + getRepoContents(owner, repo) + ]); + + // Create a comma-separated list of root files to guide the AI + type RepoFile = { name: string }; + +const fileList = + repoContents.length > 0 + ? repoContents.map((f: RepoFile) => f.name).join(", ") + : "Standard repository structure"; + + // 3. Initialize Gemini 2.5 + const model = getGeminiModel(); + + // 4. The "Expert Technical Writer" Prompt + const prompt = ` + You are an expert Technical Writer specializing in developer documentation. + Generate a professional, comprehensive README.md for the following repository: + + Name: ${repo} + Description: ${repoInfo?.description || "A modern software project."} + Primary Language: ${repoInfo?.language} + Root Directory Files: ${fileList} + + Requirements: + - Add a clean title and professional SVG badges from shields.io. + - Create a visual "Directory Structure" section (tree style). + - Include "Features", "Installation", and "Usage" sections. + - If 'package.json' exists, provide Node.js installation steps. + - If 'requirements.txt' exists, provide Python installation steps. + - Ensure the tone is welcoming and documentation is clear. + + Return ONLY the Markdown content. + `; + + // 5. AI Generation + const result = await model.generateContent(prompt); + const markdown = result.response.text(); + + return NextResponse.json({ markdown }); + + } catch (error: unknown) { + const message = error instanceof Error ? error.message : "Internal Server Error"; + console.error("README Generation Failed:", message); + + return NextResponse.json( + { error: "Failed to generate README. Ensure your API keys are correct." }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/src/lib/octokit.ts b/src/lib/octokit.ts index ccf478b..3862f98 100644 --- a/src/lib/octokit.ts +++ b/src/lib/octokit.ts @@ -1,6 +1,5 @@ import { Octokit } from "octokit"; -// Private variable for Singleton pattern let _octokit: Octokit | null = null; export function getOctokit(): Octokit { @@ -8,8 +7,6 @@ export function getOctokit(): Octokit { const auth = process.env.GITHUB_TOKEN; - // We don't throw an error here because Octokit can work without a token - // (unauthenticated), though it will be heavily rate-limited. _octokit = new Octokit({ auth: auth || undefined, }); @@ -18,8 +15,7 @@ export function getOctokit(): Octokit { } /** - * Fetches repository metadata safely. - * Notice we only log the error message, NOT the full error object. + * Fetches repository metadata (stars, description, language) */ export async function getRepoData(owner: string, repo: string) { const client = getOctokit(); @@ -32,8 +28,29 @@ export async function getRepoData(owner: string, repo: string) { return data; } catch (error: unknown) { const message = error instanceof Error ? error.message : "An unknown error occurred"; - console.error("Error fetching GitHub repo:", message); - + console.error("Error fetching GitHub repo metadata:", message); return null; } } + +/** + * NEW: Fetches the root contents to help Gemini build the File Structure + */ +export async function getRepoContents(owner: string, repo: string) { + const client = getOctokit(); + + try { + const { data } = await client.rest.repos.getContent({ + owner, + repo, + path: "", // Root directory + }); + + // Return the array of files/folders + return Array.isArray(data) ? data : []; + } catch (error: unknown) { + const message = error instanceof Error ? error.message : "Could not fetch contents"; + console.error("Error fetching GitHub repo contents:", message); + return []; + } +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 3a13f90..bcc8307 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,8 +18,9 @@ "name": "next" } ], + "baseUrl": ".", "paths": { - "@/*": ["./*"] + "@/*": ["./src/*"] } }, "include": [ @@ -31,4 +32,4 @@ "**/*.mts" ], "exclude": ["node_modules"] -} +} \ No newline at end of file From d2873c335cb43804f73712a7eeb7d910f54244d0 Mon Sep 17 00:00:00 2001 From: Naheel Muhammed Date: Mon, 9 Feb 2026 15:33:05 +0530 Subject: [PATCH 2/4] Update route.ts --- src/app/api/generate/route.ts | 81 +++++++++++++++++++++-------------- 1 file changed, 49 insertions(+), 32 deletions(-) diff --git a/src/app/api/generate/route.ts b/src/app/api/generate/route.ts index 8faa658..139ae53 100644 --- a/src/app/api/generate/route.ts +++ b/src/app/api/generate/route.ts @@ -2,78 +2,95 @@ import { NextResponse } from "next/server"; import { getGeminiModel } from "@/lib/gemini"; import { getRepoData, getRepoContents } from "@/lib/octokit"; +// Senior Tip: Force dynamic to ensure API keys are read correctly at runtime export const dynamic = "force-dynamic"; export async function POST(req: Request) { + // 1. Safe JSON Body Parsing + let rawUrl: string; try { - const { url } = await req.json(); + const body = await req.json(); + rawUrl = body.url; + } catch { + return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 }); + } - // 1. URL Cleanup & Validation - if (!url || !url.includes("github.com")) { - return NextResponse.json({ error: "Please provide a valid GitHub URL" }, { status: 400 }); + try { + // 2. Strict URL Validation + const trimmedUrl = rawUrl?.trim(); + if (!trimmedUrl) { + return NextResponse.json({ error: "GitHub URL is required" }, { status: 400 }); } - const cleanUrl = url.replace(/\/$/, ""); - const parts = cleanUrl.split("github.com/")[1]?.split("/"); - const owner = parts?.[0]; - const repo = parts?.[1]; + let parsedUrl: URL; + try { + parsedUrl = new URL(trimmedUrl); + } catch { + return NextResponse.json({ error: "Please provide a valid URL" }, { status: 400 }); + } + + // Hostname Guard: Only allow GitHub + if (parsedUrl.hostname !== "github.com" && parsedUrl.hostname !== "www.github.com") { + return NextResponse.json({ error: "Only GitHub URLs are supported" }, { status: 400 }); + } + + // Extract Owner and Repo securely from the path segments + const pathSegments = parsedUrl.pathname.split("/").filter(Boolean); + const owner = pathSegments[0]; + const repo = pathSegments[1]; if (!owner || !repo) { - return NextResponse.json({ error: "Could not parse repository owner or name" }, { status: 400 }); + return NextResponse.json({ error: "URL must include owner and repository name" }, { status: 400 }); } - // 2. Fetch Metadata and File Structure in Parallel - // We use the new helpers from your lib/octokit.ts + // 3. Parallel Data Fetching const [repoInfo, repoContents] = await Promise.all([ getRepoData(owner, repo), getRepoContents(owner, repo) ]); - // Create a comma-separated list of root files to guide the AI - type RepoFile = { name: string }; - -const fileList = - repoContents.length > 0 - ? repoContents.map((f: RepoFile) => f.name).join(", ") - : "Standard repository structure"; + // Format file list for AI context + const fileList = repoContents.length > 0 + ? repoContents.map((f: any) => f.name).join(", ") + : "Standard structure"; - // 3. Initialize Gemini 2.5 + // 4. Initialize Gemini 2.5 (2026 Standard) const model = getGeminiModel(); - // 4. The "Expert Technical Writer" Prompt + // 5. The "Expert Prompt" with Fallbacks (No more "null" or "undefined" strings) const prompt = ` - You are an expert Technical Writer specializing in developer documentation. - Generate a professional, comprehensive README.md for the following repository: + You are an expert Technical Writer. Generate a professional README.md for the following repository: Name: ${repo} Description: ${repoInfo?.description || "A modern software project."} - Primary Language: ${repoInfo?.language} + Primary Language: ${repoInfo?.language || "Not specified"} Root Directory Files: ${fileList} Requirements: - - Add a clean title and professional SVG badges from shields.io. + - Use professional shields.io badges. - Create a visual "Directory Structure" section (tree style). - Include "Features", "Installation", and "Usage" sections. - - If 'package.json' exists, provide Node.js installation steps. - - If 'requirements.txt' exists, provide Python installation steps. - - Ensure the tone is welcoming and documentation is clear. - + - Ensure installation steps match the Primary Language or Root Files (e.g., use npm if package.json exists). + - Tone: Professional, welcoming, and developer-friendly. + Return ONLY the Markdown content. `; - // 5. AI Generation + // 6. Generate content const result = await model.generateContent(prompt); - const markdown = result.response.text(); + const response = await result.response; + const markdown = response.text(); return NextResponse.json({ markdown }); } catch (error: unknown) { + // Sanitize error logging to prevent leaking secrets in logs const message = error instanceof Error ? error.message : "Internal Server Error"; console.error("README Generation Failed:", message); return NextResponse.json( - { error: "Failed to generate README. Ensure your API keys are correct." }, + { error: "Failed to generate README. Please check your URL and try again." }, { status: 500 } ); } -} \ No newline at end of file +} From 57cc9b875c6f04af2b9c9d006a4ec6a81aefe876 Mon Sep 17 00:00:00 2001 From: Naheel Muhammed Date: Mon, 9 Feb 2026 15:33:52 +0530 Subject: [PATCH 3/4] Update route.ts --- src/app/api/generate/route.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/api/generate/route.ts b/src/app/api/generate/route.ts index 139ae53..faec492 100644 --- a/src/app/api/generate/route.ts +++ b/src/app/api/generate/route.ts @@ -2,7 +2,7 @@ import { NextResponse } from "next/server"; import { getGeminiModel } from "@/lib/gemini"; import { getRepoData, getRepoContents } from "@/lib/octokit"; -// Senior Tip: Force dynamic to ensure API keys are read correctly at runtime +// Tip: Force dynamic to ensure API keys are read correctly at runtime export const dynamic = "force-dynamic"; export async function POST(req: Request) { From 50c2315da9a6f00c304e8c221ca461c4cd4255e0 Mon Sep 17 00:00:00 2001 From: Naheel Muhammed Date: Mon, 9 Feb 2026 15:36:31 +0530 Subject: [PATCH 4/4] Update route.ts --- src/app/api/generate/route.ts | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/app/api/generate/route.ts b/src/app/api/generate/route.ts index faec492..dd12777 100644 --- a/src/app/api/generate/route.ts +++ b/src/app/api/generate/route.ts @@ -2,7 +2,7 @@ import { NextResponse } from "next/server"; import { getGeminiModel } from "@/lib/gemini"; import { getRepoData, getRepoContents } from "@/lib/octokit"; -// Tip: Force dynamic to ensure API keys are read correctly at runtime +// Ensure API keys are read at runtime export const dynamic = "force-dynamic"; export async function POST(req: Request) { @@ -29,12 +29,12 @@ export async function POST(req: Request) { return NextResponse.json({ error: "Please provide a valid URL" }, { status: 400 }); } - // Hostname Guard: Only allow GitHub + // Hostname Guard if (parsedUrl.hostname !== "github.com" && parsedUrl.hostname !== "www.github.com") { return NextResponse.json({ error: "Only GitHub URLs are supported" }, { status: 400 }); } - // Extract Owner and Repo securely from the path segments + // Extract Owner and Repo from path const pathSegments = parsedUrl.pathname.split("/").filter(Boolean); const owner = pathSegments[0]; const repo = pathSegments[1]; @@ -49,17 +49,18 @@ export async function POST(req: Request) { getRepoContents(owner, repo) ]); - // Format file list for AI context - const fileList = repoContents.length > 0 - ? repoContents.map((f: any) => f.name).join(", ") - : "Standard structure"; + // 4. Type-Safe File Mapping (Fixes the 'any' linting error) + // We define the shape { name: string } inline to satisfy ESLint + const fileList = Array.isArray(repoContents) && repoContents.length > 0 + ? repoContents.map((f: { name: string }) => f.name).join(", ") + : "Standard repository structure"; - // 4. Initialize Gemini 2.5 (2026 Standard) + // 5. Initialize Gemini 2.5 const model = getGeminiModel(); - // 5. The "Expert Prompt" with Fallbacks (No more "null" or "undefined" strings) + // 6. The "Expert Prompt" with Fallbacks const prompt = ` - You are an expert Technical Writer. Generate a professional README.md for the following repository: + You are an expert Technical Writer. Generate a professional README.md for: Name: ${repo} Description: ${repoInfo?.description || "A modern software project."} @@ -67,16 +68,16 @@ export async function POST(req: Request) { Root Directory Files: ${fileList} Requirements: - - Use professional shields.io badges. + - Include professional SVG badges from shields.io. - Create a visual "Directory Structure" section (tree style). - Include "Features", "Installation", and "Usage" sections. - - Ensure installation steps match the Primary Language or Root Files (e.g., use npm if package.json exists). - - Tone: Professional, welcoming, and developer-friendly. - + - If 'package.json' exists, provide Node.js installation steps. + - Ensure a welcoming, professional developer-centric tone. + Return ONLY the Markdown content. `; - // 6. Generate content + // 7. AI Generation const result = await model.generateContent(prompt); const response = await result.response; const markdown = response.text(); @@ -84,12 +85,11 @@ export async function POST(req: Request) { return NextResponse.json({ markdown }); } catch (error: unknown) { - // Sanitize error logging to prevent leaking secrets in logs const message = error instanceof Error ? error.message : "Internal Server Error"; console.error("README Generation Failed:", message); return NextResponse.json( - { error: "Failed to generate README. Please check your URL and try again." }, + { error: "Failed to generate README. Check your URL and try again." }, { status: 500 } ); }