hana-cli
Version:
HANA Developer Command Line Interface
483 lines (439 loc) • 14.4 kB
text/typescript
import { spawn } from 'child_process';
import { existsSync } from 'fs';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
import { formatOutput } from './output-formatter.js';
import { getNextSteps, analyzeOutputForTips } from './next-steps.js';
import { ConnectionContext } from './connection-context.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
/**
* Result of command execution
*/
export interface ExecutionResult {
success: boolean;
output: string;
error?: string;
}
/**
* Enhanced error information with suggestions
*/
interface ErrorAnalysis {
errorType: string;
originalError: string;
possibleCauses: string[];
suggestions: Array<{
action: string;
command?: string;
parameters?: Record<string, any>;
}>;
}
/**
* Analyzes error messages and provides actionable suggestions
*/
function analyzeError(commandName: string, error: string, output: string): ErrorAnalysis {
const errorLower = error.toLowerCase();
const outputLower = output.toLowerCase();
const combined = errorLower + ' ' + outputLower;
// Table not found errors
if (combined.includes('table') && (combined.includes('not found') || combined.includes('does not exist') || combined.includes('invalid'))) {
return {
errorType: 'TABLE_NOT_FOUND',
originalError: error,
possibleCauses: [
'Table name is case-sensitive - check capitalization',
'Table may be in a different schema',
'Table may not exist yet',
'User may not have permission to see the table',
],
suggestions: [
{
action: 'List tables in the schema to verify the table name',
command: 'hana_tables',
parameters: { schema: '<schema-name>' },
},
{
action: 'List all available schemas',
command: 'hana_schemas',
},
{
action: 'Check current user and permissions',
command: 'hana_status',
},
],
};
}
// Schema not found errors
if (combined.includes('schema') && (combined.includes('not found') || combined.includes('does not exist') || combined.includes('invalid'))) {
return {
errorType: 'SCHEMA_NOT_FOUND',
originalError: error,
possibleCauses: [
'Schema name is case-sensitive',
'Schema does not exist',
'User does not have access to the schema',
],
suggestions: [
{
action: 'List all available schemas',
command: 'hana_schemas',
},
{
action: 'Check current user permissions',
command: 'hana_status',
},
],
};
}
// File not found errors
if (combined.includes('file') && (combined.includes('not found') || combined.includes('enoent') || combined.includes('cannot find'))) {
return {
errorType: 'FILE_NOT_FOUND',
originalError: error,
possibleCauses: [
'File path is incorrect',
'File does not exist at the specified location',
'Relative path may need to be absolute',
],
suggestions: [
{
action: 'Check that the file exists and path is correct',
},
{
action: 'Use absolute file paths instead of relative paths',
},
],
};
}
// Connection errors
if (combined.includes('connect') || combined.includes('connection') || combined.includes('econnrefused') || combined.includes('etimedout')) {
return {
errorType: 'CONNECTION_ERROR',
originalError: error,
possibleCauses: [
'Database credentials not configured',
'Database server is not reachable',
'Network connectivity issue',
'Database is offline or maintenance',
],
suggestions: [
{
action: 'Verify database connection settings in .env or default-env.json',
},
{
action: 'Check if database server is running',
},
{
action: 'Test basic connectivity',
command: 'hana_status',
},
],
};
}
// Authentication errors
if (combined.includes('authenticat') || combined.includes('authorization') || combined.includes('credential') || combined.includes('permission denied')) {
return {
errorType: 'AUTHENTICATION_ERROR',
originalError: error,
possibleCauses: [
'Invalid username or password',
'User account may be locked or expired',
'Insufficient privileges for the operation',
],
suggestions: [
{
action: 'Verify credentials in .env or default-env.json',
},
{
action: 'Check user status and roles',
command: 'hana_status',
},
{
action: 'Contact database administrator for access',
},
],
};
}
// Timeout errors
if (combined.includes('timeout') || combined.includes('timed out')) {
return {
errorType: 'TIMEOUT',
originalError: error,
possibleCauses: [
'Operation taking too long (default 30s)',
'Large dataset requires more time',
'Database performance issue',
],
suggestions: [
{
action: 'For data operations, consider filtering or limiting results',
},
{
action: 'Check system health',
command: 'hana_healthCheck',
},
{
action: 'For import/export, use timeoutSeconds parameter to increase timeout',
},
],
};
}
// Parameter/syntax errors
if (combined.includes('parameter') || combined.includes('argument') || combined.includes('required') || combined.includes('missing')) {
return {
errorType: 'PARAMETER_ERROR',
originalError: error,
possibleCauses: [
'Required parameter is missing',
'Parameter value format is incorrect',
'Parameter name may be misspelled',
],
suggestions: [
{
action: 'Check parameter requirements and examples',
command: 'hana_examples',
parameters: { command: commandName },
},
{
action: 'View parameter presets for this command',
command: 'hana_parameter_presets',
parameters: { command: commandName },
},
],
};
}
// Generic error with generic suggestions
return {
errorType: 'UNKNOWN_ERROR',
originalError: error,
possibleCauses: [
'Check the error message for specific details',
],
suggestions: [
{
action: 'Try checking system health',
command: 'hana_healthCheck',
},
{
action: 'View examples for this command',
command: 'hana_examples',
parameters: { command: commandName },
},
],
};
}
/**
* Executes a hana-cli command and captures its output
*
* @param commandName - The command to execute (e.g., 'status', 'tables')
* @param args - Arguments to pass to the command as key-value pairs
* @param context - Optional connection context for project-specific connections
* @returns Promise with execution result including the command name for formatting
*/
export async function executeCommand(
commandName: string,
args: Record<string, any> = {},
context?: ConnectionContext
): Promise<ExecutionResult & { commandName: string }> {
return new Promise((resolve) => {
try {
// Build the CLI path - go up from build/ to project root, then to bin/cli.js
const cliPath = join(__dirname, '..', '..', 'bin', 'cli.js');
// Convert args object to command line arguments
const commandArgs: string[] = [commandName];
for (const [key, value] of Object.entries(args)) {
if (value === undefined || value === null) continue;
// Handle boolean flags
if (typeof value === 'boolean') {
if (value) {
commandArgs.push(`--${key}`);
}
continue;
}
// Handle arrays
if (Array.isArray(value)) {
value.forEach(v => {
commandArgs.push(`--${key}`, String(v));
});
continue;
}
// Handle other values
commandArgs.push(`--${key}`, String(value));
}
let stdout = '';
let stderr = '';
// Build environment with connection context
const env: Record<string, string> = {
...process.env,
// Ensure stdio output is captured
FORCE_COLOR: '0',
};
// Apply project context to environment
if (context?.projectPath) {
env.HANA_CLI_PROJECT_PATH = context.projectPath;
}
if (context?.connectionFile) {
env.HANA_CLI_CONN_FILE = context.connectionFile;
}
// Set direct credentials if provided (use cautiously for security)
if (context?.host) {
env.HANA_CLI_HOST = context.host;
env.HANA_CLI_PORT = String(context.port || 30013);
if (context.user) {
env.HANA_CLI_USER = context.user;
}
if (context.password) {
env.HANA_CLI_PASSWORD = context.password;
}
if (context.database) {
env.HANA_CLI_DATABASE = context.database;
}
}
// Determine working directory based on context
let cwd: string;
if (context?.projectPath) {
cwd = context.projectPath;
} else {
// Prefer the process working directory (where MCP server was launched)
// over the hana-cli install directory, since agents typically launch
// the MCP server from within the target project
const launchDir = process.cwd();
const hasProjectMarker = ['default-env.json', '.env', 'package.json', '.cdsrc-private.json']
.some(f => existsSync(join(launchDir, f)));
cwd = hasProjectMarker ? launchDir : join(__dirname, '..', '..');
}
// Spawn the CLI process
const child = spawn('node', [cliPath, ...commandArgs], {
env,
cwd,
});
// Capture stdout
child.stdout.on('data', (data) => {
stdout += data.toString();
});
// Capture stderr
child.stderr.on('data', (data) => {
stderr += data.toString();
});
// Handle process completion
child.on('close', (code) => {
if (code === 0) {
resolve({
success: true,
output: stdout || 'Command completed successfully',
commandName,
});
} else {
resolve({
success: false,
output: stdout,
error: stderr || `Command exited with code ${code}`,
commandName,
});
}
});
// Handle process errors
child.on('error', (error) => {
resolve({
success: false,
output: '',
error: `Failed to execute command: ${error.message}`,
commandName,
});
});
// Set a timeout to prevent hanging
const timeout = setTimeout(() => {
child.kill();
resolve({
success: false,
output: stdout,
error: 'Command execution timeout (30s)',
commandName,
});
}, 30000);
child.on('close', () => {
clearTimeout(timeout);
});
} catch (error) {
resolve({
success: false,
output: '',
error: `Execution error: ${error instanceof Error ? error.message : String(error)}`,
commandName,
});
}
});
}
/**
* Formats execution result for display using the output formatter
*/
export function formatResult(result: ExecutionResult & { commandName: string }): string {
if (result.success) {
// Apply the formatter to the output
let formattedOutput = formatOutput(result.commandName, result.output);
// Add context-aware tips based on output analysis
const tips = analyzeOutputForTips(result.commandName, result.output);
if (tips.length > 0) {
formattedOutput += '\n\n**📌 Tips:**\n' + tips.join('\n');
}
// Add suggested next steps
const nextSteps = getNextSteps(result.commandName, result.output);
if (nextSteps.length > 0) {
formattedOutput += '\n\n**🔄 Suggested Next Steps:**\n';
nextSteps.forEach((step, i) => {
formattedOutput += `${i + 1}. **${step.description}**\n`;
if (step.reason) {
formattedOutput += ` ${step.reason}\n`;
}
if (step.parameters) {
const paramStr = Object.entries(step.parameters)
.map(([k, v]) => `${k}: "${v}"`)
.join(', ');
formattedOutput += ` → Use: \`hana_${step.command}\` with { ${paramStr} }\n`;
} else {
formattedOutput += ` → Use: \`hana_${step.command}\`\n`;
}
});
}
return formattedOutput;
} else {
// Analyze the error and provide helpful suggestions
const errorAnalysis = analyzeError(result.commandName, result.error || '', result.output);
const parts = [];
// Add the error header
parts.push('❌ **Command Failed**\n');
// Add original error
parts.push('**Error:**');
parts.push(errorAnalysis.originalError);
// Add output if available
if (result.output && result.output.trim()) {
parts.push('\n**Output:**');
parts.push(result.output);
}
// Add possible causes
if (errorAnalysis.possibleCauses.length > 0) {
parts.push('\n**Possible Causes:**');
errorAnalysis.possibleCauses.forEach((cause, i) => {
parts.push(`${i + 1}. ${cause}`);
});
}
// Add actionable suggestions
if (errorAnalysis.suggestions.length > 0) {
parts.push('\n**💡 Suggestions:**');
errorAnalysis.suggestions.forEach((suggestion, i) => {
parts.push(`${i + 1}. ${suggestion.action}`);
if (suggestion.command) {
if (suggestion.parameters) {
const paramStr = Object.entries(suggestion.parameters)
.map(([k, v]) => `${k}: "${v}"`)
.join(', ');
parts.push(` → Try: \`${suggestion.command}\` with parameters: { ${paramStr} }`);
} else {
parts.push(` → Try: \`${suggestion.command}\``);
}
}
});
}
return parts.join('\n');
}
}