cli.js 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. #!/usr/bin/env node
  2. import fs from 'fs';
  3. import net from 'net';
  4. import os from 'os';
  5. import path from 'path';
  6. import { spawnSync } from 'child_process';
  7. import { fileURLToPath } from 'url';
  8. import {
  9. getPilotDeckConfigPath,
  10. readPilotDeckConfigFile,
  11. validatePilotDeckConfig,
  12. } from './services/pilotdeckConfig.js';
  13. const __filename = fileURLToPath(import.meta.url);
  14. const __dirname = path.dirname(__filename);
  15. const packageJsonPath = path.join(__dirname, '../package.json');
  16. const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
  17. const colors = {
  18. reset: '\x1b[0m',
  19. bright: '\x1b[1m',
  20. dim: '\x1b[2m',
  21. cyan: '\x1b[36m',
  22. green: '\x1b[32m',
  23. yellow: '\x1b[33m',
  24. red: '\x1b[31m',
  25. blue: '\x1b[34m',
  26. };
  27. const c = {
  28. info: (text) => `${colors.cyan}${text}${colors.reset}`,
  29. ok: (text) => `${colors.green}${text}${colors.reset}`,
  30. warn: (text) => `${colors.yellow}${text}${colors.reset}`,
  31. error: (text) => `${colors.red}${text}${colors.reset}`,
  32. tip: (text) => `${colors.blue}${text}${colors.reset}`,
  33. bright: (text) => `${colors.bright}${text}${colors.reset}`,
  34. dim: (text) => `${colors.dim}${text}${colors.reset}`,
  35. };
  36. function defaultDatabasePath() {
  37. return path.join(process.env.PILOT_HOME || path.join(os.homedir(), '.pilotdeck'), 'auth.db');
  38. }
  39. function getInstallDir() {
  40. return path.join(__dirname, '..');
  41. }
  42. function parseArgs(args) {
  43. const parsed = { command: 'start', options: {} };
  44. for (let index = 0; index < args.length; index += 1) {
  45. const arg = args[index];
  46. if (arg === '--port' || arg === '-p') {
  47. parsed.options.serverPort = args[++index];
  48. } else if (arg.startsWith('--port=')) {
  49. parsed.options.serverPort = arg.split('=')[1];
  50. } else if (arg === '--database-path') {
  51. parsed.options.databasePath = args[++index];
  52. } else if (arg.startsWith('--database-path=')) {
  53. parsed.options.databasePath = arg.split('=')[1];
  54. } else if (arg === '--config') {
  55. parsed.options.configPath = args[++index];
  56. } else if (arg.startsWith('--config=')) {
  57. parsed.options.configPath = arg.split('=')[1];
  58. } else if (arg === '--help' || arg === '-h') {
  59. parsed.command = 'help';
  60. } else if (arg === '--version' || arg === '-v') {
  61. parsed.command = 'version';
  62. } else if (!arg.startsWith('-')) {
  63. parsed.command = arg;
  64. }
  65. }
  66. return parsed;
  67. }
  68. function applyOptions(options) {
  69. if (options.serverPort) process.env.SERVER_PORT = options.serverPort;
  70. else if (!process.env.SERVER_PORT && process.env.PORT) process.env.SERVER_PORT = process.env.PORT;
  71. if (options.databasePath) process.env.DATABASE_PATH = options.databasePath;
  72. if (options.configPath) process.env.PILOTDECK_CONFIG_PATH = options.configPath;
  73. if (!process.env.DATABASE_PATH) process.env.DATABASE_PATH = defaultDatabasePath();
  74. }
  75. function showHelp() {
  76. console.log(`
  77. ${c.bright('pilotdeck - Command Line Tool')}
  78. Usage:
  79. pilotdeck [command] [options]
  80. Commands:
  81. start Start the PilotDeck web UI (default)
  82. status Show configuration and data locations
  83. help Show this help information
  84. version Show version information
  85. Options:
  86. -p, --port <port> Set server port (default: 3001)
  87. --database-path <path> Set database location
  88. --config <path> Set pilotdeck.yaml location
  89. -h, --help Show this help information
  90. -v, --version Show version information
  91. Examples:
  92. pilotdeck
  93. pilotdeck --port 8080
  94. pilotdeck status
  95. Configuration:
  96. PilotDeck reads ~/.pilotdeck/pilotdeck.yaml by default.
  97. First run opens the onboarding UI if no usable config exists.
  98. `);
  99. }
  100. function showVersion() {
  101. console.log(packageJson.version);
  102. }
  103. function hasUsableConfig(record) {
  104. const validation = validatePilotDeckConfig(record.config);
  105. if (!record.exists || !validation.valid) return false;
  106. const mainModel = record.config?.agents?.main?.model;
  107. const entry = mainModel ? record.config?.models?.entries?.[mainModel] : null;
  108. const provider = entry?.provider ? record.config?.models?.providers?.[entry.provider] : null;
  109. return Boolean(mainModel && entry?.name && provider?.baseUrl && provider?.apiKey);
  110. }
  111. function showStatus() {
  112. const configPath = getPilotDeckConfigPath();
  113. const record = readPilotDeckConfigFile();
  114. const dbPath = process.env.DATABASE_PATH || defaultDatabasePath();
  115. console.log(`\n${c.bright('pilotdeck - Status')}\n`);
  116. console.log(c.dim('═'.repeat(60)));
  117. console.log(`\n${c.info('[INFO]')} Version: ${c.bright(packageJson.version)}`);
  118. console.log(`${c.info('[INFO]')} Installation Directory: ${c.dim(getInstallDir())}`);
  119. console.log(`${c.info('[INFO]')} Server Port: ${c.bright(process.env.SERVER_PORT || '3001')}`);
  120. console.log(`${c.info('[INFO]')} Config File: ${c.dim(configPath)}`);
  121. console.log(` Status: ${record.exists ? c.ok('[OK] Exists') : c.warn('[WARN] Not found')}`);
  122. console.log(` Onboarding: ${hasUsableConfig(record) ? c.ok('[OK] Complete') : c.warn('[WARN] Required')}`);
  123. console.log(`${c.info('[INFO]')} Database: ${c.dim(dbPath)}`);
  124. console.log(` Status: ${fs.existsSync(dbPath) ? c.ok('[OK] Exists') : c.warn('[WARN] Not created yet')}`);
  125. console.log('\n' + c.dim('═'.repeat(60)));
  126. console.log(`\n${c.tip('[TIP]')} Start with ${c.bright('pilotdeck')} and open http://localhost:${process.env.SERVER_PORT || '3001'}\n`);
  127. }
  128. function assertPortAvailable(port, host) {
  129. return new Promise((resolve, reject) => {
  130. const server = net.createServer();
  131. server.once('error', (error) => {
  132. if (error.code === 'EADDRINUSE') {
  133. reject(new Error(`Port ${port} is already in use. Try: pilotdeck --port ${Number(port) + 1}`));
  134. } else {
  135. reject(error);
  136. }
  137. });
  138. server.once('listening', () => {
  139. server.close(() => resolve());
  140. });
  141. server.listen(Number(port), host);
  142. });
  143. }
  144. function ensureFrontendBuild() {
  145. const installDir = getInstallDir();
  146. const distIndexPath = path.join(installDir, 'dist', 'index.html');
  147. if (fs.existsSync(distIndexPath)) return;
  148. console.log(`${c.warn('[WARN]')} Frontend build not found at ${c.dim(distIndexPath)}`);
  149. console.log(`${c.info('[INFO]')} Building frontend before starting production server...`);
  150. const result = spawnSync('npm', ['run', 'build'], {
  151. cwd: installDir,
  152. stdio: 'inherit',
  153. env: { ...process.env, HUSKY: '0' },
  154. });
  155. if (result.status !== 0) {
  156. throw new Error('Frontend build failed. Run "cd ui && npm install && npm run build" manually, then retry pilotdeck.');
  157. }
  158. if (!fs.existsSync(distIndexPath)) {
  159. throw new Error(`Frontend build completed but ${distIndexPath} was not created.`);
  160. }
  161. }
  162. async function startServer() {
  163. const host = process.env.HOST || '0.0.0.0';
  164. const port = process.env.SERVER_PORT || '3001';
  165. await assertPortAvailable(port, host);
  166. ensureFrontendBuild();
  167. console.log(`\n${c.bright('pilotdeck')} starting...\n`);
  168. console.log(`${c.info('[INFO]')} Config: ${c.dim(getPilotDeckConfigPath())}`);
  169. console.log(`${c.info('[INFO]')} Database: ${c.dim(process.env.DATABASE_PATH || defaultDatabasePath())}`);
  170. console.log(`${c.info('[INFO]')} Server: http://localhost:${port}\n`);
  171. await import('./index.js');
  172. }
  173. async function main() {
  174. const { command, options } = parseArgs(process.argv.slice(2));
  175. applyOptions(options);
  176. switch (command) {
  177. case 'start':
  178. await startServer();
  179. break;
  180. case 'status':
  181. case 'info':
  182. showStatus();
  183. break;
  184. case 'help':
  185. showHelp();
  186. break;
  187. case 'version':
  188. showVersion();
  189. break;
  190. default:
  191. console.error(`${c.error('[ERROR]')} Unknown command: ${command}`);
  192. console.error(`Run ${c.bright('pilotdeck help')} for usage information.`);
  193. process.exit(1);
  194. }
  195. }
  196. main().catch((error) => {
  197. console.error(`${c.error('[ERROR]')} ${error.message}`);
  198. process.exit(1);
  199. });