@kaifronsdal/transcript-viewer
Version:
A web-based viewer for AI conversation transcripts with rollback support
198 lines (172 loc) ⢠6.48 kB
JavaScript
import { exec } from 'child_process';
import { fileURLToPath } from 'url';
import { dirname, join, resolve } from 'path';
import { existsSync } from 'fs';
import express from 'express';
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const yargs = require('yargs/yargs');
const { hideBin } = require('yargs/helpers');
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const packageRoot = join(__dirname, '..');
const buildDir = join(packageRoot, 'build');
// Parse command line arguments using yargs
const argv = yargs(hideBin(process.argv))
.option('dir', {
alias: 'd',
type: 'string',
description: 'Specify the transcript directory to load',
default: './transcripts'
})
.option('host', {
type: 'string',
description: 'Specify the host to bind to',
default: 'localhost'
})
.option('port', {
alias: 'p',
type: 'number',
description: 'Specify the port to bind to',
default: 3000
})
.option('force-rebuild', {
alias: 'f',
type: 'boolean',
description: 'Force rebuild the application even if build exists',
default: false
})
.option('open', {
alias: 'o',
type: 'boolean',
description: 'Open browser automatically',
default: false
})
.help()
.example('transcript-viewer', 'Start with default directory (./transcripts)')
.example('transcript-viewer --dir ./my-transcripts', 'Start with custom directory')
.example('transcript-viewer -d /path/to/transcripts', 'Start with custom directory (short form)')
.example('transcript-viewer --host 0.0.0.0 --port 8080', 'Start accessible from all interfaces on port 8080')
.example('transcript-viewer --force-rebuild', 'Force rebuild and start')
.epilog('š A web-based viewer for AI conversation transcripts')
.argv;
const transcriptDir = argv.dir ? resolve(argv.dir) : null;
const host = argv.host || process.env.HOST || 'localhost';
const port = argv.port || parseInt(process.env.PORT || '3000');
const forceRebuild = argv['force-rebuild'];
const openBrowser = argv.open;
console.log('š Starting Transcript Viewer...');
if (transcriptDir) {
if (!existsSync(transcriptDir)) {
console.error(`ā Error: Transcript directory does not exist: ${transcriptDir}`);
process.exit(1);
}
console.log(`š Using transcript directory: ${transcriptDir}`);
} else {
const defaultPath = resolve(process.cwd(), 'transcripts');
console.log(`š Using default transcript directory: ${defaultPath}`);
console.log(`š” Tip: Use --dir <path> to specify a different directory`);
}
console.log(`š Server will bind to: ${host}:${port}`);
if (host === '0.0.0.0') {
console.log(`š Server will be accessible from all interfaces`);
}
// Check if build directory exists and if we should force rebuild
if (!existsSync(buildDir) || forceRebuild) {
if (forceRebuild) {
console.log('š Force rebuilding application...');
} else {
console.log('š¦ Building application...');
}
exec('npm run build', { cwd: packageRoot }, (error, stdout, stderr) => {
if (error) {
console.error('ā Build failed:', error);
process.exit(1);
}
// console.log(stdout);
// console.log(stderr);
console.log('šļø Build completed');
startServer();
});
} else {
startServer();
}
async function startServer() {
console.log('š Starting server...');
// Set up environment variables
const env = { ...process.env };
if (transcriptDir) {
env.TRANSCRIPT_DIR = transcriptDir;
} else {
// If no directory specified, use ./transcripts relative to where the user ran the command
env.TRANSCRIPT_DIR = resolve(process.cwd(), 'transcripts');
}
// Set environment variables for the SvelteKit app
Object.assign(process.env, env);
try {
// Import the SvelteKit handler using dynamic import
const { handler } = await import(join(buildDir, 'handler.js'));
// Create Express app
const app = express();
// Serve static files from the build/client directory
const clientDir = join(buildDir, 'client');
if (existsSync(clientDir)) {
app.use(express.static(clientDir));
}
// Use SvelteKit handler for all routes
app.use(handler);
// Start the server
const server = app.listen(port, host, () => {
const url = `http://${host}:${port}`;
console.log('\nā
Transcript Viewer is running!');
console.log(`š Open your browser to: ${url}`);
// Show additional info if binding to all interfaces
if (host === '0.0.0.0') {
console.log(`š Make sure port ${port} is accessible if running remotely`);
}
console.log('\nš” To stop the server, press Ctrl+C');
// Open browser if requested
if (openBrowser) {
try {
const open = require('open');
open(url);
} catch (error) {
console.log('š” Could not open browser automatically. Please install "open" package or open manually.');
}
}
});
// Handle graceful shutdown
const gracefulShutdown = (signal) => {
console.log(`\nš Received ${signal}. Stopping server gracefully...`);
server.close(() => {
console.log('š Server stopped');
process.exit(0);
});
// Force exit after 10 seconds
setTimeout(() => {
console.log('ā° Force exit after timeout');
process.exit(1);
}, 10000);
};
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
// Handle server errors
server.on('error', (error) => {
if (error.code === 'EADDRINUSE') {
console.error(`ā Error: Port ${port} is already in use`);
console.error('š” Try using a different port with --port <number>');
} else if (error.code === 'EACCES') {
console.error(`ā Error: Permission denied to bind to ${host}:${port}`);
console.error('š” Try using a port above 1024 or run with elevated privileges');
} else {
console.error('ā Server error:', error.message);
}
process.exit(1);
});
} catch (error) {
console.error('ā Failed to load SvelteKit handler:', error.message);
console.error('š” Make sure the application has been built with "npm run build"');
process.exit(1);
}
}