dreamhost-deployer
Version:
A stylish, interactive CLI tool for deploying websites to DreamHost shared hosting with automated build integration
459 lines (396 loc) ⢠19.2 kB
JavaScript
/**
* Server environment check utility
* Checks if the DreamHost server has the required NVM and Node.js versions
*/
const { execSync } = require('child_process');
const chalk = require('chalk');
const inquirer = require('inquirer');
const fs = require('fs');
const path = require('path');
const os = require('os');
const { Client } = require('ssh2'); // Add ssh2 client for cross-platform compatibility
// Recommended versions
const RECOMMENDED_NVM_VERSION = '0.40.1';
const RECOMMENDED_NODE_VERSION = '22.14.0';
const RECOMMENDED_PYTHON_VERSION = '3.9.0'; // Minimum recommended Python version
/**
* Check if NVM and Node.js are installed on the server
* @param {Object} config - Configuration object with SSH details
* @returns {Promise<boolean>} - True if setup is needed, false if everything is OK
*/
async function checkServerEnvironment(config) {
console.log(chalk.blue('\nđ Checking server environment...'));
try {
// Verify SSH connectivity first
if (!await verifySSHConnection(config)) {
console.log(chalk.yellow('â ď¸ Cannot proceed with server environment check due to SSH connection issues.'));
return true; // Setup needed, but will be handled by verifySSHConnection
}
// Create SSH connection for all checks
const conn = new Client();
// Set up authentication
let authConfig = {
host: config.host,
username: config.username,
readyTimeout: 30000
};
// Only password authentication is supported
authConfig.password = config.password;
// Connect to the server
try {
await new Promise((resolve, reject) => {
let connTimeout = setTimeout(() => {
conn.end();
reject(new Error('SSH connection timeout after 15 seconds'));
}, 15000);
conn.on('ready', () => {
clearTimeout(connTimeout);
resolve();
}).on('error', (err) => {
clearTimeout(connTimeout);
reject(err);
}).connect(authConfig);
});
} catch (error) {
console.error(chalk.red(`â Failed to connect to server: ${error.message}`));
return true; // Setup needed due to connection issues
}
// Helper function to execute commands over SSH
const executeCommand = async (command) => {
return new Promise((resolve, reject) => {
conn.exec(command, (err, stream) => {
if (err) return reject(err);
let output = '';
stream.on('close', () => {
resolve(output);
}).on('data', (data) => {
output += data.toString();
}).stderr.on('data', (data) => {
output += data.toString();
});
});
});
};
// Get the project type to determine what environment checks to run
const buildIntegration = require('./build-integration');
const projectInfo = buildIntegration.detectProjectType();
const projectType = projectInfo.type || 'unknown';
// Check if it's a Python project
const isPythonProject = ['python', 'django', 'flask', 'fastapi'].includes(projectType);
const isNodeProject = ['vite', 'cra', 'nextjs', 'gatsby', 'nuxt', 'vue-cli', 'svelte', 'angular', 'generic'].includes(projectType);
console.log(chalk.blue(`Detected project type: ${projectType}`));
// Check for Python if it's a Python project
let pythonInstalled = false;
let pythonVersion = '';
let pythonNeedsUpdate = false;
let pipInstalled = false;
let venvInstalled = false;
if (isPythonProject) {
console.log(chalk.cyan('Checking for Python installation...'));
try {
// Check for Python 3
const pythonOutput = await executeCommand('python3 --version || python --version');
const trimmedOutput = pythonOutput.trim();
if (trimmedOutput && trimmedOutput.includes('Python')) {
pythonInstalled = true;
pythonVersion = trimmedOutput.replace('Python ', '');
console.log(chalk.green(`â
Python is installed (version ${pythonVersion})`));
// Check if Python version meets recommendations
pythonNeedsUpdate = compareVersions(pythonVersion, RECOMMENDED_PYTHON_VERSION) < 0;
if (pythonNeedsUpdate) {
console.log(chalk.yellow(`â ď¸ Python version ${pythonVersion} is older than recommended version ${RECOMMENDED_PYTHON_VERSION}`));
}
// Check for pip
try {
const pipOutput = await executeCommand('pip3 --version || pip --version');
if (pipOutput && pipOutput.includes('pip')) {
pipInstalled = true;
console.log(chalk.green('â
pip is installed'));
} else {
console.log(chalk.yellow('â ď¸ pip is not installed'));
}
} catch (error) {
console.log(chalk.yellow('â ď¸ pip is not installed or not in PATH'));
}
// Check for virtualenv/venv
try {
const venvOutput = await executeCommand('python3 -m venv --help || python -m venv --help || virtualenv --version');
if (venvOutput) {
venvInstalled = true;
console.log(chalk.green('â
virtualenv/venv is installed'));
} else {
console.log(chalk.yellow('â ď¸ virtualenv/venv is not installed'));
}
} catch (error) {
console.log(chalk.yellow('â ď¸ virtualenv/venv is not installed'));
}
} else {
console.log(chalk.yellow('â ď¸ Python is not installed or not properly configured'));
}
} catch (error) {
console.log(chalk.yellow(`â ď¸ Python is not installed or not properly configured: ${error.message}`));
}
}
// Check for Node.js environment if needed
let nvmInstalled = false;
let nvmVersion = '';
let nodeInstalled = false;
let nodeVersion = '';
let nvmNeedsUpdate = false;
let nodeNeedsUpdate = false;
if (isNodeProject || !isPythonProject) {
// Check if NVM is installed
console.log(chalk.cyan('Checking for NVM installation...'));
try {
const nvmOutput = await executeCommand('source ~/.nvm/nvm.sh 2>/dev/null && nvm --version');
nvmVersion = nvmOutput.trim();
if (nvmVersion) {
nvmInstalled = true;
console.log(chalk.green(`â
NVM is installed (version ${nvmVersion})`));
// Check if NVM version meets recommendations
nvmNeedsUpdate = compareVersions(nvmVersion, RECOMMENDED_NVM_VERSION) < 0;
if (nvmNeedsUpdate) {
console.log(chalk.yellow(`â ď¸ NVM version ${nvmVersion} is older than recommended version ${RECOMMENDED_NVM_VERSION}`));
}
} else {
console.log(chalk.yellow('â ď¸ NVM is not installed or not properly configured'));
}
} catch (error) {
console.log(chalk.yellow('â ď¸ NVM is not installed or not properly configured'));
}
// Check if Node.js is installed
console.log(chalk.cyan('Checking for Node.js installation...'));
try {
const nodeOutput = await executeCommand('source ~/.nvm/nvm.sh 2>/dev/null && node --version');
const trimmedOutput = nodeOutput.trim();
if (trimmedOutput) {
nodeInstalled = true;
nodeVersion = trimmedOutput.replace('v', '');
console.log(chalk.green(`â
Node.js is installed (version ${trimmedOutput})`));
// Check if Node.js version meets recommendations
nodeNeedsUpdate = compareVersions(nodeVersion, RECOMMENDED_NODE_VERSION) < 0;
if (nodeNeedsUpdate) {
console.log(chalk.yellow(`â ď¸ Node.js version ${nodeVersion} is older than recommended version ${RECOMMENDED_NODE_VERSION}`));
}
} else {
console.log(chalk.yellow('â ď¸ Node.js is not installed or not properly configured'));
}
} catch (error) {
console.log(chalk.yellow('â ď¸ Node.js is not installed or not properly configured'));
}
}
// Close the SSH connection
conn.end();
// Determine if setup is needed based on project type
let setupNeeded = false;
if (isPythonProject) {
setupNeeded = !pythonInstalled || pythonNeedsUpdate || !pipInstalled;
if (setupNeeded) {
console.log(chalk.yellow('â ď¸ Python environment needs setup or updates'));
} else {
console.log(chalk.green('â
Python environment is properly configured'));
}
} else if (isNodeProject) {
setupNeeded = !nvmInstalled || !nodeInstalled || nvmNeedsUpdate || nodeNeedsUpdate;
if (setupNeeded) {
console.log(chalk.yellow('â ď¸ Node.js environment needs setup or updates'));
} else {
console.log(chalk.green('â
Node.js environment is properly configured'));
}
} else {
// For unknown project types, check both environments
setupNeeded = (!pythonInstalled && !nodeInstalled);
if (setupNeeded) {
console.log(chalk.yellow('â ď¸ Neither Python nor Node.js environments are properly configured'));
} else {
console.log(chalk.green('â
Server environment is properly configured'));
}
}
return setupNeeded;
} catch (error) {
console.error(chalk.red(`â Error checking server environment: ${error.message}`));
return true; // Assume setup is needed due to error
}
}
/**
* Verify SSH connection to the server
* @param {Object} config - Configuration object with SSH details
* @returns {Promise<boolean>} - True if connection is successful, false otherwise
*/
async function verifySSHConnection(config) {
console.log(chalk.cyan('Verifying SSH connection...'));
// Add timeout for the entire verification process
const connectionPromise = verifySSHConnectionInternal(config);
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('SSH verification timed out after 30 seconds')), 30000)
);
try {
return await Promise.race([connectionPromise, timeoutPromise]);
} catch (error) {
console.error(chalk.red(`â SSH verification failed: ${error.message}`));
return false;
}
}
/**
* Internal function for SSH connection verification with proper error handling
*/
async function verifySSHConnectionInternal(config) {
// Check if we have a password or need to ask for one
let password = config.password;
// Only try password authentication
try {
console.log(chalk.cyan('Attempting password authentication...'));
await new Promise((resolve, reject) => {
const conn = new Client();
let passwordTimeout = setTimeout(() => {
conn.end();
reject(new Error('Password authentication timeout after 15 seconds'));
}, 15000);
conn.on('ready', () => {
clearTimeout(passwordTimeout);
console.log(chalk.green('â
Password authentication successful'));
conn.end();
resolve(true);
}).on('error', (err) => {
clearTimeout(passwordTimeout);
reject(err);
}).connect({
host: config.host,
username: config.username,
password: config.password,
readyTimeout: 30000
});
});
return true;
} catch (error) {
console.log(chalk.yellow(`â ď¸ Password authentication failed: ${error.message}`));
// If we don't have a password, ask for one
if (!password) {
const { sshPassword } = await inquirer.prompt([
{
type: 'password',
name: 'sshPassword',
message: 'Enter your SSH password:',
mask: '*'
}
]);
password = sshPassword;
// Ask if user wants to save the password
const { savePassword } = await inquirer.prompt([
{
type: 'confirm',
name: 'savePassword',
message: 'Would you like to save your password for future operations? (Not recommended for security reasons)',
default: false
}
]);
if (savePassword) {
config.password = password;
const configPath = path.resolve(process.cwd(), 'deploy.config.json');
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
console.log(chalk.green('â
Password saved to configuration.'));
}
}
// Try password authentication using ssh2 library
try {
console.log(chalk.cyan('Attempting password authentication...'));
await new Promise((resolve, reject) => {
const conn = new Client();
let passwordTimeout = setTimeout(() => {
conn.end();
reject(new Error('Password authentication timeout after 15 seconds'));
}, 15000);
conn.on('ready', () => {
clearTimeout(passwordTimeout);
console.log(chalk.green('â
Password authentication successful'));
conn.end();
resolve(true);
}).on('error', (err) => {
clearTimeout(passwordTimeout);
reject(err);
}).connect({
host: config.host,
username: config.username,
password: password,
readyTimeout: 30000
});
});
// Save password to config if successful
if (!config.password && password) {
config.password = password;
console.log(chalk.green('â
Password saved for future connections'));
}
return true;
} catch (error) {
console.error(chalk.red(`â Password authentication failed: ${error.message}`));
// Provide troubleshooting guidance
console.log(chalk.yellow('\nâ ď¸ SSH Authentication Troubleshooting:'));
console.log(chalk.cyan('1. Verify your username and host are correct'));
console.log(chalk.cyan('2. Check if password authentication is enabled on the server'));
console.log(chalk.cyan('3. Ensure your SSH key permissions are correct (if using key authentication)'));
console.log(chalk.cyan(' - ~/.ssh directory: 700 (drwx------)'));
console.log(chalk.cyan(' - SSH private key: 600 (-rw-------)'));
console.log(chalk.cyan(' - SSH public key: 644 (-rw-r--r--)'));
console.log(chalk.cyan(' - authorized_keys: 600 (-rw-------)'));
console.log(chalk.cyan('4. Try connecting manually with verbose output:'));
console.log(chalk.cyan(` ssh -v ${config.username}@${config.host}`));
return false;
}
}
}
/**
* Compare two version strings
* @param {string} v1 - First version
* @param {string} v2 - Second version
* @returns {number} - -1 if v1 < v2, 0 if v1 == v2, 1 if v1 > v2
*/
function compareVersions(v1, v2) {
const v1Parts = v1.split('.').map(Number);
const v2Parts = v2.split('.').map(Number);
for (let i = 0; i < Math.max(v1Parts.length, v2Parts.length); i++) {
const v1Part = v1Parts[i] || 0;
const v2Part = v2Parts[i] || 0;
if (v1Part < v2Part) return -1;
if (v1Part > v2Part) return 1;
}
return 0;
}
/**
* Check server environment and offer to set up if needed
* @param {Object} config - Configuration object with SSH details
*/
async function checkAndSetupServerIfNeeded(config) {
// First verify SSH connection
if (!await verifySSHConnection(config)) {
console.log(chalk.yellow('\nâ ď¸ Cannot proceed with server environment check due to SSH connection issues.'));
console.log(chalk.yellow('â ď¸ Please fix SSH connection issues before continuing.'));
return;
}
const setupNeeded = await checkServerEnvironment(config);
if (setupNeeded) {
const { shouldSetup } = await inquirer.prompt([
{
type: 'confirm',
name: 'shouldSetup',
message: 'Would you like to set up NVM and Node.js on your DreamHost server now?',
default: true
}
]);
if (shouldSetup) {
// Use dynamic require to avoid circular dependency
const setupNode = require('../commands/setup-node');
await setupNode.run(config);
} else {
console.log(chalk.yellow('\nâ ď¸ Skipping server setup. You can run it later with:'));
console.log(chalk.cyan('dreamhost-deployer setup-node'));
}
}
}
module.exports = {
checkServerEnvironment,
checkAndSetupServerIfNeeded,
verifySSHConnection,
RECOMMENDED_NVM_VERSION,
RECOMMENDED_NODE_VERSION,
RECOMMENDED_PYTHON_VERSION
};