Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
id: 01M5JRQPYQBFM1R674ESDP4DR9
anchor: function Checks
created: 2026-10-22T21:37:06Z
norm: '1'
sig: afce47395c279e36
body_hash: e0c2b3947f605c0a
raw_hash: 9875d4d01a08d2a7
lines: 611-641
---

hello's GitHub login can arrive before gh answers and isn't resent, so the setup check reads gh's sign-in from diagnostics
6 changes: 5 additions & 1 deletion apps/app/src/host-settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,11 @@ export function HostStatus({ active }: { active: boolean }) {
description={tool.path}
footer={tool.error && <Problem>{tool.error}</Problem>}
>
{(tool.version || !tool.path) && <Value>{tool.version || "Not found"}</Value>}
{(tool.version || tool.account || !tool.path) && (
<Value>
{tool.version || (tool.account ? `Signed in as ${tool.account}` : "Not found")}
</Value>
)}
</Row>
))}
</>
Expand Down
21 changes: 15 additions & 6 deletions apps/app/src/team-setup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -611,6 +611,9 @@ function Join({ initial, deployed, onJoined, back }: {
export function Checks({ github }: { github?: string }) {
const { value } = useDiagnostics(true);
const tailscale = value?.tailscale;
const cli = value?.tools.find((tool) => tool.name === "GitHub CLI");
// hello's login can arrive before gh answers and isn't resent, so a fresh host's check reads gh here.
const login = cli?.account || github;
return (
<div className="flex flex-wrap gap-x-4 gap-y-1 text-xs text-muted-foreground">
<Check ok={tailscale?.state === "running"}>
Expand All @@ -620,12 +623,18 @@ export function Checks({ github }: { github?: string }) {
? "isn't connected"
: "…"}
</Check>
<Check ok={!!github}>
{github ? `GitHub · ${github}` : (
<>
GitHub: run <code>gh auth login</code>
</>
)}
<Check ok={!!login}>
{login ? `GitHub · ${login}` : cli && !cli.path
? (
<>
GitHub: install GitHub CLI, then run <code>gh auth login</code>
</>
)
: (
<>
GitHub: run <code>gh auth login</code>
</>
)}
</Check>
</div>
);
Expand Down
29 changes: 28 additions & 1 deletion apps/host/src/diagnostics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { config } from "./config";
import * as directory from "./directory";
import * as served from "./directory-server";
import { shell as environment } from "./environment";
import * as github from "./github";
import { latest } from "./keys";
import type { Diagnostics } from "./protocol";
import * as tailnet from "./tailnet";
Expand Down Expand Up @@ -41,6 +42,32 @@ async function shell(): Promise<Diagnostics["tools"][number]> {
}
}

async function gh(): Promise<Diagnostics["tools"][number]> {
const name = "GitHub CLI";
const path = Bun.which("gh", { PATH: process.env.PATH }) || undefined;
if (!path) {
return { name, error: "Install GitHub CLI, then sign in with gh auth login, to use GitHub." };
}
const child = Bun.spawn([path, "auth", "status", "--hostname", "github.com", "--json", "hosts"], {
env: github.environment(),
stdin: "ignore",
stdout: "pipe",
stderr: "ignore",
timeout: 5000,
});
const [out, code] = await Promise.all([new Response(child.stdout).text(), child.exited]);
if (code !== 0) return { name, path, error: "GitHub CLI could not start." };
const { hosts } = JSON.parse(out) as {
hosts: Record<string, { state: string; active: boolean; login: string }[]>;
};
const account = hosts["github.com"]?.find((account) => account.active);
if (!account) return { name, path, error: "Sign in with gh auth login to use GitHub." };
if (account.state !== "success" || !account.login) {
return { name, path, error: "GitHub didn't accept this sign-in. Run gh auth login again." };
}
return { name, path, account: account.login };
}

export async function diagnostics(sync?: boolean): Promise<Diagnostics> {
const secret = latest("ACE_SECRET", sync);
if (sync) {
Expand All @@ -49,7 +76,7 @@ export async function diagnostics(sync?: boolean): Promise<Diagnostics> {
if (!(await secret).waiting) await Promise.race([synced, Bun.sleep(25_000)]);
}
const [tools, tailscale] = await Promise.all([
Promise.all([git(), shell()]),
Promise.all([git(), gh(), shell()]),
tailnet.diagnostics(started),
]);
const { hosted, app, compute, hosts } = directory.read();
Expand Down
13 changes: 9 additions & 4 deletions apps/host/src/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,9 +147,8 @@ function sentence(error: string): string {
return error.replace(/^GraphQL: /, "GitHub refused the request: ");
}

async function gh<T>(args: string[]): Promise<T> {
const path = Bun.which("gh", { PATH: process.env.PATH });
if (!path) throw new Error("Install GitHub CLI on this host, then sign in with gh auth login.");
/** The environment gh runs in: this host's GitHub token, github.com, and no prompts. */
export function environment(): NodeJS.ProcessEnv {
const env: NodeJS.ProcessEnv = {
...process.env,
...githubEnv(),
Expand All @@ -158,8 +157,14 @@ async function gh<T>(args: string[]): Promise<T> {
GH_PAGER: "cat",
};
delete env.GH_REPO;
return env;
}

async function gh<T>(args: string[]): Promise<T> {
const path = Bun.which("gh", { PATH: process.env.PATH });
if (!path) throw new Error("Install GitHub CLI on this host, then sign in with gh auth login.");
const child = Bun.spawn([path, ...args], {
env,
env: environment(),
stdin: "ignore",
stdout: "pipe",
stderr: "pipe",
Expand Down
2 changes: 1 addition & 1 deletion apps/host/src/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export type WorkerInfo = { id: string; pid: number };
export type WorkerRequest = { op: "worker" } | { op: "freeze" } | { op: "export" };

export type Diagnostics = {
tools: { name: string; path?: string; version?: string; error?: string }[];
tools: { name: string; path?: string; version?: string; account?: string; error?: string }[];
tailscale: {
state: "missing" | "offline" | "running" | "error";
name?: string;
Expand Down