Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,17 @@ PORT=3001
#Frontend port
VITE_PORT=5173

# Optional HTTPS/SSL support (backend + Vite dev server)
# Set SSL_ENABLED=true to run backend over HTTPS.
# Set VITE_HTTPS=true to run Vite dev server over HTTPS.
# Provide paths to PEM files. You can use the same files for both.
#SSL_ENABLED=false
#VITE_HTTPS=false
#SSL_CERT_PATH=./certs/localhost.crt
#SSL_KEY_PATH=./certs/localhost.key
#VITE_SSL_CERT_PATH=./certs/localhost.crt
#VITE_SSL_KEY_PATH=./certs/localhost.key

# Uncomment the following line if you have a custom claude cli path other than the default "claude"
# CLAUDE_CLI_PATH=claude

Expand Down
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,25 @@ cp .env.example .env
# Edit .env with your preferred settings
```

Optional: **Enable HTTPS with a self-signed certificate** (for local SSL testing)
```bash
mkdir -p certs
openssl req -x509 -newkey rsa:2048 -nodes \
-keyout certs/localhost.key \
-out certs/localhost.crt \
-days 365 \
-subj "/CN=localhost"
```
Then set the following in `.env`:
```bash
SSL_ENABLED=true
VITE_HTTPS=true
SSL_CERT_PATH=./certs/localhost.crt
SSL_KEY_PATH=./certs/localhost.key
VITE_SSL_CERT_PATH=./certs/localhost.crt
VITE_SSL_KEY_PATH=./certs/localhost.key
```

4. **Start the application:**
```bash
# Development mode (with hot reload)
Expand Down
40 changes: 36 additions & 4 deletions server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import express from 'express';
import { WebSocketServer, WebSocket } from 'ws';
import os from 'os';
import http from 'http';
import https from 'https';
import cors from 'cors';
import { promises as fsPromises } from 'fs';
import { spawn } from 'child_process';
Expand Down Expand Up @@ -206,7 +207,36 @@ async function setupProjectsWatcher() {


const app = express();
const server = http.createServer(app);

function isTruthyEnv(value) {
return typeof value === 'string' && ['1', 'true', 'yes', 'on'].includes(value.toLowerCase());
}

function createWebServer(expressApp) {
const sslEnabled = isTruthyEnv(process.env.SSL_ENABLED);
if (!sslEnabled) {
return { server: http.createServer(expressApp), protocol: 'http' };
}

const keyPath = process.env.SSL_KEY_PATH;
const certPath = process.env.SSL_CERT_PATH;

if (!keyPath || !certPath) {
console.warn(`${c.warn('[WARN]')} SSL is enabled but SSL_KEY_PATH/SSL_CERT_PATH are missing. Falling back to HTTP.`);
return { server: http.createServer(expressApp), protocol: 'http' };
}

try {
const key = fs.readFileSync(path.resolve(keyPath));
const cert = fs.readFileSync(path.resolve(certPath));
return { server: https.createServer({ key, cert }, expressApp), protocol: 'https' };
} catch (error) {
console.warn(`${c.warn('[WARN]')} Failed to load SSL certificate files (${error.message}). Falling back to HTTP.`);
return { server: http.createServer(expressApp), protocol: 'http' };
}
}

const { server, protocol: serverProtocol } = createWebServer(app);

const ptySessionsMap = new Map();
const PTY_SESSION_TIMEOUT = 30 * 60 * 1000;
Expand Down Expand Up @@ -1797,7 +1827,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 viteProtocol = isTruthyEnv(process.env.VITE_HTTPS) ? 'https' : 'http';
res.redirect(`${viteProtocol}://localhost:${process.env.VITE_PORT || 5173}`);
}
});

Expand Down Expand Up @@ -1902,7 +1933,8 @@ async function startServer() {
console.log(`${c.info('[INFO]')} Running in ${c.bright(isProduction ? 'PRODUCTION' : 'DEVELOPMENT')} mode`);

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))}`);
const viteProtocol = isTruthyEnv(process.env.VITE_HTTPS) ? 'https' : 'http';
console.log(`${c.warn('[WARN]')} Note: Requests will be proxied to Vite dev server at ${c.dim(viteProtocol + '://localhost:' + (process.env.VITE_PORT || 5173))}`);
}

server.listen(PORT, '0.0.0.0', async () => {
Expand All @@ -1913,7 +1945,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://0.0.0.0:' + PORT)}`);
console.log(`${c.info('[INFO]')} Server URL: ${c.bright(serverProtocol + '://0.0.0.0:' + 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('');
Expand Down
38 changes: 33 additions & 5 deletions vite.config.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,51 @@
import { defineConfig, loadEnv } from 'vite'
import react from '@vitejs/plugin-react'
import fs from 'fs'
import path from 'path'

const isTruthyEnv = (value) =>
typeof value === 'string' && ['1', 'true', 'yes', 'on'].includes(value.toLowerCase())

export default defineConfig(({ command, mode }) => {
// Load env file based on `mode` in the current working directory.
const env = loadEnv(mode, process.cwd(), '')



const httpsEnabled = isTruthyEnv(env.VITE_HTTPS) || isTruthyEnv(env.SSL_ENABLED)
const certPath = env.VITE_SSL_CERT_PATH || env.SSL_CERT_PATH
const keyPath = env.VITE_SSL_KEY_PATH || env.SSL_KEY_PATH

let httpsConfig
if (httpsEnabled) {
if (certPath && keyPath) {
try {
httpsConfig = {
cert: fs.readFileSync(path.resolve(certPath)),
key: fs.readFileSync(path.resolve(keyPath))
}
} catch (error) {
console.warn('[vite] HTTPS enabled but SSL cert files could not be read. Falling back to HTTP.', error?.message || error)
}
} else {
console.warn('[vite] HTTPS enabled but VITE_SSL_CERT_PATH/VITE_SSL_KEY_PATH (or SSL_CERT_PATH/SSL_KEY_PATH) are missing. Falling back to HTTP.')
}
}

const apiProtocol = httpsConfig ? 'https' : 'http'
const wsProtocol = httpsConfig ? 'wss' : 'ws'

return {
plugins: [react()],
server: {
port: parseInt(env.VITE_PORT) || 5173,
https: httpsConfig,
proxy: {
'/api': `http://localhost:${env.PORT || 3001}`,
'/api': `${apiProtocol}://localhost:${env.PORT || 3001}`,
'/ws': {
target: `ws://localhost:${env.PORT || 3001}`,
target: `${wsProtocol}://localhost:${env.PORT || 3001}`,
ws: true
},
'/shell': {
target: `ws://localhost:${env.PORT || 3001}`,
target: `${wsProtocol}://localhost:${env.PORT || 3001}`,
ws: true
}
}
Expand Down