feat(backend): /settings GET+PUT, /models PiModel shape, app wiring

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-28 16:09:04 +02:00
co-authored by Claude Opus 4.8
parent c4b599ec00
commit bfbb017413
5 changed files with 154 additions and 32 deletions
+8 -25
View File
@@ -1,14 +1,15 @@
import { readdirSync } from "node:fs";
import { join } from "node:path";
import type { FastifyInstance } from "fastify";
import type { PiModel } from "../pi/list-models.js";
export type ListModelsFn = () => Promise<string[]>;
export type ListModelsFn = () => Promise<PiModel[]>;
/**
* List YAML workspace configs found in <harnessDir>/workspaces/*.yaml.
* Returns [{name, file}] — no database credentials or secrets.
*/
function listWorkspaces(harnessDir: string): { name: string; file: string }[] {
export function listWorkspaces(harnessDir: string): { name: string; file: string }[] {
const dir = join(harnessDir, "workspaces");
let entries: string[];
try {
@@ -18,10 +19,7 @@ function listWorkspaces(harnessDir: string): { name: string; file: string }[] {
}
return entries
.filter((f) => f.endsWith(".yaml") || f.endsWith(".yml"))
.map((f) => ({
name: f.replace(/\.ya?ml$/, ""),
file: f,
}));
.map((f) => ({ name: f.replace(/\.ya?ml$/, ""), file: f }));
}
export function metaRoutes(
@@ -32,28 +30,13 @@ export function metaRoutes(
return listWorkspaces(deps.harnessDir);
});
app.get("/models", async (_req, reply) => {
const fn = deps.listModels ?? defaultListModels;
app.get("/models", async () => {
const fn = deps.listModels ?? (async () => []);
try {
const models = await fn();
return { models };
return { models: await fn() };
} catch {
// Graceful fallback: Pi may not be running; don't crash the server.
return { models: [] };
return { models: [] as PiModel[] };
}
});
}
/**
* Default implementation: spawns a short-lived `tht` invocation that asks a Pi
* process for available models via get_available_models. This is intentionally
* behind the injectable seam so tests can stub it without spawning real processes.
*
* In the MVP we return an empty list — the real spawn path can be wired in later
* once a Pi-side "list models" RPC stabilises.
*/
async function defaultListModels(): Promise<string[]> {
// Real ephemeral Pi spawn left for a follow-up task.
// Returning [] here triggers the graceful fallback seen by clients.
return [];
}