diff --git a/src/agents/registry.ts b/src/agents/registry.ts index 5345d634..75fd82b8 100644 --- a/src/agents/registry.ts +++ b/src/agents/registry.ts @@ -65,13 +65,16 @@ export class AgentRegistry { static async getInstalledAgents(): Promise { AgentRegistry.initialize(); - const agents: AgentAdapter[] = []; - for (const adapter of AgentRegistry.adapters.values()) { - if (await adapter.isInstalled()) { - agents.push(adapter); - } - } - return agents; + const allAdapters = Array.from(AgentRegistry.adapters.values()); + const installResults = await Promise.all( + allAdapters.map(async (adapter) => ({ + adapter, + installed: await adapter.isInstalled() + })) + ); + return installResults + .filter(({ installed }) => installed) + .map(({ adapter }) => adapter); } /** diff --git a/src/cli/commands/doctor/checks/AgentsCheck.ts b/src/cli/commands/doctor/checks/AgentsCheck.ts index c3aab320..e396ba96 100644 --- a/src/cli/commands/doctor/checks/AgentsCheck.ts +++ b/src/cli/commands/doctor/checks/AgentsCheck.ts @@ -36,12 +36,17 @@ export class AgentsCheck implements ItemWiseHealthCheck { const installedAgents = await AgentRegistry.getInstalledAgents(); if (installedAgents.length > 0) { - for (const agent of installedAgents) { - const version = await agent.getVersion(); - const versionStr = version ? ` (${version})` : ''; + // Parallelize version + installation method checks across all agents + const agentResults = await Promise.all( + installedAgents.map(async (agent) => { + const version = await agent.getVersion(); + const versionStr = version ? ` (${version})` : ''; + const deprecationWarning = await this.checkDeprecatedInstallation(agent, versionStr); + return { agent, versionStr, deprecationWarning }; + }) + ); - // Check for deprecated npm installation - const deprecationWarning = await this.checkDeprecatedInstallation(agent, versionStr); + for (const { agent, versionStr, deprecationWarning } of agentResults) { if (deprecationWarning) { details.push(deprecationWarning); continue; diff --git a/src/cli/commands/doctor/checks/FrameworksCheck.ts b/src/cli/commands/doctor/checks/FrameworksCheck.ts index 5e50028e..85d8d776 100644 --- a/src/cli/commands/doctor/checks/FrameworksCheck.ts +++ b/src/cli/commands/doctor/checks/FrameworksCheck.ts @@ -23,10 +23,16 @@ export class FrameworksCheck implements ItemWiseHealthCheck { message: 'No frameworks registered' }); } else { - // Check each framework - for (const framework of frameworks) { - const installed = await framework.isInstalled(); - const version = installed ? await framework.getVersion() : null; + // Check all frameworks in parallel + const frameworkResults = await Promise.all( + frameworks.map(async (framework) => { + const installed = await framework.isInstalled(); + const version = installed ? await framework.getVersion() : null; + return { framework, installed, version }; + }) + ); + + for (const { framework, installed, version } of frameworkResults) { const versionStr = version ? ` (${version})` : ''; if (installed) { diff --git a/src/cli/commands/doctor/index.ts b/src/cli/commands/doctor/index.ts index c23c40a2..153bccfc 100644 --- a/src/cli/commands/doctor/index.ts +++ b/src/cli/commands/doctor/index.ts @@ -5,7 +5,7 @@ import { Command } from 'commander'; import chalk from 'chalk'; import os from 'os'; -import { HealthCheck, ItemWiseHealthCheck, HealthCheckResult } from './types.js'; +import { HealthCheckResult } from './types.js'; import { HealthCheckFormatter } from './formatter.js'; import { NodeVersionCheck, @@ -79,120 +79,112 @@ export function createDoctorCommand(): Command { // Display header formatter.displayHeader(); - // Define standard health checks - const checks: HealthCheck[] = [ - new NodeVersionCheck(), - new NpmCheck(), - new PythonCheck(), - new UvCheck(), - new AwsCliCheck(), - new AIConfigCheck(), - new AgentsCheck(), - new WorkflowsCheck(), - new FrameworksCheck() - ]; - - // Run and display standard checks immediately - for (const check of checks) { - logger.debug(`=== Running Check: ${check.name} ===`); - const startTime = Date.now(); - - // Check if this is an ItemWiseHealthCheck - const isItemWise = 'runWithItemDisplay' in check; - - if (isItemWise) { - // Display section header - console.log(formatter['getCheckHeader'](check.name)); - - // Run with item-by-item display - const result = await (check as ItemWiseHealthCheck).runWithItemDisplay( - (itemName) => { - logger.debug(` Checking item: ${itemName}`); - formatter.startItem(itemName); - }, - (detail) => { - logger.debug(` Result: ${detail.status} - ${detail.message}`); - formatter.displayItem(detail); - } - ); - results.push(result); - - const elapsed = Date.now() - startTime; - logger.debug(`Check completed in ${elapsed}ms: ${result.success ? 'SUCCESS' : 'FAILED'}`); - logger.debug(''); - - // Add blank line after section - console.log(); - } else { - // Regular check with section-level progress - formatter.startCheck(check.name); - const result = await check.run((message) => { - logger.debug(` Progress: ${message}`); - formatter.updateProgress(message); + // Helper to display a pre-computed check result + const displayResult = (result: HealthCheckResult): void => { + formatter.displayCheck(result); + if (result.details && result.details.length > 0) { + result.details.forEach(detail => { + logger.debug(` - ${detail.status}: ${detail.message}`); }); - results.push(result); - formatter.displayCheck(result); - - const elapsed = Date.now() - startTime; - logger.debug(`Check completed in ${elapsed}ms: ${result.success ? 'SUCCESS' : 'FAILED'}`); - if (result.details && result.details.length > 0) { - result.details.forEach(detail => { - logger.debug(` - ${detail.status}: ${detail.message}`); - }); - } - logger.debug(''); } + logger.debug(''); + }; + + // --- Group 1: Independent tool checks (run in parallel) --- + const nodeCheck = new NodeVersionCheck(); + const npmCheck = new NpmCheck(); + const pythonCheck = new PythonCheck(); + const uvCheck = new UvCheck(); + const awsCheck = new AwsCliCheck(); + + logger.debug('=== Running Tool Checks (parallel) ==='); + const toolStartTime = Date.now(); + const [nodeResult, npmResult, pythonResult, uvResult, awsResult] = await Promise.all([ + nodeCheck.run(), + npmCheck.run(), + pythonCheck.run(), + uvCheck.run(), + awsCheck.run() + ]); + logger.debug(`Tool checks completed in ${Date.now() - toolStartTime}ms`); + + // Display tool check results sequentially + for (const result of [nodeResult, npmResult, pythonResult, uvResult, awsResult]) { + results.push(result); + displayResult(result); + } - // After AIConfigCheck, immediately run provider-specific checks - if (check instanceof AIConfigCheck) { - const config = check.getConfig(); - - if (config && config.provider) { - logger.debug(`=== Running Provider Check: ${config.provider} ===`); - logger.debug(`Base URL: ${config.baseUrl}`); - logger.debug(`Model: ${config.model}`); - - // Get health check from ProviderRegistry - const healthCheck = ProviderRegistry.getHealthCheck(config.provider); - - if (healthCheck) { - formatter.startCheck('Provider'); - - try { - const providerStartTime = Date.now(); - const providerResult = await healthCheck.check(config); - const elapsed = Date.now() - providerStartTime; - - logger.debug(`Provider check completed in ${elapsed}ms`); - logger.debug(`Status: ${providerResult.status}`); - - const doctorResult = adaptProviderResult(providerResult); - results.push(doctorResult); - formatter.displayCheckWithHeader(doctorResult); - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - logger.error(`Provider check failed: ${errorMessage}`); - if (error instanceof Error && error.stack) { - logger.debug(`Stack trace: ${error.stack}`); - } - - // If check throws, capture error - results.push({ - name: 'Provider Check Error', - success: false, - details: [{ - status: 'error', - message: `Check failed: ${errorMessage}` - }] - }); - } - } else { - logger.debug(`No health check available for provider: ${config.provider}`); + // --- Group 2: AI Config + Provider check (sequential, provider depends on config) --- + const aiConfigCheck = new AIConfigCheck(); + logger.debug('=== Running Check: Active Profile ==='); + const configStartTime = Date.now(); + const configResult = await aiConfigCheck.run(); + logger.debug(`Check completed in ${Date.now() - configStartTime}ms`); + results.push(configResult); + displayResult(configResult); + + // Run provider-specific checks if config is available + const config = aiConfigCheck.getConfig(); + if (config && config.provider) { + logger.debug(`=== Running Provider Check: ${config.provider} ===`); + logger.debug(`Base URL: ${config.baseUrl}`); + logger.debug(`Model: ${config.model}`); + + const healthCheck = ProviderRegistry.getHealthCheck(config.provider); + + if (healthCheck) { + formatter.startCheck('Provider'); + + try { + const providerStartTime = Date.now(); + const providerResult = await healthCheck.check(config); + logger.debug(`Provider check completed in ${Date.now() - providerStartTime}ms`); + logger.debug(`Status: ${providerResult.status}`); + + const doctorResult = adaptProviderResult(providerResult); + results.push(doctorResult); + formatter.displayCheckWithHeader(doctorResult); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + logger.error(`Provider check failed: ${errorMessage}`); + if (error instanceof Error && error.stack) { + logger.debug(`Stack trace: ${error.stack}`); } + + results.push({ + name: 'Provider Check Error', + success: false, + details: [{ + status: 'error', + message: `Check failed: ${errorMessage}` + }] + }); } + } else { + logger.debug(`No health check available for provider: ${config.provider}`); } } + // --- Group 3: Discovery checks (run in parallel) --- + const agentsCheck = new AgentsCheck(); + const workflowsCheck = new WorkflowsCheck(); + const frameworksCheck = new FrameworksCheck(); + + logger.debug('=== Running Discovery Checks (parallel) ==='); + const discoveryStartTime = Date.now(); + const [agentsResult, workflowsResult, frameworksResult] = await Promise.all([ + agentsCheck.run(), + workflowsCheck.run(), + frameworksCheck.run() + ]); + logger.debug(`Discovery checks completed in ${Date.now() - discoveryStartTime}ms`); + + // Display discovery check results sequentially + for (const result of [agentsResult, workflowsResult, frameworksResult]) { + results.push(result); + displayResult(result); + } + logger.debug('=== All Checks Completed ==='); const successCount = results.filter(r => r.success).length; logger.debug(`Passed: ${successCount}/${results.length}`); diff --git a/src/utils/native-installer.ts b/src/utils/native-installer.ts index a624aa34..072a9138 100644 --- a/src/utils/native-installer.ts +++ b/src/utils/native-installer.ts @@ -194,6 +194,37 @@ async function verifyInstallation( return null; } +/** + * Detect if installer output contains HTML instead of expected script output + * This occurs when the installer URL returns an HTML page (e.g., region block, + * maintenance page) instead of the actual installer script. Bash then fails + * trying to execute HTML as shell commands. + * + * @param output - Combined stdout/stderr from the installer process + * @returns true if the output contains HTML markers + */ +function isHtmlInstallerResponse(output: string): boolean { + return /]/i.test(output); +} + +/** + * Extract a user-friendly error message from HTML installer response + * Checks for known error patterns (region block, service unavailability) + * and returns an appropriate message. + * + * @param output - Combined stdout/stderr containing HTML content + * @returns User-friendly error message + */ +function detectHtmlErrorMessage(output: string): string { + // Check for region-specific unavailability + if (/unavailable.*region|not.*available.*here|app.unavailable/i.test(output)) { + return 'Claude Code is not available in your current region. Visit https://claude.ai for information about supported regions.'; + } + + // Generic HTML response (maintenance page, unexpected redirect, etc.) + return 'Installer URL returned an HTML page instead of an installation script. The service may be temporarily unavailable or not accessible from your location. Visit https://claude.ai for more information.'; +} + /** * Install agent using native platform installer * Detects platform and executes appropriate installation script @@ -246,6 +277,18 @@ export async function installNativeAgent( // Check if installation succeeded if (result.code !== 0) { + const combinedOutput = `${result.stderr || ''} ${result.stdout || ''}`; + + // Detect HTML response instead of installer script + // This happens when the service returns an error page (e.g., region block) + // instead of the actual installer script, and bash fails trying to parse HTML + if (isHtmlInstallerResponse(combinedOutput)) { + throw new AgentInstallationError( + agentName, + detectHtmlErrorMessage(combinedOutput) + ); + } + // SECURITY: Sanitize output before including in error message // Installer scripts might echo sensitive environment variables const sanitizedOutput = sanitizeValue(result.stderr || result.stdout);