import * as plugins from './plugins.js'; import * as paths from './paths.js'; import type * as interfaces from './interfaces/index.js'; import { TsViewConfig } from './config/index.js'; import { ViewServer } from './server/index.js'; import { exec } from 'child_process'; import { promisify } from 'util'; const execAsync = promisify(exec); /** * Main TsView class. * Provides both CLI and programmatic access to S3 and MongoDB viewing. */ export class TsView { public config: TsViewConfig; public server: ViewServer | null = null; private smartbucketInstance: plugins.smartbucket.SmartBucket | null = null; private mongoDbConnection: plugins.smartdata.SmartdataDb | null = null; constructor() { this.config = new TsViewConfig(); } /** * Load configuration from .nogit/env.json */ public async loadConfigFromEnv(cwd?: string): Promise { await this.config.loadFromEnv(cwd); } /** * Set S3 configuration programmatically */ public setS3Config(config: interfaces.IS3Config): void { this.config.setS3Config(config); } /** * Set MongoDB configuration programmatically */ public setMongoConfig(config: interfaces.IMongoConfig): void { this.config.setMongoConfig(config); } /** * Get the SmartBucket instance (lazy initialization) */ public async getSmartBucket(): Promise { if (this.smartbucketInstance) { return this.smartbucketInstance; } const s3Config = this.config.getS3Config(); if (!s3Config) { return null; } this.smartbucketInstance = new plugins.smartbucket.SmartBucket({ endpoint: s3Config.endpoint, port: s3Config.port, accessKey: s3Config.accessKey, accessSecret: s3Config.accessSecret, useSsl: s3Config.useSsl ?? true, }); return this.smartbucketInstance; } /** * Get the MongoDB connection (lazy initialization) */ public async getMongoDb(): Promise { if (this.mongoDbConnection) { return this.mongoDbConnection; } const mongoConfig = this.config.getMongoConfig(); if (!mongoConfig) { return null; } this.mongoDbConnection = new plugins.smartdata.SmartdataDb({ mongoDbUrl: mongoConfig.mongoDbUrl, mongoDbName: mongoConfig.mongoDbName, }); await this.mongoDbConnection.init(); return this.mongoDbConnection; } /** * Find a free port starting from the given port */ private async findFreePort(startPort: number = 3010): Promise { const network = new plugins.smartnetwork.SmartNetwork(); const freePort = await network.findFreePort(startPort, startPort + 100); if (freePort === null) { throw new Error(`No free port found between ${startPort} and ${startPort + 100}`); } return freePort; } /** * Load configuration from npmextra.json */ private loadNpmextraConfig(cwd?: string): interfaces.INpmextraConfig { const npmextra = new plugins.npmextra.Npmextra(cwd || process.cwd()); const config = npmextra.dataFor('@git.zone/tsview', {}); return config || {}; } /** * Kill process running on the specified port */ private async killProcessOnPort(port: number): Promise { try { // Get PID using lsof (works on Linux and macOS) const { stdout } = await execAsync(`lsof -ti :${port}`); const pids = stdout.trim().split('\n').filter(Boolean); for (const pid of pids) { console.log(`Killing process ${pid} on port ${port}`); await execAsync(`kill -9 ${pid}`); } // Brief wait for port to be released await new Promise(resolve => setTimeout(resolve, 500)); } catch (e) { // No process on port or lsof not available, ignore } } /** * Start the viewer server * @param cliPort - Optional port number from CLI (highest priority) */ public async start(cliPort?: number): Promise { const npmextraConfig = await this.loadNpmextraConfig(); let port: number; let portWasExplicitlySet = false; if (cliPort) { // CLI has highest priority port = cliPort; portWasExplicitlySet = true; } else if (npmextraConfig.port) { // Config port specified port = npmextraConfig.port; portWasExplicitlySet = true; } else { // Auto-find free port port = await this.findFreePort(3010); } // Check if port is busy and handle accordingly const network = new plugins.smartnetwork.SmartNetwork(); const isFree = await network.isLocalPortUnused(port); if (!isFree) { if (npmextraConfig.killIfBusy) { console.log(`Port ${port} is busy. Killing existing process...`); await this.killProcessOnPort(port); } else if (portWasExplicitlySet) { throw new Error(`Port ${port} is busy. Set "killIfBusy": true in npmextra.json to auto-kill, or use a different port.`); } else { // Auto port was already free, shouldn't happen, but fallback port = await this.findFreePort(port + 1); } } this.server = new ViewServer(this, port); await this.server.start(); console.log(`TsView server started on http://localhost:${port}`); // Open browser (default: true, can be disabled via config) const shouldOpenBrowser = npmextraConfig.openBrowser !== false; if (shouldOpenBrowser) { try { await plugins.smartopen.openUrl(`http://localhost:${port}`); } catch (err) { // Ignore browser open errors } } return port; } /** * Stop the viewer server */ public async stop(): Promise { if (this.server) { await this.server.stop(); this.server = null; } if (this.mongoDbConnection) { await this.mongoDbConnection.close(); this.mongoDbConnection = null; } } }