feat(api): add workspace list and switch API routes

New /api/workspace/list and /api/workspace/switch for workspace discovery and switching.
This commit is contained in:
kumarabhirup 2026-03-03 13:46:30 -08:00
parent 796a2fcb34
commit 232d6640ba
No known key found for this signature in database
GPG Key ID: DB7CA2289CAB0167
2 changed files with 109 additions and 0 deletions

View File

@ -0,0 +1,53 @@
import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { discoverWorkspaces, getActiveWorkspaceName } from "@/lib/workspace";
export const dynamic = "force-dynamic";
export const runtime = "nodejs";
type GatewayMeta = {
mode?: string;
port?: number;
url?: string;
};
function readGatewayMeta(stateDir: string): GatewayMeta | null {
for (const filename of ["openclaw.json", "config.json"]) {
const configPath = join(stateDir, filename);
if (!existsSync(configPath)) {
continue;
}
try {
const raw = JSON.parse(readFileSync(configPath, "utf-8")) as {
gateway?: { mode?: unknown; port?: unknown };
};
const port = typeof raw.gateway?.port === "number"
? raw.gateway.port
: typeof raw.gateway?.port === "string"
? Number.parseInt(raw.gateway.port, 10)
: undefined;
const mode = typeof raw.gateway?.mode === "string" ? raw.gateway.mode : undefined;
return {
...(mode ? { mode } : {}),
...(Number.isFinite(port) ? { port } : {}),
...(Number.isFinite(port) ? { url: `ws://127.0.0.1:${port}` } : {}),
};
} catch {
// Continue to fallback config file candidate.
}
}
return null;
}
export async function GET() {
const workspaces = discoverWorkspaces().map((workspace) => ({
...workspace,
gateway: readGatewayMeta(workspace.stateDir),
}));
const activeWorkspace = getActiveWorkspaceName() ?? workspaces.find((item) => item.isActive)?.name ?? null;
return Response.json({
workspaces,
activeWorkspace,
});
}

View File

@ -0,0 +1,56 @@
import {
discoverWorkspaces,
getActiveWorkspaceName,
resolveOpenClawStateDir,
resolveWorkspaceRoot,
setUIActiveWorkspace,
} from "@/lib/workspace";
export const dynamic = "force-dynamic";
export const runtime = "nodejs";
const WORKSPACE_NAME_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/i;
function normalizeSwitchWorkspace(raw: unknown): string | null {
if (typeof raw !== "string") {
return null;
}
const trimmed = raw.trim();
if (!trimmed) {
return null;
}
if (!WORKSPACE_NAME_RE.test(trimmed)) {
return null;
}
return trimmed;
}
export async function POST(req: Request) {
const body = (await req.json().catch(() => ({}))) as { workspace?: unknown };
const requestedWorkspace = normalizeSwitchWorkspace(body.workspace);
if (!requestedWorkspace) {
return Response.json(
{ error: "Invalid workspace name. Use letters, numbers, hyphens, or underscores." },
{ status: 400 },
);
}
const discovered = discoverWorkspaces();
const availableNames = new Set(discovered.map((workspace) => workspace.name));
if (!availableNames.has(requestedWorkspace)) {
return Response.json(
{ error: `Workspace '${requestedWorkspace}' was not found.` },
{ status: 404 },
);
}
setUIActiveWorkspace(requestedWorkspace);
const activeWorkspace = getActiveWorkspaceName();
const selected = discoverWorkspaces().find((workspace) => workspace.name === activeWorkspace) ?? null;
return Response.json({
activeWorkspace,
stateDir: resolveOpenClawStateDir(),
workspaceRoot: resolveWorkspaceRoot(),
workspace: selected,
});
}