UNPKG

accs-cli

Version:

ACCS CLI — Full-featured developer tool for scaffolding, running, building, and managing multi-language projects

323 lines (273 loc) 9.42 kB
/** * Deploy command - Deploy to various platforms */ import path from 'path'; import chalk from 'chalk'; import inquirer from 'inquirer'; import { logger } from '../utils/logger.js'; import { FileUtils } from '../utils/file-utils.js'; import { configManager } from '../config/config-manager.js'; import { execa } from 'execa'; const DEPLOY_TARGETS = { 'github-pages': { name: 'GitHub Pages', description: 'Deploy to GitHub Pages using gh-pages' }, 'sftp': { name: 'SFTP', description: 'Deploy via SFTP to remote server' }, 'netlify': { name: 'Netlify', description: 'Deploy to Netlify (manual upload)' } }; export function deployCommand(program) { program .command('deploy') .option('-t, --target <target>', 'Deployment target') .option('-b, --build', 'Build before deploying') .option('-c, --config <file>', 'Deployment configuration file') .option('-v, --verbose', 'Verbose output') .description('Deploy project to various platforms') .action(async (options) => { try { await deployProject(options); } catch (error) { logger.error('Deployment failed:', error.message); process.exit(1); } }); } async function deployProject(options) { const projectRoot = FileUtils.getProjectRoot(); // Build first if requested if (options.build) { logger.info('Building project before deployment...'); await buildBeforeDeploy(projectRoot); } // Get deployment target let target = options.target || configManager.get('deployTarget'); if (!target || !DEPLOY_TARGETS[target]) { const { selectedTarget } = await inquirer.prompt([ { type: 'list', name: 'selectedTarget', message: 'Choose deployment target:', choices: Object.entries(DEPLOY_TARGETS).map(([key, info]) => ({ name: `${info.name} - ${chalk.gray(info.description)}`, value: key })) } ]); target = selectedTarget; } // Execute deployment switch (target) { case 'github-pages': await deployToGitHubPages(projectRoot, options); break; case 'sftp': await deployViaSFTP(projectRoot, options); break; case 'netlify': await deployToNetlify(projectRoot, options); break; default: throw new Error(`Unknown deployment target: ${target}`); } } async function buildBeforeDeploy(projectRoot) { try { const { execa } = await import('execa'); await execa('npm', ['run', 'build'], { cwd: projectRoot }); logger.success('Build completed'); } catch (error) { // Try accs build as fallback try { await execa('node', [path.join(import.meta.url, '../../../bin/accs.js'), 'build'], { cwd: projectRoot }); logger.success('Build completed with ACCS'); } catch (buildError) { throw new Error(`Build failed: ${buildError.message}`); } } } async function deployToGitHubPages(projectRoot) { logger.info('🚀 Deploying to GitHub Pages...'); const buildDir = path.join(projectRoot, 'dist'); if (!FileUtils.exists(buildDir)) { throw new Error('Build directory not found. Run with --build flag or build manually first.'); } // Check if gh-pages is installed try { await import('gh-pages'); } catch (error) { logger.warn('gh-pages package not found, installing...'); const { execa } = await import('execa'); await execa('npm', ['install', '--save-dev', 'gh-pages'], { cwd: projectRoot }); } try { const ghpages = await import('gh-pages'); const deployOptions = { src: '**/*', dotfiles: true, message: `Deploy ${new Date().toISOString()}` }; // Check for custom domain const cnameFile = path.join(buildDir, 'CNAME'); if (FileUtils.exists(cnameFile)) { logger.info('Custom domain detected'); } logger.startSpinner('Publishing to GitHub Pages...'); await new Promise((resolve, reject) => { ghpages.publish(buildDir, deployOptions, (err) => { if (err) reject(err); else resolve(); }); }); logger.succeedSpinner('Successfully deployed to GitHub Pages!'); // Try to get repository info for URL try { const packageJson = await FileUtils.readJson(path.join(projectRoot, 'package.json')); if (packageJson.repository?.url) { const repoUrl = packageJson.repository.url .replace('git+', '') .replace('.git', ''); const githubUrl = repoUrl.replace('https://github.com/', 'https://'); logger.info(`Your site should be available at: ${chalk.cyan(githubUrl.replace('https://github.com/', 'https://').replace('/', '.github.io/'))}`); } } catch (error) { // Ignore errors getting repo info } } catch (error) { throw new Error(`GitHub Pages deployment failed: ${error.message}`); } } async function deployViaSFTP(projectRoot, options) { logger.info('📤 Deploying via SFTP...'); const buildDir = path.join(projectRoot, 'dist'); if (!FileUtils.exists(buildDir)) { throw new Error('Build directory not found. Run with --build flag or build manually first.'); } // Load SFTP configuration let sftpConfig; if (options.config) { try { const configPath = path.resolve(options.config); sftpConfig = await import(configPath); sftpConfig = sftpConfig.default || sftpConfig; } catch (error) { throw new Error(`Failed to load config file: ${error.message}`); } } else { // Interactive configuration sftpConfig = await promptForSftpConfig(); } try { const { NodeSSH } = await import('node-ssh'); const ssh = new NodeSSH(); logger.startSpinner('Connecting to server...'); await ssh.connect({ host: sftpConfig.host, username: sftpConfig.username, password: sftpConfig.password, port: sftpConfig.port || 22 }); logger.succeedSpinner('Connected to server'); // Upload files logger.startSpinner('Uploading files...'); await ssh.putDirectory(buildDir, sftpConfig.remotePath, { recursive: true, concurrency: 5, validate: (itemPath) => { // Skip hidden files and directories return !path.basename(itemPath).startsWith('.'); } }); logger.succeedSpinner('Files uploaded successfully!'); ssh.dispose(); logger.success(`Deployed to: ${sftpConfig.host}:${sftpConfig.remotePath}`); } catch (error) { throw new Error(`SFTP deployment failed: ${error.message}`); } } async function promptForSftpConfig() { logger.info('SFTP configuration needed:'); const config = await inquirer.prompt([ { type: 'input', name: 'host', message: 'Server hostname:', validate: (input) => input.length > 0 || 'Hostname is required' }, { type: 'input', name: 'username', message: 'Username:', validate: (input) => input.length > 0 || 'Username is required' }, { type: 'password', name: 'password', message: 'Password:', validate: (input) => input.length > 0 || 'Password is required' }, { type: 'input', name: 'remotePath', message: 'Remote path:', default: '/var/www/html', validate: (input) => input.length > 0 || 'Remote path is required' }, { type: 'number', name: 'port', message: 'Port:', default: 22 } ]); return config; } async function deployToNetlify(projectRoot) { logger.info('🌐 Preparing for Netlify deployment...'); const buildDir = path.join(projectRoot, 'dist'); if (!FileUtils.exists(buildDir)) { throw new Error('Build directory not found. Run with --build flag or build manually first.'); } // Create a zip file for manual upload try { const archiver = await import('archiver'); const fs = await import('fs'); const zipPath = path.join(projectRoot, 'netlify-deploy.zip'); const output = fs.createWriteStream(zipPath); const archive = archiver.default('zip', { zlib: { level: 9 } }); logger.startSpinner('Creating deployment archive...'); output.on('close', () => { const sizeInMB = (archive.pointer() / 1024 / 1024).toFixed(2); logger.succeedSpinner(`Archive created: ${sizeInMB} MB`); logger.info('Manual Netlify deployment instructions:'); logger.info(`1. Go to: ${chalk.cyan('https://netlify.com/drop')}`); logger.info(`2. Drag and drop: ${chalk.yellow(zipPath)}`); logger.info('3. Your site will be deployed automatically!'); logger.separator(); logger.info('For automated deployments, consider:'); logger.info('• Netlify CLI: npm install -g netlify-cli'); logger.info('• Git integration: Connect your repository to Netlify'); }); archive.on('error', (err) => { throw err; }); archive.pipe(output); archive.directory(buildDir, false); await archive.finalize(); } catch (error) { // Fallback: just show manual instructions logger.warn('Could not create zip file, showing manual instructions:'); logger.info('Manual Netlify deployment:'); logger.info(`1. Zip the contents of: ${chalk.yellow(path.relative(projectRoot, buildDir))}`); logger.info(`2. Go to: ${chalk.cyan('https://netlify.com/drop')}`); logger.info('3. Drag and drop your zip file'); } }