#!/usr/bin/env node
import { program } from 'commander';
import { createOpencodeServer } from '@opencode-ai/sdk';
import { networkInterfaces } from 'os';
import { spawn } from 'child_process';
import { createConnection } from 'net';
import { existsSync, readdirSync } from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const VERSION = '0.1.2';
/**
 * Check if running inside Docker container
 */
function isDocker() {
    return existsSync('/.dockerenv');
}
/**
 * Check if a port is available
 */
async function isPortAvailable(port) {
    return new Promise((resolve) => {
        const socket = createConnection({ port, host: '127.0.0.1' });
        socket.setTimeout(1000);
        socket.on('connect', () => {
            socket.destroy();
            resolve(false); // Port is in use
        });
        socket.on('timeout', () => {
            socket.destroy();
            resolve(true); // Port is available
        });
        socket.on('error', () => {
            resolve(true); // Port is available (connection refused)
        });
    });
}
/**
 * Get the LAN IP address for mobile access
 */
function getLanIp() {
    const interfaces = networkInterfaces();
    for (const [name, addrs] of Object.entries(interfaces)) {
        // Skip loopback, docker, veth, bridge interfaces
        if (/^(lo|docker|veth|br-|virbr)/.test(name))
            continue;
        for (const addr of addrs || []) {
            if (addr.family === 'IPv4' && !addr.internal) {
                return addr.address;
            }
        }
    }
    return 'localhost';
}
/**
 * Print a nice boxed message with the access URL
 */
function printAccessBox(lanIp, port) {
    const url = `http://${lanIp}:${port}`;
    const urlLine = `    ${url}`;
    const width = Math.max(44, urlLine.length + 4);
    const border = '-'.repeat(width);
    const pad = (s) => s + ' '.repeat(width - s.length);
    console.log(`
+${border}+
|${pad('')}|
|${pad('  Access from any device:')}|
|${pad('')}|
|${pad(urlLine)}|
|${pad('')}|
+${border}+
`);
}
/**
 * Register projects with OpenCode
 */
async function registerProjects(opencodePort, projectsDir) {
    const baseUrl = `http://localhost:${opencodePort}`;
    if (projectsDir) {
        // Resolve to absolute path
        const absDir = path.resolve(projectsDir);
        if (!existsSync(absDir)) {
            console.error(`Error: Projects directory not found: ${absDir}`);
            process.exit(1);
        }
        // Register each subdirectory
        const entries = readdirSync(absDir, { withFileTypes: true });
        const dirs = entries.filter((e) => e.isDirectory() && !e.name.startsWith('.'));
        if (dirs.length === 0) {
            console.error(`Error: No project directories found in ${absDir}`);
            process.exit(1);
        }
        for (const entry of dirs) {
            const dir = path.join(absDir, entry.name);
            try {
                await fetch(`${baseUrl}/project?directory=${encodeURIComponent(dir)}`);
                console.log(`  Registered: ${entry.name}`);
            }
            catch (err) {
                console.error(`  Failed to register ${entry.name}: ${err}`);
            }
        }
    }
    else {
        // Register cwd
        const dir = process.cwd();
        try {
            await fetch(`${baseUrl}/project?directory=${encodeURIComponent(dir)}`);
            console.log(`  Registered: ${path.basename(dir)}`);
        }
        catch (err) {
            console.error(`  Failed to register ${path.basename(dir)}: ${err}`);
        }
    }
}
let opencodeServer = null;
let expressProcess = null;
async function startCommand(options) {
    const port = parseInt(options.port);
    const opencodePort = parseInt(options.opencodePort);
    console.log(`Shella v${VERSION}\n`);
    // Docker requires --projects-dir
    if (isDocker() && !options.projectsDir) {
        console.error('Error: --projects-dir is required when running in Docker.\n');
        console.error('Example:');
        console.error('  docker run -v ~/code:/project shella --projects-dir /project\n');
        console.error('Or in docker-compose.yml:');
        console.error('  command: ["--projects-dir", "/project"]');
        process.exit(1);
    }
    // Check if ports are available
    const webPortAvailable = await isPortAvailable(port);
    const ocPortAvailable = await isPortAvailable(opencodePort);
    if (!webPortAvailable) {
        console.error(`Error: Port ${port} is already in use.`);
        console.error(`\nTry one of these:`);
        console.error(`  - Stop the process using port ${port}`);
        console.error(`  - Use a different port: shella --port 3070`);
        process.exit(1);
    }
    if (!ocPortAvailable) {
        console.error(`Error: Port ${opencodePort} is already in use (OpenCode port).`);
        console.error(`\nThis usually means OpenCode is already running.`);
        console.error(`\nTry one of these:`);
        console.error(`  - Kill the existing OpenCode: pkill -f "opencode serve"`);
        console.error(`  - Use a different port: shella --opencode-port 4097`);
        process.exit(1);
    }
    // Start OpenCode server using the bundled binary
    console.log('Starting OpenCode...');
    try {
        opencodeServer = await createOpencodeServer({
            port: opencodePort,
            timeout: 30000,
        });
        console.log(`  OpenCode ready (port ${opencodePort})`);
    }
    catch (err) {
        const message = err instanceof Error ? err.message : String(err);
        console.error(`\nFailed to start OpenCode: ${message}`);
        if (message.includes('port')) {
            console.error(`\nThe port ${opencodePort} may be in use. Try:`);
            console.error(`  shella --opencode-port 4097`);
        }
        process.exit(1);
    }
    // Register projects with OpenCode
    console.log('Registering projects...');
    await registerProjects(opencodePort, options.projectsDir);
    // Start Express server in production mode
    console.log('Starting Shella server...');
    const serverPath = path.join(__dirname, '..', 'dist', 'server', 'index.js');
    expressProcess = spawn('node', [serverPath], {
        env: {
            ...process.env,
            NODE_ENV: 'production',
            PORT: String(port),
        },
        stdio: ['ignore', 'pipe', 'pipe'],
    });
    // Wait for server to be ready
    await new Promise((resolve, reject) => {
        const timeout = setTimeout(() => {
            reject(new Error('Server startup timeout (10s). Check logs for errors.'));
        }, 10000);
        expressProcess.stdout?.on('data', (data) => {
            const output = data.toString();
            if (output.includes('Running on')) {
                clearTimeout(timeout);
                resolve();
            }
            // Forward output but strip the prefix since we add our own
            process.stdout.write(output.replace(/\[shella-server\] /g, '  '));
        });
        expressProcess.stderr?.on('data', (data) => {
            process.stderr.write(data);
        });
        expressProcess.on('error', (err) => {
            clearTimeout(timeout);
            reject(new Error(`Failed to spawn server: ${err.message}`));
        });
        expressProcess.on('exit', (code) => {
            if (code !== 0 && code !== null) {
                clearTimeout(timeout);
                reject(new Error(`Server exited with code ${code}`));
            }
        });
    });
    console.log(`  Shella ready (port ${port})`);
    // Print access box
    const lanIp = getLanIp();
    printAccessBox(lanIp, port);
    console.log('Press Ctrl+C to stop\n');
    // Open browser if requested
    if (options.open) {
        const url = `http://localhost:${port}`;
        const openCommand = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
        spawn(openCommand, [url], { stdio: 'ignore', detached: true }).unref();
    }
}
/**
 * Graceful shutdown handler
 */
function shutdown() {
    console.log('\nShutting down...');
    if (expressProcess) {
        expressProcess.kill('SIGTERM');
        expressProcess = null;
    }
    if (opencodeServer) {
        opencodeServer.close();
        opencodeServer = null;
    }
    console.log('Goodbye!');
    process.exit(0);
}
// Handle shutdown signals
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);
// CLI definition
program
    .name('shella')
    .description('Self-hosted AI coding agents. Access from your phone.')
    .version(VERSION);
program
    .command('start', { isDefault: true })
    .description('Start Shella')
    .option('-p, --port <port>', 'Web server port', '3067')
    .option('--opencode-port <port>', 'OpenCode server port', '4096')
    .option('--projects-dir <path>', 'Directory containing projects (each subdirectory becomes a project)')
    .option('--no-open', "Don't open browser automatically")
    .action(startCommand);
program.parse();
