UNPKG

accs-cli

Version:

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

569 lines (482 loc) 14.7 kB
/** * Initialize new project command */ import inquirer from 'inquirer'; import path from 'path'; import chalk from 'chalk'; import boxen from 'boxen'; import { logger } from '../utils/logger.js'; import { FileUtils } from '../utils/file-utils.js'; import { configManager } from '../config/config-manager.js'; const TEMPLATES = { 'vanilla-js': { name: 'Vanilla JavaScript', description: 'Plain HTML, CSS, and JavaScript project' }, 'node-express': { name: 'Node.js Express', description: 'Express.js web server with modern setup' }, 'react': { name: 'React', description: 'React application with Vite' }, 'vue': { name: 'Vue.js', description: 'Vue.js application with Vite' }, 'typescript': { name: 'TypeScript', description: 'TypeScript project with modern tooling' }, 'python-flask': { name: 'Python Flask', description: 'Flask web application' }, 'php-basic': { name: 'PHP Basic', description: 'Basic PHP web application' } }; export function initCommand(program) { program .command('init') .argument('[project-name]', 'Project name') .option('-t, --template <template>', 'Template to use') .option('-f, --force', 'Overwrite existing directory') .option('--no-install', 'Skip dependency installation') .option('--no-git', 'Skip git initialization') .description('Initialize a new project') .action(async (projectName, options) => { try { await initProject(projectName, options); } catch (error) { logger.error('Failed to initialize project:', error.message); process.exit(1); } }); } async function initProject(projectName, options) { let name = projectName; let template = options.template || configManager.get('defaultTemplate'); // Interactive prompts if no arguments provided if (!name) { const { projectName: inputName } = await inquirer.prompt([ { type: 'input', name: 'projectName', message: 'Project name:', default: 'my-project', validate: (input) => { if (!input.trim()) return 'Project name is required'; if (!/^[a-zA-Z0-9-_]+$/.test(input)) { return 'Project name can only contain letters, numbers, hyphens, and underscores'; } return true; } } ]); name = inputName; } if (!template || !TEMPLATES[template]) { const { selectedTemplate } = await inquirer.prompt([ { type: 'list', name: 'selectedTemplate', message: 'Choose a template:', choices: Object.entries(TEMPLATES).map(([key, info]) => ({ name: `${info.name} - ${chalk.gray(info.description)}`, value: key })), default: template || 'vanilla-js' } ]); template = selectedTemplate; } const projectDir = path.join(process.cwd(), name); // Check if directory exists if (FileUtils.exists(projectDir)) { if (!options.force) { const { overwrite } = await inquirer.prompt([ { type: 'confirm', name: 'overwrite', message: `Directory "${name}" already exists. Overwrite?`, default: false } ]); if (!overwrite) { logger.info('Project initialization cancelled'); return; } } await FileUtils.remove(projectDir); } logger.startSpinner(`Creating project "${name}" with ${TEMPLATES[template].name} template...`); try { // Create project directory await FileUtils.createDir(projectDir); // Copy template files const templateDir = path.join(FileUtils.getTemplatesDir(), template); if (!FileUtils.exists(templateDir)) { // Create basic template if it doesn't exist await createBasicTemplate(projectDir, template, name); } else { await FileUtils.copy(templateDir, projectDir); } // Process template variables await processTemplateVariables(projectDir, { projectName: name, template: template, description: `A ${TEMPLATES[template].name} project` }); logger.succeedSpinner(`Project "${name}" created successfully!`); // Initialize git if requested if (options.git !== false) { await initializeGit(projectDir); } // Install dependencies if requested if (options.install !== false) { await installDependencies(projectDir); } // Show success message showSuccessMessage(name, template); } catch (error) { logger.failSpinner('Failed to create project'); throw error; } } async function createBasicTemplate(projectDir, template, name) { const templates = { 'vanilla-js': () => createVanillaJSTemplate(projectDir, name), 'node-express': () => createNodeExpressTemplate(projectDir, name), 'react': () => createReactTemplate(projectDir, name), 'typescript': () => createTypeScriptTemplate(projectDir, name), 'python-flask': () => createPythonFlaskTemplate(projectDir, name), 'php-basic': () => createPhpBasicTemplate(projectDir, name) }; const createTemplate = templates[template]; if (createTemplate) { await createTemplate(); } else { // Fallback to vanilla JS await createVanillaJSTemplate(projectDir, name); } } async function createVanillaJSTemplate(projectDir, name) { // Create directory structure await FileUtils.createDir(path.join(projectDir, 'src')); await FileUtils.createDir(path.join(projectDir, 'public')); // Package.json const packageJson = { name: name, version: '1.0.0', description: `A vanilla JavaScript project`, main: 'src/index.js', scripts: { dev: 'accs serve', build: 'accs build', clean: 'accs clean' }, keywords: ['javascript', 'web'], author: '', license: 'MIT' }; await FileUtils.writeJson(path.join(projectDir, 'package.json'), packageJson); // HTML file const htmlContent = `<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>{{projectName}}</title> <link rel="stylesheet" href="styles.css"> </head> <body> <div id="app"> <h1>Welcome to {{projectName}}!</h1> <p>This is a vanilla JavaScript project created with ACCS.</p> <button id="clickBtn">Click me!</button> <p id="counter">Clicks: 0</p> </div> <script src="index.js"></script> </body> </html>`; await import('fs').then(fs => fs.promises.writeFile(path.join(projectDir, 'src', 'index.html'), htmlContent, 'utf8') ); // CSS file const cssContent = `/* {{projectName}} Styles */ * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; display: flex; align-items: center; justify-content: center; } #app { background: white; padding: 2rem; border-radius: 10px; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); text-align: center; max-width: 400px; width: 90%; } h1 { color: #667eea; margin-bottom: 1rem; } button { background: #667eea; color: white; border: none; padding: 0.75rem 2rem; border-radius: 5px; cursor: pointer; font-size: 1rem; margin: 1rem 0; transition: background 0.3s; } button:hover { background: #5a6fd8; } #counter { font-weight: bold; color: #764ba2; }`; await import('fs').then(fs => fs.promises.writeFile(path.join(projectDir, 'src', 'styles.css'), cssContent, 'utf8') ); // JavaScript file const jsContent = `// {{projectName}} - Main JavaScript file class App { constructor() { this.clickCount = 0; this.init(); } init() { this.bindEvents(); this.updateCounter(); } bindEvents() { const button = document.getElementById('clickBtn'); if (button) { button.addEventListener('click', () => this.handleClick()); } } handleClick() { this.clickCount++; this.updateCounter(); this.showEffect(); } updateCounter() { const counter = document.getElementById('counter'); if (counter) { counter.textContent = \`Clicks: \${this.clickCount}\`; } } showEffect() { const button = document.getElementById('clickBtn'); button.style.transform = 'scale(0.95)'; setTimeout(() => { button.style.transform = 'scale(1)'; }, 100); } } // Initialize app when DOM is loaded document.addEventListener('DOMContentLoaded', () => { new App(); });`; await import('fs').then(fs => fs.promises.writeFile(path.join(projectDir, 'src', 'index.js'), jsContent, 'utf8') ); // README const readmeContent = `# {{projectName}} A vanilla JavaScript project created with ACCS. ## Getting Started \`\`\`bash # Development server accs serve # Build for production accs build # Clean build directory accs clean \`\`\` ## Project Structure \`\`\` src/ ├── index.html ├── styles.css └── index.js \`\`\` `; await import('fs').then(fs => fs.promises.writeFile(path.join(projectDir, 'README.md'), readmeContent, 'utf8') ); } async function createNodeExpressTemplate(projectDir, name) { // Create directory structure await FileUtils.createDir(path.join(projectDir, 'src')); await FileUtils.createDir(path.join(projectDir, 'public')); await FileUtils.createDir(path.join(projectDir, 'views')); // Package.json const packageJson = { name: name, version: '1.0.0', description: 'A Node.js Express application', main: 'src/server.js', type: 'module', scripts: { start: 'node src/server.js', dev: 'node src/server.js', build: 'accs build' }, dependencies: { express: '^4.18.2', cors: '^2.8.5' }, keywords: ['nodejs', 'express', 'web'], author: '', license: 'MIT' }; await FileUtils.writeJson(path.join(projectDir, 'package.json'), packageJson); // Server.js const serverContent = `import express from 'express'; import cors from 'cors'; import path from 'path'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const app = express(); const PORT = process.env.PORT || 3000; // Middleware app.use(cors()); app.use(express.json()); app.use(express.static(path.join(__dirname, '../public'))); // Routes app.get('/', (req, res) => { res.json({ message: 'Welcome to {{projectName}}!', timestamp: new Date().toISOString(), version: '1.0.0' }); }); app.get('/api/health', (req, res) => { res.json({ status: 'OK', uptime: process.uptime() }); }); // Error handling middleware app.use((err, req, res, next) => { console.error(err.stack); res.status(500).json({ error: 'Something went wrong!' }); }); // 404 handler app.use('*', (req, res) => { res.status(404).json({ error: 'Route not found' }); }); app.listen(PORT, () => { console.log(\`🚀 Server running on http://localhost:\${PORT}\`); });`; await import('fs').then(fs => fs.promises.writeFile(path.join(projectDir, 'src', 'server.js'), serverContent, 'utf8') ); } async function processTemplateVariables(projectDir, variables) { const processFile = async (filePath) => { if (!FileUtils.isFile(filePath)) return; try { const content = await import('fs').then(fs => fs.promises.readFile(filePath, 'utf8') ); const processedContent = FileUtils.resolveTemplate(content, variables); await import('fs').then(fs => fs.promises.writeFile(filePath, processedContent, 'utf8') ); } catch (error) { // Skip binary files or files that can't be processed } }; const processDirectory = async (dir) => { const entries = await import('fs').then(fs => fs.promises.readdir(dir)); for (const entry of entries) { const fullPath = path.join(dir, entry); if (FileUtils.isDirectory(fullPath)) { await processDirectory(fullPath); } else { await processFile(fullPath); } } }; await processDirectory(projectDir); } async function initializeGit(projectDir) { try { const { execa } = await import('execa'); await execa('git', ['init'], { cwd: projectDir }); // Create .gitignore const gitignoreContent = `node_modules/ dist/ .env .env.local .DS_Store *.log coverage/ .nyc_output/`; await import('fs').then(fs => fs.promises.writeFile(path.join(projectDir, '.gitignore'), gitignoreContent, 'utf8') ); logger.success('Git repository initialized'); } catch (error) { logger.warn('Failed to initialize git repository:', error.message); } } async function installDependencies(projectDir) { const packageJsonPath = path.join(projectDir, 'package.json'); if (!FileUtils.exists(packageJsonPath)) return; try { logger.startSpinner('Installing dependencies...'); const { execa } = await import('execa'); await execa('npm', ['install'], { cwd: projectDir }); logger.succeedSpinner('Dependencies installed successfully'); } catch (error) { logger.warn('Failed to install dependencies:', error.message); } } function showSuccessMessage(name, template) { const templateName = TEMPLATES[template]?.name || template; const message = chalk.green(`✨ Project "${name}" created successfully! Template: ${templateName} Next steps: ${chalk.cyan(` cd ${name}`)} ${chalk.cyan(' accs serve')} ${chalk.gray('# Start development server')} ${chalk.cyan(' accs build')} ${chalk.gray('# Build for production')} Need help? Run: ${chalk.cyan('accs --help')} `); console.log(boxen(message, { padding: 1, margin: 1, borderStyle: 'round', borderColor: 'green' })); } // Placeholder functions for template creation async function createReactTemplate(projectDir, name) { logger.warn(`React template creation not implemented for ${name} in ${projectDir}`); } async function createTypeScriptTemplate(projectDir, name) { logger.warn(`TypeScript template creation not implemented for ${name} in ${projectDir}`); } async function createPythonFlaskTemplate(projectDir, name) { logger.warn(`Python Flask template creation not implemented for ${name} in ${projectDir}`); } async function createPhpBasicTemplate(projectDir, name) { logger.warn(`PHP Basic template creation not implemented for ${name} in ${projectDir}`); }