← snapshot
5346 bytes
import type {
ApiErrorBody,
BlobDto,
DecisionDto,
InitRepoRequest,
InitRepoResponse,
IntroductionRow,
PromotePreview,
PublicationRow,
RefDto,
RemarkDto,
RemarkKind,
RepoDetail,
RepoSummary,
SnapshotDetail,
SnapshotDto,
TreeEntryDto,
VerdictDto,
} from "./types";
/**
* One typed client for both sides: server components hit jac-serve
* directly; the browser goes through the Next /api rewrite.
*/
function base(): string {
if (typeof window !== "undefined") return "";
return process.env.JAC_SERVE_URL ?? "http://localhost:8787";
}
export class ApiError extends Error {
readonly code: string;
readonly status: number;
constructor(status: number, code: string, message: string) {
super(message);
this.code = code;
this.status = status;
}
}
async function request<T>(
method: "GET" | "POST",
path: string,
body?: unknown,
): Promise<T> {
const res = await fetch(`${base()}${path}`, {
method,
cache: "no-store",
headers: body === undefined ? undefined : { "content-type": "application/json" },
body: body === undefined ? undefined : JSON.stringify(body),
});
if (!res.ok) {
let code = "unknown";
let message = `${res.status} ${res.statusText}`;
try {
const parsed = (await res.json()) as ApiErrorBody;
code = parsed.error.code;
message = parsed.error.message;
} catch {
// non-JSON error body; keep the status line
}
throw new ApiError(res.status, code, message);
}
return (await res.json()) as T;
}
const get = <T>(path: string) => request<T>("GET", path);
const post = <T>(path: string, body?: unknown) => request<T>("POST", path, body);
const enc = encodeURIComponent;
export const api = {
listRepos: () => get<{ repos: RepoSummary[] }>("/api/repos"),
repo: (slug: string) => get<RepoDetail>(`/api/repos/${enc(slug)}`),
initRepo: (req: InitRepoRequest) => post<InitRepoResponse>("/api/repos", req),
seedDemo: () => post<{ repos: string[] }>("/api/demo"),
refs: (slug: string) => get<{ refs: RefDto[] }>(`/api/repos/${enc(slug)}/refs`),
branch: (slug: string, name: string, fromRef: string) =>
post<RefDto>(`/api/repos/${enc(slug)}/refs`, { name, from: { ref: fromRef } }),
log: (slug: string, ref: string, limit = 200) =>
get<{ ref: string; head: string; entries: SnapshotDto[] }>(
`/api/repos/${enc(slug)}/log?ref=${enc(ref)}&limit=${limit}`,
),
snapshot: (slug: string, id: string) =>
get<SnapshotDetail>(`/api/repos/${enc(slug)}/snapshots/${id}`),
tree: (slug: string, id: string, path: string) =>
get<{ path: string; entries: TreeEntryDto[] }>(
`/api/repos/${enc(slug)}/snapshots/${id}/tree${path ? `?path=${enc(path)}` : ""}`,
),
blob: (slug: string, id: string) => get<BlobDto>(`/api/repos/${enc(slug)}/blobs/${id}`),
decisions: (slug: string, state?: "unsettled" | "settled") =>
get<{ decisions: DecisionDto[] }>(
`/api/repos/${enc(slug)}/decisions${state ? `?state=${state}` : ""}`,
),
decision: (slug: string, id: string) =>
get<DecisionDto>(`/api/repos/${enc(slug)}/decisions/${id}`),
attest: (slug: string, id: string, by: string, statement: string) =>
post<DecisionDto>(`/api/repos/${enc(slug)}/decisions/${id}/attest`, {
by,
statement,
}),
promotePreview: (slug: string, from: string, into: string) =>
get<PromotePreview>(
`/api/repos/${enc(slug)}/promote/preview?from=${enc(from)}&into=${enc(into)}`,
),
promote: (slug: string, from: string, into: string) =>
post<VerdictDto>(`/api/repos/${enc(slug)}/promote`, { from, into }),
addHuman: (slug: string, display_name: string) =>
post<{ kind: string; id: string; display_name?: string }>(
`/api/repos/${enc(slug)}/identities`,
{ human: { display_name } },
),
addAgent: (slug: string, model: string) =>
post<{ kind: string; id: string; display_name?: string }>(
`/api/repos/${enc(slug)}/identities`,
{ agent: { model } },
),
remarks: (slug: string, state?: "open" | "settled") =>
get<{ remarks: RemarkDto[]; open: number }>(
`/api/repos/${enc(slug)}/remarks${state ? `?state=${state}` : ""}`,
),
raiseRemark: (
slug: string,
body: {
anchor: { kind: string; id: string; quote?: string };
body: string;
kind: RemarkKind;
by: string;
},
) => post<RemarkDto>(`/api/repos/${enc(slug)}/remarks`, body),
settleRemark: (
slug: string,
id: string,
outcome: { outcome: "drafted"; decision: string } | { outcome: "declined"; because?: string },
) => post<RemarkDto>(`/api/repos/${enc(slug)}/remarks/${enc(id)}/settle`, outcome),
proposeDecision: (
slug: string,
body: {
title: string;
rationale: string;
families: string[];
actor: { kind: "human" | "agent"; id: string };
scope: string[];
},
) => post<DecisionDto>(`/api/repos/${enc(slug)}/decisions`, body),
sketch: (slug: string, kind: "snapshot" | "decision", id: string) =>
get<{ kind: string; lanes: string[] }>(
`/api/repos/${enc(slug)}/sketches/${kind}/${id}`,
),
publications: () =>
get<{ publications: PublicationRow[]; note: string }>("/api/rendezvous/publications"),
introductions: () =>
get<{ introductions: IntroductionRow[] }>("/api/rendezvous/introductions"),
};