import fs from "node:fs/promises"; import path from "node:path"; import { resolveDefaultAgentWorkspaceDir } from "../agents/workspace.js"; import type { ClawdbotConfig } from "../config/config.js"; import type { RuntimeEnv } from "../runtime.js"; import { resolveHomeDir, resolveUserPath } from "../utils.js"; export type RemovalResult = { ok: boolean; skipped?: boolean; }; export function collectWorkspaceDirs(cfg: ClawdbotConfig | undefined): string[] { const dirs = new Set(); const defaults = cfg?.agents?.defaults; if (typeof defaults?.workspace === "string" && defaults.workspace.trim()) { dirs.add(resolveUserPath(defaults.workspace)); } const list = Array.isArray(cfg?.agents?.list) ? cfg?.agents?.list : []; for (const agent of list) { const workspace = (agent as { workspace?: unknown }).workspace; if (typeof workspace === "string" && workspace.trim()) { dirs.add(resolveUserPath(workspace)); } } if (dirs.size === 0) { dirs.add(resolveDefaultAgentWorkspaceDir()); } return [...dirs]; } export function isPathWithin(child: string, parent: string): boolean { const relative = path.relative(parent, child); return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative)); } function isUnsafeRemovalTarget(target: string): boolean { if (!target.trim()) return true; const resolved = path.resolve(target); const root = path.parse(resolved).root; if (resolved === root) return true; const home = resolveHomeDir(); if (home && resolved === path.resolve(home)) return true; return false; } export async function removePath( target: string, runtime: RuntimeEnv, opts?: { dryRun?: boolean; label?: string }, ): Promise { if (!target?.trim()) return { ok: false, skipped: true }; const resolved = path.resolve(target); const label = opts?.label ?? resolved; if (isUnsafeRemovalTarget(resolved)) { runtime.error(`Refusing to remove unsafe path: ${label}`); return { ok: false }; } if (opts?.dryRun) { runtime.log(`[dry-run] remove ${label}`); return { ok: true, skipped: true }; } try { await fs.rm(resolved, { recursive: true, force: true }); runtime.log(`Removed ${label}`); return { ok: true }; } catch (err) { runtime.error(`Failed to remove ${label}: ${String(err)}`); return { ok: false }; } } export async function listAgentSessionDirs(stateDir: string): Promise { const root = path.join(stateDir, "agents"); try { const entries = await fs.readdir(root, { withFileTypes: true }); return entries .filter((entry) => entry.isDirectory()) .map((entry) => path.join(root, entry.name, "sessions")); } catch { return []; } }