Source: bin/avenx.js

#!/usr/bin/env node

const fs = require('fs');
const path = require('path');
const http = require('http');
const { exec } = require('child_process');
const AvenxCompiler = require('../lib/compiler');

const [, , command, ...args] = process.argv;

/**
 * Avenx CLI - Command Line Interface for Avenx-JS.
 */
class AvenxCLI {
    constructor() {
        this.baseDir = process.cwd();
        this.frameworkDir = path.join(__dirname, '..');
    }

    run(command, args) {
        const type = args[0];
        const name = args[1];

        switch (command) {
            case 'init':
                this.initProject();
                break;
            case 'generate':
            case 'g':
                if (type === 'bridge') {
                    this.generateBridge(name);
                } else if (type === 'guard') {
                    this.generateGuard(name);
                } else if (type === 'page' || type === 'p') {
                    this.generatePage(name);
                } else {
                    // Default to component if only one arg or type is 'component'
                    this.generateComponent(name || type);
                }
                break;
            case 'build':
            case 'b':
                this.buildProject();
                break;
            case 'serve':
                this.serveProject(args[0] || process.env.PORT || 3000);
                break;
            case 'help':
            default:
                this.printHelp();
                break;
        }
    }

    /**
     * Initializes a new Avenx project structure.
     */
    initProject() {
        console.log('🚀 Initializing new Avenx-JS project...');
        const dirs = [
            'src/components',
            'src/pages',
            'src/global',
            'src/guards',
            'dist',
            '.vscode'
        ];

        dirs.forEach(dir => {
            const fullPath = path.join(this.baseDir, dir);
            if (!fs.existsSync(fullPath)) {
                fs.mkdirSync(fullPath, { recursive: true });
                console.log(`  Created: ${dir}`);
            }
        });

        // Create initial .vscode files
        const jsConfigPath = path.join(this.baseDir, '.vscode/jsconfig.json');
        if (!fs.existsSync(jsConfigPath)) {
            const template = fs.readFileSync(path.join(this.frameworkDir, 'templates/vscode/jsconfig.json.template'), 'utf-8');
            fs.writeFileSync(jsConfigPath, template);
            console.log('  Created: .vscode/jsconfig.json');
        }

        const settingsPath = path.join(this.baseDir, '.vscode/settings.json');
        if (!fs.existsSync(settingsPath)) {
            const template = fs.readFileSync(path.join(this.frameworkDir, 'templates/vscode/settings.json.template'), 'utf-8');
            fs.writeFileSync(settingsPath, template);
            console.log('  Created: .vscode/settings.json');
        }

        // Create initial index.html
        const indexHtmlPath = path.join(this.baseDir, 'index.html');
        if (!fs.existsSync(indexHtmlPath)) {
            fs.writeFileSync(indexHtmlPath, this.getInitialHtml());
            console.log('  Created: index.html');
        }

        // Create initial main.app.js
        const mainAppPath = path.join(this.baseDir, 'src/main.app.js');
        if (!fs.existsSync(mainAppPath)) {
            fs.writeFileSync(mainAppPath, "import { AvenxApp } from 'avenx-core/runtime';\n\nconst app = new AvenxApp({ target: '#app' });\n");
            console.log('  Created: src/main.app.js');
        }

        console.log('✅ Project initialized successfully!');
    }

    /**
     * Generates a new Bridge class and template file.
     */
    generateBridge(name) {
        if (!name) {
            console.error('❌ Error: Please provide a bridge name (e.g., avenx g bridge auth)');
            return;
        }

        const lowerName = name.toLowerCase();
        const capitalizedName = lowerName
            .split(/[-_]/)
            .map(part => part.charAt(0).toUpperCase() + part.slice(1))
            .join('') + "Bridge";

        const globalDir = path.join(this.baseDir, 'src/global');
        if (!fs.existsSync(globalDir)) {
            fs.mkdirSync(globalDir, { recursive: true });
        }

        const bridgePath = path.join(globalDir, `${lowerName}.bridge.js`);

        if (fs.existsSync(bridgePath)) {
            console.error(`❌ Error: Bridge '${lowerName}' already exists.`);
            return;
        }

        const template = fs.readFileSync(path.join(this.frameworkDir, 'templates/bridge/bridge.js.template'), 'utf-8');

        fs.writeFileSync(
            bridgePath,
            template.replace(/{{ name }}/g, capitalizedName)
        );

        console.log(`✅ Bridge '${capitalizedName}' generated at src/global/${lowerName}.bridge.js`);
        console.log(`ℹ️ It will be automatically registered as '${capitalizedName}' on the next build.`);
    }

    /**
     * Generates a new Guard class and template file.
     */
    generateGuard(name) {
        if (!name) {
            console.error('❌ Error: Please provide a guard name (e.g., avenx g guard auth)');
            return;
        }

        const lowerName = name.toLowerCase();
        const capitalizedName = lowerName
            .split(/[-_]/)
            .map(part => part.charAt(0).toUpperCase() + part.slice(1))
            .join('') + "Guard";

        const guardDir = path.join(this.baseDir, 'src/guards');
        if (!fs.existsSync(guardDir)) {
            fs.mkdirSync(guardDir, { recursive: true });
        }

        const guardPath = path.join(guardDir, `${lowerName}.guard.js`);

        if (fs.existsSync(guardPath)) {
            console.error(`❌ Error: Guard '${lowerName}' already exists.`);
            return;
        }

        const template = fs.readFileSync(path.join(this.frameworkDir, 'templates/guard/guard.js.template'), 'utf-8');

        fs.writeFileSync(
            guardPath,
            template.replace(/{{ name }}/g, capitalizedName)
        );

        console.log(`✅ Guard '${capitalizedName}' generated at src/guards/${lowerName}.guard.js`);
        console.log(`ℹ️ It can be used in your route configurations.`);
    }

    /**
     * Generates a new Page class and template files.
     */
    generatePage(name) {
        if (!name) {
            console.error('❌ Error: Please provide a page name (e.g., avenx g page home)');
            return;
        }

        const lowerName = name.toLowerCase();
        const capitalizedName = lowerName
            .split(/[-_]/)
            .map(part => part.charAt(0).toUpperCase() + part.slice(1))
            .join('');

        const pageDir = path.join(this.baseDir, 'src/pages');
        if (!fs.existsSync(pageDir)) {
            fs.mkdirSync(pageDir, { recursive: true });
        }

        const jsPath = path.join(pageDir, `${lowerName}.page.js`);
        const cssPath = path.join(pageDir, `${lowerName}.page.css`);

        if (fs.existsSync(jsPath)) {
            console.error(`❌ Error: Page '${lowerName}' already exists.`);
            return;
        }

        const jsTemplate = fs.readFileSync(path.join(this.frameworkDir, 'templates/page/page.js.template'), 'utf-8');
        const cssTemplate = fs.readFileSync(path.join(this.frameworkDir, 'templates/page/page.css.template'), 'utf-8');

        fs.writeFileSync(jsPath, jsTemplate.replace(/{{ name }}/g, capitalizedName));
        fs.writeFileSync(cssPath, cssTemplate);

        console.log(`✅ Page '${capitalizedName}' generated at src/pages/${lowerName}.page.js`);
        console.log(`ℹ️ It will be automatically registered and routed if you update src/main.app.js.`);
    }

    /**
     * Generates a new component folder and template files, and registers it in main.app.js.
     */
    generateComponent(name) {
        if (!name) {
            console.error('❌ Error: Please provide a component name (e.g., avenx g my-component)');
            return;
        }

        const lowerName = name.toLowerCase();
        const capitalizedName = lowerName
            .split(/[-_]/)
            .map(part => part.charAt(0).toUpperCase() + part.slice(1))
            .join('');

        const compDir = path.join(this.baseDir, 'src/components', lowerName);

        if (fs.existsSync(compDir)) {
            console.error(`❌ Error: Component '${lowerName}' already exists.`);
            return;
        }

        fs.mkdirSync(compDir, { recursive: true });

        const jsTemplate = fs.readFileSync(path.join(this.frameworkDir, 'templates/component/component.js.template'), 'utf-8');
        const cssTemplate = fs.readFileSync(path.join(this.frameworkDir, 'templates/component/component.css.template'), 'utf-8');

        fs.writeFileSync(
            path.join(compDir, `${lowerName}.component.js`),
            jsTemplate.replace('{{ name }}', capitalizedName)
        );
        fs.writeFileSync(
            path.join(compDir, `${lowerName}.component.css`),
            cssTemplate
        );

        console.log(`✅ Component '${lowerName}' generated at src/components/${lowerName}/`);
        this.registerInMainApp(capitalizedName, lowerName);
    }

    /**
     * Automatically adds import and registration for a component in src/main.app.js.
     */
    registerInMainApp(className, folderName) {
        const mainPath = path.join(this.baseDir, 'src/main.app.js');
        if (!fs.existsSync(mainPath)) return;

        let content = fs.readFileSync(mainPath, 'utf-8');
        const importStatement = `import ${className} from './components/${folderName}/${folderName}.component.js';`;
        const registerStatement = `app.register('${className}', ${className});`;

        const lines = content.split('\n');
        let lastImportIndex = -1;
        for (let i = 0; i < lines.length; i++) {
            if (lines[i].startsWith('import ')) lastImportIndex = i;
        }

        if (lastImportIndex !== -1) {
            lines.splice(lastImportIndex + 1, 0, importStatement);
        } else {
            lines.unshift(importStatement);
        }

        let lastRegisterIndex = -1;
        let appInstanceIndex = -1;
        for (let i = 0; i < lines.length; i++) {
            if (lines[i].includes('app.register(')) lastRegisterIndex = i;
            if (lines[i].includes('new AvenxApp')) appInstanceIndex = i;
        }

        if (lastRegisterIndex !== -1) {
            lines.splice(lastRegisterIndex + 1, 0, registerStatement);
        } else if (appInstanceIndex !== -1) {
            lines.splice(appInstanceIndex + 1, 0, '', registerStatement);
        } else {
            lines.push('', registerStatement);
        }

        const hasMount = lines.some(line => line.includes('app.mount('));
        if (!hasMount) {
            lines.push(`\napp.mount('${className}');`);
        } else {
            lines.push(`// app.mount('${className}'); // Uncomment to mount this component`);
        }

        fs.writeFileSync(mainPath, lines.join('\n'));
        console.log(`✅ Component '${className}' registered in src/main.app.js`);
    }

    /**
     * Runs the compiler build.
     */
    buildProject() {
        new AvenxCompiler().build();
    }

    /**
     * Starts a local development server and watches for changes.
     */
    serveProject(port) {
        this.liveReloadClients = [];
        this.buildProject();
        this.watchProject();

        const server = http.createServer((req, res) => {
            if (req.url === '/__avenx_live_reload__') {
                res.writeHead(200, {
                    'Content-Type': 'text/event-stream',
                    'Cache-Control': 'no-cache',
                    'Connection': 'keep-alive'
                });
                res.write('data: connected\n\n');

                this.liveReloadClients.push(res);

                req.on('close', () => {
                    this.liveReloadClients = this.liveReloadClients.filter(client => client !== res);
                });
                return;
            }

            let filePath = path.join(this.baseDir, req.url === '/' ? 'index.html' : req.url);

            if (!fs.existsSync(filePath) && !path.extname(filePath)) {
                filePath = path.join(this.baseDir, 'index.html');
            }

            const extname = String(path.extname(filePath)).toLowerCase();
            const mimeTypes = {
                '.html': 'text/html',
                '.js': 'text/javascript',
                '.css': 'text/css',
                '.json': 'application/json',
                '.png': 'image/png',
                '.jpg': 'image/jpg',
                '.gif': 'image/gif',
                '.svg': 'image/svg+xml',
            };

            const contentType = mimeTypes[extname] || 'application/octet-stream';

            fs.readFile(filePath, (error, content) => {
                if (error) {
                    if (error.code === 'ENOENT') {
                        res.writeHead(404);
                        res.end('File not found');
                    } else {
                        res.writeHead(500);
                        res.end('Server error: ' + error.code);
                    }
                } else {
                    let responseContent = content;
                    if (contentType === 'text/html') {
                        const script = `
<script>
    if ('EventSource' in window) {
        const source = new EventSource('/__avenx_live_reload__');
        source.onmessage = (e) => {
            if (e.data === 'reload') {
                window.location.reload();
            }
        };
    }
</script>
`;
                        const contentStr = content.toString('utf-8');
                        if (contentStr.includes('</body>')) {
                            responseContent = contentStr.replace('</body>', `${script}</body>`);
                        } else {
                            responseContent = contentStr + script;
                        }
                    }

                    res.writeHead(200, { 'Content-Type': contentType });
                    res.end(responseContent, 'utf-8');
                }
            });
        });

        server.listen(port, () => {
            const url = `http://localhost:${port}`;
            console.log(`\n🚀 Dev-Server running at ${url}`);
            console.log(`👀 Watching for changes in src/...\n`);
            this.openBrowser(url);
        });
    }

    /**
     * Watches the src directory for changes and triggers a rebuild.
     */
    watchProject() {
        let timeout;
        const srcPath = path.join(this.baseDir, 'src');

        if (!fs.existsSync(srcPath)) return;

        fs.watch(srcPath, { recursive: true }, (eventType, filename) => {
            if (filename) {
                clearTimeout(timeout);
                timeout = setTimeout(() => {
                    console.log(`\n📄 Change detected: ${filename}. Rebuilding...`);
                    this.buildProject();

                    if (this.liveReloadClients) {
                        this.liveReloadClients.forEach(client => {
                            client.write('data: reload\n\n');
                        });
                    }
                }, 100);
            }
        });
    }

    /**
     * Opens the browser to the specified URL.
     */
    openBrowser(url) {
        const start = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
        exec(`${start} ${url}`);
    }

    getInitialHtml() {
        return `<!DOCTYPE html>
<html>
<head>
    <title>My Avenx App</title>
    <link rel="stylesheet" href="dist/bundle.css">
</head>
<body>
    <div id="app"></div>
    <script src="dist/bundle.js"></script>
</body>
</html>`;
    }

    printHelp() {
        console.log(`
Avenx-JS CLI
Usage: avenx <command> [type] [name]

Commands:
  init                      Initialize a new Avenx project structure
  generate component <name> Generate a new component (alias: g)
  generate page <name>      Generate a new page (alias: g p)
  generate bridge <name>    Generate a new shared reactive bridge
  generate guard <name>     Generate a new route guard
  build (b)                 Build the project into dist/bundle.js
  serve [port]              Start dev server with hot-reload (default: 3000)
  help                      Show this help message
        `);
    }
}

const cli = new AvenxCLI();
cli.run(command, args);