claude-code-web
Version:
Web-based interface for Claude Code CLI accessible via browser
203 lines (170 loc) • 7.03 kB
JavaScript
const { Command } = require('commander');
const path = require('path');
const open = require('open');
const crypto = require('crypto');
const { startServer } = require('../src/server');
const program = new Command();
program
.name('cc-web')
.description('Web-based interface for Claude Code CLI')
.version('3.4.0')
.option('-p, --port <number>', 'port to run the server on', '32352')
.option('--no-open', 'do not automatically open browser')
.option('--auth <token>', 'authentication token for secure access')
.option('--disable-auth', 'disable authentication (not recommended for production)')
.option('--https', 'enable HTTPS (requires cert files)')
.option('--cert <path>', 'path to SSL certificate file')
.option('--key <path>', 'path to SSL private key file')
.option('--dev', 'development mode with additional logging')
.option('--plan <type>', 'subscription plan (pro, max5, max20)', 'max20')
.option('--claude-alias <name>', 'display alias for Claude (default: env CLAUDE_ALIAS or "Claude")')
.option('--codex-alias <name>', 'display alias for Codex (default: env CODEX_ALIAS or "Codex")')
.option('--agent-alias <name>', 'display alias for Agent (default: env AGENT_ALIAS or "Cursor")')
.option('--ngrok-auth-token <token>', 'ngrok auth token to open a public tunnel')
.option('--ngrok-domain <domain>', 'ngrok reserved domain to use for the tunnel')
.parse();
const options = program.opts();
function generateRandomToken(length = 10) {
const chars = 'ABCDEFGHJKMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789';
let result = '';
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}
async function main() {
try {
const port = parseInt(options.port, 10);
if (isNaN(port) || port < 1 || port > 65535) {
console.error('Error: Port must be a number between 1 and 65535');
process.exit(1);
}
// Handle authentication logic
let authToken = null;
let noAuth = options.disableAuth === true;
if (!noAuth) {
if (options.auth) {
// Use provided token
authToken = options.auth;
} else {
// Generate random token
authToken = generateRandomToken();
}
}
const serverOptions = {
port,
auth: authToken,
noAuth: noAuth,
https: options.https,
cert: options.cert,
key: options.key,
dev: options.dev,
plan: options.plan,
// UI aliases for assistants
claudeAlias: options.claudeAlias || process.env.CLAUDE_ALIAS || 'Claude',
codexAlias: options.codexAlias || process.env.CODEX_ALIAS || 'Codex',
agentAlias: options.agentAlias || process.env.AGENT_ALIAS || 'Cursor',
folderMode: true // Always use folder mode
};
console.log('Starting Claude Code Web Interface...');
console.log(`Port: ${port}`);
console.log('Mode: Folder selection mode');
console.log(`Plan: ${options.plan}`);
console.log(`Aliases: Claude → "${serverOptions.claudeAlias}", Codex → "${serverOptions.codexAlias}", Agent → "${serverOptions.agentAlias}"`);
// Display authentication status prominently
if (noAuth) {
console.log('\n⚠️ AUTHENTICATION DISABLED - Server is accessible without a token');
console.log(' (Use without --disable-auth flag for security in production)');
} else {
console.log('\n🔐 AUTHENTICATION ENABLED');
if (options.auth) {
console.log(' Using provided authentication token');
} else {
console.log(' Generated random authentication token:');
console.log(` \x1b[1m\x1b[33m${authToken}\x1b[0m`);
console.log(' \x1b[2mSave this token - you\'ll need it to access the interface\x1b[0m');
}
}
const server = await startServer(serverOptions);
// ngrok setup
const hasNgrokToken = !!options.ngrokAuthToken;
const hasNgrokDomain = !!options.ngrokDomain;
if ((hasNgrokToken && !hasNgrokDomain) || (!hasNgrokToken && hasNgrokDomain)) {
console.error('Error: Both --ngrok-auth-token and --ngrok-domain are required to enable ngrok tunneling');
process.exit(1);
}
let ngrokListener = null;
const protocol = options.https ? 'https' : 'http';
const url = `${protocol}://localhost:${port}`;
console.log(`\n🚀 Claude Code Web Interface is running at: ${url}`);
if (!noAuth) {
console.log('\n📋 Authentication Required:');
if (options.auth) {
console.log(' Use your provided authentication token to access the interface');
} else {
console.log(` Enter this token when prompted: \x1b[1m\x1b[33m${authToken}\x1b[0m`);
}
}
// Start ngrok tunnel if both flags provided
let publicUrl = null;
if (hasNgrokToken && hasNgrokDomain) {
console.log('\n🌐 Starting ngrok tunnel...');
try {
const mod = await import('@ngrok/ngrok');
const ngrok = mod.default || mod;
if (typeof ngrok.authtoken === 'function') {
try { await ngrok.authtoken(options.ngrokAuthToken); } catch (_) {}
}
ngrokListener = await ngrok.connect({
addr: port,
authtoken: options.ngrokAuthToken,
domain: options.ngrokDomain
});
if (ngrokListener && typeof ngrokListener.url === 'function') {
publicUrl = ngrokListener.url();
}
if (!publicUrl && ngrokListener && ngrokListener.url) {
publicUrl = ngrokListener.url; // fallback in case API exposes property
}
if (publicUrl) {
console.log(`\n🌍 ngrok tunnel established: ${publicUrl}`);
} else {
console.log('\n🌍 ngrok tunnel established');
}
if (options.open && publicUrl) {
try { await open(publicUrl); } catch (error) {
console.warn('Could not automatically open browser:', error.message);
}
}
} catch (error) {
console.error('Failed to start ngrok tunnel:', error.message);
}
} else if (options.open) {
// Open local URL only when ngrok not used and auto-open enabled
try {
await open(url);
} catch (error) {
console.warn('Could not automatically open browser:', error.message);
}
}
console.log('\nPress Ctrl+C to stop the server\n');
const shutdown = async () => {
console.log('\nShutting down server...');
// Close ngrok tunnel first if active
if (ngrokListener && typeof ngrokListener.close === 'function') {
try { await ngrokListener.close(); } catch (_) {}
}
server.close(() => {
console.log('Server closed');
process.exit(0);
});
};
process.on('SIGINT', () => { shutdown(); });
process.on('SIGTERM', () => { shutdown(); });
} catch (error) {
console.error('Error starting server:', error.message);
process.exit(1);
}
}
main();