refactor(api): update workspace tree and virtual-file routes

Use workspace lib for path resolution.
This commit is contained in:
kumarabhirup 2026-03-03 13:46:49 -08:00
parent 6084381346
commit 974ba61b48
No known key found for this signature in database
GPG Key ID: DB7CA2289CAB0167
3 changed files with 36 additions and 35 deletions

View File

@ -17,8 +17,8 @@ vi.mock("node:os", () => ({
// Mock workspace
vi.mock("@/lib/workspace", () => ({
resolveWorkspaceRoot: vi.fn(() => null),
resolveOpenClawStateDir: vi.fn(() => "/home/testuser/.openclaw"),
getEffectiveProfile: vi.fn(() => "default"),
resolveOpenClawStateDir: vi.fn(() => "/home/testuser/.openclaw-ironclaw"),
getActiveWorkspaceName: vi.fn(() => null),
parseSimpleYaml: vi.fn(() => ({})),
duckdbQueryAll: vi.fn(() => []),
duckdbQueryAllAsync: vi.fn(async () => []),
@ -57,8 +57,8 @@ describe("Workspace Tree & Browse API", () => {
}));
vi.mock("@/lib/workspace", () => ({
resolveWorkspaceRoot: vi.fn(() => null),
resolveOpenClawStateDir: vi.fn(() => "/home/testuser/.openclaw"),
getEffectiveProfile: vi.fn(() => "default"),
resolveOpenClawStateDir: vi.fn(() => "/home/testuser/.openclaw-ironclaw"),
getActiveWorkspaceName: vi.fn(() => null),
parseSimpleYaml: vi.fn(() => ({})),
duckdbQueryAll: vi.fn(() => []),
duckdbQueryAllAsync: vi.fn(async () => []),
@ -83,11 +83,13 @@ describe("Workspace Tree & Browse API", () => {
const json = await res.json();
expect(json.exists).toBe(false);
expect(json.tree).toEqual([]);
expect(json.workspace).toBeNull();
});
it("returns tree with workspace files", async () => {
const { resolveWorkspaceRoot } = await import("@/lib/workspace");
const { resolveWorkspaceRoot, getActiveWorkspaceName } = await import("@/lib/workspace");
vi.mocked(resolveWorkspaceRoot).mockReturnValue("/ws");
vi.mocked(getActiveWorkspaceName).mockReturnValue("default");
const { readdirSync: mockReaddir, existsSync: mockExists } = await import("node:fs");
vi.mocked(mockExists).mockReturnValue(true);
vi.mocked(mockReaddir).mockImplementation((dir) => {
@ -106,6 +108,7 @@ describe("Workspace Tree & Browse API", () => {
const json = await res.json();
expect(json.exists).toBe(true);
expect(json.tree.length).toBeGreaterThan(0);
expect(json.workspace).toBe("default");
});
it("includes workspaceRoot in response", async () => {
@ -153,16 +156,16 @@ describe("Workspace Tree & Browse API", () => {
const value = String(p);
return (
value === "/ws" ||
value === "/home/testuser/.openclaw/skills" ||
value === "/home/testuser/.openclaw/skills/alpha/SKILL.md" ||
value === "/home/testuser/.openclaw/skills/dench/SKILL.md"
value === "/ws/skills" ||
value === "/ws/skills/alpha/SKILL.md" ||
value === "/ws/skills/dench/SKILL.md"
);
});
vi.mocked(mockReaddir).mockImplementation((dir) => {
if (String(dir) === "/ws") {
return [] as unknown as Dirent[];
}
if (String(dir) === "/home/testuser/.openclaw/skills") {
if (String(dir) === "/ws/skills") {
return [
makeDirent("alpha", true),
makeDirent("dench", true),

View File

@ -1,6 +1,6 @@
import { readdirSync, readFileSync, existsSync, statSync, type Dirent } from "node:fs";
import { join } from "node:path";
import { resolveWorkspaceRoot, resolveOpenClawStateDir, getEffectiveProfile, parseSimpleYaml, duckdbQueryAll, isDatabaseFile } from "@/lib/workspace";
import { resolveWorkspaceRoot, resolveOpenClawStateDir, getActiveWorkspaceName, parseSimpleYaml, duckdbQueryAll, isDatabaseFile } from "@/lib/workspace";
export const dynamic = "force-dynamic";
export const runtime = "nodejs";
@ -186,12 +186,13 @@ function parseSkillFrontmatter(content: string): { name?: string; emoji?: string
return { name: result.name, emoji: result.emoji };
}
/** Build a virtual "Skills" folder from <stateDir>/skills/. */
/** Build a virtual "Skills" folder from <workspace>/skills/. */
function buildSkillsVirtualFolder(): TreeNode | null {
const stateDir = resolveOpenClawStateDir();
const dirs = [
join(stateDir, "skills"),
];
const workspaceRoot = resolveWorkspaceRoot();
if (!workspaceRoot) {
return null;
}
const dirs = [join(workspaceRoot, "skills")];
const children: TreeNode[] = [];
const seen = new Set<string>();
@ -247,13 +248,13 @@ export async function GET(req: Request) {
const showHidden = url.searchParams.get("showHidden") === "1";
const openclawDir = resolveOpenClawStateDir();
const profile = getEffectiveProfile();
const workspace = getActiveWorkspaceName();
const root = resolveWorkspaceRoot();
if (!root) {
const tree: TreeNode[] = [];
const skillsFolder = buildSkillsVirtualFolder();
if (skillsFolder) {tree.push(skillsFolder);}
return Response.json({ tree, exists: false, workspaceRoot: null, openclawDir, profile });
return Response.json({ tree, exists: false, workspaceRoot: null, openclawDir, workspace });
}
const dbObjects = loadDbObjects();
@ -263,5 +264,5 @@ export async function GET(req: Request) {
const skillsFolder = buildSkillsVirtualFolder();
if (skillsFolder) {tree.push(skillsFolder);}
return Response.json({ tree, exists: true, workspaceRoot: root, openclawDir, profile });
return Response.json({ tree, exists: true, workspaceRoot: root, openclawDir, workspace });
}

View File

@ -1,6 +1,6 @@
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
import { join, dirname, resolve, normalize } from "node:path";
import { resolveOpenClawStateDir, resolveWorkspaceRoot } from "@/lib/workspace";
import { resolveWorkspaceRoot } from "@/lib/workspace";
export const dynamic = "force-dynamic";
export const runtime = "nodejs";
@ -10,8 +10,10 @@ export const runtime = "nodejs";
* Returns null if the path is invalid or tries to escape.
*/
function resolveVirtualPath(virtualPath: string): string | null {
const stateDir = resolveOpenClawStateDir();
const workspaceDir = resolveWorkspaceRoot() ?? join(stateDir, "workspace");
const workspaceDir = resolveWorkspaceRoot();
if (!workspaceDir) {
return null;
}
if (virtualPath.startsWith("~skills/")) {
// ~skills/<skillName>/SKILL.md
@ -27,18 +29,12 @@ function resolveVirtualPath(virtualPath: string): string | null {
return null;
}
// Check workspace skills first, then managed skills
const candidates = [
join(workspaceDir, "skills", skillName, "SKILL.md"),
join(stateDir, "skills", skillName, "SKILL.md"),
];
for (const candidate of candidates) {
if (existsSync(candidate)) {
return candidate;
}
const skillPath = join(workspaceDir, "skills", skillName, "SKILL.md");
if (existsSync(skillPath)) {
return skillPath;
}
// Default to workspace skills dir for new files
return candidates[0];
// Default to workspace skills dir for new files.
return skillPath;
}
if (virtualPath.startsWith("~memories/")) {
@ -83,11 +79,12 @@ function resolveVirtualPath(virtualPath: string): string | null {
* Double-check that the resolved path stays within expected directories.
*/
function isSafePath(absPath: string): boolean {
const stateDir = resolveOpenClawStateDir();
const workspaceDir = resolveWorkspaceRoot() ?? join(stateDir, "workspace");
const workspaceDir = resolveWorkspaceRoot();
if (!workspaceDir) {
return false;
}
const normalized = normalize(resolve(absPath));
const allowed = [
normalize(join(stateDir, "skills")),
normalize(join(workspaceDir, "skills")),
normalize(workspaceDir),
];