@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
JavaScript
/**
* 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