diff --git a/src/helpers/index.ts b/src/helpers/index.ts index 542ef00..4c61ad4 100644 --- a/src/helpers/index.ts +++ b/src/helpers/index.ts @@ -8,5 +8,45 @@ export const packageHelpers: PackageHelpers = { required: true } } + }, + '@modelcontextprotocol/server-github': { + requiredEnvVars: { + GITHUB_PERSONAL_ACCESS_TOKEN: { + description: 'Personal access token for GitHub API access', + required: true + } + } + }, + '@modelcontextprotocol/server-gitlab': { + requiredEnvVars: { + GITLAB_PERSONAL_ACCESS_TOKEN: { + description: 'Personal access token for GitLab API access', + required: true + }, + GITLAB_API_URL: { + description: 'GitLab API URL (optional, for self-hosted instances)', + required: false + } + } + }, + '@modelcontextprotocol/server-google-maps': { + requiredEnvVars: { + GOOGLE_MAPS_API_KEY: { + description: 'API key for Google Maps services', + required: true + } + } + }, + '@modelcontextprotocol/server-slack': { + requiredEnvVars: { + SLACK_BOT_TOKEN: { + description: 'Slack Bot User OAuth Token (starts with xoxb-)', + required: true + }, + SLACK_TEAM_ID: { + description: 'Slack Team/Workspace ID', + required: true + } + } } }; \ No newline at end of file diff --git a/src/install.ts b/src/install.ts index f4e8542..60c2e3b 100644 --- a/src/install.ts +++ b/src/install.ts @@ -8,6 +8,7 @@ import inquirer from 'inquirer'; import { exec } from 'child_process'; import { promisify } from 'util'; import { packageHelpers } from './helpers'; +import chalk from 'chalk'; const execAsync = promisify(exec); @@ -21,46 +22,75 @@ async function handlePackageHelper(packageName: string): Promise = {}; + console.log(chalk.cyan('\nEnvironment Variable Configuration:')); + + // First check if env vars are needed and get user confirmation + console.log(chalk.yellow('\nThis package requires configuration of environment variables.')); + const { proceed } = await inquirer.prompt<{ proceed: boolean }>([{ + type: 'confirm', + name: 'proceed', + message: 'Would you like to configure them now?', + default: true + }]); + + if (!proceed) { + console.log(chalk.yellow('\nInstallation cancelled. Package requires environment configuration.')); + process.exit(0); + } for (const [envVar, config] of Object.entries(helper.requiredEnvVars)) { - const envConfig = config as { description: string; required: boolean }; + const envConfig = config as { description: string; required: boolean; default?: string }; const existingValue = process.env[envVar]; + console.log(chalk.gray(`\n${envVar}:`)); + console.log(chalk.gray(`Description: ${envConfig.description}`)); + if (envConfig.default) { + console.log(chalk.gray(`Default: ${envConfig.default}`)); + } + if (existingValue) { const { useExisting } = await inquirer.prompt<{ useExisting: boolean }>([{ type: 'confirm', name: 'useExisting', - message: `Found existing ${envVar} in your environment. Would you like to use it?`, + message: `Found existing ${envVar} in environment. Use this value?`, default: true }]); if (useExisting) { envVars[envVar] = existingValue; + console.log(chalk.green(`Using existing ${envVar}`)); continue; } } - if (envConfig.required) { - const { value, configure } = await inquirer.prompt([ - { - type: 'confirm', - name: 'configure', - message: `${envVar} is required for ${packageName}. Would you like to configure it now?`, - default: true - }, - { - type: 'input', - name: 'value', - message: `Please enter your ${envVar} (${envConfig.description}):`, - when: (answers) => answers.configure - } - ]); + const { configure } = await inquirer.prompt([{ + type: 'confirm', + name: 'configure', + message: `Would you like to configure ${envVar}${envConfig.required ? ' (required)' : ' (optional)'}?`, + default: envConfig.required + }]); - if (configure && value) { + if (configure) { + const { value } = await inquirer.prompt([{ + type: 'input', + name: 'value', + message: `Enter value for ${envVar}:`, + default: envConfig.default, + validate: (input) => { + if (envConfig.required && !input) { + return `${envVar} is required`; + } + return true; + } + }]); + + if (value) { envVars[envVar] = value; - } else if (envConfig.required) { - console.log(`\nSkipping ${envVar} configuration. You'll need to set it in your environment before using ${packageName}.`); + console.log(chalk.green(`āœ“ ${envVar} configured`)); } + } else if (envConfig.required) { + console.log(chalk.yellow(`\nāš ļø Warning: ${envVar} is required but not configured. You'll need to set it manually.`)); + process.exit(0); } } diff --git a/src/installed.ts b/src/installed.ts index 75d65c1..c62dc98 100644 --- a/src/installed.ts +++ b/src/installed.ts @@ -1,58 +1,127 @@ import inquirer from 'inquirer'; -import { readConfig, writeConfig } from './utils/config'; -import { formatPackageInfo } from './utils/display'; +import chalk from 'chalk'; +import { readConfig } from './utils/config'; +import { displayPackageDetailsWithActions } from './utils/display'; import { uninstallPackage } from './utils/package-management'; +import { Package } from './types'; +import { readFileSync } from 'fs'; +import { join } from 'path'; +import { fileURLToPath } from 'url'; +import { dirname } from 'path'; +import AutocompletePrompt from 'inquirer-autocomplete-prompt'; +import fuzzy from 'fuzzy'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const packageListPath = join(__dirname, '../packages/package-list.json'); + +inquirer.registerPrompt('autocomplete', AutocompletePrompt); export async function listInstalledPackages(): Promise { + // Read full package list + const allPackages: Package[] = JSON.parse(readFileSync(packageListPath, 'utf-8')); + + // Get installed packages from config const config = readConfig(); const installedServers = config.mcpServers || {}; const serverNames = Object.keys(installedServers); if (serverNames.length === 0) { - console.log('No MCP servers are currently installed.'); + console.log(chalk.yellow('\nNo MCP servers are currently installed.')); return; } - console.log('Installed MCP servers:\n'); - serverNames.forEach(name => { - console.log(`- ${name}`); - }); + // Filter for only installed packages + const installedPackages = allPackages.filter(pkg => + serverNames.includes(pkg.name.replace(/\//g, '-')) + ); + + console.log(chalk.bold.cyan('\nšŸ“¦ Installed Packages')); + console.log(chalk.gray(`Found ${installedPackages.length} installed packages\n`)); + + // Prepare choices for inquirer using table-like format + const choices = installedPackages.map(pkg => ({ + name: `${pkg.name.padEnd(24)} ā”‚ ${ + pkg.description.length > 47 ? `${pkg.description.slice(0, 44)}...` : pkg.description.padEnd(49) + } ā”‚ ${pkg.vendor.padEnd(19)} ā”‚ ${pkg.license.padEnd(14)}`, + value: pkg, + short: pkg.name + })); - const { action } = await inquirer.prompt([ + const answer = await inquirer.prompt<{ selectedPackage: Package }>([ { - type: 'list', - name: 'action', - message: 'What would you like to do?', - choices: [ - { name: 'Uninstall a server', value: 'uninstall' }, - { name: 'Exit', value: 'exit' } - ] + type: 'autocomplete', + name: 'selectedPackage', + message: 'Search and select a package:', + source: async (_answersSoFar: any, input: string) => { + if (!input) return choices; + + return fuzzy + .filter(input.toLowerCase(), choices, { + extract: (choice) => `${choice.value.name} ${choice.value.description} ${choice.value.vendor}`.toLowerCase() + }) + .map(result => result.original); + }, + pageSize: 10 } ]); - if (action === 'uninstall') { - const { packageToUninstall } = await inquirer.prompt([ - { - type: 'list', - name: 'packageToUninstall', - message: 'Select a server to uninstall:', - choices: serverNames + const displayPackages = answer.selectedPackage ? [answer.selectedPackage] : installedPackages; + + if (displayPackages.length === 0) { + console.log(chalk.yellow('\nNo packages found matching your search.')); + return; + } + + console.log(chalk.bold.white(`\nShowing ${displayPackages.length} package(s):`)); + displayPackages.forEach(displayPackageDetailsWithActions); + + if (displayPackages.length === 1) { + const pkg = displayPackages[0]; + await handleSelectedPackage(pkg); + } +} + +async function handleSelectedPackage(pkg: Package) { + const action = await displayPackageDetailsWithActions(pkg); + + switch (action) { + case 'uninstall': + const { confirmUninstall } = await inquirer.prompt<{ confirmUninstall: boolean }>([ + { + type: 'confirm', + name: 'confirmUninstall', + message: `Are you sure you want to uninstall ${pkg.name}?`, + default: false + } + ]); + + if (confirmUninstall) { + await uninstallPackage(pkg.name); + console.log(chalk.green(`Successfully uninstalled ${pkg.name}`)); + // Return to installed packages list + await listInstalledPackages(); + } else { + console.log('Uninstallation cancelled.'); + // Show actions again + await handleSelectedPackage(pkg); } - ]); - - const { confirmUninstall } = await inquirer.prompt([ - { - type: 'confirm', - name: 'confirmUninstall', - message: `Are you sure you want to uninstall ${packageToUninstall}?`, - default: false + break; + case 'open': + if (pkg.sourceUrl) { + const open = (await import('open')).default; + await open(pkg.sourceUrl); + console.log(chalk.green(`\nOpened ${pkg.sourceUrl} in your browser`)); + } else { + console.log(chalk.yellow('\nNo source URL available for this package')); } - ]); - - if (confirmUninstall) { - await uninstallPackage(packageToUninstall); - } else { - console.log('Uninstallation cancelled.'); - } + // Show actions again after opening URL + await handleSelectedPackage(pkg); + break; + case 'back': + await listInstalledPackages(); + return; + case 'exit': + process.exit(0); } } \ No newline at end of file diff --git a/src/list.ts b/src/list.ts index ff3e600..9bd2f5f 100644 --- a/src/list.ts +++ b/src/list.ts @@ -6,8 +6,8 @@ import fuzzy from 'fuzzy'; import { fileURLToPath } from 'url'; import { dirname } from 'path'; import { Package } from './types/index.js'; -import { displayPackageDetails } from './utils/display.js'; -import { installPackage } from './utils/package-management'; +import { displayPackageDetailsWithActions } from './utils/display.js'; +import { installPackage, uninstallPackage } from './utils/package-management'; import { createInterface } from 'readline'; import Table from 'cli-table3'; // Import cli-table3 import stringWidth from 'string-width'; // Import string-width @@ -71,43 +71,39 @@ export async function list() { } console.log(chalk.bold.white(`\nShowing ${displayPackages.length} package(s):`)); - displayPackages.forEach(displayPackageDetails); + displayPackages.forEach(displayPackageDetailsWithActions); if (displayPackages.length === 1) { const pkg = displayPackages[0]; - - // Set up readline interface to handle keypress events - const rl = createInterface({ - input: process.stdin, - output: process.stdout - }); - - // Enable keypress events - process.stdin.setRawMode(true); - process.stdin.resume(); - - // Handle keypress events - process.stdin.on('keypress', async (str, key) => { - if (key.name === 'i' || key.name === 'I') { - rl.close(); - process.stdin.setRawMode(false); - await handleAction('install', pkg); - } else if (key.name === 'o' || key.name === 'O') { - rl.close(); - process.stdin.setRawMode(false); - await handleAction('open', pkg); - } - }); - - await promptForAction(pkg); + await handleSelectedPackage(pkg); } } -async function handleAction(action: string, pkg: Package) { +async function handleSelectedPackage(pkg: Package) { + const action = await displayPackageDetailsWithActions(pkg); + switch (action) { case 'install': + console.log(chalk.cyan(`\nPreparing to install ${pkg.name}...`)); await installPackage(pkg); break; + case 'uninstall': + const { confirmUninstall } = await inquirer.prompt<{ confirmUninstall: boolean }>([ + { + type: 'confirm', + name: 'confirmUninstall', + message: `Are you sure you want to uninstall ${pkg.name}?`, + default: false + } + ]); + + if (confirmUninstall) { + await uninstallPackage(pkg.name); + console.log(chalk.green(`Successfully uninstalled ${pkg.name}`)); + } else { + console.log('Uninstallation cancelled.'); + } + break; case 'open': if (pkg.sourceUrl) { const open = (await import('open')).default; @@ -119,27 +115,11 @@ async function handleAction(action: string, pkg: Package) { break; case 'back': await list(); - return; // Return to prevent showing prompt again + return; case 'exit': process.exit(0); } - await promptForAction(pkg); -} - -async function promptForAction(pkg: Package) { - const { action } = await inquirer.prompt<{ action: string }>([ - { - type: 'list', - name: 'action', - message: 'What would you like to do?', - choices: [ - { name: 'Install package (i)', value: 'install' }, - { name: 'Open source URL (o)', value: 'open' }, - { name: 'Back to package list', value: 'back' }, - { name: 'Exit', value: 'exit' } - ] - } - ]); - - await handleAction(action, pkg); + + // Show actions again after completing an action (except for exit/back) + await handleSelectedPackage(pkg); } diff --git a/src/uninstall.ts b/src/uninstall.ts index 240d7bf..d8b737f 100644 --- a/src/uninstall.ts +++ b/src/uninstall.ts @@ -1,25 +1,108 @@ import inquirer from 'inquirer'; +import chalk from 'chalk'; +import { readFileSync } from 'fs'; +import { join } from 'path'; +import { fileURLToPath } from 'url'; +import { dirname } from 'path'; +import { Package } from './types'; import { uninstallPackage } from './utils/package-management'; +import { displayPackageDetailsWithActions } from './utils/display'; +import { list } from './list'; -export async function uninstall(packageName: string): Promise { +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const packageListPath = join(__dirname, '../packages/package-list.json'); + +export async function uninstall(packageName?: string): Promise { try { - const { confirmUninstall } = await inquirer.prompt<{ confirmUninstall: boolean }>([ - { - type: 'confirm', - name: 'confirmUninstall', - message: `Are you sure you want to uninstall ${packageName}?`, - default: false - } - ]); + const packageList: Package[] = JSON.parse(readFileSync(packageListPath, 'utf-8')); + let selectedPackage: Package | undefined; - if (confirmUninstall) { - await uninstallPackage(packageName); - console.log(`Successfully uninstalled ${packageName}`); + if (packageName) { + selectedPackage = packageList.find(p => p.name === packageName); + if (!selectedPackage) { + console.log(chalk.yellow(`Package ${packageName} not found in the package list.`)); + return; + } } else { - console.log('Uninstallation cancelled.'); + // Use same selection interface as list command + const choices = packageList.map((pkg, index) => ({ + name: `${pkg.name.padEnd(24)} ā”‚ ${ + pkg.description.length > 47 ? `${pkg.description.slice(0, 44)}...` : pkg.description.padEnd(49) + } ā”‚ ${pkg.vendor.padEnd(19)} ā”‚ ${pkg.license.padEnd(14)}`, + value: pkg, + short: pkg.name + })); + + const { selectedPkg } = await inquirer.prompt<{ selectedPkg: Package }>([ + { + type: 'autocomplete', + name: 'selectedPkg', + message: 'Select a package to uninstall:', + source: async (_answersSoFar: any, input: string) => { + if (!input) return choices; + return choices.filter(choice => + choice.value.name.toLowerCase().includes(input.toLowerCase()) || + choice.value.description.toLowerCase().includes(input.toLowerCase()) + ); + }, + pageSize: 10 + } + ]); + selectedPackage = selectedPkg; + } + + if (selectedPackage) { + const action = await displayPackageDetailsWithActions(selectedPackage); + await handlePackageAction(action, selectedPackage); } } catch (error) { console.error('Failed to uninstall package:', error); throw error; } +} + +async function handlePackageAction(action: string, pkg: Package) { + switch (action) { + case 'install': + // Import and call install function + const { installPackage } = await import('./install'); + await installPackage(pkg); + break; + case 'uninstall': + const { confirmUninstall } = await inquirer.prompt<{ confirmUninstall: boolean }>([ + { + type: 'confirm', + name: 'confirmUninstall', + message: `Are you sure you want to uninstall ${pkg.name}?`, + default: false + } + ]); + + if (confirmUninstall) { + await uninstallPackage(pkg.name); + console.log(chalk.green(`Successfully uninstalled ${pkg.name}`)); + } else { + console.log('Uninstallation cancelled.'); + } + break; + case 'open': + if (pkg.sourceUrl) { + const open = (await import('open')).default; + await open(pkg.sourceUrl); + console.log(chalk.green(`\nOpened ${pkg.sourceUrl} in your browser`)); + } else { + console.log(chalk.yellow('\nNo source URL available for this package')); + } + break; + case 'back': + await list(); + return; + case 'exit': + process.exit(0); + } + + // Show actions again after completing an action (except for exit/back) + const nextAction = await displayPackageDetailsWithActions(pkg); + await handlePackageAction(nextAction, pkg); } \ No newline at end of file diff --git a/src/utils/config.ts b/src/utils/config.ts index 4e8c8eb..5298070 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -37,7 +37,7 @@ export function writeConfig(config: ClaudeConfig): void { fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); } -export function installMCPServer(packageName: string, env?: Record): void { +export function installMCPServer(packageName: string, envVars?: Record): void { const config = readConfig(); const serverName = packageName.replace(/\//g, '-'); @@ -48,7 +48,7 @@ export function installMCPServer(packageName: string, env?: Record([ + { + type: 'list', + name: 'action', + message: 'What would you like to do?', + choices + } + ]); + + return action; } \ No newline at end of file diff --git a/src/utils/package-management.ts b/src/utils/package-management.ts index 14e33a0..98f4986 100644 --- a/src/utils/package-management.ts +++ b/src/utils/package-management.ts @@ -1,11 +1,111 @@ import inquirer from 'inquirer'; import { Package } from '../types'; -import { installMCPServer, readConfig, writeConfig } from './config'; +import { getConfigPath, installMCPServer, readConfig, writeConfig } from './config'; import { exec } from 'child_process'; import { promisify } from 'util'; +import { packageHelpers } from '../helpers/index.js'; const execAsync = promisify(exec); +async function promptForEnvVars(packageName: string): Promise | undefined> { + const helpers = packageHelpers[packageName]; + if (!helpers?.requiredEnvVars) { + return undefined; + } + + // Check if all required variables exist in environment + const existingEnvVars: Record = {}; + let hasAllRequired = true; + + for (const [key, value] of Object.entries(helpers.requiredEnvVars)) { + const existingValue = process.env[key]; + if (existingValue) { + existingEnvVars[key] = existingValue; + } else if (value.required) { + hasAllRequired = false; + } + } + + if (hasAllRequired && Object.keys(existingEnvVars).length > 0) { + const { useAutoSetup } = await inquirer.prompt<{ useAutoSetup: boolean }>([{ + type: 'confirm', + name: 'useAutoSetup', + message: 'Found all required environment variables. Would you like to use them automatically?', + default: true + }]); + + if (useAutoSetup) { + return existingEnvVars; + } + } + + const { configureEnv } = await inquirer.prompt<{ configureEnv: boolean }>([{ + type: 'confirm', + name: 'configureEnv', + message: hasAllRequired + ? 'Would you like to manually configure environment variables for this package?' + : 'Some required environment variables are missing. Would you like to configure them now?', + default: !hasAllRequired + }]); + + if (!configureEnv) { + if (!hasAllRequired) { + const configPath = getConfigPath(); + console.log('\nNote: Some required environment variables are not configured.'); + console.log(`You can set them later by editing the config file at:`); + console.log(configPath); + } + return undefined; + } + + const envVars: Record = {}; + + for (const [key, value] of Object.entries(helpers.requiredEnvVars)) { + const existingEnvVar = process.env[key]; + + if (existingEnvVar) { + const { reuseExisting } = await inquirer.prompt<{ reuseExisting: boolean }>([{ + type: 'confirm', + name: 'reuseExisting', + message: `Found ${key} in your environment variables. Would you like to use it?`, + default: true + }]); + + if (reuseExisting) { + envVars[key] = existingEnvVar; + continue; + } + } + + const { envValue } = await inquirer.prompt([{ + type: 'input', + name: 'envValue', + message: `Please enter ${value.description}:`, + default: value.required ? undefined : null, + validate: (input: string) => { + if (value.required && !input) { + return `${key} is required`; + } + return true; + } + }]); + + if (envValue !== null) { + envVars[key] = envValue; + } + } + + if (Object.keys(envVars).length === 0) { + const configPath = getConfigPath(); + console.log('\nNo environment variables were configured.'); + console.log(`You can set them later by editing the config file at:`); + console.log(configPath); + return undefined; + } + + return envVars; +} + async function promptForRestart(): Promise { const { shouldRestart } = await inquirer.prompt<{ shouldRestart: boolean }>([ { @@ -51,7 +151,9 @@ async function promptForRestart(): Promise { export async function installPackage(pkg: Package): Promise { try { - installMCPServer(pkg.name); + const envVars = await promptForEnvVars(pkg.name); + + installMCPServer(pkg.name, envVars); console.log('Updated Claude desktop configuration'); await promptForRestart(); } catch (error) {