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