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
JavaScript
/**
* 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');
}
}