#!/usr/bin/env node
import fs from 'fs';
import net from 'net';
import os from 'os';
import path from 'path';
import { spawnSync } from 'child_process';
import { fileURLToPath } from 'url';
import {
getPilotDeckConfigPath,
readPilotDeckConfigFile,
validatePilotDeckConfig,
} from './services/pilotdeckConfig.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const packageJsonPath = path.join(__dirname, '../package.json');
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
const colors = {
reset: '\x1b[0m',
bright: '\x1b[1m',
dim: '\x1b[2m',
cyan: '\x1b[36m',
green: '\x1b[32m',
yellow: '\x1b[33m',
red: '\x1b[31m',
blue: '\x1b[34m',
};
const c = {
info: (text) => `${colors.cyan}${text}${colors.reset}`,
ok: (text) => `${colors.green}${text}${colors.reset}`,
warn: (text) => `${colors.yellow}${text}${colors.reset}`,
error: (text) => `${colors.red}${text}${colors.reset}`,
tip: (text) => `${colors.blue}${text}${colors.reset}`,
bright: (text) => `${colors.bright}${text}${colors.reset}`,
dim: (text) => `${colors.dim}${text}${colors.reset}`,
};
function defaultDatabasePath() {
return path.join(process.env.PILOT_HOME || path.join(os.homedir(), '.pilotdeck'), 'auth.db');
}
function getInstallDir() {
return path.join(__dirname, '..');
}
function parseArgs(args) {
const parsed = { command: 'start', options: {} };
for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
if (arg === '--port' || arg === '-p') {
parsed.options.serverPort = args[++index];
} else if (arg.startsWith('--port=')) {
parsed.options.serverPort = arg.split('=')[1];
} else if (arg === '--database-path') {
parsed.options.databasePath = args[++index];
} else if (arg.startsWith('--database-path=')) {
parsed.options.databasePath = arg.split('=')[1];
} else if (arg === '--config') {
parsed.options.configPath = args[++index];
} else if (arg.startsWith('--config=')) {
parsed.options.configPath = arg.split('=')[1];
} else if (arg === '--help' || arg === '-h') {
parsed.command = 'help';
} else if (arg === '--version' || arg === '-v') {
parsed.command = 'version';
} else if (!arg.startsWith('-')) {
parsed.command = arg;
}
}
return parsed;
}
function applyOptions(options) {
if (options.serverPort) process.env.SERVER_PORT = options.serverPort;
else if (!process.env.SERVER_PORT && process.env.PORT) process.env.SERVER_PORT = process.env.PORT;
if (options.databasePath) process.env.DATABASE_PATH = options.databasePath;
if (options.configPath) process.env.PILOTDECK_CONFIG_PATH = options.configPath;
if (!process.env.DATABASE_PATH) process.env.DATABASE_PATH = defaultDatabasePath();
}
function showHelp() {
console.log(`
${c.bright('pilotdeck - Command Line Tool')}
Usage:
pilotdeck [command] [options]
Commands:
start Start the PilotDeck web UI (default)
status Show configuration and data locations
help Show this help information
version Show version information
Options:
-p, --port <port> Set server port (default: 3001)
--database-path <path> Set database location
--config <path> Set pilotdeck.yaml location
-h, --help Show this help information
-v, --version Show version information
Examples:
pilotdeck
pilotdeck --port 8080
pilotdeck status
Configuration:
PilotDeck reads ~/.pilotdeck/pilotdeck.yaml by default.
First run opens the onboarding UI if no usable config exists.
`);
}
function showVersion() {
console.log(packageJson.version);
}
function hasUsableConfig(record) {
const validation = validatePilotDeckConfig(record.config);
if (!record.exists || !validation.valid) return false;
const mainModel = record.config?.agents?.main?.model;
const entry = mainModel ? record.config?.models?.entries?.[mainModel] : null;
const provider = entry?.provider ? record.config?.models?.providers?.[entry.provider] : null;
return Boolean(mainModel && entry?.name && provider?.baseUrl && provider?.apiKey);
}
function showStatus() {
const configPath = getPilotDeckConfigPath();
const record = readPilotDeckConfigFile();
const dbPath = process.env.DATABASE_PATH || defaultDatabasePath();
console.log(`\n${c.bright('pilotdeck - Status')}\n`);
console.log(c.dim('═'.repeat(60)));
console.log(`\n${c.info('[INFO]')} Version: ${c.bright(packageJson.version)}`);
console.log(`${c.info('[INFO]')} Installation Directory: ${c.dim(getInstallDir())}`);
console.log(`${c.info('[INFO]')} Server Port: ${c.bright(process.env.SERVER_PORT || '3001')}`);
console.log(`${c.info('[INFO]')} Config File: ${c.dim(configPath)}`);
console.log(` Status: ${record.exists ? c.ok('[OK] Exists') : c.warn('[WARN] Not found')}`);
console.log(` Onboarding: ${hasUsableConfig(record) ? c.ok('[OK] Complete') : c.warn('[WARN] Required')}`);
console.log(`${c.info('[INFO]')} Database: ${c.dim(dbPath)}`);
console.log(` Status: ${fs.existsSync(dbPath) ? c.ok('[OK] Exists') : c.warn('[WARN] Not created yet')}`);
console.log('\n' + c.dim('═'.repeat(60)));
console.log(`\n${c.tip('[TIP]')} Start with ${c.bright('pilotdeck')} and open http://localhost:${process.env.SERVER_PORT || '3001'}\n`);
}
function assertPortAvailable(port, host) {
return new Promise((resolve, reject) => {
const server = net.createServer();
server.once('error', (error) => {
if (error.code === 'EADDRINUSE') {
reject(new Error(`Port ${port} is already in use. Try: pilotdeck --port ${Number(port) + 1}`));
} else {
reject(error);
}
});
server.once('listening', () => {
server.close(() => resolve());
});
server.listen(Number(port), host);
});
}
function ensureFrontendBuild() {
const installDir = getInstallDir();
const distIndexPath = path.join(installDir, 'dist', 'index.html');
if (fs.existsSync(distIndexPath)) return;
console.log(`${c.warn('[WARN]')} Frontend build not found at ${c.dim(distIndexPath)}`);
console.log(`${c.info('[INFO]')} Building frontend before starting production server...`);
const result = spawnSync('npm', ['run', 'build'], {
cwd: installDir,
stdio: 'inherit',
env: { ...process.env, HUSKY: '0' },
});
if (result.status !== 0) {
throw new Error('Frontend build failed. Run "cd ui && npm install && npm run build" manually, then retry pilotdeck.');
}
if (!fs.existsSync(distIndexPath)) {
throw new Error(`Frontend build completed but ${distIndexPath} was not created.`);
}
}
async function startServer() {
const host = process.env.HOST || '0.0.0.0';
const port = process.env.SERVER_PORT || '3001';
await assertPortAvailable(port, host);
ensureFrontendBuild();
console.log(`\n${c.bright('pilotdeck')} starting...\n`);
console.log(`${c.info('[INFO]')} Config: ${c.dim(getPilotDeckConfigPath())}`);
console.log(`${c.info('[INFO]')} Database: ${c.dim(process.env.DATABASE_PATH || defaultDatabasePath())}`);
console.log(`${c.info('[INFO]')} Server: http://localhost:${port}\n`);
await import('./index.js');
}
async function main() {
const { command, options } = parseArgs(process.argv.slice(2));
applyOptions(options);
switch (command) {
case 'start':
await startServer();
break;
case 'status':
case 'info':
showStatus();
break;
case 'help':
showHelp();
break;
case 'version':
showVersion();
break;
default:
console.error(`${c.error('[ERROR]')} Unknown command: ${command}`);
console.error(`Run ${c.bright('pilotdeck help')} for usage information.`);
process.exit(1);
}
}
main().catch((error) => {
console.error(`${c.error('[ERROR]')} ${error.message}`);
process.exit(1);
});