codeverse / app /api /projects /route.ts
shubhjn's picture
fix ai asistance and make it per user friendly isolated enviroment
5dc2f11
import { NextRequest, NextResponse } from "next/server";
import fs from "fs/promises";
import simpleGit from "simple-git";
import { spawn } from "child_process";
import type { PackageManager } from "@/lib/package-managers";
import { auth } from "@/auth";
import { db } from "@/lib/db";
import { randomUUID } from "crypto";
import { resolveSafePath } from "@/lib/fs/isolation";
// Removed global WORKSPACE_ROOT
// POST /api/projects
export async function POST(req: NextRequest) {
const session = await auth();
if (!session?.user?.id) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const userId = session.user.id;
const { searchParams } = new URL(req.url);
const action = searchParams.get("action");
if (action === "clone") {
return handleClone(req, userId);
} else if (action === "scaffold") {
return handleScaffold(req, userId);
} else if (action === "install") {
return handleInstall(req); // install doesn't insert to db
}
return NextResponse.json({ error: "Unknown action" }, { status: 400 });
}
export async function GET() {
const session = await auth();
if (!session?.user?.id) {
return NextResponse.json({ projects: [] });
}
return handleList(session.user.id);
}
export async function DELETE(req: NextRequest) {
const session = await auth();
if (!session?.user?.id) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const { searchParams } = new URL(req.url);
const workspaceId = searchParams.get("workspaceId");
if (!workspaceId) {
return NextResponse.json({ error: "Missing workspaceId" }, { status: 400 });
}
try {
// Fetch it from DB to ensure it belongs to the user
const res = await db.execute({
sql: "SELECT project_name FROM workspaces WHERE id = ? AND user_id = ?",
args: [workspaceId, session.user.id]
});
if (res.rows.length === 0) {
return NextResponse.json({ error: "Workspace not found or unauthorized" }, { status: 404 });
}
const projectName = res.rows[0].project_name as string;
const targetPath = await resolveSafePath(session.user.id, projectName.replace(/[^a-zA-Z0-9-_]/g, "-").slice(0, 60));
await fs.rm(targetPath, { recursive: true, force: true });
// Remove from database
await db.execute({
sql: "DELETE FROM workspaces WHERE id = ?",
args: [workspaceId]
});
return NextResponse.json({ success: true });
} catch (e: unknown) {
if (e instanceof Error) {
return NextResponse.json({ error: e.message }, { status: 500 });
}
return NextResponse.json({ error: e as string }, { status: 500 });
}
}
async function handleList(userId: string) {
try {
const res = await db.execute({
sql: "SELECT * FROM workspaces WHERE user_id = ?",
args: [userId]
});
const projects = await Promise.all(res.rows.map(async row => {
const safeName = (row.project_name as string).replace(/[^a-zA-Z0-9-_]/g, "-").slice(0, 60);
return {
id: row.id,
name: row.project_name,
path: await resolveSafePath(userId, safeName),
containerStatus: row.status,
gitRemote: "",
hasPackageJson: true,
starred: false
};
}));
return NextResponse.json({ projects });
} catch (e) {
console.error("[PROJECTS_LIST_ERROR]", e);
return NextResponse.json({ projects: [] });
}
}
async function handleClone(req: NextRequest, userId: string) {
const { repoUrl, projectName } = await req.json() as { repoUrl: string; projectName: string };
if (!repoUrl || !projectName) {
return NextResponse.json({ error: "repoUrl and projectName are required" }, { status: 400 });
}
const safeName = projectName.replace(/[^a-zA-Z0-9-_]/g, "-").slice(0, 60);
const dest = await resolveSafePath(userId, safeName);
// Stream progress via SSE
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
const send = (data: object) =>
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`));
try {
send({ type: "progress", message: `Cloning ${repoUrl}…` });
await simpleGit().clone(repoUrl, dest, ["--progress"]);
// Insert into Database
const workspaceId = randomUUID();
await db.execute({
sql: "INSERT INTO workspaces (id, user_id, project_name, status) VALUES (?, ?, ?, ?)",
args: [workspaceId, userId, projectName, "stopped"]
});
send({ type: "done", projectPath: dest, projectName: safeName, id: workspaceId });
} catch (e) {
send({ type: "error", message: String(e) });
} finally {
controller.close();
}
},
});
return new Response(stream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
});
}
async function handleScaffold(req: NextRequest, userId: string) {
const { templateId, projectName, packageManager } = await req.json() as {
templateId: string;
projectName: string;
packageManager: PackageManager;
};
if (!templateId || !projectName) {
return NextResponse.json({ error: "templateId and projectName are required" }, { status: 400 });
}
const safeName = projectName.replace(/[^a-zA-Z0-9-_]/g, "-").slice(0, 60);
const dest = await resolveSafePath(userId, safeName);
await fs.mkdir(dest, { recursive: true });
// Template scaffold commands (IDs must exactly match TEMPLATE_REGISTRY ids in constants/extensions.ts)
const SCAFFOLD_CMDS: Record<string, string[]> = {
"nextjs-app": ["npx", "create-next-app@latest", ".", "--typescript", "--tailwind", "--app", "--no-git", "--yes"],
"react-vite": ["npx", "create-vite@latest", ".", "--template", "react-ts"],
"sveltekit": ["npx", "sv", "create", ".", "--template", "minimal", "--types", "ts", "--no-add-ons"],
"astro": ["npx", "create-astro@latest", ".", "--template", "minimal", "--typescript", "strict", "--no-git", "--no-install"],
"express-ts": ["npx", "express-generator-typescript", "."],
"turborepo": ["npx", "create-turbo@latest", ".", "--package-manager", packageManager ?? "npm"],
"python-fastapi": ["python3", "-m", "venv", "venv"],
"django": ["python3", "-m", "venv", "venv"],
"go-gin": ["go", "mod", "init", safeName],
"rust-axum": ["cargo", "init", "."],
};
const cmd = SCAFFOLD_CMDS[templateId];
if (!cmd) {
return NextResponse.json({ error: "Unknown template" }, { status: 400 });
}
const encoder = new TextEncoder();
const stream = new ReadableStream({
start(controller) {
const send = (data: object) =>
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`));
send({ type: "progress", message: `Scaffolding ${templateId}…` });
const proc = spawn(cmd[0], cmd.slice(1), {
cwd: dest,
shell: true,
env: { ...process.env, CI: "1" },
});
proc.stdout.on("data", (d: Buffer) =>
send({ type: "stdout", message: d.toString() })
);
proc.stderr.on("data", (d: Buffer) =>
send({ type: "stderr", message: d.toString() })
);
proc.on("close", async (code) => {
if (code === 0) {
// Insert into DB
const workspaceId = randomUUID();
try {
await db.execute({
sql: "INSERT INTO workspaces (id, user_id, project_name, status) VALUES (?, ?, ?, ?)",
args: [workspaceId, userId, projectName, "stopped"]
});
send({ type: "done", projectPath: dest, projectName: safeName, id: workspaceId });
} catch (e: unknown) {
if (e instanceof Error) {
send({ type: "error", message: `DB Error: ${e.message}` });
}
send({ type: "error", message: `DB Error: ${JSON.stringify(e)}` })
}
} else {
send({ type: "error", message: `Process exited with code ${code}` });
}
controller.close();
});
proc.on("error", (e) => {
send({ type: "error", message: e.message });
controller.close();
});
},
});
return new Response(stream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
});
}
async function handleInstall(req: NextRequest) {
const { projectPath, packageManager } = await req.json() as {
projectPath: string;
packageManager: PackageManager;
};
if (!projectPath) {
return NextResponse.json({ error: "projectPath is required" }, { status: 400 });
}
const PM_CMDS: Record<PackageManager, string[]> = {
npm: ["npm", "install"],
pnpm: ["pnpm", "install"],
bun: ["bun", "install"],
yarn: ["yarn", "install"],
};
const [bin, ...args] = PM_CMDS[packageManager ?? "npm"];
const encoder = new TextEncoder();
const stream = new ReadableStream({
start(controller) {
const send = (data: object) =>
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`));
send({ type: "progress", message: `Running ${bin} install…` });
const proc = spawn(bin, args, {
cwd: projectPath,
shell: true,
env: process.env,
});
proc.stdout.on("data", (d: Buffer) =>
send({ type: "stdout", message: d.toString() })
);
proc.stderr.on("data", (d: Buffer) =>
send({ type: "stderr", message: d.toString() })
);
proc.on("close", (code) => {
send({ type: code === 0 ? "done" : "error", message: code === 0 ? "Installation complete!" : `Exited with ${code}` });
controller.close();
});
},
});
return new Response(stream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
});
}