UNPKG

@172ai/containers-mcp-server

Version:

MCP server for 172.ai container management platform - enables AI assistants to manage containers, builds, and files with comprehensive workflow prompts

480 lines • 18.5 kB
#!/usr/bin/env node /** * 172.ai Container CLI Tool * Command line interface that reuses MCP service code for container operations */ // Check for commands that don't require config before importing anything const requiresSetup = !process.argv.includes('--help') && !process.argv.includes('-h') && !process.argv.includes('help') && !process.argv.includes('setup') && !process.argv.includes('--version') && process.argv.length > 2; import { Command } from 'commander'; import chalk from 'chalk'; import ora from 'ora'; const program = new Command(); const spinner = ora(); // Lazy load services only when needed let containerService; let buildService; let fileService; let capabilityService; let userService; let authManager; let config; let ErrorHandler; const loadServices = async () => { if (containerService) return; // Already loaded try { const services = await Promise.all([ import('./services/containerService'), import('./services/buildService'), import('./services/fileService'), import('./services/capabilityService'), import('./services/userService'), import('./auth'), import('./config'), import('./utils/errorHandler') ]); containerService = services[0].containerService; buildService = services[1].buildService; fileService = services[2].fileService; capabilityService = services[3].capabilityService; userService = services[4].userService; authManager = services[5].authManager; config = services[6].config; ErrorHandler = services[7].ErrorHandler; } catch (error) { if (error.message.includes('Authentication configuration required')) { console.error(chalk.red('Configuration required. Please run:')); console.error(chalk.yellow('containers-cli setup')); process.exit(1); } throw error; } }; // Helper functions for formatting output const formatTimestamp = (timestamp) => { if (!timestamp) return 'N/A'; let date; // Handle different timestamp formats if (typeof timestamp === 'string') { date = new Date(timestamp); } else if (typeof timestamp === 'number') { // Check if it looks like Unix timestamp (seconds) vs milliseconds if (timestamp < 10000000000) { // Likely Unix timestamp in seconds - convert to milliseconds date = new Date(timestamp * 1000); } else { // Already in milliseconds date = new Date(timestamp); } } else { date = new Date(timestamp); } // Check if date is valid if (isNaN(date.getTime())) { return 'Invalid Date'; } return date.toLocaleString(); }; const formatContainer = (container) => { return ` ${chalk.bold.blue(container.name)} (${container.id}) ${chalk.gray('Description:')} ${container.description} ${chalk.gray('Private:')} ${container.isPrivate ? chalk.red('Yes') : chalk.green('No')} ${chalk.gray('Tags:')} ${container.tags?.join(', ') || 'None'} ${chalk.gray('Created:')} ${formatTimestamp(container.createdAt)} ${chalk.gray('URL:')} ${container.url || 'N/A'}`; }; const formatFileSize = (bytes) => { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; }; // Global error handler const handleError = (error) => { spinner.stop(); const processedError = ErrorHandler.processError(error, 'CLI'); console.error(chalk.red('Error:'), processedError.message); if (config.isDevelopment()) { console.error(chalk.gray('Details:'), processedError.details); } process.exit(1); }; // Authentication check wrapper const withAuth = (fn) => { return async (...args) => { try { await loadServices(); const isAuthenticated = await authManager.testAuthentication(); if (!isAuthenticated) { console.error(chalk.red('Authentication failed. Please check your credentials.')); console.error(chalk.yellow('Run with --setup to configure authentication.')); process.exit(1); } return await fn(...args); } catch (error) { handleError(error); } }; }; // Container Commands program .command('containers') .alias('ls') .description('List containers') .option('-s, --scope <scope>', 'Filter scope: public, myCollection, all', 'all') .option('-l, --limit <limit>', 'Number of containers to fetch', '20') .option('-q, --query <query>', 'Search query') .action(withAuth(async (options) => { spinner.start('Fetching containers...'); const response = await containerService.listContainers({ scope: options.scope, limit: parseInt(options.limit), query: options.query }); spinner.stop(); if (response.containers.length === 0) { console.log(chalk.yellow('No containers found.')); return; } const limitedContainers = response.containers.slice(0, parseInt(options.limit)); console.log(chalk.bold(`Found ${response.total} container(s), showing ${limitedContainers.length}:\n`)); limitedContainers.forEach((container, index) => { console.log(`${index + 1}.${formatContainer(container)}\n`); }); })); program .command('get <containerId>') .description('Get container details') .action(withAuth(async (containerId) => { spinner.start('Fetching container...'); const container = await containerService.getContainer({ containerId }); spinner.stop(); console.log(formatContainer(container)); })); program .command('create') .description('Create a new container') .option('-n, --name <name>', 'Container name') .option('-d, --description <description>', 'Container description') .option('-f, --dockerfile <path>', 'Path to Dockerfile') .option('-t, --tags <tags>', 'Comma-separated tags') .option('-p, --private', 'Make container private', false) .action(withAuth(async (options) => { if (!options.name || !options.description || !options.dockerfile) { console.error(chalk.red('Name, description, and dockerfile are required.')); console.error('Usage: create -n "My Container" -d "Description" -f ./Dockerfile'); process.exit(1); } let dockerfileContent; try { const fs = await import('fs/promises'); dockerfileContent = await fs.readFile(options.dockerfile, 'utf8'); } catch (error) { console.error(chalk.red(`Failed to read Dockerfile: ${options.dockerfile}`)); process.exit(1); } spinner.start('Creating container...'); const container = await containerService.createContainer({ name: options.name, description: options.description, dockerfile: dockerfileContent, tags: options.tags ? options.tags.split(',').map((t) => t.trim()) : [], isPrivate: options.private }); spinner.stop(); console.log(chalk.green('Container created successfully!')); console.log(formatContainer(container)); })); program .command('delete <containerId>') .description('Delete a container') .option('-y, --yes', 'Skip confirmation prompt') .action(withAuth(async (containerId, options) => { if (!options.yes) { const { default: inquirer } = await import('inquirer'); const { confirm } = await inquirer.prompt([{ type: 'confirm', name: 'confirm', message: `Are you sure you want to delete container ${containerId}?`, default: false }]); if (!confirm) { console.log('Operation cancelled.'); return; } } spinner.start('Deleting container...'); const result = await containerService.deleteContainer({ containerId }); spinner.stop(); console.log(chalk.green(result.message)); })); // Build Commands program .command('build <containerId>') .description('Build a container') .option('-w, --watch', 'Watch build progress until completion') .action(withAuth(async (containerId, options) => { spinner.start('Starting build...'); const buildResult = await buildService.buildContainer({ containerId }); spinner.stop(); console.log(chalk.green('Build started successfully!')); console.log(`Build ID: ${buildResult.id}`); console.log(`Status: ${buildResult.status}`); if (options.watch) { console.log(chalk.blue('\nšŸ” Watching build progress...')); let lastStatus = buildResult.status; const pollInterval = 3000; // 3 seconds const watchBuild = async () => { try { const status = await buildService.getBuildStatus({ containerId, buildId: buildResult.id }); if (status.status !== lastStatus) { console.log(`Status changed: ${chalk.yellow(lastStatus)} → ${status.status === 'completed' ? chalk.green(status.status) : status.status === 'failed' ? chalk.red(status.status) : chalk.yellow(status.status)}`); lastStatus = status.status; } if (status.status === 'completed') { console.log(chalk.green('\nāœ… Build completed successfully!')); if (status.endTime) { console.log(`Duration: ${formatTimestamp(status.startTime)} → ${formatTimestamp(status.endTime)}`); } return; } else if (status.status === 'failed') { console.log(chalk.red('\nāŒ Build failed!')); if (status.endTime) { console.log(`Duration: ${formatTimestamp(status.startTime)} → ${formatTimestamp(status.endTime)}`); } console.log(chalk.yellow('Use "containers-cli builds <containerId>" to see build details')); return; } else if (status.status === 'cancelled') { console.log(chalk.yellow('\nāš ļø Build was cancelled')); return; } // Continue watching setTimeout(watchBuild, pollInterval); } catch (error) { console.error(chalk.red('Error watching build:'), error.message); } }; // Start watching setTimeout(watchBuild, pollInterval); } })); program .command('builds <containerId>') .description('List builds for a container') .option('-l, --limit <limit>', 'Number of builds to fetch', '10') .action(withAuth(async (containerId, options) => { spinner.start('Fetching builds...'); const response = await buildService.listBuilds({ containerId, limit: parseInt(options.limit) }); spinner.stop(); if (response.builds.length === 0) { console.log(chalk.yellow('No builds found.')); return; } console.log(chalk.bold(`Found ${response.total} build(s):\n`)); response.builds.forEach((build, index) => { const statusColor = build.status === 'completed' ? chalk.green : build.status === 'failed' ? chalk.red : chalk.yellow; console.log(`${index + 1}. ${chalk.bold(build.id)}`); console.log(` Status: ${statusColor(build.status)}`); console.log(` Started: ${formatTimestamp(build.startTime)}`); if (build.endTime) { console.log(` Ended: ${formatTimestamp(build.endTime)}`); } console.log(''); }); })); // File Commands program .command('files <containerId>') .description('List files in a container') .option('-p, --path <path>', 'Directory path', '/') .action(withAuth(async (containerId, options) => { spinner.start('Fetching files...'); const response = await fileService.listContainerFiles({ containerId, path: options.path }); spinner.stop(); if (response.files.length === 0) { console.log(chalk.yellow('No files found.')); return; } console.log(chalk.bold(`Files in ${response.path}:\n`)); response.files.forEach((file, index) => { const typeIcon = file.type === 'directory' ? 'šŸ“' : 'šŸ“„'; const sizeStr = file.size ? formatFileSize(file.size) : 'N/A'; console.log(`${index + 1}. ${typeIcon} ${chalk.bold(file.name)}`); console.log(` Path: ${file.path}`); console.log(` Size: ${sizeStr}`); if (file.lastModified) { console.log(` Modified: ${formatTimestamp(file.lastModified)}`); } console.log(''); }); })); program .command('upload <containerId> <localFilePath> [remotePath]') .description('Upload a file to a container') .action(withAuth(async (containerId, localFilePath, remotePath) => { // Use the provided remote path or default to the filename const targetPath = remotePath || `/${localFilePath.split('/').pop()}`; let content; try { const fs = await import('fs/promises'); content = await fs.readFile(localFilePath, 'utf8'); } catch (error) { console.error(chalk.red(`Failed to read file: ${localFilePath}`)); process.exit(1); } spinner.start('Uploading file...'); const result = await fileService.uploadFile({ containerId, filePath: targetPath, content }); spinner.stop(); console.log(chalk.green('File uploaded successfully!')); console.log(`Local: ${localFilePath}`); console.log(`Remote: ${result.path}`); console.log(`Size: ${formatFileSize(result.size)}`); })); // User Commands program .command('balance') .description('Check token balance') .action(withAuth(async () => { spinner.start('Fetching token balance...'); const balance = await userService.getTokenBalance(); spinner.stop(); console.log(chalk.bold('Token Balance:')); console.log(`Tokens: ${chalk.green(balance.tokenBalance.toLocaleString())}`); console.log(`Last Updated: ${formatTimestamp(balance.lastUpdated)}`); })); program .command('profile') .description('Show user profile') .action(withAuth(async () => { spinner.start('Fetching profile...'); const profile = await userService.getUserProfile(); spinner.stop(); console.log(chalk.bold('User Profile:')); console.log(`Email: ${profile.email}`); console.log(`ID: ${profile.id}`); if (profile.displayName) { console.log(`Name: ${profile.displayName}`); } console.log(`Roles: ${profile.roles.join(', ')}`); console.log(`Created: ${formatTimestamp(profile.createdAt)}`); })); program .command('transactions') .description('Show transaction history') .option('-l, --limit <limit>', 'Number of transactions to show', '20') .action(withAuth(async (options) => { spinner.start('Fetching transactions...'); const response = await userService.getTransactionHistory(parseInt(options.limit)); spinner.stop(); if (response.transactions.length === 0) { console.log(chalk.yellow('No transactions found.')); return; } console.log(chalk.bold(`Transaction History (${response.transactions.length} of ${response.total}):\n`)); response.transactions.forEach((transaction, index) => { const typeColor = transaction.type === 'purchase' ? chalk.green : chalk.red; const typeSymbol = transaction.type === 'purchase' ? '+' : '-'; console.log(`${index + 1}. ${typeColor(typeSymbol + transaction.tokens)} tokens`); console.log(` Type: ${transaction.type}`); console.log(` Date: ${formatTimestamp(transaction.timestamp)}`); if (transaction.description) { console.log(` Description: ${transaction.description}`); } console.log(''); }); })); // Setup Command program .command('setup') .description('Configure authentication and settings') .action(async () => { const { MCPServerSetup } = await import('./setup.js'); const setup = new MCPServerSetup(); await setup.run(); }); // Test Command program .command('test') .description('Test authentication and connection') .action(async () => { console.log(chalk.blue('šŸ” Testing authentication and connection...\n')); try { await loadServices(); const isAuthenticated = await authManager.testAuthentication(); if (isAuthenticated) { console.log(chalk.green('āœ… Authentication successful!')); console.log(chalk.green('āœ… Connection to API is working')); // Test a simple API call spinner.start('Testing API endpoints...'); const response = await containerService.listContainers({ limit: 1 }); spinner.stop(); console.log(chalk.green('āœ… API endpoints are accessible')); console.log(chalk.blue(`Found ${response.total} containers in your account`)); } else { console.log(chalk.red('āŒ Authentication failed')); console.log(chalk.yellow('Run "setup" command to configure authentication')); process.exit(1); } } catch (error) { handleError(error); } }); // Program configuration program .name('containers-cli') .description('172.ai Container Management CLI Tool') .version('1.0.17') .option('-c, --config <path>', 'Path to config file') .option('-v, --verbose', 'Verbose output') .hook('preAction', async (thisCommand, actionCommand) => { if (thisCommand.opts().verbose) { process.env.LOG_LEVEL = 'debug'; } if (thisCommand.opts().config) { process.env.CONFIG_PATH = thisCommand.opts().config; } }); // Handle unknown commands program.on('command:*', () => { console.error(chalk.red('Invalid command: %s'), program.args.join(' ')); console.log('See --help for a list of available commands.'); process.exit(1); }); // Parse command line arguments if (process.argv.length === 2) { program.help(); } program.parse(process.argv); //# sourceMappingURL=cli-tool.js.map