* Connect GitHub from a token alone, without a vendor account Integrations only reached GitHub through Composio or Pipedream, which a self-hosted instance does not have. A built-in GitHub row now opens GitHub's prefilled token form, and the pasted token connects GitHub's own MCP server through the existing installed-connector path. Web and mobile. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tgvrj34NMnaqvtgvruYQZZ * Sync web catalogs for the GitHub connect strings Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tgvrj34NMnaqvtgvruYQZZ --------- Co-authored-by: Claude <noreply@anthropic.com>
131 lines
4.5 KiB
TypeScript
131 lines
4.5 KiB
TypeScript
import { Trans, useLingui } from "@lingui/react/macro";
|
|
import type { CapabilityInstall } from "@rakazo/contracts";
|
|
import { GITHUB_MCP_SOURCE, GITHUB_TOKEN_URL, isGithubInstall } from "@rakazo/core";
|
|
import { Button, buttonVariants, cn, Input } from "@rakazo/ui-web";
|
|
import { useState } from "react";
|
|
import { rpc } from "../../lib/rpc";
|
|
|
|
/** GitHub's mark, monochrome like the rest of the app. */
|
|
function GithubMark({ className }: { className?: string }) {
|
|
return (
|
|
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" className={className}>
|
|
<path d="M12 .3a12 12 0 0 0-3.8 23.4c.6.1.8-.3.8-.6v-2.2c-3.3.7-4-1.4-4-1.4-.6-1.4-1.4-1.8-1.4-1.8-1-.7.1-.7.1-.7 1.2.1 1.8 1.2 1.8 1.2 1 1.8 2.8 1.3 3.5 1 0-.8.4-1.3.7-1.6-2.7-.3-5.5-1.3-5.5-6 0-1.2.5-2.3 1.3-3.1-.2-.4-.6-1.6 0-3.2 0 0 1-.3 3.4 1.2a11.5 11.5 0 0 1 6 0c2.3-1.5 3.3-1.2 3.3-1.2.6 1.6.2 2.8.1 3.2.8.8 1.2 1.9 1.2 3.1 0 4.6-2.8 5.6-5.5 5.9.5.4.9 1.1.9 2.2v3.3c0 .3.1.7.8.6A12 12 0 0 0 12 .3" />
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* GitHub without a vendor account or an app registration: a token from GitHub's prefilled form
|
|
* connects GitHub's own MCP server for every bot.
|
|
*/
|
|
export function GithubConnect({
|
|
sources,
|
|
onChanged,
|
|
}: {
|
|
sources: CapabilityInstall[];
|
|
onChanged: () => Promise<unknown>;
|
|
}) {
|
|
const { t } = useLingui();
|
|
const installed = sources.find(isGithubInstall);
|
|
const [open, setOpen] = useState(false);
|
|
const [token, setToken] = useState("");
|
|
const [pending, setPending] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
async function run(task: () => Promise<unknown>) {
|
|
setPending(true);
|
|
setError(null);
|
|
try {
|
|
await task();
|
|
await onChanged();
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : t`Could not connect GitHub`);
|
|
} finally {
|
|
setPending(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div data-testid="github-connect" className="mb-6 rounded-xl border border-border px-3 py-2.5">
|
|
<div className="flex items-center gap-3">
|
|
<div className="grid size-9 shrink-0 place-items-center rounded-xl bg-accent text-foreground">
|
|
<GithubMark className="size-5" />
|
|
</div>
|
|
<div className="min-w-0 flex-1 truncate text-ui-body font-medium text-foreground">
|
|
GitHub
|
|
</div>
|
|
{installed ? (
|
|
<>
|
|
<span className="text-ui-body-sm text-muted-foreground">
|
|
<Trans>Connected</Trans>
|
|
</span>
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="sm"
|
|
className="rounded-full"
|
|
disabled={pending}
|
|
onClick={() => void run(() => rpc.capabilities.remove({ id: installed.id }))}
|
|
>
|
|
<Trans>Disconnect</Trans>
|
|
</Button>
|
|
</>
|
|
) : open ? null : (
|
|
<Button
|
|
type="button"
|
|
variant="secondary"
|
|
size="sm"
|
|
className="rounded-full"
|
|
onClick={() => setOpen(true)}
|
|
>
|
|
<Trans>Connect</Trans>
|
|
</Button>
|
|
)}
|
|
</div>
|
|
{!installed && open ? (
|
|
<form
|
|
className="mt-3 flex flex-col gap-2"
|
|
onSubmit={(event) => {
|
|
event.preventDefault();
|
|
void run(async () => {
|
|
await rpc.capabilities.install({
|
|
kind: "mcp",
|
|
name: "GitHub",
|
|
source: GITHUB_MCP_SOURCE,
|
|
credential: token.trim(),
|
|
config: { preset: "github" },
|
|
});
|
|
setToken("");
|
|
setOpen(false);
|
|
});
|
|
}}
|
|
>
|
|
<a
|
|
href={GITHUB_TOKEN_URL}
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
className={cn(buttonVariants({ variant: "outline" }), "rounded-full")}
|
|
>
|
|
<Trans>Create token on GitHub</Trans>
|
|
</a>
|
|
<div className="flex gap-2">
|
|
<Input
|
|
type="password"
|
|
autoComplete="off"
|
|
value={token}
|
|
onChange={(event) => setToken(event.target.value)}
|
|
aria-label={t`GitHub token`}
|
|
placeholder="ghp_…"
|
|
className="h-10 flex-1 rounded-full px-4"
|
|
/>
|
|
<Button type="submit" className="rounded-full" disabled={pending || !token.trim()}>
|
|
{pending ? <Trans>Connecting…</Trans> : <Trans>Connect</Trans>}
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
) : null}
|
|
{error ? <p className="mt-2 text-ui-body-sm text-destructive">{error}</p> : null}
|
|
</div>
|
|
);
|
|
}
|