UNPKG

@kya-os/cli

Version:

CLI for MCP-I setup and management

238 lines 9.3 kB
import { existsSync, readFileSync } from "fs"; import { join } from "path"; import { readAgentMetadata } from "./agent-metadata.js"; import { isValidSlug, keysDir } from "./paths.js"; import { CLAUDE_MD_START } from "./claude-config.js"; import { getGitConfigValue, getHooksDir, HOOK_MARKER, isGitRepo } from "./git-config.js"; import { isGhAuthenticated, isGhInstalled, listSigningKeys, signingKeyAlreadyUploaded, } from "./github.js"; import { convertEd25519ToSshKey } from "./ssh-key.js"; /** * Read a file for a diagnostic, returning null instead of throwing when the * path exists but cannot be read as a file (e.g. it is a directory -> EISDIR). * `mcpi dco status` must never crash on odd on-disk state. */ function readFileSafe(path) { try { return readFileSync(path, "utf-8"); } catch { return null; } } /** Read-only diagnostics backing `mcpi dco status`. Never writes or prompts. */ export async function collectDcoStatus(cwd) { const checks = []; const identityPath = join(cwd, ".mcpi", "identity.json"); let identity = null; let identityCorrupt = false; if (existsSync(identityPath)) { try { const parsed = JSON.parse(readFileSync(identityPath, "utf-8")); // A non-object (array, string, number) parses fine but is corrupt, not // a valid-but-missing-fields identity; match setup's preflight so status // does not tell the user to re-register over a recoverable file. if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) { identity = parsed; } else { identityCorrupt = true; } } catch { identity = null; identityCorrupt = true; } } // Setup's preflight requires BOTH privateKey and did, so an identity with a // key but no did is incomplete (warn), not ok — otherwise status reports a // healthy identity that setup would refuse to run against. const identityComplete = Boolean(identity?.privateKey && identity?.did); checks.push({ id: "identity", label: "Agent identity (.mcpi/identity.json)", state: identityComplete ? "ok" : identityCorrupt || identity?.privateKey ? "warn" : "missing", detail: identityCorrupt ? "File exists but could not be parsed; inspect or restore it (do not re-run register)" : identity?.privateKey && !identity?.did ? "Missing did; setup requires both a private key and a did" : identity?.did, }); const metadata = readAgentMetadata(cwd); // Mirror setup's trust gate: agent.json is only usable for attribution when // it is bound to this identity (its did matches identity.json). Unbound // metadata (no/mismatched did) is reported as warn, not ok, so status does // not present planted attribution as trusted while setup ignores it. const metadataBound = Boolean(metadata?.did && identity?.did && metadata.did === identity.did); const metadataComplete = Boolean(metadata?.name && metadata?.slug); checks.push({ id: "agent-metadata", label: "Agent metadata (.mcpi/agent.json)", state: metadataComplete && metadataBound ? "ok" : metadata ? "warn" : "missing", detail: !metadata ? "Run `mcpi register` or set AGENT_NAME/AGENT_SLUG" : !metadataBound ? `Not bound to this identity (${metadata.did ?? "no did"}); ignored by setup` : `${metadata.name} (${metadata.slug ?? "no slug"})`, }); // Only a bound record's slug locates the signing key; an unbound slug is // untrusted for that purpose. const boundSlug = metadataBound ? metadata?.slug : undefined; const rawSlug = boundSlug ?? process.env.AGENT_SLUG; const slug = rawSlug && isValidSlug(rawSlug) ? rawSlug : undefined; const privateKeyPath = slug ? join(keysDir(), slug) : null; checks.push({ id: "key-files", label: "SSH signing key files", state: rawSlug && !slug ? "warn" : privateKeyPath && existsSync(privateKeyPath) ? "ok" : "missing", detail: rawSlug && !slug ? `Invalid agent slug "${rawSlug}"` : (privateKeyPath ?? undefined), }); if (await isGitRepo(cwd)) { const format = await getGitConfigValue("gpg.format", cwd); const signingKey = await getGitConfigValue("user.signingkey", cwd); const gpgSign = await getGitConfigValue("commit.gpgsign", cwd); const configured = format === "ssh" && gpgSign === "true" && Boolean(signingKey); checks.push({ id: "git-config", label: "Git signing config", state: configured ? "ok" : "missing", detail: configured ? `user.signingkey=${signingKey}` : "gpg.format/user.signingkey/commit.gpgsign not fully set", }); const { dir } = await getHooksDir(cwd); const hookPath = join(dir, "prepare-commit-msg"); const hookContent = existsSync(hookPath) ? readFileSafe(hookPath) : null; if (!existsSync(hookPath)) { checks.push({ id: "trailer-hook", label: "Commit trailer hook", state: "missing", detail: hookPath, }); } else if (hookContent === null) { checks.push({ id: "trailer-hook", label: "Commit trailer hook", state: "warn", detail: `prepare-commit-msg exists but is not a readable file: ${hookPath}`, }); } else if (hookContent.includes(HOOK_MARKER)) { checks.push({ id: "trailer-hook", label: "Commit trailer hook", state: "ok", detail: hookPath, }); } else { checks.push({ id: "trailer-hook", label: "Commit trailer hook", state: "warn", detail: `Foreign prepare-commit-msg hook at ${hookPath}`, }); } } else { checks.push({ id: "git-config", label: "Git signing config", state: "missing", detail: "Not a git repository", }); } const claudeMd = existsSync(join(cwd, "CLAUDE.md")) ? readFileSafe(join(cwd, "CLAUDE.md")) : null; const hasBlock = claudeMd !== null && claudeMd.includes(CLAUDE_MD_START); checks.push({ id: "claude-md", label: "CLAUDE.md attribution block", state: hasBlock ? "ok" : "missing", }); const settingsPath = join(cwd, ".claude", "settings.json"); let settingsOk = false; let settingsCorrupt = false; if (existsSync(settingsPath)) { try { const parsed = JSON.parse(readFileSync(settingsPath, "utf-8")); if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) { settingsOk = parsed.includeCoAuthoredBy === false; } else { settingsCorrupt = true; } } catch { settingsCorrupt = true; } } checks.push({ id: "claude-settings", label: "Claude Code settings (includeCoAuthoredBy: false)", state: settingsOk ? "ok" : settingsCorrupt ? "warn" : "missing", detail: settingsCorrupt ? "settings.json exists but could not be parsed" : undefined, }); if (!(await isGhInstalled())) { checks.push({ id: "github-key", label: "Signing key on GitHub", state: "warn", detail: "GitHub CLI not installed", }); } else if (!(await isGhAuthenticated())) { checks.push({ id: "github-key", label: "Signing key on GitHub", state: "warn", detail: "gh not authenticated", }); } else if (identity?.privateKey && slug) { try { const material = convertEd25519ToSshKey(identity.privateKey, `mcpi:${slug}`, { expectedPublicKeyBase64: identity.publicKey, }); const uploaded = signingKeyAlreadyUploaded(await listSigningKeys(), material.publicKeyLine); checks.push({ id: "github-key", label: "Signing key on GitHub", state: uploaded ? "ok" : "missing", detail: material.fingerprint, }); } catch (error) { checks.push({ id: "github-key", label: "Signing key on GitHub", state: "warn", detail: error instanceof Error ? error.message : String(error), }); } } else { checks.push({ id: "github-key", label: "Signing key on GitHub", state: "warn", detail: "Needs identity and agent slug to check", }); } return { checks }; } //# sourceMappingURL=status.js.map