singularity-forge/web/app/api/switch-root/route.ts

119 lines
2.9 KiB
TypeScript
Raw Permalink Normal View History

2026-05-05 14:31:16 +02:00
import {
existsSync,
2026-05-05 14:46:18 +02:00
mkdirSync,
2026-05-05 14:31:16 +02:00
readFileSync,
statSync,
writeFileSync,
} from "node:fs";
import { homedir } from "node:os";
2026-05-05 14:46:18 +02:00
import { dirname, resolve } from "node:path";
import { webPreferencesPath } from "../../../../src/app-paths.ts";
import { discoverProjects } from "../../../../src/web/project-discovery-service.ts";
export const runtime = "nodejs";
export const dynamic = "force-dynamic";
/** Shape of persisted web preferences. */
interface WebPreferences {
2026-05-05 14:31:16 +02:00
devRoot?: string;
lastActiveProject?: string;
}
/** Expand leading `~/` to the user's home directory. */
function expandTilde(p: string): string {
2026-05-05 14:31:16 +02:00
if (p === "~") return homedir();
if (p.startsWith("~/")) return homedir() + p.slice(1);
return p;
}
/**
* POST /api/switch-root
*
* Validates the new root path, persists it as the `devRoot` preference,
* and returns the discovered projects under the new root.
*
* Request body: { "devRoot": "/absolute/path" }
* Response: { "devRoot": "/resolved/path", "projects": [...] }
*/
export async function POST(request: Request): Promise<Response> {
2026-05-05 14:31:16 +02:00
try {
const body = (await request.json()) as Record<string, unknown>;
const rawDevRoot =
typeof body.devRoot === "string" ? body.devRoot.trim() : "";
2026-05-05 14:31:16 +02:00
if (!rawDevRoot) {
return Response.json(
{ error: "Missing devRoot in request body" },
{ status: 400 },
);
}
2026-05-05 14:31:16 +02:00
const expanded = expandTilde(rawDevRoot);
const resolved = resolve(expanded);
2026-05-05 14:31:16 +02:00
// Validate: path must exist
if (!existsSync(resolved)) {
return Response.json(
{ error: `Path does not exist: ${resolved}` },
{ status: 400 },
);
}
2026-05-05 14:31:16 +02:00
// Validate: path must be a directory
try {
const stat = statSync(resolved);
if (!stat.isDirectory()) {
return Response.json(
{ error: `Not a directory: ${resolved}` },
{ status: 400 },
);
}
} catch {
return Response.json(
{ error: `Cannot access path: ${resolved}` },
{ status: 400 },
);
}
2026-05-05 14:31:16 +02:00
// Read existing preferences and merge
let existing: WebPreferences = {};
try {
if (existsSync(webPreferencesPath)) {
existing = JSON.parse(readFileSync(webPreferencesPath, "utf-8"));
}
} catch {
// Corrupt file — start fresh
}
2026-05-05 14:31:16 +02:00
const prefs: WebPreferences = {
...existing,
devRoot: resolved,
// Clear last active project since we're changing the root
lastActiveProject: undefined,
};
2026-05-05 14:31:16 +02:00
// Ensure parent directory exists
const dir = dirname(webPreferencesPath);
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true });
}
2026-05-05 14:31:16 +02:00
writeFileSync(webPreferencesPath, JSON.stringify(prefs, null, 2), "utf-8");
2026-05-05 14:31:16 +02:00
// Discover projects under the new root
const projects = discoverProjects(resolved, true);
2026-05-05 14:31:16 +02:00
return Response.json({
devRoot: resolved,
projects,
});
} catch (err) {
return Response.json(
{
error: `Failed to switch root: ${err instanceof Error ? err.message : String(err)}`,
},
{ status: 500 },
);
}
}