feat(server): add OPENCODE_HOST env var for external OpenCode connections (#499)
* feat(server): add OPENCODE_HOST env var for external OpenCode connections Allows specifying a full base URL (e.g. https://hostname:4096) for external OpenCode connections, supporting custom hostnames and HTTPS. When set, OPENCODE_HOST overrides OPENCODE_PORT. Malformed values are fatal at startup. * docs: document OPENCODE_HOST env var in README and AGENTS * Fatal error when OPENCODE_HOST has path, search or hash Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Colin Mollenhour <colin@mollenhour.com> * fix(server): use external origin when re-probing OpenCode --------- Signed-off-by: Colin Mollenhour <colin@mollenhour.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
co-authored by
Copilot
parent
7a151290be
commit
e02002bc9f
@@ -79,7 +79,7 @@ All scripts are in `package.json`.
|
||||
- SSE hookup: `packages/ui/src/hooks/useEventStream.ts`
|
||||
- Web server embeds/starts OpenCode server: `packages/web/server/index.js` (`createOpencodeServer`)
|
||||
- Web runtime filesystem endpoints: search `packages/web/server/index.js` for `/api/fs/`
|
||||
- External server support: Set `OPENCODE_PORT` and `OPENCODE_SKIP_START=true` to connect to existing OpenCode instance
|
||||
- External server support: Set `OPENCODE_HOST` (full base URL, e.g. `http://hostname:4096`) or `OPENCODE_PORT`, plus `OPENCODE_SKIP_START=true`, to connect to existing OpenCode instance
|
||||
|
||||
## Key UI patterns (reference files)
|
||||
- Settings shell: `packages/ui/src/components/views/SettingsView.tsx`
|
||||
|
||||
@@ -110,7 +110,8 @@ openchamber --ui-password secret # Password-protect UI
|
||||
openchamber --try-cf-tunnel # Create a Cloudflare Quick Tunnel for remote access
|
||||
openchamber --try-cf-tunnel --tunnel-qr # Show QR code for easy mobile access
|
||||
openchamber --try-cf-tunnel --tunnel-password-url # Include password in URL for auto-login
|
||||
OPENCODE_PORT=4096 OPENCODE_SKIP_START=true openchamber # Connect to external OpenCode server
|
||||
OPENCODE_PORT=4096 OPENCODE_SKIP_START=true openchamber # Connect to external OpenCode server
|
||||
OPENCODE_HOST=https://myhost:4096 OPENCODE_SKIP_START=true openchamber # Connect via custom host/HTTPS
|
||||
openchamber stop # Stop server
|
||||
openchamber update # Update to latest version
|
||||
```
|
||||
|
||||
@@ -31,15 +31,17 @@ openchamber # Start on port 3000
|
||||
openchamber --port 8080 # Custom port
|
||||
openchamber --daemon # Background mode
|
||||
openchamber --ui-password secret # Password-protect UI
|
||||
OPENCODE_PORT=4096 OPENCODE_SKIP_START=true openchamber # Connect to external OpenCode server
|
||||
OPENCODE_PORT=4096 OPENCODE_SKIP_START=true openchamber # Connect to external OpenCode server
|
||||
OPENCODE_HOST=https://myhost:4096 OPENCODE_SKIP_START=true openchamber # Connect via custom host/HTTPS
|
||||
openchamber stop # Stop server
|
||||
openchamber update # Update to latest version
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
- `OPENCODE_HOST` - Full base URL of external OpenCode server, e.g. `http://hostname:4096` or `https://hostname:4096` (overrides `OPENCODE_PORT`)
|
||||
- `OPENCODE_PORT` - Port of external OpenCode server to connect to (instead of starting embedded server)
|
||||
- `OPENCODE_SKIP_START` - Skip starting embedded OpenCode server (use with `OPENCODE_PORT` to connect to external instance)
|
||||
- `OPENCODE_SKIP_START` - Skip starting embedded OpenCode server (use with `OPENCODE_HOST` or `OPENCODE_PORT` to connect to external instance)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
|
||||
@@ -177,6 +177,7 @@ OPTIONS:
|
||||
|
||||
ENVIRONMENT:
|
||||
OPENCHAMBER_UI_PASSWORD Alternative to --ui-password flag
|
||||
OPENCODE_HOST External OpenCode server base URL, e.g. http://hostname:4096 (overrides OPENCODE_PORT)
|
||||
OPENCODE_PORT Port of external OpenCode server to connect to
|
||||
OPENCODE_SKIP_START Skip starting OpenCode, use external server
|
||||
|
||||
|
||||
@@ -2766,6 +2766,7 @@ let openCodeAuthSource =
|
||||
const syncToHmrState = () => {
|
||||
hmrState.openCodeProcess = openCodeProcess;
|
||||
hmrState.openCodePort = openCodePort;
|
||||
hmrState.openCodeBaseUrl = openCodeBaseUrl;
|
||||
hmrState.isShuttingDown = isShuttingDown;
|
||||
hmrState.signalsAttached = signalsAttached;
|
||||
hmrState.openCodeWorkingDirectory = openCodeWorkingDirectory;
|
||||
@@ -2777,6 +2778,7 @@ const syncToHmrState = () => {
|
||||
const syncFromHmrState = () => {
|
||||
openCodeProcess = hmrState.openCodeProcess;
|
||||
openCodePort = hmrState.openCodePort;
|
||||
openCodeBaseUrl = hmrState.openCodeBaseUrl ?? null;
|
||||
isShuttingDown = hmrState.isShuttingDown;
|
||||
signalsAttached = hmrState.signalsAttached;
|
||||
openCodeWorkingDirectory = hmrState.openCodeWorkingDirectory;
|
||||
@@ -2794,6 +2796,7 @@ const syncFromHmrState = () => {
|
||||
// These are synced to/from hmrState to survive HMR reloads
|
||||
let openCodeProcess = hmrState.openCodeProcess;
|
||||
let openCodePort = hmrState.openCodePort;
|
||||
let openCodeBaseUrl = hmrState.openCodeBaseUrl ?? null;
|
||||
let isShuttingDown = hmrState.isShuttingDown;
|
||||
let signalsAttached = hmrState.signalsAttached;
|
||||
let openCodeWorkingDirectory = hmrState.openCodeWorkingDirectory;
|
||||
@@ -2825,7 +2828,7 @@ async function isOpenCodeProcessHealthy() {
|
||||
* Unlike isOpenCodeProcessHealthy(), this doesn't require openCodeProcess to be set.
|
||||
* Used to auto-detect and connect to an existing OpenCode instance on startup.
|
||||
*/
|
||||
async function probeExternalOpenCode(port) {
|
||||
async function probeExternalOpenCode(port, origin) {
|
||||
if (!port || port <= 0) {
|
||||
return false;
|
||||
}
|
||||
@@ -2833,7 +2836,8 @@ async function probeExternalOpenCode(port) {
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 3000);
|
||||
const response = await fetch(`http://127.0.0.1:${port}/global/health`, {
|
||||
const base = origin ?? `http://127.0.0.1:${port}`;
|
||||
const response = await fetch(`${base}/global/health`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
@@ -2862,6 +2866,37 @@ const ENV_CONFIGURED_OPENCODE_PORT = (() => {
|
||||
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
|
||||
})();
|
||||
|
||||
const ENV_CONFIGURED_OPENCODE_HOST = (() => {
|
||||
const raw = process.env.OPENCODE_HOST?.trim();
|
||||
if (!raw) return null;
|
||||
let url;
|
||||
try {
|
||||
url = new URL(raw);
|
||||
} catch {
|
||||
console.error(`[fatal] OPENCODE_HOST is not a valid URL: ${JSON.stringify(raw)}`);
|
||||
process.exit(1);
|
||||
}
|
||||
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
||||
console.error(`[fatal] OPENCODE_HOST must use http or https scheme, got: ${JSON.stringify(url.protocol)}`);
|
||||
process.exit(1);
|
||||
}
|
||||
const port = parseInt(url.port, 10);
|
||||
if (!Number.isFinite(port) || port <= 0) {
|
||||
console.error(`[fatal] OPENCODE_HOST must include an explicit port (e.g. http://hostname:4096), got: ${JSON.stringify(raw)}`);
|
||||
process.exit(1);
|
||||
}
|
||||
if (url.pathname !== '/' || url.search || url.hash) {
|
||||
console.error(
|
||||
`[fatal] OPENCODE_HOST must not include a path, query, or hash; got: ${JSON.stringify(raw)}`
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
return { origin: url.origin, port };
|
||||
})();
|
||||
|
||||
// OPENCODE_HOST takes precedence over OPENCODE_PORT when both are set
|
||||
const ENV_EFFECTIVE_PORT = ENV_CONFIGURED_OPENCODE_HOST?.port ?? ENV_CONFIGURED_OPENCODE_PORT;
|
||||
|
||||
const ENV_SKIP_OPENCODE_START = process.env.OPENCODE_SKIP_START === 'true' ||
|
||||
process.env.OPENCHAMBER_SKIP_OPENCODE_START === 'true';
|
||||
const ENV_DESKTOP_NOTIFY = process.env.OPENCHAMBER_DESKTOP_NOTIFY === 'true';
|
||||
@@ -3834,7 +3869,8 @@ function buildOpenCodeUrl(path, prefixOverride) {
|
||||
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
|
||||
const prefix = normalizeApiPrefix(prefixOverride !== undefined ? prefixOverride : '');
|
||||
const fullPath = `${prefix}${normalizedPath}`;
|
||||
return `http://localhost:${openCodePort}${fullPath}`;
|
||||
const base = openCodeBaseUrl ?? `http://localhost:${openCodePort}`;
|
||||
return `${base}${fullPath}`;
|
||||
}
|
||||
|
||||
function parseSseDataPayload(block) {
|
||||
@@ -4820,7 +4856,8 @@ async function restartOpenCode() {
|
||||
if (isExternalOpenCode) {
|
||||
console.log('Re-probing external OpenCode server...');
|
||||
const probePort = openCodePort || ENV_CONFIGURED_OPENCODE_PORT || 4096;
|
||||
const healthy = await probeExternalOpenCode(probePort);
|
||||
const probeOrigin = openCodeBaseUrl ?? ENV_CONFIGURED_OPENCODE_HOST?.origin;
|
||||
const healthy = await probeExternalOpenCode(probePort, probeOrigin);
|
||||
if (healthy) {
|
||||
console.log(`External OpenCode server on port ${probePort} is healthy`);
|
||||
setOpenCodePort(probePort);
|
||||
@@ -11415,23 +11452,27 @@ async function main(options = {}) {
|
||||
syncFromHmrState();
|
||||
if (await isOpenCodeProcessHealthy()) {
|
||||
console.log(`[HMR] Reusing existing OpenCode process on port ${openCodePort}`);
|
||||
} else if (ENV_SKIP_OPENCODE_START && ENV_CONFIGURED_OPENCODE_PORT) {
|
||||
console.log(`Using external OpenCode server on port ${ENV_CONFIGURED_OPENCODE_PORT} (skip-start mode)`);
|
||||
setOpenCodePort(ENV_CONFIGURED_OPENCODE_PORT);
|
||||
} else if (ENV_SKIP_OPENCODE_START && ENV_EFFECTIVE_PORT) {
|
||||
const label = ENV_CONFIGURED_OPENCODE_HOST ? ENV_CONFIGURED_OPENCODE_HOST.origin : `http://localhost:${ENV_EFFECTIVE_PORT}`;
|
||||
console.log(`Using external OpenCode server at ${label} (skip-start mode)`);
|
||||
openCodeBaseUrl = ENV_CONFIGURED_OPENCODE_HOST?.origin ?? null;
|
||||
setOpenCodePort(ENV_EFFECTIVE_PORT);
|
||||
isOpenCodeReady = true;
|
||||
isExternalOpenCode = true;
|
||||
lastOpenCodeError = null;
|
||||
openCodeNotReadySince = 0;
|
||||
syncToHmrState();
|
||||
} else if (ENV_CONFIGURED_OPENCODE_PORT && await probeExternalOpenCode(ENV_CONFIGURED_OPENCODE_PORT)) {
|
||||
console.log(`Auto-detected existing OpenCode server on port ${ENV_CONFIGURED_OPENCODE_PORT}`);
|
||||
setOpenCodePort(ENV_CONFIGURED_OPENCODE_PORT);
|
||||
} else if (ENV_EFFECTIVE_PORT && await probeExternalOpenCode(ENV_EFFECTIVE_PORT, ENV_CONFIGURED_OPENCODE_HOST?.origin)) {
|
||||
const label = ENV_CONFIGURED_OPENCODE_HOST ? ENV_CONFIGURED_OPENCODE_HOST.origin : `http://localhost:${ENV_EFFECTIVE_PORT}`;
|
||||
console.log(`Auto-detected existing OpenCode server at ${label}`);
|
||||
openCodeBaseUrl = ENV_CONFIGURED_OPENCODE_HOST?.origin ?? null;
|
||||
setOpenCodePort(ENV_EFFECTIVE_PORT);
|
||||
isOpenCodeReady = true;
|
||||
isExternalOpenCode = true;
|
||||
lastOpenCodeError = null;
|
||||
openCodeNotReadySince = 0;
|
||||
syncToHmrState();
|
||||
} else if (!ENV_CONFIGURED_OPENCODE_PORT && await probeExternalOpenCode(4096)) {
|
||||
} else if (!ENV_EFFECTIVE_PORT && await probeExternalOpenCode(4096)) {
|
||||
console.log('Auto-detected existing OpenCode server on default port 4096');
|
||||
setOpenCodePort(4096);
|
||||
isOpenCodeReady = true;
|
||||
@@ -11440,9 +11481,9 @@ async function main(options = {}) {
|
||||
openCodeNotReadySince = 0;
|
||||
syncToHmrState();
|
||||
} else {
|
||||
if (ENV_CONFIGURED_OPENCODE_PORT) {
|
||||
console.log(`Using OpenCode port from environment: ${ENV_CONFIGURED_OPENCODE_PORT}`);
|
||||
setOpenCodePort(ENV_CONFIGURED_OPENCODE_PORT);
|
||||
if (ENV_EFFECTIVE_PORT) {
|
||||
console.log(`Using OpenCode port from environment: ${ENV_EFFECTIVE_PORT}`);
|
||||
setOpenCodePort(ENV_EFFECTIVE_PORT);
|
||||
} else {
|
||||
openCodePort = null;
|
||||
syncToHmrState();
|
||||
|
||||
Reference in New Issue
Block a user