import { execFile } from "node:child_process"; import { constants, lstatSync, mkdirSync, openSync, closeSync, unlinkSync } from "node:fs"; import { access, lstat, mkdir } from "node:fs/promises"; import { basename, isAbsolute, join } from "node:path"; import { promisify } from "node:util"; import type { WorkspaceErrorCode, WorkspaceRegistryConfig } from "./types.js"; const execFileAsync = promisify(execFile); export interface GitStatus { branch: string; head?: string; ahead: number; behind: number; degraded: boolean; lastError?: WorkspaceErrorCode; } export class WorkspaceRegistryError extends Error { constructor(readonly code: WorkspaceErrorCode, message: string) { super(message); this.name = "WorkspaceRegistryError"; } } function isMissing(path: string): boolean { try { lstatSync(path); return false; } catch { return true; } } function assertDirectory(path: string): void { const entry = lstatSync(path); if (!entry.isDirectory() || entry.isSymbolicLink()) { throw new WorkspaceRegistryError("git_unavailable", "Workspace registry path is unavailable"); } } function gitErrorCode(error: unknown): WorkspaceErrorCode { const detail = [ error instanceof Error ? error.message : "", typeof error === "object" && error !== null && "stderr" in error ? String((error as { stderr?: unknown }).stderr ?? "") : "", ].join("\n").toLowerCase(); if (/authentication failed|could not read username|permission denied|publickey/.test(detail)) { return "git_auth_failed"; } if (/non-fast-forward|not possible to fast-forward|fast-forward/.test(detail)) { return "git_non_fast_forward"; } if (/remote rejected|pre-receive hook declined|push.*rejected/.test(detail)) { return "git_push_rejected"; } return "git_unavailable"; } /** * A persistent checkout that executes Git only through fixed argument vectors. Git's stdout and * stderr are intentionally never exposed: they can contain remote URLs or credential hints. */ export class GitWorkspaceRepository { readonly root: string; readonly repoPath: string; readonly snapshotsPath: string; readonly statePath: string; readonly locksPath: string; private readonly hooksPath: string; constructor(private readonly config: WorkspaceRegistryConfig) { if (!isAbsolute(config.root)) { throw new WorkspaceRegistryError("git_unavailable", "Workspace registry root is unavailable"); } this.root = config.root; this.repoPath = join(this.root, "repo"); this.snapshotsPath = join(this.root, "snapshots"); this.statePath = join(this.root, "state"); this.locksPath = join(this.root, "locks"); this.hooksPath = join(this.locksPath, "empty-hooks"); } async ensureLayout(): Promise { for (const path of [this.root, this.snapshotsPath, this.statePath, this.locksPath, this.hooksPath]) { await mkdir(path, { recursive: true, mode: 0o700 }); assertDirectory(path); } } async bootstrap(): Promise { await this.ensureLayout(); if (isMissing(this.repoPath)) { if (!this.config.remoteUrl) { throw new WorkspaceRegistryError("git_unavailable", "Workspace registry remote is unavailable"); } await this.clone(); } else { assertDirectory(this.repoPath); await this.refresh(); } return await this.status(); } async pull(): Promise { await this.ensureLayout(); if (isMissing(this.repoPath)) return await this.bootstrap(); assertDirectory(this.repoPath); await this.refresh(); return await this.status(); } async status(): Promise { const head = (await this.git(["rev-parse", "HEAD"])).trim(); const tracking = await this.gitOptional(["rev-list", "--left-right", "--count", "HEAD...@{upstream}"]); const [ahead = "0", behind = "0"] = tracking ? tracking.trim().split(/\s+/) : []; return { branch: this.config.branch, head, ahead: Number(ahead), behind: Number(behind), degraded: false, }; } async workspacePaths(): Promise { const output = await this.git(["ls-tree", "-r", "--name-only", "HEAD", "--", "workspaces"]); const paths = output.trim() === "" ? [] : output.trim().split("\n"); for (const path of paths) { if (!/^workspaces\/[a-z][a-z0-9-]{2,62}\.yaml$/.test(path)) { throw new WorkspaceRegistryError("workspace_invalid", "Workspace repository contains an invalid path"); } } return paths; } async readWorkspace(path: string): Promise { if (!/^workspaces\/[a-z][a-z0-9-]{2,62}\.yaml$/.test(path)) { throw new WorkspaceRegistryError("workspace_invalid", "Workspace repository path is invalid"); } return await this.git(["show", `HEAD:${path}`]); } async blob(path: string): Promise { if (!/^workspaces\/[a-z][a-z0-9-]{2,62}\.yaml$/.test(path)) { throw new WorkspaceRegistryError("workspace_invalid", "Workspace repository path is invalid"); } return (await this.git(["rev-parse", `HEAD:${path}`])).trim(); } private async clone(): Promise { try { await execFileAsync("git", [ "-c", `core.hooksPath=${this.hooksPath}`, "clone", "--branch", this.config.branch, "--single-branch", "--", this.config.remoteUrl!, this.repoPath, ], { cwd: this.root, env: { ...process.env, GIT_TERMINAL_PROMPT: "0" } }); assertDirectory(this.repoPath); } catch (error) { throw this.sanitizeGitError(error); } } private async refresh(): Promise { if (this.config.remoteUrl) { await this.git(["remote", "set-url", "origin", "--", this.config.remoteUrl]); } await this.git(["fetch", "--no-tags", "origin", this.config.branch]); await this.git(["merge", "--ff-only", "FETCH_HEAD"]); } private async git(args: string[]): Promise { try { const { stdout } = await execFileAsync( "git", ["-c", `core.hooksPath=${this.hooksPath}`, ...args], { cwd: this.repoPath, env: { ...process.env, GIT_TERMINAL_PROMPT: "0" } }, ); return stdout; } catch (error) { throw this.sanitizeGitError(error); } } private async gitOptional(args: string[]): Promise { try { return await this.git(args); } catch (error) { if (error instanceof WorkspaceRegistryError && error.code === "git_unavailable") return undefined; throw error; } } private sanitizeGitError(error: unknown): WorkspaceRegistryError { return new WorkspaceRegistryError(gitErrorCode(error), "Workspace Git operation failed"); } } export class WorkspaceRepositoryLock { private queue = Promise.resolve(); constructor(private readonly locksPath: string) {} async run(operation: () => Promise): Promise { const previous = this.queue; let releaseQueue!: () => void; this.queue = new Promise((resolve) => { releaseQueue = resolve; }); await previous; mkdirSync(this.locksPath, { recursive: true, mode: 0o700 }); assertDirectory(this.locksPath); let descriptor: number | undefined; const lockPath = join(this.locksPath, "repository.lock"); try { descriptor = openSync(lockPath, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY, 0o600); return await operation(); } catch (error) { if (typeof error === "object" && error !== null && "code" in error && error.code === "EEXIST") { throw new WorkspaceRegistryError("workspace_stale", "Workspace registry is busy"); } throw error; } finally { if (descriptor !== undefined) closeSync(descriptor); if (descriptor !== undefined) { try { unlinkSync(lockPath); } catch { /* stale lock cleanup is retried by the operator */ } } releaseQueue(); } } }