import { useMemo, useReducer, useRef, useState } from "react"; import type { FormEvent, ReactNode, Ref } from "react"; import { Link, Navigate, useLocation, useNavigate, } from "react-router"; import { ArrowLeftIcon, ArrowRightIcon, ArrowTopRightOnSquareIcon, CheckCircleIcon, CheckIcon, ChevronDownIcon, ClipboardDocumentCheckIcon, ClipboardIcon, EyeIcon, EyeSlashIcon, } from "@heroicons/react/16/solid"; import { type InstallFinishResponse, type InstallGithubAppOwner, type InstallLlmProviderInput, type InstallObjectStoreInput, type InstallSandboxInput, type InstallSessionResponse, createInstallGithubAppManifest, finishInstall, getInstallSession, persistInstallToken, putInstallGithubToken, putInstallLlm, putInstallObjectStore, putInstallSandbox, putInstallServer, readStoredInstallToken, testInstallGithubToken, testInstallLlm, testInstallObjectStore, testInstallSandbox, } from "./install-api"; import { INSTALL_PROVIDERS } from "./install-config"; import { useInstallSessionQuery } from "./install-query"; import { CopyButton, ErrorMessage, INPUT_CLASS, PRIMARY_BUTTON_CLASS, SECONDARY_BUTTON_CLASS, } from "./components/ui"; import { LoadingState } from "./components/state"; import { useInstallGithubCallbackError, useInstallRestartHealthPolling, useInstallTokenFromUrl, } from "./hooks/use-install-effects"; import { consumeInstallTokenFromUrl } from "./mode"; const INSTALL_STEPS = [ { id: "welcome", label: "Welcome", href: "/install/welcome" }, { id: "server", label: "Server", href: "/install/server" }, { id: "object_store", label: "Storage", href: "/install/object-store" }, { id: "sandbox", label: "Sandbox", href: "/install/sandbox" }, { id: "llm", label: "LLMs", href: "/install/llm" }, { id: "github", label: "GitHub", href: "/install/github" }, { id: "review", label: "Review", href: "/install/review" }, ] as const; const STEPPER_STEPS = INSTALL_STEPS.slice(1); type StepId = (typeof INSTALL_STEPS)[number]["id"]; type FinishState = InstallFinishResponse | null; type GithubStrategy = "token" | "app"; type GithubOwnerKind = "personal" | "org"; type SessionState = | { status: "idle" } | { status: "loading"; token: string } | { status: "error"; token: string | null; message: string } | { status: "ready"; token: string; data: InstallSessionResponse }; type TokenForm = { token: string; username: string }; type AppForm = { owner: InstallGithubAppOwner; appName: string; allowedUsername: string; }; type ProviderSelection = Record; type ObjectStoreProvider = "local" | "s3"; type ObjectStoreCredentialMode = "runtime" | "access_key"; type ObjectStoreForm = { provider: ObjectStoreProvider; localRoot: string; bucket: string; region: string; credentialMode: ObjectStoreCredentialMode; accessKeyId: string; secretAccessKey: string; manualCredentialsSaved: boolean; }; type SandboxProvider = NonNullable; type SandboxForm = { provider: SandboxProvider; apiKey: string; apiKeySaved: boolean; allowLocal: boolean; }; type RunStepSubmit = (args: { action: () => Promise; fallback: string; next?: string; }) => Promise; type InstallDispatch = (action: InstallAction) => void; type InstallState = { sessionState: SessionState; manualToken: string; llmSelection: ProviderSelection; objectStoreForm: ObjectStoreForm; sandboxForm: SandboxForm; canonicalUrl: string; githubStrategy: GithubStrategy; tokenForm: TokenForm; appForm: AppForm; saveError: string | null; submitting: boolean; finishState: FinishState; timedOut: boolean; }; type InstallAction = | { type: "manualTokenChanged"; value: string } | { type: "sessionCleared" } | { type: "sessionReady"; token: string; session: InstallSessionResponse } | { type: "sessionFailed"; token: string | null; message: string } | { type: "saveErrorChanged"; message: string | null } | { type: "submittingChanged"; submitting: boolean } | { type: "timedOutChanged"; timedOut: boolean } | { type: "finishStarted"; result: FinishState } | { type: "canonicalUrlChanged"; value: string } | { type: "llmProviderApiKeyChanged"; provider: string; apiKey: string } | { type: "llmSelectionChanged"; value: ProviderSelection } | { type: "objectStorePatched"; patch: Partial } | { type: "sandboxPatched"; patch: Partial } | { type: "githubStrategyChanged"; strategy: GithubStrategy } | { type: "tokenFormPatched"; patch: Partial } | { type: "tokenFormReplaced"; value: TokenForm } | { type: "appFormPatched"; patch: Partial } | { type: "githubOwnerChanged"; kind: GithubOwnerKind } | { type: "githubOrgSlugChanged"; slug: string }; function initialInstallState(): InstallState { return { sessionState: { status: "idle" }, manualToken: "", llmSelection: defaultProviderSelection(), objectStoreForm: defaultObjectStoreForm(), sandboxForm: defaultSandboxForm(), canonicalUrl: "", githubStrategy: "token", tokenForm: { token: "", username: "" }, appForm: { owner: { kind: "personal" }, appName: "Fabro", allowedUsername: "", }, saveError: null, submitting: false, finishState: null, timedOut: false, }; } function hydrateInstallState( state: InstallState, token: string, session: InstallSessionResponse, ): InstallState { let githubStrategy = state.githubStrategy; let tokenForm = state.tokenForm; let appForm = state.appForm; if (session.github?.strategy === "app") { githubStrategy = "app"; appForm = { owner: session.github.owner ?? { kind: "personal" }, appName: session.github.app_name || "Fabro", allowedUsername: session.github.allowed_username || "", }; } else if (session.github?.strategy === "token") { githubStrategy = "token"; tokenForm = { ...state.tokenForm, username: session.github.username || state.tokenForm.username, }; } return { ...state, sessionState: { status: "ready", token, data: session }, canonicalUrl: state.canonicalUrl || session.server?.canonical_url || session.prefill.canonical_url, objectStoreForm: hydrateObjectStoreForm(session), sandboxForm: hydrateSandboxForm(state.sandboxForm, session), llmSelection: hydrateProviderSelection(state.llmSelection, session), githubStrategy, tokenForm, appForm, }; } function installReducer(state: InstallState, action: InstallAction): InstallState { switch (action.type) { case "manualTokenChanged": return { ...state, manualToken: action.value }; case "sessionCleared": return { ...state, sessionState: { status: "idle" } }; case "sessionReady": return hydrateInstallState(state, action.token, action.session); case "sessionFailed": return { ...state, sessionState: { status: "error", token: action.token, message: action.message } }; case "saveErrorChanged": return { ...state, saveError: action.message }; case "submittingChanged": return { ...state, submitting: action.submitting }; case "timedOutChanged": return { ...state, timedOut: action.timedOut }; case "finishStarted": return { ...state, finishState: action.result }; case "canonicalUrlChanged": return { ...state, canonicalUrl: action.value }; case "llmProviderApiKeyChanged": return { ...state, llmSelection: { ...state.llmSelection, [action.provider]: { apiKey: action.apiKey }, }, }; case "llmSelectionChanged": return { ...state, llmSelection: action.value }; case "objectStorePatched": return { ...state, objectStoreForm: { ...state.objectStoreForm, ...action.patch }, }; case "sandboxPatched": return { ...state, sandboxForm: { ...state.sandboxForm, ...action.patch } }; case "githubStrategyChanged": return { ...state, githubStrategy: action.strategy }; case "tokenFormPatched": return { ...state, tokenForm: { ...state.tokenForm, ...action.patch } }; case "tokenFormReplaced": return { ...state, tokenForm: action.value }; case "appFormPatched": return { ...state, appForm: { ...state.appForm, ...action.patch } }; case "githubOwnerChanged": return { ...state, appForm: { ...state.appForm, owner: action.kind === "org" ? { kind: "org", slug: state.appForm.owner.kind === "org" ? state.appForm.owner.slug ?? "" : "", } : { kind: "personal" }, }, }; case "githubOrgSlugChanged": return { ...state, appForm: { ...state.appForm, owner: { kind: "org", slug: action.slug }, }, }; } } function installSessionErrorMessage(error: unknown): string { return error instanceof Error ? error.message : "Install session failed"; } function sessionStateForInstallToken( installToken: string | null, sessionState: SessionState, queryError: unknown, ): SessionState { if (!installToken) { return sessionState.status === "error" && sessionState.token === null ? sessionState : { status: "idle" }; } if ( (sessionState.status === "ready" || sessionState.status === "error") && sessionState.token === installToken ) { return sessionState; } if (queryError) { return { status: "error", token: installToken, message: installSessionErrorMessage(queryError), }; } return { status: "loading", token: installToken }; } function readInitialInstallToken(): string | null { const stored = readStoredInstallToken(); if (stored) return stored; if (typeof window === "undefined") return null; return consumeInstallTokenFromUrl(window.location.href).token; } /** * Coordinates install-mode browser integrations: token/error URL scrubbing, * install-session query state, and restart health polling. Timers, intervals, * and in-flight requests are cancelled when their install identity changes. */ function useInstallController() { const { pathname } = useLocation(); const [installToken, setInstallToken] = useState(() => readInitialInstallToken(), ); const [installState, dispatchInstall] = useReducer( installReducer, undefined, initialInstallState, ); const { finishState } = installState; const installSessionQuery = useInstallSessionQuery(installToken, { onSuccess: (session) => { if (!installToken) return; dispatchInstall({ type: "sessionReady", token: installToken, session }); }, onError: (error) => { dispatchInstall({ type: "sessionFailed", token: installToken, message: installSessionErrorMessage(error), }); }, }); useInstallTokenFromUrl({ setInstallToken }); useInstallGithubCallbackError({ dispatchInstall, pathname }); useInstallRestartHealthPolling({ dispatchInstall, finishState }); const sessionState = sessionStateForInstallToken( installToken, installState.sessionState, installSessionQuery.error, ); const controllerState = sessionState === installState.sessionState ? installState : { ...installState, sessionState }; const refreshInstallSession = async () => { if (!installToken) { throw new Error("Install token is required to refresh the session."); } const nextSession = await getInstallSession(installToken); dispatchInstall({ type: "sessionReady", token: installToken, session: nextSession, }); await installSessionQuery.mutate(nextSession, { revalidate: false }); return nextSession; }; return { pathname, installToken, setInstallToken, installState: controllerState, dispatchInstall, refreshInstallSession, }; } export default function InstallApp() { const navigate = useNavigate(); const { pathname, installToken, setInstallToken, installState, dispatchInstall, refreshInstallSession, } = useInstallController(); const { sessionState, manualToken, llmSelection, objectStoreForm, sandboxForm, canonicalUrl, githubStrategy, tokenForm, appForm, saveError, submitting, finishState, timedOut, } = installState; const session = sessionState.status === "ready" ? sessionState.data : null; const currentStep = useMemo( () => STEPPER_STEPS.find((step) => pathname.startsWith(step.href))?.id ?? "welcome", [pathname], ); const completedSteps = new Set(session?.completed_steps ?? []); const sessionError = sessionState.status === "error" ? sessionState.message : null; if (!installToken) { return ( dispatchInstall({ type: "manualTokenChanged", value }) } sessionError={sessionError} onSubmit={() => { const nextToken = manualToken.trim(); if (!nextToken) { dispatchInstall({ type: "sessionFailed", token: null, message: "Paste the install token from the server logs.", }); return; } persistInstallToken(nextToken); setInstallToken(nextToken); dispatchInstall({ type: "sessionCleared" }); }} /> ); } const runStepSubmit: RunStepSubmit = async (args) => { // Re-entrancy guard: the StepPanel form guards its own onSubmit, but the // LLM step's "Skip LLM setup" button calls this directly, so a fast // double-click could otherwise fire two requests before `submitting` // re-renders the disabled state. if (submitting) return; dispatchInstall({ type: "submittingChanged", submitting: true }); dispatchInstall({ type: "saveErrorChanged", message: null }); try { await args.action(); if (args.next) { await refreshInstallSession(); navigate(args.next); } } catch (error) { dispatchInstall({ type: "saveErrorChanged", message: error instanceof Error ? error.message : args.fallback, }); } finally { dispatchInstall({ type: "submittingChanged", submitting: false }); } }; if (sessionState.status === "error") { return ( dispatchInstall({ type: "manualTokenChanged", value }) } sessionError={sessionError} onSubmit={() => { const nextToken = manualToken.trim(); persistInstallToken(nextToken); setInstallToken(nextToken || null); }} /> ); } // Covers both sessionState "loading" AND the brief "idle" window before the // install session query reports data. Without this guard, // screens like GithubAppDoneScreen see `session == null` and navigate away // before the first fetch finishes — trapping the user in a redirect loop. if (!session) { return ( ); } if ((pathname === "/" || pathname === "/install") && !finishState) { return ; } if (finishState && pathname !== "/install/finishing") { return ; } return ( {pathname === "/install/finishing" ? ( ) : pathname === "/install/llm" ? ( ) : pathname === "/install/server" ? ( ) : pathname === "/install/object-store" ? ( ) : pathname === "/install/sandbox" ? ( ) : pathname === "/install/github/done" ? ( ) : pathname === "/install/github" ? ( ) : pathname === "/install/review" ? ( { dispatchInstall({ type: "submittingChanged", submitting: true }); dispatchInstall({ type: "saveErrorChanged", message: null }); try { const result = await finishInstall(installToken); dispatchInstall({ type: "finishStarted", result }); navigate("/install/finishing"); } catch (error) { dispatchInstall({ type: "saveErrorChanged", message: error instanceof Error ? error.message : "Install failed.", }); } finally { dispatchInstall({ type: "submittingChanged", submitting: false }); } }} /> ) : ( )} ); } function LlmStep({ installToken, llmSelection, saveError, submitting, runStepSubmit, dispatchInstall, }: { installToken: string; llmSelection: ProviderSelection; saveError: string | null; submitting: boolean; runStepSubmit: RunStepSubmit; dispatchInstall: (action: InstallAction) => void; }) { return ( { void runStepSubmit({ action: () => putInstallLlm(installToken, []), fallback: "Failed to skip LLM setup.", next: "/install/github", }); }} > Skip LLM setup } onSubmit={async () => { const providers: InstallLlmProviderInput[] = []; for (const { id } of INSTALL_PROVIDERS) { const current = llmSelection[id] ?? { apiKey: "" }; const provider = { provider: id, api_key: current.apiKey.trim(), }; if (provider.api_key.length > 0) providers.push(provider); } if (providers.length === 0) { dispatchInstall({ type: "saveErrorChanged", message: "Add at least one provider API key before continuing.", }); return; } await runStepSubmit({ action: async () => { await Promise.all( providers.map((provider) => testInstallLlm(installToken, provider)), ); await putInstallLlm(installToken, providers); }, fallback: "Failed to save LLM settings.", next: "/install/github", }); }} > dispatchInstall({ type: "llmProviderApiKeyChanged", provider, apiKey, }) } /> ); } function ServerStep({ installToken, canonicalUrl, saveError, submitting, runStepSubmit, dispatchInstall, }: { installToken: string; canonicalUrl: string; saveError: string | null; submitting: boolean; runStepSubmit: RunStepSubmit; dispatchInstall: (action: InstallAction) => void; }) { const canonicalUrlInputRef = useRef(null); return ( { if (!canonicalUrl.trim()) { dispatchInstall({ type: "saveErrorChanged", message: "Enter the canonical server URL before continuing.", }); focusInput(canonicalUrlInputRef); return; } await runStepSubmit({ action: () => putInstallServer(installToken, canonicalUrl.trim()), fallback: "Failed to save server settings.", next: "/install/object-store", }); }} > dispatchInstall({ type: "canonicalUrlChanged", value: event.target.value, }) } className={INPUT_CLASS} placeholder="https://fabro.example.com" autoComplete="url" spellCheck={false} /> ); } function ObjectStoreStep({ installToken, objectStoreForm, saveError, submitting, runStepSubmit, dispatchInstall, }: { installToken: string; objectStoreForm: ObjectStoreForm; saveError: string | null; submitting: boolean; runStepSubmit: RunStepSubmit; dispatchInstall: (action: InstallAction) => void; }) { const localRootInputRef = useRef(null); const bucketInputRef = useRef(null); const regionInputRef = useRef(null); const accessKeyIdInputRef = useRef(null); const secretAccessKeyInputRef = useRef(null); return ( { if (objectStoreForm.provider === "local") { if (!objectStoreForm.localRoot.trim()) { dispatchInstall({ type: "saveErrorChanged", message: "Enter the local object-store directory before continuing.", }); focusInput(localRootInputRef); return; } } else { if (!objectStoreForm.bucket.trim()) { dispatchInstall({ type: "saveErrorChanged", message: "Enter the S3 bucket before continuing.", }); focusInput(bucketInputRef); return; } if (!objectStoreForm.region.trim()) { dispatchInstall({ type: "saveErrorChanged", message: "Enter the AWS region before continuing.", }); focusInput(regionInputRef); return; } if (objectStoreForm.credentialMode === "access_key") { const accessKeyId = objectStoreForm.accessKeyId.trim(); const secretAccessKey = objectStoreForm.secretAccessKey.trim(); const keepStoredCredentials = objectStoreForm.manualCredentialsSaved && !accessKeyId && !secretAccessKey; if (!keepStoredCredentials && !accessKeyId) { dispatchInstall({ type: "saveErrorChanged", message: "Enter the AWS access key ID before continuing.", }); focusInput(accessKeyIdInputRef); return; } if (!keepStoredCredentials && !secretAccessKey) { dispatchInstall({ type: "saveErrorChanged", message: "Enter the AWS secret access key before continuing.", }); focusInput(secretAccessKeyInputRef); return; } } } const payload = buildObjectStorePayload(objectStoreForm); await runStepSubmit({ action: async () => { if (objectStoreForm.provider === "s3") { await testInstallObjectStore(installToken, payload); } await putInstallObjectStore(installToken, payload); }, fallback: "Failed to save object-store settings.", next: "/install/sandbox", }); }} > { dispatchInstall({ type: "objectStorePatched", patch: { provider }, }); if (provider === "s3") { focusInput(bucketInputRef); } else { focusInput(localRootInputRef); } }} /> {objectStoreForm.provider === "s3" ? (
dispatchInstall({ type: "objectStorePatched", patch: { bucket: event.target.value }, }) } className={`${INPUT_CLASS} font-mono`} placeholder="my-fabro-data" spellCheck={false} autoCapitalize="off" /> dispatchInstall({ type: "objectStorePatched", patch: { region: event.target.value }, }) } className={`${INPUT_CLASS} font-mono`} placeholder="us-east-1" spellCheck={false} autoCapitalize="off" /> { dispatchInstall({ type: "objectStorePatched", patch: { credentialMode }, }); if (credentialMode === "access_key") { focusInput(accessKeyIdInputRef); } }} /> {objectStoreForm.credentialMode === "access_key" ? (
dispatchInstall({ type: "objectStorePatched", patch: { accessKeyId: event.target.value }, }) } className={`${INPUT_CLASS} font-mono`} placeholder="AKIA..." spellCheck={false} autoComplete="off" autoCapitalize="off" /> dispatchInstall({ type: "objectStorePatched", patch: { secretAccessKey: value }, }) } placeholder="Secret access key" /> {objectStoreForm.manualCredentialsSaved ? (

Credentials saved. Leave both fields blank to keep them, or enter both fields to replace them.

) : null}
) : (

Fabro will use AWS credentials already provided by the runtime, such as EC2, ECS, or IRSA-based auth.

)}
) : (
dispatchInstall({ type: "objectStorePatched", patch: { localRoot: event.target.value }, }) } className={`${INPUT_CLASS} font-mono`} placeholder="Local object-store directory" spellCheck={false} autoCapitalize="off" />

Fabro will store SlateDB and run artifacts under this directory.

)}
); } function SandboxStep({ installToken, sandboxForm, saveError, submitting, runStepSubmit, dispatchInstall, }: { installToken: string; sandboxForm: SandboxForm; saveError: string | null; submitting: boolean; runStepSubmit: RunStepSubmit; dispatchInstall: (action: InstallAction) => void; }) { const sandboxApiKeyInputRef = useRef(null); return ( { if (sandboxForm.provider === "daytona") { const apiKey = sandboxForm.apiKey.trim(); const keepStoredKey = sandboxForm.apiKeySaved && !apiKey; if (!keepStoredKey && !apiKey) { dispatchInstall({ type: "saveErrorChanged", message: "Enter the Daytona API key before continuing.", }); focusInput(sandboxApiKeyInputRef); return; } } const payload = buildSandboxPayload(sandboxForm); await runStepSubmit({ action: async () => { if (sandboxForm.provider === "daytona") { await testInstallSandbox(installToken, payload); } await putInstallSandbox(installToken, payload); }, fallback: "Failed to save sandbox settings.", next: "/install/llm", }); }} > { dispatchInstall({ type: "sandboxPatched", patch: { provider }, }); if (provider === "daytona") { focusInput(sandboxApiKeyInputRef); } }} /> {sandboxForm.provider === "daytona" ? (
dispatchInstall({ type: "sandboxPatched", patch: { apiKey: event.target.value }, }) } className={`${INPUT_CLASS} font-mono`} placeholder={sandboxForm.apiKeySaved ? "•••• (saved)" : "dtn_..."} autoComplete="off" spellCheck={false} />
) : (

Fabro will use the host Docker daemon. Make sure the server has access to /var/run/docker.sock.

)}
); } function GithubStep({ installToken, session, githubStrategy, tokenForm, appForm, saveError, submitting, runStepSubmit, dispatchInstall, }: { installToken: string; session: InstallSessionResponse; githubStrategy: GithubStrategy; tokenForm: TokenForm; appForm: AppForm; saveError: string | null; submitting: boolean; runStepSubmit: RunStepSubmit; dispatchInstall: (action: InstallAction) => void; }) { return ( { if (githubStrategy === "token") { const trimmedToken = tokenForm.token.trim(); if (!trimmedToken) { dispatchInstall({ type: "saveErrorChanged", message: "Provide the GitHub token before continuing.", }); return; } await runStepSubmit({ action: async () => { const username = await testInstallGithubToken(installToken, trimmedToken); dispatchInstall({ type: "tokenFormReplaced", value: { token: trimmedToken, username }, }); await putInstallGithubToken(installToken, trimmedToken, username); }, fallback: "Failed to start GitHub setup.", next: "/install/review", }); return; } const { owner, appName, allowedUsername } = appForm; if (owner.kind === "org" && !(owner.slug ?? "").trim()) { dispatchInstall({ type: "saveErrorChanged", message: "Enter the organization slug for the GitHub App.", }); return; } if (!appName.trim()) { dispatchInstall({ type: "saveErrorChanged", message: "Enter the GitHub App name before continuing.", }); return; } if (!allowedUsername.trim()) { dispatchInstall({ type: "saveErrorChanged", message: "Enter the GitHub username that should be allowed to log in.", }); return; } await runStepSubmit({ action: async () => { const manifest = await createInstallGithubAppManifest(installToken, { owner: owner.kind === "org" ? { kind: "org", slug: (owner.slug ?? "").trim() } : { kind: "personal" }, app_name: appName.trim(), allowed_username: allowedUsername.trim(), }); submitGithubManifest( manifest.github_form_action, manifest.manifest, manifest.state, ); }, fallback: "Failed to start GitHub setup.", }); }} > dispatchInstall({ type: "githubStrategyChanged", strategy }) } /> {githubStrategy === "token" ? (
dispatchInstall({ type: "tokenFormPatched", patch: { token: value }, }) } placeholder="ghp_..." />
{tokenForm.username ? (

Previously validated as{" "} @{tokenForm.username}

) : null}

Create a fine-grained or classic token with{" "} repo scope.

github.com/settings/tokens
) : (
dispatchInstall({ type: "githubOwnerChanged", kind }) } /> {appForm.owner.kind === "org" ? ( dispatchInstall({ type: "githubOrgSlugChanged", slug: event.target.value, }) } className={INPUT_CLASS} placeholder="acme" spellCheck={false} /> ) : null} dispatchInstall({ type: "appFormPatched", patch: { allowedUsername: event.target.value }, }) } className={INPUT_CLASS} placeholder="octocat" spellCheck={false} /> {session.server?.canonical_url ? (

After creating the app, GitHub will redirect back to{" "} {session.server.canonical_url}.

) : null}
)}
); } function TokenEntryScreen({ manualToken, onManualTokenChange, sessionError, onSubmit, }: { manualToken: string; onManualTokenChange: (value: string) => void; sessionError: string | null; onSubmit: () => void; }) { // react-doctor-disable-next-line react-doctor/no-prevent-default -- Install finalization writes server config through the install API, not a native form action. return (
Fabro Install

Finish configuring this Fabro server

Find the one-time install token in your terminal, Docker logs, or platform log viewer, then paste it here to continue.

{/* react-doctor-disable-next-line react-doctor/no-prevent-default -- Install token entry is a client-side API step with no meaningful non-JS endpoint. */}
{ event.preventDefault(); onSubmit(); }} className="mt-8 space-y-5" >
onManualTokenChange(event.target.value)} className={`${INPUT_CLASS} font-mono`} placeholder="Paste install token" spellCheck={false} autoComplete="off" autoCapitalize="off" />
{sessionError ? : null}

Where to find it

Local
Output of{" "} fabro server start
Docker
docker logs <container>
Hosted
Your platform's log viewer or journalctl

Install mode is temporary and only available until setup completes.

); } function InstallLayout({ children, currentStep, completedSteps, }: { children: ReactNode; currentStep: StepId; completedSteps: Set; }) { const showStepper = currentStep !== "welcome"; return (
Fabro Install
{showStepper ? (
) : null}
{children}
); } function Stepper({ currentStep, completedSteps, }: { currentStep: StepId; completedSteps: Set; }) { const activeIndex = STEPPER_STEPS.findIndex((step) => step.id === currentStep); const safeIndex = activeIndex === -1 ? 0 : activeIndex; const activeStep = STEPPER_STEPS[safeIndex]; const progress = ((safeIndex + 1) / STEPPER_STEPS.length) * 100; return ( ); } function WelcomeScreen() { return (

Set up your Fabro server

A short walkthrough to confirm the public server URL, choose the shared object store and sandbox runtime, validate your LLM credentials, and connect GitHub. When you finish, Fabro restarts into normal mode.

    {[ ["Server URL", "Confirm where operators will reach Fabro."], [ "Object store", "Choose local disk or AWS S3 for SlateDB and artifacts.", ], ["Sandbox", "Choose Docker or Daytona for workflow execution."], ["LLMs", "Validate API keys for Anthropic, OpenAI, or Gemini."], ["GitHub", "Choose a personal access token or a GitHub App."], ["Review", "Double-check the plan, then write the files."], ].map(([title, body], index) => (
  1. {title}

    {body}

  2. ))}
Start setup
); } function StepPanel({ title, description, children, error, submitting, submitLabel = "Continue", submittingLabel = "Saving...", backHref, secondaryAction, onSubmit, }: { title: string; description: string; children: ReactNode; error: string | null; submitting: boolean; submitLabel?: string; submittingLabel?: string; backHref?: string; secondaryAction?: ReactNode; onSubmit: () => Promise; }) { return ( // react-doctor-disable-next-line react-doctor/no-prevent-default -- Install wizard forms are client-side API steps with no meaningful non-JS endpoint.
) => { event.preventDefault(); if (submitting) return; void onSubmit(); }} className="space-y-8" >

{title}

{description}

{children}
{error ? : null}
{backHref ? ( Back ) : ( )}
{secondaryAction}
); } function ReviewScreen({ session, error, submitting, onInstall, }: { session: InstallSessionResponse | null; error: string | null; submitting: boolean; onInstall: () => Promise; }) { const llmSummary = describeLlmSummary(session?.llm); const serverUrl = session?.server?.canonical_url || session?.prefill.canonical_url || "Unknown"; return ( // react-doctor-disable-next-line react-doctor/no-prevent-default -- Install finalization writes server config through the install API, not a native form action.
) => { event.preventDefault(); if (submitting) return; void onInstall(); }} className="space-y-8" >

Review and install

Confirm the plan below. Fabro writes the configuration to disk, then restarts into normal mode.

} />
{error ? : null}
Back
); } function FinishingScreen({ finishState, timedOut, }: { finishState: FinishState; timedOut: boolean; }) { if (!finishState) { return ; } return (

{timedOut ? "Install complete" : "Finishing up"}

{timedOut ? "The server didn't come back automatically. Start it manually and return to the URL below." : "Configuration written. Waiting for the server to restart into normal mode."}

{timedOut ? (
Run fabro server start, then visit{" "} {finishState.restart_url}.
) : (

Polling /health

)} {finishState.dev_token ? (

Development token

Use this to sign in after the server restarts.

) : null}
); } function ProviderFields({ value, onProviderApiKeyChange, }: { value: ProviderSelection; onProviderApiKeyChange: (provider: string, apiKey: string) => void; }) { return (
{INSTALL_PROVIDERS.map((provider) => { const current = value[provider.id] ?? { apiKey: "" }; return (
onProviderApiKeyChange(provider.id, next)} placeholder={provider.envVar} />

{provider.keyHelp.text}

{provider.keyHelp.url.replace(/^https?:\/\//, "")}
); })}
); } type CardOption = { id: T; title: string; body: string }; function CardPicker({ legend, options, value, onChange, }: { legend: string; options: ReadonlyArray>; value: T; onChange: (value: T) => void; }) { return (
{legend}
{options.map((option) => ( onChange(option.id)} title={option.title} body={option.body} /> ))}
); } const GITHUB_STRATEGY_OPTIONS: ReadonlyArray> = [ { id: "token", title: "Personal access token", body: "Quickest path. Validates a PAT and stores it in the vault.", }, { id: "app", title: "GitHub App", body: "Recommended for teams. Enables OAuth.", }, ]; const OBJECT_STORE_PROVIDER_OPTIONS: ReadonlyArray> = [ { id: "local", title: "Local disk", body: "Uses the host filesystem for SlateDB and run artifacts.", }, { id: "s3", title: "AWS S3", body: "Uses one S3 bucket with fixed slatedb/ and artifacts/ prefixes.", }, ]; const OBJECT_STORE_CREDENTIAL_MODE_OPTIONS: ReadonlyArray< CardOption > = [ { id: "runtime", title: "Use AWS runtime credentials", body: "Use credentials already supplied by the deployment environment.", }, { id: "access_key", title: "Enter AWS access key credentials", body: "Store an access key pair in server.env for startup and validation.", }, ]; const SANDBOX_PROVIDER_OPTIONS: ReadonlyArray> = [ { id: "docker", title: "Docker", body: "Default. Uses the host Docker daemon to run sandbox containers.", }, { id: "daytona", title: "Daytona", body: "Each run gets a managed Daytona cloud sandbox. Requires an API key.", }, ]; const GITHUB_OWNER_OPTIONS: ReadonlyArray> = [ { id: "personal", title: "Personal account", body: "GitHub's personal app creation flow.", }, { id: "org", title: "Organization", body: "GitHub's org flow — requires the org slug.", }, ]; function OptionCard({ selected, onSelect, title, body, }: { selected: boolean; onSelect: () => void; title: string; body: string; }) { const base = "group relative flex items-start gap-3 rounded-lg px-4 py-3.5 text-left outline-1 -outline-offset-1 transition-colors"; const state = selected ? "bg-teal-500/10 outline-teal-500/60" : "bg-overlay outline-white/10 hover:bg-overlay-strong hover:outline-white/15"; return ( ); } function GithubAppDoneScreen({ github, }: { github: InstallSessionResponse["github"]; }) { if (!github || github.strategy !== "app") { return ; } return (

GitHub App connected

The app credentials are staged. They'll be written into the runtime env file when the install finishes.

Continue to review
); } function SummaryRow({ label, value, mono, action, }: { label: string; value: string; mono?: boolean; action?: ReactNode; }) { return (
{label}
{value} {action}
); } function Field({ label, hint, children, }: { label: string; hint?: string; children: ReactNode; }) { return ( ); } function PasswordInput({ id, name, value, onChange, placeholder, inputRef, }: { id?: string; name: string; value: string; onChange: (value: string) => void; placeholder?: string; inputRef?: Ref; }) { const [visible, setVisible] = useState(false); return (
onChange(event.target.value)} className={`${INPUT_CLASS} pr-11 font-mono`} placeholder={placeholder} spellCheck={false} autoComplete="off" autoCapitalize="off" />
); } function CopyableToken({ token }: { token: string }) { const [copied, setCopied] = useState(false); return (
        {token}
      
); } function HelpDisclosure({ summary, children, }: { summary: string; children: ReactNode; }) { return (
{summary}
{children}
); } function ExternalLink({ href, children, }: { href: string; children: ReactNode; }) { return ( {children} ); } function Spinner({ className = "" }: { className?: string }) { return ( ); } function defaultProviderSelection(): ProviderSelection { return Object.fromEntries( INSTALL_PROVIDERS.map((provider) => [provider.id, { apiKey: "" }]), ); } function defaultObjectStoreForm(localRoot = ""): ObjectStoreForm { return { provider: "local", localRoot, bucket: "", region: "", credentialMode: "runtime", accessKeyId: "", secretAccessKey: "", manualCredentialsSaved: false, }; } function hydrateProviderSelection( current: ProviderSelection, session: InstallSessionResponse, ): ProviderSelection { const hasUserInput = Object.values(current).some((provider) => provider.apiKey); if (hasUserInput) return current; const next = defaultProviderSelection(); for (const provider of session.llm?.providers ?? []) { next[provider.provider] = { apiKey: "" }; } return next; } function hydrateObjectStoreForm(session: InstallSessionResponse): ObjectStoreForm { const summary = session.object_store; if (!summary || summary.provider === "local") { return defaultObjectStoreForm( summary?.root ?? session.prefill.object_store_local_root, ); } return { provider: "s3", localRoot: session.prefill.object_store_local_root, bucket: summary.bucket ?? "", region: summary.region ?? "", credentialMode: summary.credential_mode === "access_key" ? "access_key" : "runtime", accessKeyId: "", secretAccessKey: "", manualCredentialsSaved: Boolean(summary.manual_credentials_saved), }; } function buildObjectStorePayload(form: ObjectStoreForm): InstallObjectStoreInput { if (form.provider === "local") { return { provider: "local", root: form.localRoot.trim() }; } const payload: InstallObjectStoreInput = { provider: "s3", bucket: form.bucket.trim(), region: form.region.trim(), credential_mode: form.credentialMode, }; const accessKeyId = form.accessKeyId.trim(); const secretAccessKey = form.secretAccessKey.trim(); if (form.credentialMode === "access_key") { if (accessKeyId) { payload.access_key_id = accessKeyId; } if (secretAccessKey) { payload.secret_access_key = secretAccessKey; } } return payload; } function defaultSandboxForm(): SandboxForm { return { provider: "docker", apiKey: "", apiKeySaved: false, allowLocal: true }; } function hydrateSandboxForm( current: SandboxForm, session: InstallSessionResponse, ): SandboxForm { const summary = session.sandbox; if (!summary) { return current.apiKey ? { ...current, apiKeySaved: false } : defaultSandboxForm(); } if (current.apiKey) { return { ...current, apiKeySaved: Boolean(summary.api_key_saved) }; } return { provider: summary.provider === "daytona" ? "daytona" : "docker", apiKey: "", apiKeySaved: Boolean(summary.api_key_saved), allowLocal: summary.allow_local ?? true, }; } function buildSandboxPayload(form: SandboxForm): InstallSandboxInput { if (form.provider === "docker") { return { provider: "docker", allow_local: form.allowLocal }; } const apiKey = form.apiKey.trim(); const payload: InstallSandboxInput = { provider: "daytona", allow_local: form.allowLocal, }; if (apiKey) { payload.api_key = apiKey; } return payload; } function focusInput(ref: { current: HTMLInputElement | null }): void { window.setTimeout(() => ref.current?.focus(), 0); } function describeProvider(id: string): string { const match = INSTALL_PROVIDERS.find((provider) => provider.id === id); return match?.label ?? id; } function describeLlmSummary(llm: InstallSessionResponse["llm"]): string { // `null` means the LLM step has not been completed. A present summary with // an empty providers list is an explicit skip. if (!llm) { return "Not configured"; } const providers = (llm.providers ?? []).map((provider) => describeProvider(provider.provider), ); return providers.length > 0 ? providers.join(", ") : "Skipped"; } function GithubSummaryRows({ github, serverUrl, }: { github: InstallSessionResponse["github"]; serverUrl: string; }) { if (!github) { return ; } if (github.strategy === "app") { return ( <> ); } return ( <> ); } function githubCallbackUrl(serverUrl: string): string { return `${serverUrl.replace(/\/+$/, "")}/auth/callback/github`; } function ObjectStoreSummaryRows({ objectStore, }: { objectStore: InstallSessionResponse["object_store"]; }) { if (!objectStore) { return ; } if (objectStore.provider === "local") { return ( <> ); } return ( <> ); } function SandboxSummaryRows({ sandbox, }: { sandbox: InstallSessionResponse["sandbox"]; }) { if (!sandbox) { return ; } if (sandbox.provider === "daytona") { return ( <> ); } return ; } function describeGithubAppOwner( owner: InstallGithubAppOwner | undefined, ): string { if (!owner || owner.kind === "personal") return "Personal account"; return owner.slug ? `@${owner.slug} (organization)` : "Organization"; } function submitGithubManifest( formAction: string, manifest: Record, state: string, ): void { const form = document.createElement("form"); form.method = "post"; form.action = formAction; form.style.display = "none"; const manifestInput = document.createElement("input"); manifestInput.type = "hidden"; manifestInput.name = "manifest"; manifestInput.value = JSON.stringify(manifest); form.appendChild(manifestInput); const stateInput = document.createElement("input"); stateInput.type = "hidden"; stateInput.name = "state"; stateInput.value = state; form.appendChild(stateInput); document.body.appendChild(form); form.submit(); }