import { createRootRoute, Outlet, useRouterState } from "@tanstack/react-router";
import { useEffect, useState, type ReactNode } from "react";
import { ExecutorProvider } from "@executor-js/react/api/provider";
import { ExecutorPluginsProvider } from "@executor-js/sdk/client";
import { OrganizationProvider } from "@executor-js/react/api/organization-context";
import { OrgSlugGate } from "@executor-js/react/multiplayer/org-slug-gate";
import { Toaster } from "@executor-js/react/components/sonner";
import { Button } from "@executor-js/react/components/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@executor-js/react/components/card";
import { AuthProvider, useAuth } from "@executor-js/react/multiplayer/auth-context";
import { Shell, defaultShellNavItems } from "@executor-js/react/multiplayer/shell";
import { plugins as clientPlugins } from "virtual:executor/plugins-client";
import { authClient } from "../auth-client";
import { DevicePage } from "../chromeless/device-page";
import { McpConsentPage } from "../chromeless/mcp-consent-page";
import { LoginPage } from "../login";
import { SetupPage } from "../setup";
import { fetchNeedsSetup } from "../setup-status";
// ---------------------------------------------------------------------------
// Self-host root: the SHARED multiplayer composition with Better Auth as the
// provider. Same shell, pages, and account surface as cloud — the only
// self-host specifics are the login form (email/password) and sign-out (Better
// Auth), injected here. No billing, Sentry, or PostHog.
// ---------------------------------------------------------------------------
export const Route = createRootRoute({
notFoundComponent: NotFoundPage,
component: RootComponent,
});
// Self-host adds the account's API keys and the instance Admin page (members +
// invite links) to the shared nav. The Admin page and its API gate to
// owner/admin, so a non-admin who opens it just sees the access notice.
const selfHostNavItems = [
...defaultShellNavItems,
{ to: "/api-keys", label: "API keys" },
{ to: "/admin", label: "Admin" },
];
const signOut = async () => {
await authClient.signOut();
window.location.href = "/";
};
function NotFoundPage() {
return (
Can't reach the server
Executor couldn't check this instance's setup state. Make sure the server is running, then
retry.
);
function AuthGate({ children }: { children: ReactNode }) {
const auth = useAuth();
// When unauthenticated, decide between first-run setup and sign-in by asking
// the server whether the instance still has zero members.
const [setupStatus, setSetupStatus] = useState<
| { state: "checking"; attempt: number }
| { state: "ready"; needsSetup: boolean }
| { state: "error"; attempt: number }
>({ state: "checking", attempt: 0 });
useEffect(() => {
if (auth.status !== "unauthenticated") return;
let alive = true;
setSetupStatus((current) => ({ state: "checking", attempt: current.attempt }));
void fetchNeedsSetup().then(
(value) => {
if (alive) setSetupStatus({ state: "ready", needsSetup: value });
},
() => {
if (alive) {
setSetupStatus((current) => ({
state: "error",
attempt: current.state === "ready" ? 0 : current.attempt,
}));
}
},
);
return () => {
alive = false;
};
}, [auth.status, setupStatus.attempt]);
if (auth.status === "loading") return ;
if (auth.status === "unauthenticated") {
if (setupStatus.state === "checking") return ;
if (setupStatus.state === "error") {
return (
setSetupStatus((current) => ({
state: "checking",
attempt: current.state === "ready" ? 0 : current.attempt + 1,
}))
}
/>
);
}
return setupStatus.needsSetup ? : ;
}
return <>{children}>;
}
function AuthenticatedApp() {
const auth = useAuth();
const organization = auth.status === "authenticated" ? (auth.organization ?? null) : null;
// Single-org instance: a bare URL canonicalizes onto the instance org's
// slug. There's only ever one org, so no other slug is reachable.
const gated = (
<>
>
);
return (
{/* No organizationSlug: the self-host MCP endpoint is the bare /mcp —
a slug-pinned URL would 404, and a single-org instance has nothing
to select anyway. */}
{organization ? {gated} : gated}
);
}
function RootComponent() {
const pathname = useRouterState({ select: (s) => s.location.pathname });
// The join page is public + chromeless: a new user redeeming an invite link
// has no session yet, so it renders outside the auth gate and the shell.
if (pathname.startsWith("/join/")) {
return (
<>
>
);
}
// The MCP OAuth approval screen: chromeless (no shell) but inside the auth
// gate — the user is already signed in (Better Auth's authorize requires a
// session before redirecting here). Rendered directly (static import), not as
// a lazy route, so it can't hit Vite's dynamic-import dep flake.
if (pathname === "/mcp-consent") {
return (
);
}
// The CLI device-login verification page: chromeless, inside the auth gate
// (the user signs in here if needed, then authorizes the code). Same
// static-import + pathname-branch convention as /mcp-consent.
if (pathname === "/device") {
return (
);
}
return (
);
}