import { ClientResponseError, ok } from "@atcute/client"; import type { Cid, Nsid, ResourceUri } from "@atcute/lexicons/syntax"; import type { OAuthUserAgent } from "@atcute/oauth-browser-client"; import { awaitDeletedRecord, awaitIndexedRecord } from "$lib/api/awaitRecord"; import { serviceClient } from "$lib/api/_request"; import { createBobbinClient } from "$lib/api/client"; import { pingIndexNow } from "$lib/api/indexnow"; import { mainSchema as addCollaboratorSchema } from "$lib/api/lexicons/types/sh/tangled/repo/addCollaborator"; import { mainSchema as deleteRepoSchema, type $input as DeleteRepoInput } from "$lib/api/lexicons/types/sh/tangled/repo/delete"; import { mainSchema as removeCollaboratorSchema } from "$lib/api/lexicons/types/sh/tangled/repo/removeCollaborator"; import { mainSchema as setDefaultBranchSchema } from "$lib/api/lexicons/types/sh/tangled/repo/setDefaultBranch"; import type { RecordView, RepoRecord } from "$lib/api/records"; import { rkeyFromUri } from "$lib/api/uri"; import { deleteRecord, putRecord, type WrittenRecord } from "$lib/api/write"; import { didOf, recordKeyOf } from "$lib/api/syntax"; const REPO_COLLECTION = "sh.tangled.repo" as Nsid; export interface RepoRecordEdit { name?: string; description?: string; website?: string; topics?: readonly string[]; spindle?: string; labels?: readonly string[]; } const empty = (value: unknown): boolean => value === undefined || value === "" || (Array.isArray(value) && value.length === 0); export const editedRepoRecord = (record: RepoRecord, edit: RepoRecordEdit): RepoRecord => Object.fromEntries( Object.entries({ ...record, ...edit, $type: "sh.tangled.repo" }).filter( ([, value]) => !empty(value) ) ) as RepoRecord; export const saveRepoRecord = async ( agent: OAuthUserAgent, bobbinUrl: string, view: RecordView, edit: RepoRecordEdit ): Promise => { const written = await putRecord( agent, REPO_COLLECTION, rkeyFromUri(view.uri), editedRepoRecord(view.value, edit), view.cid ); await awaitIndexedRecord(createBobbinClient({ serviceUrl: bobbinUrl }), written); return written; }; export const setDefaultBranch = async ( agent: OAuthUserAgent, knot: string, repoDid: string, defaultBranch: string ): Promise => { await ok( serviceClient(agent, knot).xrpc.call(setDefaultBranchSchema, { input: { repo: didOf(repoDid), defaultBranch } }) ); }; export const addCollaborator = async ( agent: OAuthUserAgent, knot: string, repoDid: string, subject: string ): Promise => { await ok( serviceClient(agent, knot).xrpc.call(addCollaboratorSchema, { input: { repo: didOf(repoDid), subject: didOf(subject) } }) ); }; export const removeCollaborator = async ( agent: OAuthUserAgent, knot: string, repoDid: string, subject: string ): Promise => { await ok( serviceClient(agent, knot).xrpc.call(removeCollaboratorSchema, { input: { repo: didOf(repoDid), subject: didOf(subject) } }) ); }; const isRecordGone = (cause: unknown): boolean => cause instanceof ClientResponseError && (cause.status === 404 || cause.error === "RecordNotFound"); export interface RepoLocation { uri: string; knot: string; name: string; ownerHandle: string; ownerDid: string; repoDid?: string; cid?: Cid; } export const deleteRepo = async ( agent: OAuthUserAgent, bobbinUrl: string, repo: RepoLocation ): Promise => { const rkey = rkeyFromUri(repo.uri); try { await deleteRecord(agent, REPO_COLLECTION, rkey, repo.cid); } catch (cause) { if (!isRecordGone(cause)) throw cause; } try { await ok( serviceClient(agent, repo.knot).xrpc.post(deleteRepoSchema.nsid, { as: null, input: { did: didOf(repo.ownerDid), name: repo.name, rkey: recordKeyOf(rkey), ...(repo.repoDid ? { repo: didOf(repo.repoDid) } : {}) } as DeleteRepoInput }) ); } catch (cause) { const detail = cause instanceof Error ? cause.message : String(cause); throw new Error( `The record is gone, but ${repo.knot} did not delete ${repo.name}: ${detail}`, { cause } ); } const confirmed = await awaitDeletedRecord( createBobbinClient({ serviceUrl: bobbinUrl }), repo.uri as ResourceUri ); if (!confirmed) { throw new Error("Deleted, but the appview has not caught up yet."); } pingIndexNow(`https://tangled.org/${repo.ownerHandle}/${repo.name}`); };