Files

62 lines
2.7 KiB
TypeScript

import { NextResponse } from "next/server";
import { getAgentDir } from "@earendil-works/pi-coding-agent";
import { runNpx } from "@/lib/npx";
import { getAllowedFileRoots, isExistingFilePathAllowed } from "@/lib/file-access";
import { hasJsonContentType, isApiRequestAllowed } from "@/lib/request-security";
import { getProjectTrustStatus } from "@/lib/project-trust";
import { getSkillsCliEnvironment, isManagedRuntime } from "@/lib/app-runtime";
export const dynamic = "force-dynamic";
const ANSI_RE = /\x1B\[[0-9;]*m/g;
// POST /api/skills/install body: { package: string; scope: "global" | "project"; cwd?: string }
export async function POST(req: Request) {
if (!isApiRequestAllowed(req)) {
return NextResponse.json({ error: "Untrusted API request" }, { status: 403 });
}
if (!hasJsonContentType(req)) {
return NextResponse.json({ error: "Content-Type must be application/json" }, { status: 415 });
}
try {
const { package: pkg, scope, cwd } = await req.json() as { package?: string; scope?: string; cwd?: string };
if (!pkg?.trim()) return NextResponse.json({ error: "package required" }, { status: 400 });
const isGlobal = isManagedRuntime() || scope !== "project";
if (!isGlobal) {
if (!cwd) return NextResponse.json({ error: "cwd required for project install" }, { status: 400 });
const allowedRoots = await getAllowedFileRoots();
if (!isExistingFilePathAllowed(cwd, allowedRoots)) {
return NextResponse.json({ error: "Access denied" }, { status: 403 });
}
if (!getProjectTrustStatus(cwd, getAgentDir()).trusted) {
return NextResponse.json(
{ error: "Project resources must be trusted before installing project skills" },
{ status: 403 },
);
}
}
const args = ["skills", "add", pkg.trim(), "-y", "--agent", "pi"];
if (isGlobal) args.push("-g");
console.log(`[skills/install] running: npx ${args.join(" ")}`);
const { stdout, stderr } = await runNpx(args, {
timeout: 60000,
cwd: !isGlobal && cwd ? cwd : undefined,
env: getSkillsCliEnvironment(),
});
const output = (stdout + stderr).replace(ANSI_RE, "");
const success = /Installation complete|Installed \d+ skill/.test(output);
if (!success) {
return NextResponse.json({ error: output.slice(-300) || "Install failed" }, { status: 500 });
}
return NextResponse.json({ success: true, output });
} catch (e: unknown) {
const err = e as { stdout?: string; stderr?: string; message?: string };
const output = ((err.stdout ?? "") + (err.stderr ?? "")).replace(ANSI_RE, "");
return NextResponse.json({ error: output || (err.message ?? String(e)) }, { status: 500 });
}
}