AkurAI Build
Menu

popagent

public

Latest 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();