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`
|
- SSE hookup: `packages/ui/src/hooks/useEventStream.ts`
|
||||||
- Web server embeds/starts OpenCode server: `packages/web/server/index.js` (`createOpencodeServer`)
|
- 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/`
|
- 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)
|
## Key UI patterns (reference files)
|
||||||
- Settings shell: `packages/ui/src/components/views/SettingsView.tsx`
|
- 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 # 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-qr # Show QR code for easy mobile access
|
||||||
openchamber --try-cf-tunnel --tunnel-password-url # Include password in URL for auto-login
|
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 stop # Stop server
|
||||||
openchamber update # Update to latest version
|
openchamber update # Update to latest version
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -31,15 +31,17 @@ openchamber # Start on port 3000
|
|||||||
openchamber --port 8080 # Custom port
|
openchamber --port 8080 # Custom port
|
||||||
openchamber --daemon # Background mode
|
openchamber --daemon # Background mode
|
||||||
openchamber --ui-password secret # Password-protect UI
|
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 stop # Stop server
|
||||||
openchamber update # Update to latest version
|
openchamber update # Update to latest version
|
||||||
```
|
```
|
||||||
|
|
||||||
### Environment Variables
|
### 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_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
|
## Prerequisites
|
||||||
|
|
||||||
|
|||||||
@@ -177,6 +177,7 @@ OPTIONS:
|
|||||||
|
|
||||||
ENVIRONMENT:
|
ENVIRONMENT:
|
||||||
OPENCHAMBER_UI_PASSWORD Alternative to --ui-password flag
|
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_PORT Port of external OpenCode server to connect to
|
||||||
OPENCODE_SKIP_START Skip starting OpenCode, use external server
|
OPENCODE_SKIP_START Skip starting OpenCode, use external server
|
||||||
|
|
||||||
|
|||||||
@@ -2766,6 +2766,7 @@ let openCodeAuthSource =
|
|||||||
const syncToHmrState = () => {
|
const syncToHmrState = () => {
|
||||||
hmrState.openCodeProcess = openCodeProcess;
|
hmrState.openCodeProcess = openCodeProcess;
|
||||||
hmrState.openCodePort = openCodePort;
|
hmrState.openCodePort = openCodePort;
|
||||||
|
hmrState.openCodeBaseUrl = openCodeBaseUrl;
|
||||||
hmrState.isShuttingDown = isShuttingDown;
|
hmrState.isShuttingDown = isShuttingDown;
|
||||||
hmrState.signalsAttached = signalsAttached;
|
hmrState.signalsAttached = signalsAttached;
|
||||||
hmrState.openCodeWorkingDirectory = openCodeWorkingDirectory;
|
hmrState.openCodeWorkingDirectory = openCodeWorkingDirectory;
|
||||||
@@ -2777,6 +2778,7 @@ const syncToHmrState = () => {
|
|||||||
const syncFromHmrState = () => {
|
const syncFromHmrState = () => {
|
||||||
openCodeProcess = hmrState.openCodeProcess;
|
openCodeProcess = hmrState.openCodeProcess;
|
||||||
openCodePort = hmrState.openCodePort;
|
openCodePort = hmrState.openCodePort;
|
||||||
|
openCodeBaseUrl = hmrState.openCodeBaseUrl ?? null;
|
||||||
isShuttingDown = hmrState.isShuttingDown;
|
isShuttingDown = hmrState.isShuttingDown;
|
||||||
signalsAttached = hmrState.signalsAttached;
|
signalsAttached = hmrState.signalsAttached;
|
||||||
openCodeWorkingDirectory = hmrState.openCodeWorkingDirectory;
|
openCodeWorkingDirectory = hmrState.openCodeWorkingDirectory;
|
||||||
@@ -2794,6 +2796,7 @@ const syncFromHmrState = () => {
|
|||||||
// These are synced to/from hmrState to survive HMR reloads
|
// These are synced to/from hmrState to survive HMR reloads
|
||||||
let openCodeProcess = hmrState.openCodeProcess;
|
let openCodeProcess = hmrState.openCodeProcess;
|
||||||
let openCodePort = hmrState.openCodePort;
|
let openCodePort = hmrState.openCodePort;
|
||||||
|
let openCodeBaseUrl = hmrState.openCodeBaseUrl ?? null;
|
||||||
let isShuttingDown = hmrState.isShuttingDown;
|
let isShuttingDown = hmrState.isShuttingDown;
|
||||||
let signalsAttached = hmrState.signalsAttached;
|
let signalsAttached = hmrState.signalsAttached;
|
||||||
let openCodeWorkingDirectory = hmrState.openCodeWorkingDirectory;
|
let openCodeWorkingDirectory = hmrState.openCodeWorkingDirectory;
|
||||||
@@ -2825,7 +2828,7 @@ async function isOpenCodeProcessHealthy() {
|
|||||||
* Unlike isOpenCodeProcessHealthy(), this doesn't require openCodeProcess to be set.
|
* Unlike isOpenCodeProcessHealthy(), this doesn't require openCodeProcess to be set.
|
||||||
* Used to auto-detect and connect to an existing OpenCode instance on startup.
|
* 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) {
|
if (!port || port <= 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -2833,7 +2836,8 @@ async function probeExternalOpenCode(port) {
|
|||||||
try {
|
try {
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const timeout = setTimeout(() => controller.abort(), 3000);
|
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',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
@@ -2862,6 +2866,37 @@ const ENV_CONFIGURED_OPENCODE_PORT = (() => {
|
|||||||
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
|
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' ||
|
const ENV_SKIP_OPENCODE_START = process.env.OPENCODE_SKIP_START === 'true' ||
|
||||||
process.env.OPENCHAMBER_SKIP_OPENCODE_START === 'true';
|
process.env.OPENCHAMBER_SKIP_OPENCODE_START === 'true';
|
||||||
const ENV_DESKTOP_NOTIFY = process.env.OPENCHAMBER_DESKTOP_NOTIFY === '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 normalizedPath = path.startsWith('/') ? path : `/${path}`;
|
||||||
const prefix = normalizeApiPrefix(prefixOverride !== undefined ? prefixOverride : '');
|
const prefix = normalizeApiPrefix(prefixOverride !== undefined ? prefixOverride : '');
|
||||||
const fullPath = `${prefix}${normalizedPath}`;
|
const fullPath = `${prefix}${normalizedPath}`;
|
||||||
return `http://localhost:${openCodePort}${fullPath}`;
|
const base = openCodeBaseUrl ?? `http://localhost:${openCodePort}`;
|
||||||
|
return `${base}${fullPath}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseSseDataPayload(block) {
|
function parseSseDataPayload(block) {
|
||||||
@@ -4820,7 +4856,8 @@ async function restartOpenCode() {
|
|||||||
if (isExternalOpenCode) {
|
if (isExternalOpenCode) {
|
||||||
console.log('Re-probing external OpenCode server...');
|
console.log('Re-probing external OpenCode server...');
|
||||||
const probePort = openCodePort || ENV_CONFIGURED_OPENCODE_PORT || 4096;
|
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) {
|
if (healthy) {
|
||||||
console.log(`External OpenCode server on port ${probePort} is healthy`);
|
console.log(`External OpenCode server on port ${probePort} is healthy`);
|
||||||
setOpenCodePort(probePort);
|
setOpenCodePort(probePort);
|
||||||
@@ -11415,23 +11452,27 @@ async function main(options = {}) {
|
|||||||
syncFromHmrState();
|
syncFromHmrState();
|
||||||
if (await isOpenCodeProcessHealthy()) {
|
if (await isOpenCodeProcessHealthy()) {
|
||||||
console.log(`[HMR] Reusing existing OpenCode process on port ${openCodePort}`);
|
console.log(`[HMR] Reusing existing OpenCode process on port ${openCodePort}`);
|
||||||
} else if (ENV_SKIP_OPENCODE_START && ENV_CONFIGURED_OPENCODE_PORT) {
|
} else if (ENV_SKIP_OPENCODE_START && ENV_EFFECTIVE_PORT) {
|
||||||
console.log(`Using external OpenCode server on port ${ENV_CONFIGURED_OPENCODE_PORT} (skip-start mode)`);
|
const label = ENV_CONFIGURED_OPENCODE_HOST ? ENV_CONFIGURED_OPENCODE_HOST.origin : `http://localhost:${ENV_EFFECTIVE_PORT}`;
|
||||||
setOpenCodePort(ENV_CONFIGURED_OPENCODE_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;
|
isOpenCodeReady = true;
|
||||||
isExternalOpenCode = true;
|
isExternalOpenCode = true;
|
||||||
lastOpenCodeError = null;
|
lastOpenCodeError = null;
|
||||||
openCodeNotReadySince = 0;
|
openCodeNotReadySince = 0;
|
||||||
syncToHmrState();
|
syncToHmrState();
|
||||||
} else if (ENV_CONFIGURED_OPENCODE_PORT && await probeExternalOpenCode(ENV_CONFIGURED_OPENCODE_PORT)) {
|
} else if (ENV_EFFECTIVE_PORT && await probeExternalOpenCode(ENV_EFFECTIVE_PORT, ENV_CONFIGURED_OPENCODE_HOST?.origin)) {
|
||||||
console.log(`Auto-detected existing OpenCode server on port ${ENV_CONFIGURED_OPENCODE_PORT}`);
|
const label = ENV_CONFIGURED_OPENCODE_HOST ? ENV_CONFIGURED_OPENCODE_HOST.origin : `http://localhost:${ENV_EFFECTIVE_PORT}`;
|
||||||
setOpenCodePort(ENV_CONFIGURED_OPENCODE_PORT);
|
console.log(`Auto-detected existing OpenCode server at ${label}`);
|
||||||
|
openCodeBaseUrl = ENV_CONFIGURED_OPENCODE_HOST?.origin ?? null;
|
||||||
|
setOpenCodePort(ENV_EFFECTIVE_PORT);
|
||||||
isOpenCodeReady = true;
|
isOpenCodeReady = true;
|
||||||
isExternalOpenCode = true;
|
isExternalOpenCode = true;
|
||||||
lastOpenCodeError = null;
|
lastOpenCodeError = null;
|
||||||
openCodeNotReadySince = 0;
|
openCodeNotReadySince = 0;
|
||||||
syncToHmrState();
|
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');
|
console.log('Auto-detected existing OpenCode server on default port 4096');
|
||||||
setOpenCodePort(4096);
|
setOpenCodePort(4096);
|
||||||
isOpenCodeReady = true;
|
isOpenCodeReady = true;
|
||||||
@@ -11440,9 +11481,9 @@ async function main(options = {}) {
|
|||||||
openCodeNotReadySince = 0;
|
openCodeNotReadySince = 0;
|
||||||
syncToHmrState();
|
syncToHmrState();
|
||||||
} else {
|
} else {
|
||||||
if (ENV_CONFIGURED_OPENCODE_PORT) {
|
if (ENV_EFFECTIVE_PORT) {
|
||||||
console.log(`Using OpenCode port from environment: ${ENV_CONFIGURED_OPENCODE_PORT}`);
|
console.log(`Using OpenCode port from environment: ${ENV_EFFECTIVE_PORT}`);
|
||||||
setOpenCodePort(ENV_CONFIGURED_OPENCODE_PORT);
|
setOpenCodePort(ENV_EFFECTIVE_PORT);
|
||||||
} else {
|
} else {
|
||||||
openCodePort = null;
|
openCodePort = null;
|
||||||
syncToHmrState();
|
syncToHmrState();
|
||||||
|
|||||||
Reference in New Issue
Block a user