205 lines
5.8 KiB
TypeScript
205 lines
5.8 KiB
TypeScript
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<void> {
|
|
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<plugins.smartbucket.SmartBucket | null> {
|
|
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<plugins.smartdata.SmartdataDb | null> {
|
|
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<number> {
|
|
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<interfaces.INpmextraConfig>('@git.zone/tsview', {});
|
|
return config || {};
|
|
}
|
|
|
|
/**
|
|
* Kill process running on the specified port
|
|
*/
|
|
private async killProcessOnPort(port: number): Promise<void> {
|
|
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<number> {
|
|
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<void> {
|
|
if (this.server) {
|
|
await this.server.stop();
|
|
this.server = null;
|
|
}
|
|
|
|
if (this.mongoDbConnection) {
|
|
await this.mongoDbConnection.close();
|
|
this.mongoDbConnection = null;
|
|
}
|
|
}
|
|
}
|