From 232d6640baf0e0a759ff61706a0d58a510b6dc0c Mon Sep 17 00:00:00 2001 From: kumarabhirup Date: Tue, 3 Mar 2026 13:46:30 -0800 Subject: [PATCH] feat(api): add workspace list and switch API routes New /api/workspace/list and /api/workspace/switch for workspace discovery and switching. --- apps/web/app/api/workspace/list/route.ts | 53 ++++++++++++++++++++ apps/web/app/api/workspace/switch/route.ts | 56 ++++++++++++++++++++++ 2 files changed, 109 insertions(+) create mode 100644 apps/web/app/api/workspace/list/route.ts create mode 100644 apps/web/app/api/workspace/switch/route.ts diff --git a/apps/web/app/api/workspace/list/route.ts b/apps/web/app/api/workspace/list/route.ts new file mode 100644 index 00000000000..b5cf022e6e4 --- /dev/null +++ b/apps/web/app/api/workspace/list/route.ts @@ -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, + }); +} diff --git a/apps/web/app/api/workspace/switch/route.ts b/apps/web/app/api/workspace/switch/route.ts new file mode 100644 index 00000000000..5e6d9700122 --- /dev/null +++ b/apps/web/app/api/workspace/switch/route.ts @@ -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, + }); +}