singularity-forge/web/app/api/projects/route.ts
2026-05-05 14:46:18 +02:00

109 lines
2.9 KiB
TypeScript

import { execSync } from "node:child_process";
import { existsSync, mkdirSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import { detectProjectKind } from "../../../../src/web/bridge-service.ts";
import { discoverProjects } from "../../../../src/web/project-discovery-service.ts";
export const runtime = "nodejs";
export const dynamic = "force-dynamic";
/** Expand leading `~/` to the user's home directory. */
function expandTilde(p: string): string {
if (p === "~") return homedir();
if (p.startsWith("~/")) return join(homedir(), p.slice(2));
return p;
}
export async function GET(request: Request): Promise<Response> {
const url = new URL(request.url);
const root = url.searchParams.get("root");
if (!root) {
return Response.json(
{ error: "Missing ?root= parameter" },
{ status: 400 },
);
}
const detail = url.searchParams.get("detail") === "true";
const projects = discoverProjects(expandTilde(root), detail);
return Response.json(projects, {
headers: {
"Cache-Control": "no-store",
},
});
}
// ─── POST: create a new project directory ──────────────────────────────────
export async function POST(request: Request): Promise<Response> {
try {
const body = (await request.json()) as Record<string, unknown>;
const rawDevRoot =
typeof body.devRoot === "string" ? body.devRoot.trim() : "";
const name = typeof body.name === "string" ? body.name.trim() : "";
if (!rawDevRoot) {
return Response.json({ error: "Missing devRoot" }, { status: 400 });
}
const devRoot = expandTilde(rawDevRoot);
if (!name) {
return Response.json({ error: "Missing project name" }, { status: 400 });
}
// Validate name: allow alphanumeric, hyphens, underscores, dots — no slashes or spaces
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(name)) {
return Response.json(
{
error:
"Invalid name. Use letters, numbers, hyphens, underscores, and dots. Must start with a letter or number.",
},
{ status: 400 },
);
}
if (!existsSync(devRoot)) {
return Response.json(
{ error: `Dev root does not exist: ${devRoot}` },
{ status: 400 },
);
}
const projectPath = join(devRoot, name);
if (existsSync(projectPath)) {
return Response.json(
{ error: `Directory already exists: ${name}` },
{ status: 409 },
);
}
// Create directory and initialize git repo
mkdirSync(projectPath, { recursive: true });
execSync("git init", { cwd: projectPath, stdio: "ignore" });
// Detect project kind for consistent response
const { kind, signals } = detectProjectKind(projectPath);
return Response.json(
{
name,
path: projectPath,
kind,
signals,
lastModified: Date.now(),
},
{ status: 201 },
);
} catch (err) {
return Response.json(
{
error: `Failed to create project: ${err instanceof Error ? err.message : String(err)}`,
},
{ status: 500 },
);
}
}