feat(tsview): add database and S3 handlers, tswatch/watch scripts, web utilities, assets and release config
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
/**
|
||||
* autocreated commitance data by @push.rocks/commitinfo
|
||||
* autocreated commitinfo by @push.rocks/commitinfo
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@git.zone/tsview',
|
||||
version: '1.0.0',
|
||||
description: 'A CLI tool for viewing S3 and MongoDB data with a web UI',
|
||||
};
|
||||
version: '1.1.0',
|
||||
description: 'A CLI tool for viewing S3 and MongoDB data with a web UI'
|
||||
}
|
||||
|
||||
@@ -87,6 +87,43 @@ export async function registerMongoHandlers(
|
||||
)
|
||||
);
|
||||
|
||||
// Create database (by creating a placeholder collection)
|
||||
typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.IReq_CreateDatabase>(
|
||||
'createDatabase',
|
||||
async (reqData) => {
|
||||
try {
|
||||
const client = await getMongoClient();
|
||||
const db = client.db(reqData.databaseName);
|
||||
// Create a placeholder collection to materialize the database
|
||||
await db.createCollection('_tsview_init');
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
console.error('Error creating database:', err);
|
||||
return { success: false };
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// Drop database
|
||||
typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.IReq_DropDatabase>(
|
||||
'dropDatabase',
|
||||
async (reqData) => {
|
||||
try {
|
||||
const client = await getMongoClient();
|
||||
const db = client.db(reqData.databaseName);
|
||||
await db.dropDatabase();
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
console.error('Error dropping database:', err);
|
||||
return { success: false };
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// Create collection
|
||||
typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.IReq_CreateCollection>(
|
||||
@@ -105,6 +142,24 @@ export async function registerMongoHandlers(
|
||||
)
|
||||
);
|
||||
|
||||
// Drop collection
|
||||
typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.IReq_DropCollection>(
|
||||
'dropCollection',
|
||||
async (reqData) => {
|
||||
try {
|
||||
const client = await getMongoClient();
|
||||
const db = client.db(reqData.databaseName);
|
||||
await db.dropCollection(reqData.collectionName);
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
console.error('Error dropping collection:', err);
|
||||
return { success: false };
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// Find documents
|
||||
typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.IReq_FindDocuments>(
|
||||
|
||||
@@ -9,20 +9,29 @@ export async function registerS3Handlers(
|
||||
typedrouter: plugins.typedrequest.TypedRouter,
|
||||
tsview: TsView
|
||||
): Promise<void> {
|
||||
console.log('Registering S3 handlers...');
|
||||
|
||||
// List all buckets
|
||||
console.log('Registering listBuckets handler');
|
||||
typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.IReq_ListBuckets>(
|
||||
'listBuckets',
|
||||
async () => {
|
||||
console.log('listBuckets handler called');
|
||||
const smartbucket = await tsview.getSmartBucket();
|
||||
console.log('smartbucket:', smartbucket ? 'initialized' : 'null');
|
||||
if (!smartbucket) {
|
||||
console.log('returning empty buckets');
|
||||
return { buckets: [] };
|
||||
}
|
||||
|
||||
try {
|
||||
const command = new plugins.s3.ListBucketsCommand({});
|
||||
console.log('sending ListBucketsCommand...');
|
||||
const response = await smartbucket.s3Client.send(command) as plugins.s3.ListBucketsCommandOutput;
|
||||
console.log('response:', response);
|
||||
const buckets = response.Buckets?.map(b => b.Name).filter((name): name is string => !!name) || [];
|
||||
console.log('returning buckets:', buckets);
|
||||
return { buckets };
|
||||
} catch (err) {
|
||||
console.error('Error listing buckets:', err);
|
||||
@@ -169,10 +178,21 @@ export async function registerS3Handlers(
|
||||
'html': 'text/html',
|
||||
'css': 'text/css',
|
||||
'js': 'application/javascript',
|
||||
'ts': 'text/plain',
|
||||
'tsx': 'text/plain',
|
||||
'jsx': 'text/plain',
|
||||
'md': 'text/markdown',
|
||||
'csv': 'text/csv',
|
||||
'yaml': 'text/yaml',
|
||||
'yml': 'text/yaml',
|
||||
'log': 'text/plain',
|
||||
'sh': 'text/plain',
|
||||
'env': 'text/plain',
|
||||
'png': 'image/png',
|
||||
'jpg': 'image/jpeg',
|
||||
'jpeg': 'image/jpeg',
|
||||
'gif': 'image/gif',
|
||||
'webp': 'image/webp',
|
||||
'svg': 'image/svg+xml',
|
||||
'pdf': 'application/pdf',
|
||||
'xml': 'application/xml',
|
||||
@@ -216,11 +236,26 @@ export async function registerS3Handlers(
|
||||
'json': 'application/json',
|
||||
'txt': 'text/plain',
|
||||
'html': 'text/html',
|
||||
'css': 'text/css',
|
||||
'js': 'application/javascript',
|
||||
'ts': 'text/plain',
|
||||
'tsx': 'text/plain',
|
||||
'jsx': 'text/plain',
|
||||
'md': 'text/markdown',
|
||||
'csv': 'text/csv',
|
||||
'yaml': 'text/yaml',
|
||||
'yml': 'text/yaml',
|
||||
'log': 'text/plain',
|
||||
'sh': 'text/plain',
|
||||
'env': 'text/plain',
|
||||
'png': 'image/png',
|
||||
'jpg': 'image/jpeg',
|
||||
'jpeg': 'image/jpeg',
|
||||
'gif': 'image/gif',
|
||||
'webp': 'image/webp',
|
||||
'svg': 'image/svg+xml',
|
||||
'pdf': 'application/pdf',
|
||||
'xml': 'application/xml',
|
||||
};
|
||||
const contentType = contentTypeMap[ext] || 'application/octet-stream';
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -28,6 +28,15 @@ export interface ITsViewConfig {
|
||||
mongo?: IMongoConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration from npmextra.json for @git.zone/tsview
|
||||
*/
|
||||
export interface INpmextraConfig {
|
||||
port?: number; // Fixed port to use (optional)
|
||||
killIfBusy?: boolean; // Kill process on port if busy (default: false)
|
||||
openBrowser?: boolean; // Open browser on start (default: true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Environment configuration from .nogit/env.json (gitzone service format)
|
||||
*/
|
||||
@@ -229,6 +238,32 @@ export interface IReq_ListCollections extends plugins.typedrequestInterfaces.imp
|
||||
};
|
||||
}
|
||||
|
||||
export interface IReq_CreateDatabase extends plugins.typedrequestInterfaces.implementsTR<
|
||||
plugins.typedrequestInterfaces.ITypedRequest,
|
||||
IReq_CreateDatabase
|
||||
> {
|
||||
method: 'createDatabase';
|
||||
request: {
|
||||
databaseName: string;
|
||||
};
|
||||
response: {
|
||||
success: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IReq_DropDatabase extends plugins.typedrequestInterfaces.implementsTR<
|
||||
plugins.typedrequestInterfaces.ITypedRequest,
|
||||
IReq_DropDatabase
|
||||
> {
|
||||
method: 'dropDatabase';
|
||||
request: {
|
||||
databaseName: string;
|
||||
};
|
||||
response: {
|
||||
success: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IReq_CreateCollection extends plugins.typedrequestInterfaces.implementsTR<
|
||||
plugins.typedrequestInterfaces.ITypedRequest,
|
||||
IReq_CreateCollection
|
||||
@@ -243,6 +278,20 @@ export interface IReq_CreateCollection extends plugins.typedrequestInterfaces.im
|
||||
};
|
||||
}
|
||||
|
||||
export interface IReq_DropCollection extends plugins.typedrequestInterfaces.implementsTR<
|
||||
plugins.typedrequestInterfaces.ITypedRequest,
|
||||
IReq_DropCollection
|
||||
> {
|
||||
method: 'dropCollection';
|
||||
request: {
|
||||
databaseName: string;
|
||||
collectionName: string;
|
||||
};
|
||||
response: {
|
||||
success: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IReq_FindDocuments extends plugins.typedrequestInterfaces.implementsTR<
|
||||
plugins.typedrequestInterfaces.ITypedRequest,
|
||||
IReq_FindDocuments
|
||||
|
||||
@@ -23,15 +23,6 @@ export class ViewServer {
|
||||
* Start the server
|
||||
*/
|
||||
public async start(): Promise<void> {
|
||||
// Register API handlers
|
||||
if (this.tsview.config.hasS3()) {
|
||||
await registerS3Handlers(this.typedrouter, this.tsview);
|
||||
}
|
||||
|
||||
if (this.tsview.config.hasMongo()) {
|
||||
await registerMongoHandlers(this.typedrouter, this.tsview);
|
||||
}
|
||||
|
||||
// Create typed server with bundled content
|
||||
this.typedServer = new plugins.typedserver.TypedServer({
|
||||
cors: true,
|
||||
@@ -41,8 +32,14 @@ export class ViewServer {
|
||||
noCache: true,
|
||||
});
|
||||
|
||||
// Add the router
|
||||
this.typedServer.typedrouter.addTypedRouter(this.typedrouter);
|
||||
// Register API handlers directly to server's router
|
||||
if (this.tsview.config.hasS3()) {
|
||||
await registerS3Handlers(this.typedServer.typedrouter, this.tsview);
|
||||
}
|
||||
|
||||
if (this.tsview.config.hasMongo()) {
|
||||
await registerMongoHandlers(this.typedServer.typedrouter, this.tsview);
|
||||
}
|
||||
|
||||
// Start server
|
||||
await this.typedServer.start();
|
||||
|
||||
@@ -3,6 +3,10 @@ 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.
|
||||
@@ -99,25 +103,88 @@ export class TsView {
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the viewer server
|
||||
* @param port - Optional port number (if not provided, finds a free port from 3010+)
|
||||
* Load configuration from npmextra.json
|
||||
*/
|
||||
public async start(port?: number): Promise<number> {
|
||||
const actualPort = port ?? await this.findFreePort(3010);
|
||||
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 || {};
|
||||
}
|
||||
|
||||
this.server = new ViewServer(this, actualPort);
|
||||
await this.server.start();
|
||||
|
||||
console.log(`TsView server started on http://localhost:${actualPort}`);
|
||||
|
||||
// Open browser
|
||||
/**
|
||||
* Kill process running on the specified port
|
||||
*/
|
||||
private async killProcessOnPort(port: number): Promise<void> {
|
||||
try {
|
||||
await plugins.smartopen.openUrl(`http://localhost:${actualPort}`);
|
||||
} catch (err) {
|
||||
// Ignore browser open errors
|
||||
// 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);
|
||||
}
|
||||
|
||||
return actualPort;
|
||||
// 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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user