UNPKG

@kya-os/cli

Version:

CLI for MCP-I setup and management

154 lines 5 kB
import { execFile } from "child_process"; import { promisify } from "util"; const execFileAsync = promisify(execFile); /** * Thin wrapper around the GitHub CLI (`gh`). We deliberately shell out to gh * instead of handling OAuth tokens ourselves: the user's existing gh auth is * the credential, and it never passes through this process beyond gh's own * environment. */ export const SIGNING_KEY_SCOPE = "admin:ssh_signing_key"; export const SCOPE_REFRESH_COMMAND = `gh auth refresh -h github.com -s ${SIGNING_KEY_SCOPE}`; export class GhNotInstalledError extends Error { constructor() { super("GitHub CLI (gh) is not installed. Install it from https://cli.github.com and run `gh auth login`."); this.name = "GhNotInstalledError"; } } /** * The default gh token does not include the admin:ssh_signing_key scope, so * both listing and uploading signing keys fail until the user refreshes. * GitHub reports the missing scope as HTTP 404 on these endpoints. */ export class GhScopeError extends Error { constructor() { super(`Your GitHub token is missing the ${SIGNING_KEY_SCOPE} scope. Run: ${SCOPE_REFRESH_COMMAND}`); this.refreshCommand = SCOPE_REFRESH_COMMAND; this.name = "GhScopeError"; } } async function gh(args) { try { const { stdout } = await execFileAsync("gh", args, { encoding: "utf8", maxBuffer: 10 * 1024 * 1024, }); return stdout; } catch (error) { if (error?.code === "ENOENT") { throw new GhNotInstalledError(); } throw error; } } function isScopeError(error) { const err = error; const message = `${err?.stderr ?? ""} ${err?.message ?? ""}`; // Deliberately narrow: never key off the bare 403 status, which GitHub // also uses for rate limits and SSO enforcement where scope-refresh advice // would be wrong. Only the explicit scope name, HTTP 404, or a "Resource // not accessible" body count as scope problems. return (message.includes(SIGNING_KEY_SCOPE) || /HTTP 404/.test(message) || /Resource not accessible/i.test(message)); } export async function isGhInstalled() { try { await gh(["--version"]); return true; } catch { return false; } } export async function isGhAuthenticated() { try { await gh(["auth", "status", "--hostname", "github.com"]); return true; } catch (error) { if (error instanceof GhNotInstalledError) { throw error; } return false; } } export async function getGhUser() { const stdout = await gh([ "api", "user", "--jq", "{login: .login, id: .id, name: .name}", ]); const parsed = JSON.parse(stdout); if (!parsed?.login || typeof parsed.id !== "number") { throw new Error("Unexpected response from `gh api user`"); } return parsed; } /** GitHub's commit-attribution noreply address for the authenticated user. */ export function githubNoreplyEmail(user) { return `${user.id}+${user.login}@users.noreply.github.com`; } export async function listSigningKeys() { try { const stdout = await gh(["api", "user/ssh_signing_keys", "--paginate"]); return JSON.parse(stdout); } catch (error) { if (error instanceof GhNotInstalledError) { throw error; } if (isScopeError(error)) { throw new GhScopeError(); } throw error; } } export async function uploadSigningKey(publicKeyLine, title) { try { await gh([ "api", "user/ssh_signing_keys", "-f", `key=${publicKeyLine}`, "-f", `title=${title}`, ]); } catch (error) { if (error instanceof GhNotInstalledError) { throw error; } if (isScopeError(error)) { throw new GhScopeError(); } throw error; } } /** * Compare the base64 body of an authorized_keys style line against uploaded * keys. GitHub returns keys without comments, so comparing the full line * would always miss. */ export function signingKeyAlreadyUploaded(keys, publicKeyLine) { const body = publicKeyLine.split(/\s+/)[1]; if (!body) { return false; } return keys.some((entry) => entry.key.split(/\s+/)[1] === body); } /** * Run `gh auth refresh` interactively so the user can grant the signing key * scope without leaving the flow. Requires a TTY; the caller must check. */ export async function refreshAuthScope() { const { spawn } = await import("child_process"); return new Promise((resolve) => { const child = spawn("gh", ["auth", "refresh", "-h", "github.com", "-s", SIGNING_KEY_SCOPE], { stdio: "inherit" }); child.on("error", () => resolve(false)); child.on("close", (code) => resolve(code === 0)); }); } //# sourceMappingURL=github.js.map