UNPKG

@kya-os/cli

Version:

CLI for MCP-I setup and management

530 lines • 20.4 kB
import chalk from "chalk"; import ora from "ora"; import { existsSync, readFileSync } from "fs"; import { join } from "path"; import { exec } from "child_process"; import { promisify } from "util"; import { EnvManager } from "../utils/env-manager.js"; import { detectPlatform } from "../utils/platform-detector.js"; import { ktaApi, isKTAAvailable } from "../utils/kta-api.js"; import { showSuccess, showError, showWarning, } from "../utils/prompts.js"; const execAsync = promisify(exec); /** * Comprehensive system health check for XMCP-I * Implements Requirements 16.2, 16.3, 16.4, 16.5, 16.6 */ export async function doctor(options = {}) { const { json = false, verbose = false } = options; if (!json) { console.log(chalk.cyan("\n🩺 XMCP-I Doctor - System Health Check\n")); } const spinner = json ? null : ora("Running diagnostics...").start(); try { // Run all diagnostic checks const [packages, xmcpUpstream, environment, kta, cache] = await Promise.all([ checkPackageVersions(verbose), checkXMCPUpstreamCompatibility(verbose), checkEnvironmentConfiguration(verbose), checkKTAConnectivity(verbose), checkNonceCacheConfiguration(verbose), ]); if (spinner) { spinner.succeed("Diagnostics complete"); } const result = { packages, xmcpUpstream, environment, kta, cache, }; if (json) { const output = { success: true, result, schemaId: "https://schemas.kya-os.ai/mcpi/doctor-result/v1.0.0.json", timestamp: new Date().toISOString(), }; console.log(JSON.stringify(output, null, 2)); } else { displayDoctorResults(result, verbose); } // Determine exit code based on critical issues const hasCriticalIssues = !environment.valid || packages.some((p) => !p.compatible) || !xmcpUpstream.compatible; if (hasCriticalIssues && process.env.NODE_ENV !== "test") { process.exit(1); } } catch (error) { if (spinner) { spinner.fail("Diagnostics failed"); } const errorMessage = error.message || "Failed to run diagnostics"; if (json) { console.log(JSON.stringify({ success: false, error: errorMessage, code: "XMCP_I_EDOCTOR", }, null, 2)); } else { showError(`Doctor check failed: ${errorMessage}`); } if (process.env.NODE_ENV !== "test") { process.exit(1); } } } /** * Check all package versions for compatibility (R16.2) */ async function checkPackageVersions(verbose) { const packages = []; const packageJsonPath = join(process.cwd(), "package.json"); if (!existsSync(packageJsonPath)) { return [ { name: "package.json", version: "not found", compatible: false, issues: ["No package.json found in current directory"], }, ]; } try { const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8")); const allDeps = { ...packageJson.dependencies, ...packageJson.devDependencies, ...packageJson.peerDependencies, }; // Check XMCP-I related packages const xmcpPackages = [ "mcpi", "@kya-os/cli", "@kya-os/contracts", "@kya-os/create-mcpi-app", "create-mcpi-app", "@kya-os/mcp-i", // deprecated ]; for (const pkgName of xmcpPackages) { const version = allDeps[pkgName]; if (version) { const compatible = await checkPackageCompatibility(pkgName, version, verbose); packages.push({ name: pkgName, version, compatible: compatible.compatible, issues: compatible.issues, }); } } // Check for deprecated packages if (allDeps["@kya-os/mcp-i"]) { const mcpIPackage = packages.find((p) => p.name === "@kya-os/mcp-i"); if (mcpIPackage) { mcpIPackage.issues = mcpIPackage.issues || []; mcpIPackage.issues.push("This package is deprecated. Migrate to 'mcpi'"); } } } catch (error) { packages.push({ name: "package.json", version: "error", compatible: false, issues: [`Failed to parse package.json: ${error.message}`], }); } return packages; } /** * Check individual package compatibility */ async function checkPackageCompatibility(name, version, verbose) { const issues = []; let compatible = true; try { // Check if package is installed const { stdout } = await execAsync(`npm list ${name} --depth=0 --json`, { cwd: process.cwd(), }); const listResult = JSON.parse(stdout); const installedVersion = listResult.dependencies?.[name]?.version; if (!installedVersion) { compatible = false; issues.push(`Package ${name} is not installed`); return { compatible, issues }; } // Version compatibility checks if (name === "mcpi" || name === "@kya-os/cli" || name === "@kya-os/contracts") { // These should be on compatible versions (1.x.x) if (!installedVersion.startsWith("1.")) { compatible = false; issues.push(`Expected version 1.x.x, found ${installedVersion}`); } } if (verbose) { issues.push(`Installed version: ${installedVersion}`); } } catch (error) { if (verbose) { issues.push(`Could not verify installation: ${error.message}`); } } return { compatible, issues }; } /** * Verify XMCP upstream version compatibility (R16.3) */ async function checkXMCPUpstreamCompatibility(verbose) { const issues = []; let compatible = true; let version = "unknown"; try { // Check if XMCP is installed const { stdout } = await execAsync("npm list xmcp --depth=0 --json", { cwd: process.cwd(), }); const listResult = JSON.parse(stdout); const installedVersion = listResult.dependencies?.xmcp?.version; if (installedVersion) { version = installedVersion; // Check compatibility with known versions // Based on design doc: default caret ^0.3.1 const majorVersion = installedVersion.split(".")[0]; const minorVersion = installedVersion.split(".")[1]; if (majorVersion !== "0" || parseInt(minorVersion) < 3) { compatible = false; issues.push(`XMCP version ${installedVersion} may not be compatible. Expected ^0.3.1`); } if (verbose) { issues.push(`Found XMCP version: ${installedVersion}`); } } else { compatible = false; issues.push("XMCP package not found. This is required for XMCP-I runtime"); } // Check for latest version availability if (verbose) { try { const { stdout: viewOutput } = await execAsync("npm view xmcp version"); const latestVersion = viewOutput.trim(); if (latestVersion !== installedVersion) { issues.push(`Latest available version: ${latestVersion}`); } } catch (error) { if (verbose) { issues.push("Could not check latest version"); } } } } catch (error) { compatible = false; version = "not found"; issues.push("XMCP package not installed"); if (verbose) { issues.push(`Error: ${error.message}`); } } return { version, compatible, issues }; } /** * Validate platform environment variables and identity configuration (R16.4) */ async function checkEnvironmentConfiguration(verbose) { const envManager = new EnvManager(); const issues = []; // Check process environment const processVars = envManager.getFromProcess(); const { valid, missing } = envManager.validateVariables(processVars); // Check environment files const envFiles = envManager.scanEnvFiles(); const hasValidEnvFile = envFiles.some((f) => f.hasmcpidentity && f.exists); // Platform-specific checks const platform = detectPlatform(); if (verbose) { issues.push(`Detected platform: ${platform.platform}`); issues.push(`Package manager: ${platform.packageManager}`); } // Environment validation if (!valid && !hasValidEnvFile) { issues.push("No valid environment configuration found"); issues.push("Run 'mcpi init' to set up identity"); } if (missing.length > 0) { issues.push(`Missing environment variables: ${missing.join(", ")}`); } // Platform-specific environment checks if (platform.platform === "vercel" && process.env.VERCEL) { if (!valid) { issues.push("Running on Vercel but environment variables not configured"); issues.push("Add variables to Vercel Dashboard → Settings → Environment Variables"); } } // Check .gitignore for security const sensitiveFiles = [".env", ".env.local", ".mcpi/identity.json"]; const { missing: notIgnored } = envManager.checkGitignore(sensitiveFiles); if (notIgnored.length > 0) { issues.push(`Sensitive files not in .gitignore: ${notIgnored.join(", ")}`); } // Check identity file in development const identityPath = join(process.cwd(), ".mcpi", "identity.json"); if (existsSync(identityPath)) { if (verbose) { issues.push("Development identity file found"); } try { const identity = JSON.parse(readFileSync(identityPath, "utf-8")); if (!identity.did || !identity.kid || !identity.privateKey) { issues.push("Identity file is incomplete or corrupted"); } } catch (error) { issues.push("Identity file is corrupted or unreadable"); } } return { valid: valid || hasValidEnvFile, missing, issues, }; } /** * Test KTA connectivity and authentication (R16.5) */ async function checkKTAConnectivity(verbose) { const issues = []; let reachable = false; let authenticated = false; try { // Test basic connectivity reachable = await isKTAAvailable(); if (reachable) { if (verbose) { issues.push("KTA endpoint is reachable"); } // Test authentication if we have an API key const apiKey = process.env.KYA_VOUCHED_API_KEY || process.env.VOUCHED_API_KEY; if (apiKey) { try { // Try to make an authenticated request const envManager = new EnvManager(); const processVars = envManager.getFromProcess(); if (processVars.AGENT_DID) { await ktaApi.getAgentStatus(processVars.AGENT_DID); authenticated = true; if (verbose) { issues.push("KTA authentication successful"); } } else { issues.push("Cannot test authentication without AGENT_DID"); } } catch (error) { if (error.message.includes("404")) { // Agent not found is OK for auth test authenticated = true; if (verbose) { issues.push("KTA authentication successful (agent not registered)"); } } else if (error.message.includes("401") || error.message.includes("403")) { issues.push("KTA authentication failed - check API key"); } else { issues.push(`KTA authentication test failed: ${error.message}`); } } } else { issues.push("No KTA API key found - cannot test authentication"); } } else { issues.push("KTA endpoint is not reachable"); issues.push("Check internet connection and firewall settings"); } } catch (error) { issues.push(`KTA connectivity check failed: ${error.message}`); } return { reachable, authenticated, issues }; } /** * Check nonce cache configuration (R16.5) */ async function checkNonceCacheConfiguration(verbose) { const issues = []; let type = "memory"; let functional = true; try { // Detect cache type based on environment if (process.env.REDIS_URL) { type = "redis"; // Test Redis connectivity if available try { // This is a basic check - in a real implementation we'd test the actual connection if (verbose) { issues.push("Redis URL detected"); } } catch (error) { functional = false; issues.push("Redis connection failed"); } } else if (process.env.AWS_REGION) { type = "dynamodb"; if (verbose) { issues.push("AWS region detected - will use DynamoDB cache"); } } else if (typeof globalThis !== "undefined" && "caches" in globalThis) { type = "cloudflare-kv"; if (verbose) { issues.push("Cloudflare Workers environment detected"); } } else { type = "memory"; // Memory cache warnings for production if (process.env.NODE_ENV === "production") { issues.push("Using memory cache in production - not suitable for multi-instance deployments"); issues.push("Consider configuring Redis, DynamoDB, or Cloudflare KV"); } if (verbose) { issues.push("Using in-memory cache (suitable for single-instance only)"); } } // Platform-specific cache recommendations const platform = detectPlatform(); if (platform.platform === "vercel" && type === "memory") { issues.push("Vercel deployment detected - consider using external cache for multi-region"); } if (platform.platform === "lambda" && type === "memory") { issues.push("Lambda deployment detected - consider DynamoDB for persistence"); } } catch (error) { functional = false; issues.push(`Cache configuration check failed: ${error.message}`); } return { type, functional, issues }; } /** * Display doctor results in human-readable format */ function displayDoctorResults(result, verbose) { console.log(chalk.bold("šŸ“¦ Package Compatibility:")); if (result.packages.length === 0) { console.log(` ${chalk.yellow("āš ļø No XMCP-I packages found")}`); } else { for (const pkg of result.packages) { const status = pkg.compatible ? chalk.green("āœ“") : chalk.red("āœ—"); console.log(` ${status} ${pkg.name}@${pkg.version}`); if (pkg.issues && (verbose || !pkg.compatible)) { pkg.issues.forEach((issue) => { const color = pkg.compatible ? chalk.gray : chalk.red; console.log(` ${color(`• ${issue}`)}`); }); } } } console.log(chalk.bold("\nšŸ”— XMCP Upstream:")); const xmcpStatus = result.xmcpUpstream.compatible ? chalk.green("āœ“") : chalk.red("āœ—"); console.log(` ${xmcpStatus} XMCP ${result.xmcpUpstream.version}`); if (result.xmcpUpstream.issues && (verbose || !result.xmcpUpstream.compatible)) { result.xmcpUpstream.issues.forEach((issue) => { const color = result.xmcpUpstream.compatible ? chalk.gray : chalk.red; console.log(` ${color(`• ${issue}`)}`); }); } console.log(chalk.bold("\nšŸ” Environment Configuration:")); const envStatus = result.environment.valid ? chalk.green("āœ“") : chalk.red("āœ—"); console.log(` ${envStatus} Environment variables`); if (result.environment.missing.length > 0) { console.log(` ${chalk.red(`• Missing: ${result.environment.missing.join(", ")}`)}`); } if (result.environment.issues && (verbose || !result.environment.valid)) { result.environment.issues.forEach((issue) => { const color = result.environment.valid ? chalk.gray : chalk.yellow; console.log(` ${color(`• ${issue}`)}`); }); } console.log(chalk.bold("\n🌐 KTA Connectivity:")); const ktaStatus = result.kta.reachable ? chalk.green("āœ“") : chalk.red("āœ—"); const authStatus = result.kta.authenticated ? chalk.green("authenticated") : chalk.yellow("not authenticated"); console.log(` ${ktaStatus} KTA reachable (${authStatus})`); if (result.kta.issues && (verbose || !result.kta.reachable)) { result.kta.issues.forEach((issue) => { const color = result.kta.reachable ? chalk.gray : chalk.red; console.log(` ${color(`• ${issue}`)}`); }); } console.log(chalk.bold("\nšŸ’¾ Nonce Cache:")); const cacheStatus = result.cache.functional ? chalk.green("āœ“") : chalk.red("āœ—"); console.log(` ${cacheStatus} ${result.cache.type} cache`); if (result.cache.issues && (verbose || !result.cache.functional)) { result.cache.issues.forEach((issue) => { const color = result.cache.functional ? chalk.gray : chalk.yellow; console.log(` ${color(`• ${issue}`)}`); }); } // Overall summary const hasIssues = !result.environment.valid || result.packages.some((p) => !p.compatible) || !result.xmcpUpstream.compatible || !result.kta.reachable; console.log(chalk.bold("\nšŸ“Š Summary:")); if (!hasIssues) { showSuccess("All systems operational! šŸŽ‰"); } else { showWarning("Issues detected that may affect functionality"); console.log(chalk.bold("\nšŸ’” Recommended Actions:")); if (!result.environment.valid) { console.log(` • Run ${chalk.cyan("mcpi init")} to set up identity`); } if (result.packages.some((p) => !p.compatible)) { console.log(` • Update incompatible packages to latest versions`); } if (!result.xmcpUpstream.compatible) { console.log(` • Update XMCP to version ^0.3.1 or later`); } if (!result.kta.reachable) { console.log(` • Check internet connection and firewall settings`); } if (result.cache.type === "memory" && process.env.NODE_ENV === "production") { console.log(` • Configure external cache (Redis/DynamoDB/KV) for production`); } } console.log(chalk.bold("\nšŸ”§ Additional Commands:")); console.log(` ${chalk.cyan("mcpi status")} - Check agent registration status`); console.log(` ${chalk.cyan("mcpi env verify")} - Verify environment configuration`); console.log(` ${chalk.cyan("mcpi keys rotate")} - Rotate identity keys`); console.log(` ${chalk.cyan("mcpi --help")} - Show all available commands`); } //# sourceMappingURL=doctor.js.map