import { createServerFn } from "@tanstack/react-start"; import { z } from "zod"; import { requireViewer, viewerMiddleware } from "./middleware"; import { pullInput, requirePull, requirePullMutation } from "~/server/pull-access"; import { limitBrowse, limitWrite } from "~/server/limits"; import { currentBaseSha, loadPullRepos } from "~/server/pull-merge"; import { assertNotArchived, canAdministerAccount, requireRepoAccess } from "~/server/repos"; import { startMergeFix } from "~/server/mergefix/service"; import { applyProposal } from "~/server/mergefix/apply"; import { MERGEFIX_MODES } from "~/server/mergefix/proposal"; import { latestRuns, loadRepoSettings, saveRepoSettings } from "~/server/mergefix/store"; import { AI_ENDPOINT_KINDS, BYOK_PROVIDERS, endpointSummary } from "~/server/ai/endpoint"; import type { FixPlan } from "~/server/mergefix/plan"; import { usageSummary } from "~/server/metering/summary"; import { first } from "~/server/db"; import { forbidden } from "~/server/errors"; import { repoRef } from "~/lib/validation"; function aiUsage(summary: Awaited>) { const meter = (id: string) => summary.meters.find((entry) => entry.meter === id); const view = (id: string) => ({ used: meter(id)?.quantity ?? 0, cap: meter(id)?.hardCap ?? null }); return { month: summary.period, calls: view("ai.calls"), tokens: view("ai.tokens"), byokCalls: view("ai.byok_calls").used, byokTokens: view("ai.byok_tokens").used, }; } export const startPullMergeFix = createServerFn({ method: "POST" }) .middleware([viewerMiddleware]) .validator(pullInput.extend({ mode: z.enum(MERGEFIX_MODES).optional() })) .handler(async ({ data, context }) => { const viewer = requireViewer(context.viewer); await limitWrite(viewer); const { pull } = await requirePullMutation(data.owner, data.repo, data.number, viewer, "write"); return { runId: await startMergeFix(pull, viewer, "button", null, data.mode) }; }); /** Commits the accepted files of the latest proposal; head-branch write access is checked by the service. */ export const applyPullMergeFix = createServerFn({ method: "POST" }) .middleware([viewerMiddleware]) .validator( pullInput.extend({ files: z.array(z.string().min(1).max(4096)).min(1).max(100).optional(), confirmLowConfidence: z.boolean().optional(), }), ) .handler(async ({ data, context }) => { const viewer = requireViewer(context.viewer); await limitWrite(viewer); const { pull } = await requirePullMutation(data.owner, data.repo, data.number, viewer, "read"); return applyProposal(pull, viewer, data.files, data.confirmLowConfidence); }); export const getPullMergeFix = createServerFn({ method: "GET" }) .middleware([viewerMiddleware]) .validator(pullInput) .handler(async ({ data, context }) => { await limitBrowse(context.viewer); const { pull } = await requirePull(data.owner, data.repo, data.number, context.viewer, "read"); const [latest] = await latestRuns(pull, 1); if (!latest) return null; const baseMoved = latest.status === "suggested" && latest.base_sha !== null && (await currentBaseSha((await loadPullRepos(pull)).baseRepo, pull.base_ref)) !== latest.base_sha; const plan = latest.result_json ? (JSON.parse(latest.result_json) as FixPlan) : null; return { id: latest.id, status: latest.status, trigger: latest.trigger, mode: latest.mode, error: latest.error, headSha: latest.head_sha, commitSha: latest.commit_sha, calls: latest.calls, requeue: latest.requeue_status ? { status: latest.requeue_status, error: latest.requeue_error } : null, createdAt: latest.created_at, stale: latest.stale === 1 || (latest.head_sha !== pull.head_sha && latest.status !== "applied") || baseMoved, files: plan?.files ?? [], }; }); export const getMergeFixSettings = createServerFn({ method: "GET" }) .middleware([viewerMiddleware]) .validator(repoRef) .handler(async ({ data, context }) => { const { repo } = await requireRepoAccess(data.owner, data.repo, context.viewer, "admin"); const [settings, usage, accountEndpoint, repoEndpoint, canAdminister, owner] = await Promise.all([ loadRepoSettings(repo.id), usageSummary(repo.owner_id), endpointSummary("account", repo.owner_id), endpointSummary("repo", repo.id), canAdministerAccount(repo.owner_id, context.viewer), first<{ kind: string }>("SELECT kind FROM accounts WHERE id = ?", repo.owner_id), ]); const ownerIsOrg = owner?.kind === "org"; // The account form targets the viewer's own account or an org, so a site admin can't edit another user's. const canManageKey = canAdminister && (ownerIsOrg || repo.owner_id === context.viewer?.id); return { settings, isPrivate: repo.visibility === "private", usage: canManageKey ? aiUsage(usage) : null, accountEndpoint: canManageKey ? accountEndpoint : null, repoEndpoint, canManageKey, ownerIsOrg, providers: BYOK_PROVIDERS, kinds: AI_ENDPOINT_KINDS, }; }); export const saveMergeFixSettings = createServerFn({ method: "POST" }) .middleware([viewerMiddleware]) .validator( repoRef.extend({ queueEnabled: z.boolean(), allowPrivate: z.boolean(), autoApply: z.boolean().default(false), confidence: z.number().min(0.5).max(1), margin: z.number().min(0).max(1), }), ) .handler(async ({ data, context }) => { const viewer = requireViewer(context.viewer); await limitWrite(viewer); const { repo } = await requireRepoAccess(data.owner, data.repo, viewer, "admin"); assertNotArchived(repo); // Opting a private repo's code into AI calls spends the owner's account, so only the account's admins may. if (data.allowPrivate && !(await loadRepoSettings(repo.id)).allowPrivate) { if (!(await canAdministerAccount(repo.owner_id, viewer))) throw forbidden("Only the repository owner's admins can allow AI on private code"); } await saveRepoSettings(repo.id, { queueEnabled: data.queueEnabled, allowPrivate: data.allowPrivate, autoApply: data.autoApply, thresholds: { confidence: data.confidence, margin: data.margin }, }); return { ok: true }; });