first commit
This commit is contained in:
596
.taskmaster/tasks/task_023.md
Normal file
596
.taskmaster/tasks/task_023.md
Normal file
@@ -0,0 +1,596 @@
|
||||
# Task ID: 23
|
||||
|
||||
**Title:** Implement mcpctl discover Command
|
||||
|
||||
**Status:** pending
|
||||
|
||||
**Dependencies:** 22
|
||||
|
||||
**Priority:** medium
|
||||
|
||||
**Description:** Create the `mcpctl discover` CLI command that lets users search for MCP servers across all configured registries with filtering, multiple output formats, and an interactive browsing mode.
|
||||
|
||||
**Details:**
|
||||
|
||||
Create src/cli/src/commands/discover.ts:
|
||||
|
||||
```typescript
|
||||
import { Command } from 'commander';
|
||||
import { RegistryClient } from '../registry/client';
|
||||
import { formatTable, formatJson, formatYaml } from '../utils/output';
|
||||
import inquirer from 'inquirer';
|
||||
|
||||
export function createDiscoverCommand(): Command {
|
||||
const cmd = new Command('discover')
|
||||
.description('Search for MCP servers across registries')
|
||||
.argument('<query>', 'Search query (e.g., "slack", "database", "terraform")')
|
||||
.option('--category <category>', 'Filter by category (devops, data-platform, analytics, security)')
|
||||
.option('--verified', 'Only show verified servers')
|
||||
.option('--transport <type>', 'Filter by transport (stdio, sse)', undefined)
|
||||
.option('--registry <source>', 'Search specific registry (official, glama, smithery, all)', 'all')
|
||||
.option('--limit <n>', 'Maximum results to show', '20')
|
||||
.option('--output <format>', 'Output format (table, json, yaml)', 'table')
|
||||
.option('--interactive', 'Interactive browsing mode')
|
||||
.action(async (query, options) => {
|
||||
await discoverAction(query, options);
|
||||
});
|
||||
return cmd;
|
||||
}
|
||||
```
|
||||
|
||||
**Table Output Format:**
|
||||
```
|
||||
┌─────────────────┬────────────────────────────────┬───────────────────────┬───────────┬──────────┬────────────┐
|
||||
│ NAME │ DESCRIPTION │ PACKAGE │ TRANSPORT │ VERIFIED │ POPULARITY │
|
||||
├─────────────────┼────────────────────────────────┼───────────────────────┼───────────┼──────────┼────────────┤
|
||||
│ slack-mcp │ Slack workspace integration... │ @anthropic/slack-mcp │ stdio │ ✓ │ ★★★★☆ │
|
||||
│ slack-tools │ Send messages, manage chan... │ slack-mcp-server │ stdio │ │ ★★★☆☆ │
|
||||
└─────────────────┴────────────────────────────────┴───────────────────────┴───────────┴──────────┴────────────┘
|
||||
|
||||
Run 'mcpctl install <name>' to set up a server
|
||||
```
|
||||
|
||||
**Implementation Details:**
|
||||
|
||||
```typescript
|
||||
// discover-action.ts
|
||||
import chalk from 'chalk';
|
||||
import Table from 'cli-table3';
|
||||
|
||||
const CATEGORIES = ['devops', 'data-platform', 'analytics', 'security', 'productivity', 'development'] as const;
|
||||
|
||||
interface DiscoverOptions {
|
||||
category?: string;
|
||||
verified?: boolean;
|
||||
transport?: 'stdio' | 'sse';
|
||||
registry?: 'official' | 'glama' | 'smithery' | 'all';
|
||||
limit?: string;
|
||||
output?: 'table' | 'json' | 'yaml';
|
||||
interactive?: boolean;
|
||||
}
|
||||
|
||||
export async function discoverAction(query: string, options: DiscoverOptions): Promise<void> {
|
||||
const client = new RegistryClient();
|
||||
|
||||
const searchOptions = {
|
||||
query,
|
||||
limit: parseInt(options.limit ?? '20', 10),
|
||||
registries: options.registry === 'all'
|
||||
? ['official', 'glama', 'smithery']
|
||||
: [options.registry],
|
||||
verified: options.verified,
|
||||
transport: options.transport,
|
||||
category: options.category,
|
||||
};
|
||||
|
||||
const results = await client.search(searchOptions);
|
||||
|
||||
if (results.length === 0) {
|
||||
console.log(chalk.yellow('No MCP servers found matching your query.'));
|
||||
console.log(chalk.dim('Try a different search term or remove filters.'));
|
||||
process.exit(2); // Exit code 2 = no results
|
||||
}
|
||||
|
||||
if (options.interactive) {
|
||||
await interactiveMode(results);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (options.output) {
|
||||
case 'json':
|
||||
console.log(JSON.stringify(results, null, 2));
|
||||
break;
|
||||
case 'yaml':
|
||||
console.log(formatYaml(results));
|
||||
break;
|
||||
default:
|
||||
printTable(results);
|
||||
console.log(chalk.cyan("\nRun 'mcpctl install <name>' to set up a server"));
|
||||
}
|
||||
}
|
||||
|
||||
function printTable(servers: RegistryServer[]): void {
|
||||
const table = new Table({
|
||||
head: ['NAME', 'DESCRIPTION', 'PACKAGE', 'TRANSPORT', 'VERIFIED', 'POPULARITY'],
|
||||
colWidths: [18, 35, 25, 10, 9, 12],
|
||||
wordWrap: true,
|
||||
});
|
||||
|
||||
for (const server of servers) {
|
||||
table.push([
|
||||
server.name,
|
||||
truncate(server.description, 32),
|
||||
server.packages.npm ?? server.packages.pypi ?? '-',
|
||||
server.transport,
|
||||
server.verified ? chalk.green('✓') : '',
|
||||
popularityStars(server.popularityScore),
|
||||
]);
|
||||
}
|
||||
|
||||
console.log(table.toString());
|
||||
}
|
||||
|
||||
function popularityStars(score: number): string {
|
||||
const stars = Math.round(score / 20); // 0-100 -> 0-5 stars
|
||||
return '★'.repeat(stars) + '☆'.repeat(5 - stars);
|
||||
}
|
||||
```
|
||||
|
||||
**Interactive Mode with Inquirer:**
|
||||
```typescript
|
||||
async function interactiveMode(servers: RegistryServer[]): Promise<void> {
|
||||
const { selected } = await inquirer.prompt([
|
||||
{
|
||||
type: 'list',
|
||||
name: 'selected',
|
||||
message: 'Select an MCP server to install:',
|
||||
choices: servers.map(s => ({
|
||||
name: `${s.name} - ${truncate(s.description, 50)} ${s.verified ? '✓' : ''}`,
|
||||
value: s.name,
|
||||
})),
|
||||
pageSize: 15,
|
||||
},
|
||||
]);
|
||||
|
||||
const { confirm } = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'confirm',
|
||||
message: `Install ${selected}?`,
|
||||
default: true,
|
||||
},
|
||||
]);
|
||||
|
||||
if (confirm) {
|
||||
// Trigger install command
|
||||
const installCmd = await import('./install');
|
||||
await installCmd.installAction(selected, {});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Exit Codes for Scripting:**
|
||||
- 0: Success, results found
|
||||
- 1: Error (network, API, etc.)
|
||||
- 2: No results found
|
||||
|
||||
**Category Inference for Data Analyst Tools:**
|
||||
Include categories relevant to BI/analytics:
|
||||
- 'data-platform': BigQuery, Snowflake, Databricks, dbt
|
||||
- 'analytics': Tableau, Looker, Metabase
|
||||
- 'database': PostgreSQL, MySQL, MongoDB tools
|
||||
|
||||
**Test Strategy:**
|
||||
|
||||
TDD approach - write tests BEFORE implementation:
|
||||
|
||||
1. **Command parsing tests:**
|
||||
- Test all option combinations parse correctly
|
||||
- Test query argument is required
|
||||
- Test invalid transport value rejected
|
||||
- Test invalid registry value rejected
|
||||
- Test limit parsed as integer
|
||||
|
||||
2. **Output formatting tests:**
|
||||
- Test table format with varying description lengths
|
||||
- Test table truncation at specified width
|
||||
- Test JSON output is valid JSON array
|
||||
- Test YAML output is valid YAML
|
||||
- Test popularity score to stars conversion (0-100 -> 0-5 stars)
|
||||
- Test verified badge displays correctly
|
||||
|
||||
3. **Interactive mode tests (mock inquirer):**
|
||||
- Test server list displayed as choices
|
||||
- Test selection triggers install confirmation
|
||||
- Test cancel does not trigger install
|
||||
- Test pagination with >15 results
|
||||
|
||||
4. **Exit code tests:**
|
||||
- Test exit(0) when results found
|
||||
- Test exit(1) on registry client error
|
||||
- Test exit(2) when no results match
|
||||
|
||||
5. **Integration tests:**
|
||||
- Test full command execution with mocked RegistryClient
|
||||
- Test --verified filter reduces results
|
||||
- Test --category filter applies correctly
|
||||
- Test --registry limits to single source
|
||||
|
||||
6. **Filter combination tests:**
|
||||
- Test verified + transport + category combined
|
||||
- Test filters with no matches returns empty
|
||||
|
||||
Run: `pnpm --filter @mcpctl/cli test:run -- --coverage commands/discover`
|
||||
|
||||
## Subtasks
|
||||
|
||||
### 23.1. Write TDD Test Suites for Command Parsing, Option Validation, and Exit Codes
|
||||
|
||||
**Status:** pending
|
||||
**Dependencies:** None
|
||||
|
||||
Create comprehensive Vitest test suites for the discover command's argument parsing, option validation, and exit code behavior BEFORE implementation, following the project's TDD approach.
|
||||
|
||||
**Details:**
|
||||
|
||||
Create src/cli/tests/unit/commands/discover.test.ts with the following test categories:
|
||||
|
||||
**Command Parsing Tests:**
|
||||
- Test 'mcpctl discover' without query argument shows error and exits with code 2 (invalid arguments)
|
||||
- Test 'mcpctl discover slack' parses query correctly as 'slack'
|
||||
- Test 'mcpctl discover "database tools"' handles quoted multi-word queries
|
||||
- Test query argument is accessible in action handler
|
||||
|
||||
**Option Validation Tests:**
|
||||
- Test --category accepts valid values: 'devops', 'data-platform', 'analytics', 'security', 'productivity', 'development'
|
||||
- Test --category with invalid value shows error listing valid options
|
||||
- Test --verified flag sets verified=true in options
|
||||
- Test --transport accepts 'stdio' and 'sse' only, rejects invalid values
|
||||
- Test --registry accepts 'official', 'glama', 'smithery', 'all' (default), rejects others
|
||||
- Test --limit parses as integer (e.g., '20' -> 20)
|
||||
- Test --limit with non-numeric value shows validation error
|
||||
- Test --output accepts 'table', 'json', 'yaml', rejects others
|
||||
- Test --interactive flag sets interactive=true
|
||||
|
||||
**Default Values Tests:**
|
||||
- Test --registry defaults to 'all' when not specified
|
||||
- Test --limit defaults to '20' when not specified
|
||||
- Test --output defaults to 'table' when not specified
|
||||
|
||||
**Exit Code Tests:**
|
||||
- Test exit code 0 when results are found
|
||||
- Test exit code 1 on RegistryClient errors (network, API failures)
|
||||
- Test exit code 2 when no results match query/filters
|
||||
|
||||
**Filter Combination Tests:**
|
||||
- Test --verified + --category + --transport combined correctly
|
||||
- Test all filters with empty results returns exit code 2
|
||||
|
||||
Create src/cli/tests/fixtures/mock-registry-client.ts with MockRegistryClient class that returns configurable results or throws configurable errors for testing. Use vitest mock functions to capture calls to verify correct option passing.
|
||||
|
||||
All tests should initially fail (TDD red phase) as the discover command doesn't exist yet.
|
||||
|
||||
### 23.2. Write TDD Test Suites for Output Formatters with Security Sanitization
|
||||
|
||||
**Status:** pending
|
||||
**Dependencies:** 23.1
|
||||
|
||||
Create comprehensive Vitest test suites for all three output formats (table, JSON, YAML), popularity star rendering, description truncation, and critical security tests for terminal escape sequence sanitization.
|
||||
|
||||
**Details:**
|
||||
|
||||
Create src/cli/tests/unit/commands/discover-output.test.ts with the following test categories:
|
||||
|
||||
**Table Output Tests:**
|
||||
- Test table header contains: NAME, DESCRIPTION, PACKAGE, TRANSPORT, VERIFIED, POPULARITY
|
||||
- Test table column widths match spec: 18, 35, 25, 10, 9, 12
|
||||
- Test word wrapping works for long descriptions
|
||||
- Test description truncation at 32 characters with ellipsis
|
||||
- Test verified=true shows green checkmark (chalk.green('✓'))
|
||||
- Test verified=false shows empty string
|
||||
- Test footer shows "Run 'mcpctl install <name>' to set up a server"
|
||||
- Test empty results array shows yellow 'No MCP servers found' message
|
||||
|
||||
**Popularity Stars Tests (popularityStars function):**
|
||||
- Test score 0 returns '☆☆☆☆☆' (0 filled stars)
|
||||
- Test score 20 returns '★☆☆☆☆' (1 filled star)
|
||||
- Test score 50 returns '★★★☆☆' (2.5 rounds to 3 stars - verify rounding)
|
||||
- Test score 100 returns '★★★★★' (5 filled stars)
|
||||
- Test intermediate values: 10->1, 30->2, 60->3, 80->4
|
||||
|
||||
**JSON Output Tests:**
|
||||
- Test JSON output is valid JSON (passes JSON.parse())
|
||||
- Test JSON output is pretty-printed with 2-space indentation
|
||||
- Test JSON array contains all RegistryServer fields
|
||||
- Test JSON is jq-parseable: 'echo output | jq .[]' works
|
||||
- Test --output json does NOT print footer message
|
||||
|
||||
**YAML Output Tests:**
|
||||
- Test YAML output is valid YAML (passes yaml.load())
|
||||
- Test YAML output uses formatYaml utility from utils/output
|
||||
- Test --output yaml does NOT print footer message
|
||||
|
||||
**SECURITY - Terminal Escape Sequence Sanitization Tests:**
|
||||
- Test description containing ANSI codes '\x1b[31mRED\x1b[0m' is sanitized
|
||||
- Test description containing '\033[1mBOLD\033[0m' is sanitized
|
||||
- Test name containing escape sequences is sanitized
|
||||
- Test package name containing escape sequences is sanitized
|
||||
- Test sanitization removes all \x1b[ and \033[ patterns
|
||||
- Test sanitization preserves normal text content
|
||||
- Test prevents cursor movement codes (\x1b[2J screen clear, etc.)
|
||||
|
||||
**Truncate Function Tests:**
|
||||
- Test truncate('short', 32) returns 'short' unchanged
|
||||
- Test truncate('exactly 32 characters string!!!', 32) returns unchanged
|
||||
- Test truncate('this is a very long description that exceeds limit', 32) returns 'this is a very long description...' (29 chars + '...')
|
||||
|
||||
Create src/cli/tests/fixtures/mock-servers.ts with sample RegistryServer objects including edge cases: very long descriptions, special characters, potential injection strings, missing optional fields (packages.pypi undefined).
|
||||
|
||||
### 23.3. Implement discover Command Definition and Action Handler with Sanitization
|
||||
|
||||
**Status:** pending
|
||||
**Dependencies:** 23.1, 23.2
|
||||
|
||||
Implement the discover command using Commander.js following the project's command registration pattern, with the discoverAction handler that orchestrates RegistryClient calls, applies filters, handles errors, and sets correct exit codes.
|
||||
|
||||
**Details:**
|
||||
|
||||
Create src/cli/src/commands/discover.ts implementing the CommandModule interface from the project's command registry pattern:
|
||||
|
||||
```typescript
|
||||
// discover.ts
|
||||
import { Command } from 'commander';
|
||||
import { RegistryClient } from '../registry/client';
|
||||
import { sanitizeTerminalOutput } from '../utils/sanitize';
|
||||
import { DiscoverOptions, CATEGORIES } from './discover-types';
|
||||
import { printResults } from './discover-output';
|
||||
|
||||
const VALID_TRANSPORTS = ['stdio', 'sse'] as const;
|
||||
const VALID_REGISTRIES = ['official', 'glama', 'smithery', 'all'] as const;
|
||||
const VALID_OUTPUT_FORMATS = ['table', 'json', 'yaml'] as const;
|
||||
|
||||
export function createDiscoverCommand(): Command {
|
||||
const cmd = new Command('discover')
|
||||
.description('Search for MCP servers across registries')
|
||||
.argument('<query>', 'Search query (e.g., "slack", "database", "terraform")')
|
||||
.option('--category <category>', `Filter by category (${CATEGORIES.join(', ')})`)
|
||||
.option('--verified', 'Only show verified servers')
|
||||
.option('--transport <type>', 'Filter by transport (stdio, sse)')
|
||||
.option('--registry <source>', 'Search specific registry (official, glama, smithery, all)', 'all')
|
||||
.option('--limit <n>', 'Maximum results to show', '20')
|
||||
.option('--output <format>', 'Output format (table, json, yaml)', 'table')
|
||||
.option('--interactive', 'Interactive browsing mode')
|
||||
.action(async (query, options) => {
|
||||
await discoverAction(query, options);
|
||||
});
|
||||
return cmd;
|
||||
}
|
||||
```
|
||||
|
||||
Create src/cli/src/commands/discover-action.ts:
|
||||
```typescript
|
||||
export async function discoverAction(query: string, options: DiscoverOptions): Promise<void> {
|
||||
// 1. Validate options (transport, registry, output, category)
|
||||
// 2. Parse limit as integer with validation
|
||||
// 3. Build SearchOptions for RegistryClient
|
||||
// 4. Call client.search() wrapped in try/catch
|
||||
// 5. Handle empty results -> exit code 2
|
||||
// 6. Handle network/API errors -> exit code 1 with structured logging
|
||||
// 7. Sanitize all string fields in results (prevent terminal injection)
|
||||
// 8. Delegate to printResults() or interactiveMode() based on options
|
||||
}
|
||||
```
|
||||
|
||||
Create src/cli/src/utils/sanitize.ts:
|
||||
```typescript
|
||||
export function sanitizeTerminalOutput(text: string): string {
|
||||
// Remove ANSI escape sequences: \x1b[...m, \033[...m
|
||||
// Remove cursor control sequences
|
||||
// Preserve legitimate text content
|
||||
return text
|
||||
.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '')
|
||||
.replace(/\033\[[0-9;]*[a-zA-Z]/g, '')
|
||||
.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, '');
|
||||
}
|
||||
|
||||
export function sanitizeServerResult(server: RegistryServer): RegistryServer {
|
||||
return {
|
||||
...server,
|
||||
name: sanitizeTerminalOutput(server.name),
|
||||
description: sanitizeTerminalOutput(server.description),
|
||||
// sanitize other user-facing string fields
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
Create src/cli/src/commands/discover-types.ts with TypeScript interfaces and constants.
|
||||
|
||||
Register discover command via CommandRegistry following existing patterns in src/cli/src/commands/.
|
||||
|
||||
### 23.4. Implement Output Formatters: Table with cli-table3, JSON, and YAML
|
||||
|
||||
**Status:** pending
|
||||
**Dependencies:** 23.2, 23.3
|
||||
|
||||
Implement the three output format handlers (table, JSON, YAML) including the popularity stars renderer, description truncation, verified badge display, and footer message. Table uses cli-table3 with specified column widths.
|
||||
|
||||
**Details:**
|
||||
|
||||
Create src/cli/src/commands/discover-output.ts:
|
||||
|
||||
```typescript
|
||||
import chalk from 'chalk';
|
||||
import Table from 'cli-table3';
|
||||
import { RegistryServer } from '../registry/types';
|
||||
import { formatYaml } from '../utils/output';
|
||||
|
||||
export function printResults(servers: RegistryServer[], format: 'table' | 'json' | 'yaml'): void {
|
||||
switch (format) {
|
||||
case 'json':
|
||||
printJsonOutput(servers);
|
||||
break;
|
||||
case 'yaml':
|
||||
printYamlOutput(servers);
|
||||
break;
|
||||
default:
|
||||
printTableOutput(servers);
|
||||
console.log(chalk.cyan("\nRun 'mcpctl install <name>' to set up a server"));
|
||||
}
|
||||
}
|
||||
|
||||
function printTableOutput(servers: RegistryServer[]): void {
|
||||
const table = new Table({
|
||||
head: ['NAME', 'DESCRIPTION', 'PACKAGE', 'TRANSPORT', 'VERIFIED', 'POPULARITY'],
|
||||
colWidths: [18, 35, 25, 10, 9, 12],
|
||||
wordWrap: true,
|
||||
style: { head: ['cyan'] }
|
||||
});
|
||||
|
||||
for (const server of servers) {
|
||||
table.push([
|
||||
server.name,
|
||||
truncate(server.description, 32),
|
||||
getPackageName(server.packages),
|
||||
server.transport,
|
||||
server.verified ? chalk.green('✓') : '',
|
||||
popularityStars(server.popularityScore),
|
||||
]);
|
||||
}
|
||||
|
||||
console.log(table.toString());
|
||||
}
|
||||
|
||||
function printJsonOutput(servers: RegistryServer[]): void {
|
||||
console.log(JSON.stringify(servers, null, 2));
|
||||
}
|
||||
|
||||
function printYamlOutput(servers: RegistryServer[]): void {
|
||||
console.log(formatYaml(servers));
|
||||
}
|
||||
|
||||
export function truncate(text: string, maxLength: number): string {
|
||||
if (text.length <= maxLength) return text;
|
||||
return text.slice(0, maxLength - 3) + '...';
|
||||
}
|
||||
|
||||
export function popularityStars(score: number): string {
|
||||
const stars = Math.round(score / 20); // 0-100 -> 0-5 stars
|
||||
return '★'.repeat(stars) + '☆'.repeat(5 - stars);
|
||||
}
|
||||
|
||||
function getPackageName(packages: RegistryServer['packages']): string {
|
||||
return packages.npm ?? packages.pypi ?? packages.docker ?? '-';
|
||||
}
|
||||
```
|
||||
|
||||
Create src/cli/src/commands/discover-no-results.ts for handling empty results:
|
||||
```typescript
|
||||
export function printNoResults(): void {
|
||||
console.log(chalk.yellow('No MCP servers found matching your query.'));
|
||||
console.log(chalk.dim('Try a different search term or remove filters.'));
|
||||
}
|
||||
```
|
||||
|
||||
Ensure formatYaml utility exists in src/cli/src/utils/output.ts (may need to create if not existing from Task 7). Install cli-table3 dependency: 'pnpm --filter @mcpctl/cli add cli-table3'.
|
||||
|
||||
**Data Analyst/BI Category Support:**
|
||||
Ensure CATEGORIES constant includes categories relevant to data analysts:
|
||||
- 'data-platform': BigQuery, Snowflake, Databricks, dbt
|
||||
- 'analytics': Tableau, Looker, Metabase, Power BI
|
||||
- 'database': PostgreSQL, MySQL, MongoDB connectors
|
||||
- 'visualization': Grafana, Superset integrations
|
||||
|
||||
This supports the Data Analyst persona requirement from the task context.
|
||||
|
||||
### 23.5. Implement Interactive Mode with Inquirer and Install Integration
|
||||
|
||||
**Status:** pending
|
||||
**Dependencies:** 23.3, 23.4
|
||||
|
||||
Implement the interactive browsing mode using Inquirer.js that allows users to scroll through results, select a server, confirm installation, and trigger the install command. Include graceful handling of user cancellation.
|
||||
|
||||
**Details:**
|
||||
|
||||
Create src/cli/src/commands/discover-interactive.ts:
|
||||
|
||||
```typescript
|
||||
import inquirer from 'inquirer';
|
||||
import chalk from 'chalk';
|
||||
import { RegistryServer } from '../registry/types';
|
||||
import { truncate } from './discover-output';
|
||||
|
||||
export async function interactiveMode(servers: RegistryServer[]): Promise<void> {
|
||||
// Step 1: Display server selection list
|
||||
const { selected } = await inquirer.prompt([
|
||||
{
|
||||
type: 'list',
|
||||
name: 'selected',
|
||||
message: 'Select an MCP server to install:',
|
||||
choices: servers.map(s => ({
|
||||
name: formatChoice(s),
|
||||
value: s.name,
|
||||
})),
|
||||
pageSize: 15, // Show 15 items before scrolling
|
||||
},
|
||||
]);
|
||||
|
||||
// Step 2: Show server details and confirm installation
|
||||
const selectedServer = servers.find(s => s.name === selected);
|
||||
if (selectedServer) {
|
||||
console.log(chalk.dim('\nSelected server details:'));
|
||||
console.log(chalk.dim(` Description: ${selectedServer.description}`));
|
||||
console.log(chalk.dim(` Package: ${selectedServer.packages.npm ?? selectedServer.packages.pypi ?? '-'}`));
|
||||
console.log(chalk.dim(` Transport: ${selectedServer.transport}`));
|
||||
}
|
||||
|
||||
const { confirm } = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'confirm',
|
||||
message: `Install ${selected}?`,
|
||||
default: true,
|
||||
},
|
||||
]);
|
||||
|
||||
if (confirm) {
|
||||
// Dynamically import install command to avoid circular dependencies
|
||||
const { installAction } = await import('./install');
|
||||
await installAction([selected], {}); // Pass as array per install command spec
|
||||
} else {
|
||||
console.log(chalk.dim('Installation cancelled.'));
|
||||
}
|
||||
}
|
||||
|
||||
function formatChoice(server: RegistryServer): string {
|
||||
const verifiedBadge = server.verified ? chalk.green(' ✓') : '';
|
||||
const description = truncate(server.description, 50);
|
||||
return `${server.name} - ${description}${verifiedBadge}`;
|
||||
}
|
||||
```
|
||||
|
||||
Create src/cli/tests/unit/commands/discover-interactive.test.ts with mock inquirer tests:
|
||||
- Test server list displayed as scrollable choices
|
||||
- Test selection triggers install confirmation prompt
|
||||
- Test confirm=true triggers installAction with correct server name
|
||||
- Test confirm=false outputs 'Installation cancelled' and exits gracefully
|
||||
- Test pagination works with >15 results (pageSize check)
|
||||
- Test Ctrl+C cancellation is handled gracefully (inquirer throws on SIGINT)
|
||||
- Test formatChoice includes verified badge for verified servers
|
||||
- Test formatChoice truncates long descriptions correctly
|
||||
|
||||
Update src/cli/src/commands/discover-action.ts to call interactiveMode when options.interactive is true:
|
||||
```typescript
|
||||
if (options.interactive) {
|
||||
await interactiveMode(sanitizedResults);
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
**Error Handling:**
|
||||
- Wrap inquirer prompts in try/catch to handle Ctrl+C gracefully
|
||||
- Exit with code 0 on user cancellation (not an error)
|
||||
- Log structured message on cancellation for SRE observability
|
||||
|
||||
**Integration with Install Command:**
|
||||
- The install command (Task 24) may not exist yet - create a stub if needed
|
||||
- src/cli/src/commands/install.ts stub: export async function installAction(servers: string[], options: {}): Promise<void> { console.log('Install not implemented yet'); }
|
||||
Reference in New Issue
Block a user