feat: add Pi package management settings

This commit is contained in:
Federico Jaramillo Martinez
2026-07-01 15:34:34 +02:00
parent 3f36394c17
commit 8ade238228
28 changed files with 1009 additions and 51 deletions
+85
View File
@@ -0,0 +1,85 @@
import type { FastifyInstance, FastifyReply } from "fastify";
import type { PiPackageScope } from "../shared/apiTypes.js";
import { createDefaultPiPackageService, type PiPackageService } from "./piPackageService.js";
class PiPackageRequestValidationError extends Error {}
export function registerPiPackageRoutes(app: FastifyInstance, service: PiPackageService = createDefaultPiPackageService()): void {
app.get("/api/pi-packages", async (_request, reply) => {
try {
return await service.list();
} catch (error) {
return sendPiPackageError(reply, error);
}
});
app.post<{ Body: unknown }>("/api/pi-packages/install", async (request, reply) => {
try {
return await service.install(parseRequiredSourceRequest(request.body));
} catch (error) {
return sendPiPackageError(reply, error);
}
});
app.post<{ Body: unknown }>("/api/pi-packages/remove", async (request, reply) => {
try {
const body = requireRequestObject(request.body);
return await service.remove(parseRequiredSource(body["source"]), parseOptionalScope(body["scope"]));
} catch (error) {
return sendPiPackageError(reply, error);
}
});
app.post<{ Body: unknown }>("/api/pi-packages/update", async (request, reply) => {
try {
const source = parseOptionalUpdateSource(request.body);
return source === undefined ? await service.update() : await service.update(source);
} catch (error) {
return sendPiPackageError(reply, error);
}
});
}
function parseRequiredSourceRequest(body: unknown): string {
const request = requireRequestObject(body);
if (request["scope"] !== undefined || request["local"] !== undefined) {
throw new PiPackageRequestValidationError("Pi package install scope is not supported; installs use Pi's default package location");
}
return parseRequiredSource(request["source"]);
}
function parseRequiredSource(value: unknown): string {
if (typeof value !== "string" || value.trim() === "") throw new PiPackageRequestValidationError("Pi package source must be a non-empty string");
return value.trim();
}
function parseOptionalUpdateSource(body: unknown): string | undefined {
if (body === undefined) return undefined;
const source = requireRequestObject(body)["source"];
if (source === undefined) return undefined;
return parseRequiredSource(source);
}
function parseOptionalScope(value: unknown): PiPackageScope | undefined {
if (value === undefined) return undefined;
if (value !== "user" && value !== "project") throw new PiPackageRequestValidationError("Pi package scope must be \"user\" or \"project\"");
return value;
}
function requireRequestObject(value: unknown): Record<string, unknown> {
if (!isRecord(value)) throw new PiPackageRequestValidationError("Pi package request body must be an object");
return value;
}
function sendPiPackageError(reply: FastifyReply, error: unknown): FastifyReply {
const status = error instanceof PiPackageRequestValidationError ? 400 : 500;
return reply.code(status).send({ error: errorMessage(error) });
}
function errorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}