Menu
popagent
publicLatest change 6edc7e928c57dab23c0973ed3f7ee70b3ebb72d5 - Initial popagent baseline by AkurAI Build
import { mkdir, realpath, stat } from "node:fs/promises";
import { isAbsolute, relative, resolve, sep } from "node:path";
import { DEFAULT_WORKSPACE_ID, type AgentWorkspace } from "./api-types";
import { storage } from "./storage";
import { retryableInit } from "./retryable-init";
export const WORKSPACE_ROOT = resolve(
process.env.POPAGENT_WORKSPACE_ROOT ?? `${import.meta.dir}/../workspace`,
);
type WorkspaceInput = Pick<AgentWorkspace, "name" | "repositoryPath">;
function isWithin(root: string, candidate: string): boolean {
const path = relative(root, candidate);
return path === "" || (!path.startsWith(`..${sep}`) && path !== ".." && !isAbsolute(path));
}
export class AgentWorkspaceStore {
readonly storage = storage;
private readonly initializeOnce = retryableInit(() => this.initialize());
constructor(readonly root = WORKSPACE_ROOT) {}
init(): Promise<void> {
return this.initializeOnce();
}
private async initialize(): Promise<void> {
await mkdir(this.root, { recursive: true });
const defaultRepository = await this.normalizeRepositoryPath(
process.env.POPAGENT_DEFAULT_REPOSITORY ?? ".",
);
await this.storage.init();
await this.storage.db.none(`
CREATE TABLE IF NOT EXISTS popagent_agent_workspaces (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
repository_path TEXT NOT NULL UNIQUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)
`);
await this.storage.db.none(`
INSERT INTO popagent_agent_workspaces (id, name, repository_path)
VALUES ($1, $2, $3)
ON CONFLICT (id) DO NOTHING
`, [DEFAULT_WORKSPACE_ID, "Default workspace", defaultRepository]);
}
async list(): Promise<AgentWorkspace[]> {
await this.init();
return this.storage.db.any<AgentWorkspace>(`
SELECT id, name, repository_path AS "repositoryPath",
created_at::text AS "createdAt", updated_at::text AS "updatedAt"
FROM popagent_agent_workspaces
ORDER BY created_at, id
`);
}
async get(id: string): Promise<AgentWorkspace | undefined> {
await this.init();
const workspace = await this.storage.db.oneOrNone<AgentWorkspace>(`
SELECT id, name, repository_path AS "repositoryPath",
created_at::text AS "createdAt", updated_at::text AS "updatedAt"
FROM popagent_agent_workspaces
WHERE id = $1
`, [id]);
return workspace ?? undefined;
}
async create(input: WorkspaceInput): Promise<AgentWorkspace> {
await this.init();
const repositoryPath = await this.normalizeRepositoryPath(input.repositoryPath);
return this.storage.db.one<AgentWorkspace>(`
INSERT INTO popagent_agent_workspaces (id, name, repository_path)
VALUES ($1, $2, $3)
RETURNING id, name, repository_path AS "repositoryPath",
created_at::text AS "createdAt", updated_at::text AS "updatedAt"
`, [crypto.randomUUID(), input.name.trim(), repositoryPath]);
}
async update(id: string, input: WorkspaceInput): Promise<AgentWorkspace | undefined> {
await this.init();
const repositoryPath = await this.normalizeRepositoryPath(input.repositoryPath);
const workspace = await this.storage.db.oneOrNone<AgentWorkspace>(`
UPDATE popagent_agent_workspaces
SET name = $2, repository_path = $3, updated_at = NOW()
WHERE id = $1
RETURNING id, name, repository_path AS "repositoryPath",
created_at::text AS "createdAt", updated_at::text AS "updatedAt"
`, [id, input.name.trim(), repositoryPath]);
return workspace ?? undefined;
}
async delete(id: string): Promise<boolean> {
if (id === DEFAULT_WORKSPACE_ID) return false;
await this.init();
return Boolean(await this.storage.db.oneOrNone<{ id: string }>(
"DELETE FROM popagent_agent_workspaces WHERE id = $1 RETURNING id",
[id],
));
}
async resolveRepository(id = DEFAULT_WORKSPACE_ID): Promise<{
workspace: AgentWorkspace;
path: string;
}> {
const workspace = await this.get(id);
if (!workspace) throw new Error(`Agent workspace not found: ${id}`);
const repositoryPath = await this.normalizeRepositoryPath(workspace.repositoryPath);
return { workspace, path: resolve(this.root, repositoryPath) };
}
private async normalizeRepositoryPath(repositoryPath: string): Promise<string> {
const input = repositoryPath.trim();
if (!input || isAbsolute(input)) {
throw new Error("Repository path must be relative to the workspace root");
}
const root = await realpath(this.root);
const candidate = await realpath(resolve(root, input)).catch(() => undefined);
if (!candidate || !isWithin(root, candidate) || !(await stat(candidate)).isDirectory()) {
throw new Error("Repository path must name an existing directory inside the workspace root");
}
const normalized = relative(root, candidate);
return normalized ? normalized.split(sep).join("/") : ".";
}
}
export const agentWorkspaces = new AgentWorkspaceStore();