diff --git a/.env.example b/.env.example index d18b97e..7e1d124 100755 --- a/.env.example +++ b/.env.example @@ -17,7 +17,7 @@ # Backend server port (Express API + WebSocket server) #API server -PORT=3001 +SERVER_PORT=3001 #Frontend port VITE_PORT=5173 diff --git a/package.json b/package.json index 222f825..f98afb0 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "scripts": { "dev": "concurrently --kill-others \"npm run server\" \"npm run client\"", "server": "node server/index.js", - "client": "vite --host", + "client": "vite", "build": "vite build", "preview": "vite preview", "typecheck": "tsc --noEmit -p tsconfig.json", diff --git a/server/cli.js b/server/cli.js index ebff4a0..36ac4f0 100755 --- a/server/cli.js +++ b/server/cli.js @@ -110,7 +110,7 @@ function showStatus() { // Environment variables console.log(`\n${c.info('[INFO]')} Configuration:`); - console.log(` PORT: ${c.bright(process.env.PORT || '3001')} ${c.dim(process.env.PORT ? '' : '(default)')}`); + console.log(` SERVER_PORT: ${c.bright(process.env.SERVER_PORT || process.env.PORT || '3001')} ${c.dim(process.env.SERVER_PORT || process.env.PORT ? '' : '(default)')}`); console.log(` DATABASE_PATH: ${c.dim(process.env.DATABASE_PATH || '(using default location)')}`); console.log(` CLAUDE_CLI_PATH: ${c.dim(process.env.CLAUDE_CLI_PATH || 'claude (default)')}`); console.log(` CONTEXT_WINDOW: ${c.dim(process.env.CONTEXT_WINDOW || '160000 (default)')}`); @@ -134,7 +134,7 @@ function showStatus() { console.log(` ${c.dim('>')} Use ${c.bright('cloudcli --port 8080')} to run on a custom port`); console.log(` ${c.dim('>')} Use ${c.bright('cloudcli --database-path /path/to/db')} for custom database`); console.log(` ${c.dim('>')} Run ${c.bright('cloudcli help')} for all options`); - console.log(` ${c.dim('>')} Access the UI at http://localhost:${process.env.PORT || '3001'}\n`); + console.log(` ${c.dim('>')} Access the UI at http://localhost:${process.env.SERVER_PORT || process.env.PORT || '3001'}\n`); } // Show help @@ -169,7 +169,8 @@ Examples: $ cloudcli status # Show configuration Environment Variables: - PORT Set server port (default: 3001) + SERVER_PORT Set server port (default: 3001) + PORT Set server port (default: 3001) (LEGACY) DATABASE_PATH Set custom database location CLAUDE_CLI_PATH Set custom Claude CLI path CONTEXT_WINDOW Set context window size (default: 160000) @@ -260,9 +261,9 @@ function parseArgs(args) { const arg = args[i]; if (arg === '--port' || arg === '-p') { - parsed.options.port = args[++i]; + parsed.options.serverPort = args[++i]; } else if (arg.startsWith('--port=')) { - parsed.options.port = arg.split('=')[1]; + parsed.options.serverPort = arg.split('=')[1]; } else if (arg === '--database-path') { parsed.options.databasePath = args[++i]; } else if (arg.startsWith('--database-path=')) { @@ -285,8 +286,10 @@ async function main() { const { command, options } = parseArgs(args); // Apply CLI options to environment variables - if (options.port) { - process.env.PORT = options.port; + if (options.serverPort) { + process.env.SERVER_PORT = options.serverPort; + } else if (!process.env.SERVER_PORT && process.env.PORT) { + process.env.SERVER_PORT = process.env.PORT; } if (options.databasePath) { process.env.DATABASE_PATH = options.databasePath; diff --git a/server/index.js b/server/index.js index 27aae75..d7e4d52 100755 --- a/server/index.js +++ b/server/index.js @@ -31,7 +31,7 @@ const c = { dim: (text) => `${colors.dim}${text}${colors.reset}`, }; -console.log('PORT from env:', process.env.PORT); +console.log('SERVER_PORT from env:', process.env.SERVER_PORT); import express from 'express'; import { WebSocketServer, WebSocket } from 'ws'; @@ -70,6 +70,7 @@ import { initializeDatabase, sessionNamesDb, applyCustomSessionNames } from './d import { configureWebPush } from './services/vapid-keys.js'; import { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js'; import { IS_PLATFORM } from './constants/config.js'; +import { getConnectableHost } from '../shared/networkHosts.js'; const VALID_PROVIDERS = ['claude', 'codex', 'cursor', 'gemini']; @@ -2403,7 +2404,8 @@ app.get('*', (req, res) => { res.sendFile(indexPath); } else { // In development, redirect to Vite dev server only if dist doesn't exist - res.redirect(`http://localhost:${process.env.VITE_PORT || 5173}`); + const redirectHost = getConnectableHost(req.hostname); + res.redirect(`${req.protocol}://${redirectHost}:${VITE_PORT}`); } }); @@ -2491,10 +2493,10 @@ async function getFileTree(dirPath, maxDepth = 3, currentDepth = 0, showHidden = }); } -const PORT = process.env.PORT || 3001; +const SERVER_PORT = process.env.SERVER_PORT || 3001; const HOST = process.env.HOST || '0.0.0.0'; -// Show localhost in URL when binding to all interfaces (0.0.0.0 isn't a connectable address) -const DISPLAY_HOST = HOST === '0.0.0.0' ? 'localhost' : HOST; +const DISPLAY_HOST = getConnectableHost(HOST); +const VITE_PORT = process.env.VITE_PORT || 5173; // Initialize database and start server async function startServer() { @@ -2511,13 +2513,15 @@ async function startServer() { // Log Claude implementation mode console.log(`${c.info('[INFO]')} Using Claude Agents SDK for Claude integration`); - console.log(`${c.info('[INFO]')} Running in ${c.bright(isProduction ? 'PRODUCTION' : 'DEVELOPMENT')} mode`); + console.log(''); - if (!isProduction) { - console.log(`${c.warn('[WARN]')} Note: Requests will be proxied to Vite dev server at ${c.dim('http://localhost:' + (process.env.VITE_PORT || 5173))}`); + if (isProduction) { + console.log(`${c.info('[INFO]')} To run in production mode, go to http://${DISPLAY_HOST}:${SERVER_PORT}`); } - server.listen(PORT, HOST, async () => { + console.log(`${c.info('[INFO]')} To run in development mode with hot-module replacement, go to http://${DISPLAY_HOST}:${VITE_PORT}`); + + server.listen(SERVER_PORT, HOST, async () => { const appInstallPath = path.join(__dirname, '..'); console.log(''); @@ -2525,7 +2529,7 @@ async function startServer() { console.log(` ${c.bright('Claude Code UI Server - Ready')}`); console.log(c.dim('═'.repeat(63))); console.log(''); - console.log(`${c.info('[INFO]')} Server URL: ${c.bright('http://' + DISPLAY_HOST + ':' + PORT)}`); + console.log(`${c.info('[INFO]')} Server URL: ${c.bright('http://' + DISPLAY_HOST + ':' + SERVER_PORT)}`); console.log(`${c.info('[INFO]')} Installed at: ${c.dim(appInstallPath)}`); console.log(`${c.tip('[TIP]')} Run "cloudcli status" for full configuration details`); console.log(''); diff --git a/server/routes/taskmaster.js b/server/routes/taskmaster.js index 191742b..632d99d 100644 --- a/server/routes/taskmaster.js +++ b/server/routes/taskmaster.js @@ -529,7 +529,7 @@ router.get('/next/:projectName', async (req, res) => { // Fallback to loading tasks and finding next one locally // Use localhost to bypass proxy for internal server-to-server calls - const tasksResponse = await fetch(`http://localhost:${process.env.PORT || 3001}/api/taskmaster/tasks/${encodeURIComponent(projectName)}`, { + const tasksResponse = await fetch(`http://localhost:${process.env.SERVER_PORT || process.env.PORT || '3001'}/api/taskmaster/tasks/${encodeURIComponent(projectName)}`, { headers: { 'Authorization': req.headers.authorization } @@ -1960,4 +1960,4 @@ Brief description of what this web application will do and why it's needed. ]; } -export default router; \ No newline at end of file +export default router; diff --git a/shared/networkHosts.js b/shared/networkHosts.js new file mode 100644 index 0000000..b126387 --- /dev/null +++ b/shared/networkHosts.js @@ -0,0 +1,22 @@ +export function isWildcardHost(host) { + return host === '0.0.0.0' || host === '::'; +} + +export function isLoopbackHost(host) { + return host === 'localhost' || host === '127.0.0.1' || host === '::1' || host === '[::1]'; +} + +export function normalizeLoopbackHost(host) { + if (!host) { + return host; + } + return isLoopbackHost(host) ? 'localhost' : host; +} + +// Use localhost for connectable loopback and wildcard addresses in browser-facing URLs. +export function getConnectableHost(host) { + if (!host) { + return 'localhost'; + } + return isWildcardHost(host) || isLoopbackHost(host) ? 'localhost' : host; +} diff --git a/vite.config.js b/vite.config.js index 48030b2..88c8e28 100755 --- a/vite.config.js +++ b/vite.config.js @@ -1,15 +1,21 @@ import { defineConfig, loadEnv } from 'vite' import react from '@vitejs/plugin-react' +import { getConnectableHost, normalizeLoopbackHost } from './shared/networkHosts.js' -export default defineConfig(({ command, mode }) => { +export default defineConfig(({ mode }) => { // Load env file based on `mode` in the current working directory. const env = loadEnv(mode, process.cwd(), '') - const host = env.HOST || '0.0.0.0' - // When binding to all interfaces (0.0.0.0), proxy should connect to localhost - // Otherwise, proxy to the specific host the backend is bound to - const proxyHost = host === '0.0.0.0' ? 'localhost' : host - const port = env.PORT || 3001 + const configuredHost = env.HOST || '0.0.0.0' + // if the host is not a loopback address, it should be used directly. + // This allows the vite server to EXPOSE all interfaces when the host + // is set to '0.0.0.0' or '::', while still using 'localhost' for browser + // URLs and proxy targets. + const host = normalizeLoopbackHost(configuredHost) + + const proxyHost = getConnectableHost(configuredHost) + // TODO: Remove support for legacy PORT variables in all locations in a future major release, leaving only SERVER_PORT. + const serverPort = env.SERVER_PORT || env.PORT || 3001 return { plugins: [react()], @@ -17,13 +23,13 @@ export default defineConfig(({ command, mode }) => { host, port: parseInt(env.VITE_PORT) || 5173, proxy: { - '/api': `http://${proxyHost}:${port}`, + '/api': `http://${proxyHost}:${serverPort}`, '/ws': { - target: `ws://${proxyHost}:${port}`, + target: `ws://${proxyHost}:${serverPort}`, ws: true }, '/shell': { - target: `ws://${proxyHost}:${port}`, + target: `ws://${proxyHost}:${serverPort}`, ws: true } }