feat: add startup launch support (#1421)
Add launch-at-startup support across the Electron desktop app and the web CLI. Electron now supports macOS launch-at-login through the native login item API. Login launches start OpenChamber in the background without opening a window, while Dock activation, deep links, and second-instance launches still open or focus the normal app window. The desktop Settings UI now exposes a localized launch-at-login toggle in Desktop Network Access. The web CLI now includes `openchamber startup status|enable|disable`, backed by native user services: - macOS: launchd LaunchAgent - Linux: systemd --user service - Windows: Task Scheduler Startup services run `openchamber serve --foreground` so the OS service manager owns process lifetime and restarts. Foreground service updates now defer restarts to the service manager instead of spawning duplicate CLI restarts. Startup services snapshot useful environment variables by default so provider tokens, PATH, SSH agent settings, and OpenCode configuration survive login/reboot starts. The snapshot avoids shell/session-only state, uses systemd-compatible env quoting on Linux, and avoids unused env artifacts on macOS. Also adds localized docs for startup services and environment variables.
This commit is contained in:
committed by
GitHub
parent
e97bf0d9dc
commit
2014303bc0
@@ -105,6 +105,10 @@ openchamber --ui-password be-creative-here
|
||||
```bash
|
||||
openchamber --port 8080 # Custom port
|
||||
openchamber --ui-password secret # Password-protect UI
|
||||
openchamber startup enable # Start at login as a native service
|
||||
OPENCHAMBER_UI_PASSWORD=secret openchamber startup enable # Save service password env
|
||||
openchamber startup status # Show startup service status
|
||||
openchamber startup disable # Remove startup service
|
||||
openchamber tunnel help # Tunnel lifecycle commands
|
||||
openchamber tunnel providers # Show provider capabilities
|
||||
openchamber tunnel profile add --provider cloudflare --mode managed-remote --name prod-main --hostname app.example.com --token <token>
|
||||
@@ -120,6 +124,8 @@ openchamber stop # Stop server
|
||||
openchamber update # Update to latest
|
||||
```
|
||||
|
||||
`startup enable` snapshots your current environment into the native service so startup behaves like you launched `openchamber` from the same shell. This preserves provider tokens, PATH, SSH agent settings, and other CLI auth/config env vars. Use `--no-env-snapshot` if you want a minimal service env.
|
||||
|
||||
Connect to an existing OpenCode server:
|
||||
```bash
|
||||
OPENCODE_PORT=4096 OPENCODE_SKIP_START=true openchamber
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
---
|
||||
title: Environment Variables
|
||||
description: Configure OpenChamber and OpenCode integration with environment variables.
|
||||
---
|
||||
|
||||
# Environment Variables
|
||||
|
||||
OpenChamber reads these environment variables at startup. For startup services, `openchamber startup enable` snapshots the current environment by default, so rerun it after changing variables you want the service to use.
|
||||
|
||||
## OpenChamber server
|
||||
|
||||
### `OPENCHAMBER_HOST`
|
||||
|
||||
Bind address for the OpenChamber web server. Use `0.0.0.0` to allow access from other machines.
|
||||
|
||||
### `OPENCHAMBER_UI_PASSWORD`
|
||||
|
||||
Password for the browser UI. Use this when binding outside localhost, using tunnels, or running behind a reverse proxy.
|
||||
|
||||
### `OPENCHAMBER_DATA_DIR`
|
||||
|
||||
Overrides the OpenChamber data directory. The default is `~/.config/openchamber`.
|
||||
|
||||
### `OPENCHAMBER_COMPRESS_API`
|
||||
|
||||
Controls API response compression. Use `true` or `1` to force it on, `false` or `0` to force it off.
|
||||
|
||||
### `OPENCHAMBER_SKIP_API_COMPRESSION`
|
||||
|
||||
Disables API response compression when set to `true` or `1`. This takes precedence over `OPENCHAMBER_COMPRESS_API`.
|
||||
|
||||
### `OPENCHAMBER_VERBOSE_REQUEST_LOGS`
|
||||
|
||||
Enables verbose HTTP request logs when set to `true` or `1`.
|
||||
|
||||
### `OPENCHAMBER_UPDATE_API_URL`
|
||||
|
||||
Overrides the update-check API endpoint. Most users should leave this unset.
|
||||
|
||||
### `OPENCHAMBER_PACKAGE_MANAGER`
|
||||
|
||||
Forces the package manager used by update operations when auto-detection is wrong.
|
||||
|
||||
## OpenCode server
|
||||
|
||||
### `OPENCODE_HOST`
|
||||
|
||||
Connects OpenChamber to an existing OpenCode server. The value must be an `http` or `https` origin with an explicit port and no path, query, or hash. `OPENCODE_HOST` takes precedence over `OPENCODE_PORT`.
|
||||
|
||||
### `OPENCODE_PORT`
|
||||
|
||||
Sets the OpenCode server port. With managed OpenCode this requests the managed port; with `OPENCODE_SKIP_START=true` it connects to an external server on that port.
|
||||
|
||||
### `OPENCODE_SKIP_START`
|
||||
|
||||
Prevents OpenChamber from starting its own OpenCode server when set to `true`.
|
||||
|
||||
### `OPENCHAMBER_OPENCODE_HOSTNAME`
|
||||
|
||||
Bind hostname for the OpenCode server managed by OpenChamber. The default is `127.0.0.1`.
|
||||
|
||||
### `OPENCODE_BINARY`
|
||||
|
||||
Path to the `opencode` executable OpenChamber should run.
|
||||
|
||||
### `OPENCODE_CONFIG`
|
||||
|
||||
Path to a specific OpenCode config file.
|
||||
|
||||
### `OPENCODE_CONFIG_DIR`
|
||||
|
||||
Path to a specific OpenCode config directory for agents, skills, snippets, and config discovery.
|
||||
|
||||
### `OPENCODE_DATA_DIR`
|
||||
|
||||
Custom data directory for the managed OpenCode server.
|
||||
|
||||
### `OPENCODE_WSL_DISTRO`
|
||||
|
||||
Selects the WSL distro used for OpenCode integration on Windows.
|
||||
|
||||
### `OPENCHAMBER_OPENCODE_WSL_DISTRO`
|
||||
|
||||
OpenChamber-specific alias for selecting the WSL distro. `OPENCODE_WSL_DISTRO` takes precedence when both are set.
|
||||
|
||||
### `OPENCODE_JWT_SECRET`
|
||||
|
||||
Secret used to sign UI authentication tokens. Use a long random value for persistent service deployments.
|
||||
|
||||
## Terminal and Git
|
||||
|
||||
### `OPENCHAMBER_TERMINAL_SHELL`
|
||||
|
||||
Shell executable used for OpenChamber terminal sessions.
|
||||
|
||||
### `OPENCHAMBER_GIT_BINARY`
|
||||
|
||||
Git executable used by OpenChamber Git features.
|
||||
|
||||
### `GIT_BINARY`
|
||||
|
||||
Alternative Git executable override. Prefer `OPENCHAMBER_GIT_BINARY` for OpenChamber-specific configuration.
|
||||
|
||||
### `OPENCHAMBER_GIT_READ_CACHE_TTL_MS`
|
||||
|
||||
Time-to-live, in milliseconds, for cached Git-backed file reads. Set `0` to disable this cache while debugging.
|
||||
|
||||
## Voice and tunnels
|
||||
|
||||
### `OPENAI_API_KEY`
|
||||
|
||||
API key used by OpenChamber voice features that call OpenAI-compatible services.
|
||||
|
||||
### `OPENCHAMBER_ALLOW_REMOTE_OPENAI_COMPAT_URLS`
|
||||
|
||||
Allows remote OpenAI-compatible base URLs for voice features when set to `true` or `1`.
|
||||
|
||||
### `NGROK_AUTHTOKEN`
|
||||
|
||||
ngrok auth token used by OpenChamber tunnel commands. You can also configure ngrok with `ngrok config add-authtoken <token>`.
|
||||
|
||||
## Runtime helpers
|
||||
|
||||
### `BUN_BINARY`
|
||||
|
||||
Bun executable OpenChamber should use when spawning daemon processes.
|
||||
|
||||
### `BUN_INSTALL`
|
||||
|
||||
Bun installation root. OpenChamber uses it to find `bin/bun` for daemon startup and updates.
|
||||
|
||||
### `VITE_OPENCODE_URL`
|
||||
|
||||
Build-time API base URL for the Vite-built web app. Most users should not set it for normal CLI or desktop usage.
|
||||
@@ -0,0 +1,134 @@
|
||||
---
|
||||
title: Variables de entorno
|
||||
description: Configura la integración de OpenChamber y OpenCode con variables de entorno.
|
||||
---
|
||||
|
||||
# Variables de entorno
|
||||
|
||||
OpenChamber lee estas variables al arrancar. Para servicios de inicio, `openchamber startup enable` guarda por defecto una copia del entorno actual, así que vuelve a ejecutarlo después de cambiar variables que quieras usar en el servicio.
|
||||
|
||||
## Servidor OpenChamber
|
||||
|
||||
### `OPENCHAMBER_HOST`
|
||||
|
||||
Dirección donde escucha el servidor web de OpenChamber. Usa `0.0.0.0` para permitir acceso desde otras máquinas.
|
||||
|
||||
### `OPENCHAMBER_UI_PASSWORD`
|
||||
|
||||
Contraseña para la interfaz del navegador. Úsala cuando no te limites a localhost, o cuando uses túneles o un proxy inverso.
|
||||
|
||||
### `OPENCHAMBER_DATA_DIR`
|
||||
|
||||
Cambia el directorio de datos de OpenChamber. Por defecto es `~/.config/openchamber`.
|
||||
|
||||
### `OPENCHAMBER_COMPRESS_API`
|
||||
|
||||
Controla la compresión de respuestas de la API. `true` o `1` la activa; `false` o `0` la desactiva.
|
||||
|
||||
### `OPENCHAMBER_SKIP_API_COMPRESSION`
|
||||
|
||||
Desactiva la compresión de respuestas de la API si vale `true` o `1`. Tiene prioridad sobre `OPENCHAMBER_COMPRESS_API`.
|
||||
|
||||
### `OPENCHAMBER_VERBOSE_REQUEST_LOGS`
|
||||
|
||||
Activa logs HTTP detallados si vale `true` o `1`.
|
||||
|
||||
### `OPENCHAMBER_UPDATE_API_URL`
|
||||
|
||||
Cambia el endpoint de comprobación de actualizaciones. La mayoría de usuarios no debería tocarlo.
|
||||
|
||||
### `OPENCHAMBER_PACKAGE_MANAGER`
|
||||
|
||||
Fuerza el gestor de paquetes usado para actualizar cuando la detección automática se equivoca.
|
||||
|
||||
## Servidor OpenCode
|
||||
|
||||
### `OPENCODE_HOST`
|
||||
|
||||
Conecta OpenChamber a un servidor OpenCode existente. Debe ser un origin `http` o `https` con puerto explícito y sin ruta, query ni hash. `OPENCODE_HOST` tiene prioridad sobre `OPENCODE_PORT`.
|
||||
|
||||
### `OPENCODE_PORT`
|
||||
|
||||
Define el puerto del servidor OpenCode. Con OpenCode gestionado pide ese puerto; con `OPENCODE_SKIP_START=true` conecta a un servidor externo en ese puerto.
|
||||
|
||||
### `OPENCODE_SKIP_START`
|
||||
|
||||
Evita que OpenChamber arranque su propio servidor OpenCode cuando vale `true`.
|
||||
|
||||
### `OPENCHAMBER_OPENCODE_HOSTNAME`
|
||||
|
||||
Hostname para el servidor OpenCode gestionado por OpenChamber. Por defecto es `127.0.0.1`.
|
||||
|
||||
### `OPENCODE_BINARY`
|
||||
|
||||
Ruta al ejecutable `opencode` que debe usar OpenChamber.
|
||||
|
||||
### `OPENCODE_CONFIG`
|
||||
|
||||
Ruta a un archivo de configuración concreto de OpenCode.
|
||||
|
||||
### `OPENCODE_CONFIG_DIR`
|
||||
|
||||
Ruta al directorio de configuración de OpenCode para agents, skills, snippets y descubrimiento de config.
|
||||
|
||||
### `OPENCODE_DATA_DIR`
|
||||
|
||||
Directorio de datos personalizado para el servidor OpenCode gestionado.
|
||||
|
||||
### `OPENCODE_WSL_DISTRO`
|
||||
|
||||
Elige la distro WSL usada para la integración con OpenCode en Windows.
|
||||
|
||||
### `OPENCHAMBER_OPENCODE_WSL_DISTRO`
|
||||
|
||||
Alias específico de OpenChamber para elegir la distro WSL. `OPENCODE_WSL_DISTRO` tiene prioridad si ambas están definidas.
|
||||
|
||||
### `OPENCODE_JWT_SECRET`
|
||||
|
||||
Secreto para firmar tokens de autenticación de la UI. En servicios persistentes, usa un valor largo y aleatorio.
|
||||
|
||||
## Terminal y Git
|
||||
|
||||
### `OPENCHAMBER_TERMINAL_SHELL`
|
||||
|
||||
Ejecutable de shell para las sesiones de terminal en OpenChamber.
|
||||
|
||||
### `OPENCHAMBER_GIT_BINARY`
|
||||
|
||||
Ejecutable de Git usado por las funciones Git de OpenChamber.
|
||||
|
||||
### `GIT_BINARY`
|
||||
|
||||
Override alternativo para el ejecutable de Git. Para ajustes específicos de OpenChamber, prefiere `OPENCHAMBER_GIT_BINARY`.
|
||||
|
||||
### `OPENCHAMBER_GIT_READ_CACHE_TTL_MS`
|
||||
|
||||
TTL, en milisegundos, para lecturas de archivos con cache respaldadas por Git. Usa `0` para desactivar la cache al depurar.
|
||||
|
||||
## Voz y túneles
|
||||
|
||||
### `OPENAI_API_KEY`
|
||||
|
||||
API key para funciones de voz de OpenChamber que llaman a servicios compatibles con OpenAI.
|
||||
|
||||
### `OPENCHAMBER_ALLOW_REMOTE_OPENAI_COMPAT_URLS`
|
||||
|
||||
Permite base URLs remotas compatibles con OpenAI para funciones de voz cuando vale `true` o `1`.
|
||||
|
||||
### `NGROK_AUTHTOKEN`
|
||||
|
||||
Token de autenticación de ngrok usado por los comandos de túnel de OpenChamber. También puedes configurarlo con `ngrok config add-authtoken <token>`.
|
||||
|
||||
## Ayudas de runtime
|
||||
|
||||
### `BUN_BINARY`
|
||||
|
||||
Ejecutable de Bun que OpenChamber debe usar al iniciar procesos daemon.
|
||||
|
||||
### `BUN_INSTALL`
|
||||
|
||||
Raíz de instalación de Bun. OpenChamber la usa para encontrar `bin/bun` al iniciar daemons y actualizar.
|
||||
|
||||
### `VITE_OPENCODE_URL`
|
||||
|
||||
API base URL de build-time para la web app construida con Vite. La mayoría de usuarios no debería usarla en CLI o desktop normales.
|
||||
@@ -44,6 +44,33 @@ openchamber stop
|
||||
|
||||
`openchamber` por sí solo inicia el servidor en segundo plano. Añade `--foreground` para mantenerlo asociado a tu terminal.
|
||||
|
||||
## Iniciar OpenChamber al iniciar sesión
|
||||
|
||||
Usa `startup enable` para instalar un servicio nativo de usuario. OpenChamber usa `launchd` en macOS, `systemd --user` en Linux y Task Scheduler en Windows.
|
||||
|
||||
```bash
|
||||
openchamber startup enable
|
||||
openchamber startup status
|
||||
openchamber startup disable
|
||||
```
|
||||
|
||||
Para proteger la UI, define la contraseña al habilitar el servicio:
|
||||
|
||||
```bash
|
||||
OPENCHAMBER_UI_PASSWORD='secret' openchamber startup enable
|
||||
```
|
||||
|
||||
`startup enable` guarda una captura del entorno actual en el servicio para que el arranque se parezca más a ejecutar `openchamber` desde la misma shell. Así conserva tokens de proveedores, `PATH`, configuración del agente SSH y otras variables CLI de auth/config. Usa `--no-env-snapshot` si quieres un entorno de servicio mínimo.
|
||||
|
||||
Puedes gestionar túneles de forma independiente para ese servicio en ejecución:
|
||||
|
||||
```bash
|
||||
openchamber tunnel start --port 3000
|
||||
openchamber tunnel stop --port 3000
|
||||
```
|
||||
|
||||
Detener el túnel no reinicia el servicio ni la app.
|
||||
|
||||
## "OpenCode is restarting"
|
||||
|
||||
Mientras el servidor se inicia o reinicia, OpenChamber muestra un estado "OpenCode is restarting" y pausa las solicitudes hasta que esté listo. Esto es normal justo después de iniciar o reiniciar. Si nunca desaparece, consulta [Conexión de OpenCode](/es/troubleshooting/opencode-connection/).
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
---
|
||||
title: 환경 변수
|
||||
description: 환경 변수로 OpenChamber와 OpenCode 연동을 설정합니다.
|
||||
---
|
||||
|
||||
# 환경 변수
|
||||
|
||||
OpenChamber는 시작할 때 이 변수들을 읽습니다. 시작 서비스는 `openchamber startup enable` 실행 시 기본적으로 현재 환경의 snapshot을 저장하므로, 서비스에 적용할 변수를 바꾼 뒤에는 다시 실행하세요.
|
||||
|
||||
## OpenChamber 서버
|
||||
|
||||
### `OPENCHAMBER_HOST`
|
||||
|
||||
OpenChamber 웹 서버가 바인딩할 주소입니다. 다른 컴퓨터에서 접속해야 하면 `0.0.0.0`을 사용하세요.
|
||||
|
||||
### `OPENCHAMBER_UI_PASSWORD`
|
||||
|
||||
브라우저 UI 비밀번호입니다. localhost 밖으로 바인딩하거나 터널, 리버스 프록시를 사용할 때 설정하세요.
|
||||
|
||||
### `OPENCHAMBER_DATA_DIR`
|
||||
|
||||
OpenChamber 데이터 디렉터리를 바꿉니다. 기본값은 `~/.config/openchamber`입니다.
|
||||
|
||||
### `OPENCHAMBER_COMPRESS_API`
|
||||
|
||||
API 응답 압축을 제어합니다. `true` 또는 `1`은 켜고, `false` 또는 `0`은 끕니다.
|
||||
|
||||
### `OPENCHAMBER_SKIP_API_COMPRESSION`
|
||||
|
||||
`true` 또는 `1`이면 API 응답 압축을 끕니다. `OPENCHAMBER_COMPRESS_API`보다 우선합니다.
|
||||
|
||||
### `OPENCHAMBER_VERBOSE_REQUEST_LOGS`
|
||||
|
||||
`true` 또는 `1`이면 자세한 HTTP request logs를 켭니다.
|
||||
|
||||
### `OPENCHAMBER_UPDATE_API_URL`
|
||||
|
||||
업데이트 확인 API endpoint를 바꿉니다. 대부분의 사용자는 설정하지 않아도 됩니다.
|
||||
|
||||
### `OPENCHAMBER_PACKAGE_MANAGER`
|
||||
|
||||
자동 감지가 틀렸을 때 업데이트에 사용할 package manager를 강제로 지정합니다.
|
||||
|
||||
## OpenCode 서버
|
||||
|
||||
### `OPENCODE_HOST`
|
||||
|
||||
OpenChamber를 이미 실행 중인 OpenCode server에 연결합니다. 값은 명시적 포트가 있고 path, query, hash가 없는 `http` 또는 `https` origin이어야 합니다. `OPENCODE_HOST`는 `OPENCODE_PORT`보다 우선합니다.
|
||||
|
||||
### `OPENCODE_PORT`
|
||||
|
||||
OpenCode server 포트를 설정합니다. managed OpenCode에서는 이 포트를 요청하고, `OPENCODE_SKIP_START=true`와 함께 쓰면 해당 포트의 외부 서버에 연결합니다.
|
||||
|
||||
### `OPENCODE_SKIP_START`
|
||||
|
||||
`true`이면 OpenChamber가 자체 OpenCode server를 시작하지 않습니다.
|
||||
|
||||
### `OPENCHAMBER_OPENCODE_HOSTNAME`
|
||||
|
||||
OpenChamber가 관리하는 OpenCode server의 bind hostname입니다. 기본값은 `127.0.0.1`입니다.
|
||||
|
||||
### `OPENCODE_BINARY`
|
||||
|
||||
OpenChamber가 실행할 `opencode` executable 경로입니다.
|
||||
|
||||
### `OPENCODE_CONFIG`
|
||||
|
||||
특정 OpenCode config 파일 경로입니다.
|
||||
|
||||
### `OPENCODE_CONFIG_DIR`
|
||||
|
||||
agents, skills, snippets, config discovery에 사용할 OpenCode config 디렉터리입니다.
|
||||
|
||||
### `OPENCODE_DATA_DIR`
|
||||
|
||||
managed OpenCode server의 사용자 지정 데이터 디렉터리입니다.
|
||||
|
||||
### `OPENCODE_WSL_DISTRO`
|
||||
|
||||
Windows에서 OpenCode 연동에 사용할 WSL distro를 선택합니다.
|
||||
|
||||
### `OPENCHAMBER_OPENCODE_WSL_DISTRO`
|
||||
|
||||
WSL distro를 선택하는 OpenChamber 전용 alias입니다. 둘 다 설정하면 `OPENCODE_WSL_DISTRO`가 우선합니다.
|
||||
|
||||
### `OPENCODE_JWT_SECRET`
|
||||
|
||||
UI auth tokens 서명에 사용하는 secret입니다. 지속 실행 서비스에서는 긴 무작위 값을 사용하세요.
|
||||
|
||||
## Terminal 및 Git
|
||||
|
||||
### `OPENCHAMBER_TERMINAL_SHELL`
|
||||
|
||||
OpenChamber terminal sessions에서 사용할 shell executable입니다.
|
||||
|
||||
### `OPENCHAMBER_GIT_BINARY`
|
||||
|
||||
OpenChamber Git 기능에서 사용할 Git executable입니다.
|
||||
|
||||
### `GIT_BINARY`
|
||||
|
||||
Git executable의 대체 override입니다. OpenChamber 전용 설정에는 `OPENCHAMBER_GIT_BINARY`를 권장합니다.
|
||||
|
||||
### `OPENCHAMBER_GIT_READ_CACHE_TTL_MS`
|
||||
|
||||
Git-backed file reads cache의 TTL이며 단위는 밀리초입니다. 디버깅 중 cache를 끄려면 `0`으로 설정하세요.
|
||||
|
||||
## 음성 및 터널
|
||||
|
||||
### `OPENAI_API_KEY`
|
||||
|
||||
OpenAI-compatible services를 호출하는 OpenChamber 음성 기능에 사용하는 API key입니다.
|
||||
|
||||
### `OPENCHAMBER_ALLOW_REMOTE_OPENAI_COMPAT_URLS`
|
||||
|
||||
`true` 또는 `1`이면 음성 기능에서 원격 OpenAI-compatible base URLs를 허용합니다.
|
||||
|
||||
### `NGROK_AUTHTOKEN`
|
||||
|
||||
OpenChamber tunnel commands에서 사용하는 ngrok auth token입니다. `ngrok config add-authtoken <token>`으로도 설정할 수 있습니다.
|
||||
|
||||
## Runtime helpers
|
||||
|
||||
### `BUN_BINARY`
|
||||
|
||||
OpenChamber가 daemon processes를 시작할 때 사용할 Bun executable입니다.
|
||||
|
||||
### `BUN_INSTALL`
|
||||
|
||||
Bun 설치 루트입니다. OpenChamber는 daemon 시작과 업데이트를 위해 여기서 `bin/bun`을 찾습니다.
|
||||
|
||||
### `VITE_OPENCODE_URL`
|
||||
|
||||
Vite-built web app의 build-time API base URL입니다. 일반 CLI 또는 desktop 사용에서는 대부분 설정할 필요가 없습니다.
|
||||
@@ -44,6 +44,33 @@ openchamber stop
|
||||
|
||||
`openchamber`만 입력하면 서버가 백그라운드에서 시작됩니다. 터미널에 연결된 상태로 유지하려면 `--foreground`를 추가하세요.
|
||||
|
||||
## 로그인할 때 OpenChamber 시작하기
|
||||
|
||||
`startup enable`을 사용해 네이티브 사용자 서비스를 설치하세요. OpenChamber는 macOS에서 `launchd`, Linux에서 `systemd --user`, Windows에서 Task Scheduler를 사용합니다.
|
||||
|
||||
```bash
|
||||
openchamber startup enable
|
||||
openchamber startup status
|
||||
openchamber startup disable
|
||||
```
|
||||
|
||||
UI를 보호하려면 서비스를 활성화할 때 비밀번호를 설정하세요:
|
||||
|
||||
```bash
|
||||
OPENCHAMBER_UI_PASSWORD='secret' openchamber startup enable
|
||||
```
|
||||
|
||||
`startup enable`은 현재 환경의 스냅샷을 서비스에 저장해, 같은 셸에서 `openchamber`를 직접 실행한 것에 더 가깝게 동작하게 합니다. provider 토큰, `PATH`, SSH agent 설정, 기타 CLI auth/config 환경 변수가 유지됩니다. 최소한의 서비스 환경을 원하면 `--no-env-snapshot`을 사용하세요.
|
||||
|
||||
실행 중인 이 서비스의 터널은 별도로 관리할 수 있습니다:
|
||||
|
||||
```bash
|
||||
openchamber tunnel start --port 3000
|
||||
openchamber tunnel stop --port 3000
|
||||
```
|
||||
|
||||
터널을 중지해도 서비스나 앱은 재시작되지 않습니다.
|
||||
|
||||
## "OpenCode is restarting"
|
||||
|
||||
서버가 시작되거나 재시작되는 동안 OpenChamber는 "OpenCode is restarting" 상태를 표시하고 준비가 끝날 때까지 요청을 일시 중지합니다. 실행 직후나 재시작 직후에는 정상적인 동작입니다. 이 상태가 계속 사라지지 않으면 [OpenCode 연결](/ko/troubleshooting/opencode-connection/)을 참고하세요.
|
||||
|
||||
@@ -44,6 +44,33 @@ openchamber stop
|
||||
|
||||
`openchamber` on its own starts the server in the background. Add `--foreground` to keep it attached to your terminal.
|
||||
|
||||
## Start OpenChamber at login
|
||||
|
||||
Use `startup enable` to install a native user service. OpenChamber uses `launchd` on macOS, `systemd --user` on Linux, and Task Scheduler on Windows.
|
||||
|
||||
```bash
|
||||
openchamber startup enable
|
||||
openchamber startup status
|
||||
openchamber startup disable
|
||||
```
|
||||
|
||||
To protect the UI, set the password when enabling the service:
|
||||
|
||||
```bash
|
||||
OPENCHAMBER_UI_PASSWORD='secret' openchamber startup enable
|
||||
```
|
||||
|
||||
`startup enable` snapshots your current environment into the service so it behaves more like starting `openchamber` from the same shell. This keeps provider tokens, `PATH`, SSH agent settings, and other CLI auth/config variables available. Use `--no-env-snapshot` if you want a minimal service environment.
|
||||
|
||||
You can still manage tunnels independently for that running service:
|
||||
|
||||
```bash
|
||||
openchamber tunnel start --port 3000
|
||||
openchamber tunnel stop --port 3000
|
||||
```
|
||||
|
||||
Stopping the tunnel does not restart the service or the app.
|
||||
|
||||
## "OpenCode is restarting"
|
||||
|
||||
While the server is starting or restarting, OpenChamber shows an "OpenCode is restarting" state and pauses requests until it's ready. This is normal right after launch or a restart. If it never clears, see [OpenCode connection](/troubleshooting/opencode-connection/).
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
---
|
||||
title: Zmienne środowiskowe
|
||||
description: Skonfiguruj integrację OpenChamber i OpenCode za pomocą zmiennych środowiskowych.
|
||||
---
|
||||
|
||||
# Zmienne środowiskowe
|
||||
|
||||
OpenChamber odczytuje te zmienne przy starcie. Dla usług startowych `openchamber startup enable` domyślnie zapisuje snapshot bieżącego środowiska, więc uruchom tę komendę ponownie po zmianie zmiennych, których ma używać usługa.
|
||||
|
||||
## Serwer OpenChamber
|
||||
|
||||
### `OPENCHAMBER_HOST`
|
||||
|
||||
Adres, na którym nasłuchuje serwer web OpenChamber. Użyj `0.0.0.0`, aby pozwolić na dostęp z innych maszyn.
|
||||
|
||||
### `OPENCHAMBER_UI_PASSWORD`
|
||||
|
||||
Hasło do interfejsu w przeglądarce. Ustaw je przy dostępie spoza localhost, tunelach albo reverse proxy.
|
||||
|
||||
### `OPENCHAMBER_DATA_DIR`
|
||||
|
||||
Nadpisuje katalog danych OpenChamber. Domyślnie jest to `~/.config/openchamber`.
|
||||
|
||||
### `OPENCHAMBER_COMPRESS_API`
|
||||
|
||||
Steruje kompresją odpowiedzi API. `true` albo `1` ją włącza, `false` albo `0` wyłącza.
|
||||
|
||||
### `OPENCHAMBER_SKIP_API_COMPRESSION`
|
||||
|
||||
Wyłącza kompresję odpowiedzi API, gdy ma wartość `true` albo `1`. Ma pierwszeństwo przed `OPENCHAMBER_COMPRESS_API`.
|
||||
|
||||
### `OPENCHAMBER_VERBOSE_REQUEST_LOGS`
|
||||
|
||||
Włącza szczegółowe HTTP request logs, gdy ma wartość `true` albo `1`.
|
||||
|
||||
### `OPENCHAMBER_UPDATE_API_URL`
|
||||
|
||||
Nadpisuje endpoint sprawdzania aktualizacji. Większość użytkowników nie powinna tego ustawiać.
|
||||
|
||||
### `OPENCHAMBER_PACKAGE_MANAGER`
|
||||
|
||||
Wymusza package manager używany do aktualizacji, gdy automatyczne wykrywanie się pomyli.
|
||||
|
||||
## Serwer OpenCode
|
||||
|
||||
### `OPENCODE_HOST`
|
||||
|
||||
Łączy OpenChamber z istniejącym OpenCode server. Wartość musi być origin `http` albo `https` z jawnym portem i bez path, query ani hash. `OPENCODE_HOST` ma pierwszeństwo przed `OPENCODE_PORT`.
|
||||
|
||||
### `OPENCODE_PORT`
|
||||
|
||||
Ustawia port OpenCode server. Dla managed OpenCode prosi o ten port; z `OPENCODE_SKIP_START=true` łączy się z zewnętrznym serwerem na tym porcie.
|
||||
|
||||
### `OPENCODE_SKIP_START`
|
||||
|
||||
Gdy ma wartość `true`, blokuje uruchamianie własnego OpenCode server przez OpenChamber.
|
||||
|
||||
### `OPENCHAMBER_OPENCODE_HOSTNAME`
|
||||
|
||||
Hostname dla OpenCode server zarządzanego przez OpenChamber. Domyślnie `127.0.0.1`.
|
||||
|
||||
### `OPENCODE_BINARY`
|
||||
|
||||
Ścieżka do executable `opencode`, którego ma używać OpenChamber.
|
||||
|
||||
### `OPENCODE_CONFIG`
|
||||
|
||||
Ścieżka do konkretnego pliku config OpenCode.
|
||||
|
||||
### `OPENCODE_CONFIG_DIR`
|
||||
|
||||
Ścieżka do katalogu config OpenCode dla agents, skills, snippets i config discovery.
|
||||
|
||||
### `OPENCODE_DATA_DIR`
|
||||
|
||||
Własny katalog danych dla managed OpenCode server.
|
||||
|
||||
### `OPENCODE_WSL_DISTRO`
|
||||
|
||||
Wybiera WSL distro używane do integracji OpenCode w Windows.
|
||||
|
||||
### `OPENCHAMBER_OPENCODE_WSL_DISTRO`
|
||||
|
||||
Alias specyficzny dla OpenChamber do wyboru WSL distro. `OPENCODE_WSL_DISTRO` ma pierwszeństwo, gdy ustawione są obie zmienne.
|
||||
|
||||
### `OPENCODE_JWT_SECRET`
|
||||
|
||||
Secret używany do podpisywania UI auth tokens. Dla trwałych usług użyj długiej losowej wartości.
|
||||
|
||||
## Terminal i Git
|
||||
|
||||
### `OPENCHAMBER_TERMINAL_SHELL`
|
||||
|
||||
Shell executable używany przez terminal sessions w OpenChamber.
|
||||
|
||||
### `OPENCHAMBER_GIT_BINARY`
|
||||
|
||||
Git executable używany przez funkcje Git w OpenChamber.
|
||||
|
||||
### `GIT_BINARY`
|
||||
|
||||
Alternatywny override dla Git executable. Dla konfiguracji specyficznej dla OpenChamber preferuj `OPENCHAMBER_GIT_BINARY`.
|
||||
|
||||
### `OPENCHAMBER_GIT_READ_CACHE_TTL_MS`
|
||||
|
||||
TTL w milisekundach dla cached Git-backed file reads. Ustaw `0`, aby wyłączyć cache podczas debugowania.
|
||||
|
||||
## Głos i tunele
|
||||
|
||||
### `OPENAI_API_KEY`
|
||||
|
||||
API key używany przez funkcje głosowe OpenChamber, które wywołują OpenAI-compatible services.
|
||||
|
||||
### `OPENCHAMBER_ALLOW_REMOTE_OPENAI_COMPAT_URLS`
|
||||
|
||||
Pozwala na zdalne OpenAI-compatible base URLs dla funkcji głosowych, gdy ma wartość `true` albo `1`.
|
||||
|
||||
### `NGROK_AUTHTOKEN`
|
||||
|
||||
ngrok auth token używany przez tunnel commands OpenChamber. Możesz też skonfigurować go przez `ngrok config add-authtoken <token>`.
|
||||
|
||||
## Runtime helpers
|
||||
|
||||
### `BUN_BINARY`
|
||||
|
||||
Bun executable, którego OpenChamber ma używać do uruchamiania daemon processes.
|
||||
|
||||
### `BUN_INSTALL`
|
||||
|
||||
Katalog główny instalacji Bun. OpenChamber używa go, aby znaleźć `bin/bun` dla daemon startup i aktualizacji.
|
||||
|
||||
### `VITE_OPENCODE_URL`
|
||||
|
||||
Build-time API base URL dla Vite-built web app. Przy normalnym użyciu CLI lub desktop większość użytkowników nie powinna tego ustawiać.
|
||||
@@ -44,6 +44,33 @@ openchamber stop
|
||||
|
||||
Samo `openchamber` uruchamia serwer w tle. Dodaj `--foreground`, aby pozostał dołączony do Twojego terminala.
|
||||
|
||||
## Uruchamianie OpenChamber po zalogowaniu
|
||||
|
||||
Użyj `startup enable`, aby zainstalować natywną usługę użytkownika. OpenChamber używa `launchd` na macOS, `systemd --user` na Linuksie i Harmonogramu zadań w Windows.
|
||||
|
||||
```bash
|
||||
openchamber startup enable
|
||||
openchamber startup status
|
||||
openchamber startup disable
|
||||
```
|
||||
|
||||
Aby zabezpieczyć UI, ustaw hasło podczas włączania usługi:
|
||||
|
||||
```bash
|
||||
OPENCHAMBER_UI_PASSWORD='secret' openchamber startup enable
|
||||
```
|
||||
|
||||
`startup enable` zapisuje migawkę bieżącego środowiska w usłudze, aby uruchomienie było bliższe ręcznemu startowi `openchamber` z tej samej powłoki. Zachowuje to tokeny dostawców, `PATH`, ustawienia agenta SSH i inne zmienne CLI auth/config. Użyj `--no-env-snapshot`, jeśli chcesz minimalne środowisko usługi.
|
||||
|
||||
Tunelami dla tej działającej usługi możesz zarządzać niezależnie:
|
||||
|
||||
```bash
|
||||
openchamber tunnel start --port 3000
|
||||
openchamber tunnel stop --port 3000
|
||||
```
|
||||
|
||||
Zatrzymanie tunelu nie restartuje usługi ani aplikacji.
|
||||
|
||||
## „OpenCode uruchamia się ponownie”
|
||||
|
||||
Gdy serwer startuje lub uruchamia się ponownie, OpenChamber pokazuje stan „OpenCode uruchamia się ponownie” i wstrzymuje żądania, aż będzie gotowy. To normalne tuż po uruchomieniu lub restarcie. Jeśli ten stan nigdy nie znika, zobacz [Połączenie z OpenCode](/pl/troubleshooting/opencode-connection/).
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
---
|
||||
title: Variáveis de ambiente
|
||||
description: Configure a integração do OpenChamber e do OpenCode com variáveis de ambiente.
|
||||
---
|
||||
|
||||
# Variáveis de ambiente
|
||||
|
||||
O OpenChamber lê estas variáveis na inicialização. Para serviços de inicialização, `openchamber startup enable` salva por padrão um snapshot do ambiente atual, então execute de novo após mudar variáveis que o serviço deve usar.
|
||||
|
||||
## Servidor OpenChamber
|
||||
|
||||
### `OPENCHAMBER_HOST`
|
||||
|
||||
Endereço onde o servidor web do OpenChamber escuta. Use `0.0.0.0` para permitir acesso de outras máquinas.
|
||||
|
||||
### `OPENCHAMBER_UI_PASSWORD`
|
||||
|
||||
Senha da interface no navegador. Use quando expor fora do localhost, por túnel ou por proxy reverso.
|
||||
|
||||
### `OPENCHAMBER_DATA_DIR`
|
||||
|
||||
Altera o diretório de dados do OpenChamber. O padrão é `~/.config/openchamber`.
|
||||
|
||||
### `OPENCHAMBER_COMPRESS_API`
|
||||
|
||||
Controla a compressão das respostas da API. `true` ou `1` ativa; `false` ou `0` desativa.
|
||||
|
||||
### `OPENCHAMBER_SKIP_API_COMPRESSION`
|
||||
|
||||
Desativa a compressão das respostas da API quando definido como `true` ou `1`. Tem prioridade sobre `OPENCHAMBER_COMPRESS_API`.
|
||||
|
||||
### `OPENCHAMBER_VERBOSE_REQUEST_LOGS`
|
||||
|
||||
Ativa logs HTTP detalhados quando definido como `true` ou `1`.
|
||||
|
||||
### `OPENCHAMBER_UPDATE_API_URL`
|
||||
|
||||
Altera o endpoint de verificação de atualizações. A maioria dos usuários deve deixar isso sem definir.
|
||||
|
||||
### `OPENCHAMBER_PACKAGE_MANAGER`
|
||||
|
||||
Força o gerenciador de pacotes usado em atualizações quando a detecção automática estiver errada.
|
||||
|
||||
## Servidor OpenCode
|
||||
|
||||
### `OPENCODE_HOST`
|
||||
|
||||
Conecta o OpenChamber a um servidor OpenCode existente. O valor deve ser um origin `http` ou `https` com porta explícita e sem path, query ou hash. `OPENCODE_HOST` tem prioridade sobre `OPENCODE_PORT`.
|
||||
|
||||
### `OPENCODE_PORT`
|
||||
|
||||
Define a porta do servidor OpenCode. Com OpenCode gerenciado, solicita essa porta; com `OPENCODE_SKIP_START=true`, conecta a um servidor externo nessa porta.
|
||||
|
||||
### `OPENCODE_SKIP_START`
|
||||
|
||||
Impede o OpenChamber de iniciar seu próprio servidor OpenCode quando definido como `true`.
|
||||
|
||||
### `OPENCHAMBER_OPENCODE_HOSTNAME`
|
||||
|
||||
Hostname do servidor OpenCode gerenciado pelo OpenChamber. O padrão é `127.0.0.1`.
|
||||
|
||||
### `OPENCODE_BINARY`
|
||||
|
||||
Caminho para o executável `opencode` que o OpenChamber deve usar.
|
||||
|
||||
### `OPENCODE_CONFIG`
|
||||
|
||||
Caminho para um arquivo de configuração específico do OpenCode.
|
||||
|
||||
### `OPENCODE_CONFIG_DIR`
|
||||
|
||||
Caminho para o diretório de configuração do OpenCode para agents, skills, snippets e descoberta de config.
|
||||
|
||||
### `OPENCODE_DATA_DIR`
|
||||
|
||||
Diretório de dados personalizado para o servidor OpenCode gerenciado.
|
||||
|
||||
### `OPENCODE_WSL_DISTRO`
|
||||
|
||||
Escolhe a distro WSL usada pela integração do OpenCode no Windows.
|
||||
|
||||
### `OPENCHAMBER_OPENCODE_WSL_DISTRO`
|
||||
|
||||
Alias específico do OpenChamber para escolher a distro WSL. `OPENCODE_WSL_DISTRO` tem prioridade quando ambas estão definidas.
|
||||
|
||||
### `OPENCODE_JWT_SECRET`
|
||||
|
||||
Segredo usado para assinar tokens de autenticação da UI. Em serviços persistentes, use um valor longo e aleatório.
|
||||
|
||||
## Terminal e Git
|
||||
|
||||
### `OPENCHAMBER_TERMINAL_SHELL`
|
||||
|
||||
Executável de shell usado nas sessões de terminal do OpenChamber.
|
||||
|
||||
### `OPENCHAMBER_GIT_BINARY`
|
||||
|
||||
Executável do Git usado pelos recursos Git do OpenChamber.
|
||||
|
||||
### `GIT_BINARY`
|
||||
|
||||
Override alternativo para o executável do Git. Para configuração específica do OpenChamber, prefira `OPENCHAMBER_GIT_BINARY`.
|
||||
|
||||
### `OPENCHAMBER_GIT_READ_CACHE_TTL_MS`
|
||||
|
||||
TTL, em milissegundos, para leituras de arquivos em cache baseadas em Git. Use `0` para desativar o cache durante depuração.
|
||||
|
||||
## Voz e túneis
|
||||
|
||||
### `OPENAI_API_KEY`
|
||||
|
||||
API key usada por recursos de voz do OpenChamber que chamam serviços compatíveis com OpenAI.
|
||||
|
||||
### `OPENCHAMBER_ALLOW_REMOTE_OPENAI_COMPAT_URLS`
|
||||
|
||||
Permite base URLs remotas compatíveis com OpenAI para recursos de voz quando definido como `true` ou `1`.
|
||||
|
||||
### `NGROK_AUTHTOKEN`
|
||||
|
||||
Token de autenticação do ngrok usado pelos comandos de túnel do OpenChamber. Você também pode configurar com `ngrok config add-authtoken <token>`.
|
||||
|
||||
## Auxiliares de runtime
|
||||
|
||||
### `BUN_BINARY`
|
||||
|
||||
Executável do Bun que o OpenChamber deve usar ao iniciar processos daemon.
|
||||
|
||||
### `BUN_INSTALL`
|
||||
|
||||
Raiz da instalação do Bun. O OpenChamber usa isso para encontrar `bin/bun` ao iniciar daemons e atualizar.
|
||||
|
||||
### `VITE_OPENCODE_URL`
|
||||
|
||||
API base URL de build-time para a web app criada com Vite. A maioria dos usuários não deve usar isso no CLI ou desktop normal.
|
||||
@@ -44,6 +44,33 @@ openchamber stop
|
||||
|
||||
`openchamber` sozinho inicia o servidor em segundo plano. Adicione `--foreground` para mantê-lo anexado ao seu terminal.
|
||||
|
||||
## Iniciar o OpenChamber no login
|
||||
|
||||
Use `startup enable` para instalar um serviço nativo do usuário. O OpenChamber usa `launchd` no macOS, `systemd --user` no Linux e Task Scheduler no Windows.
|
||||
|
||||
```bash
|
||||
openchamber startup enable
|
||||
openchamber startup status
|
||||
openchamber startup disable
|
||||
```
|
||||
|
||||
Para proteger a UI, defina a senha ao habilitar o serviço:
|
||||
|
||||
```bash
|
||||
OPENCHAMBER_UI_PASSWORD='secret' openchamber startup enable
|
||||
```
|
||||
|
||||
`startup enable` salva um snapshot do ambiente atual no serviço para que a inicialização se pareça mais com executar `openchamber` na mesma shell. Isso preserva tokens de provedores, `PATH`, configurações do agente SSH e outras variáveis CLI de auth/config. Use `--no-env-snapshot` se quiser um ambiente de serviço mínimo.
|
||||
|
||||
Você ainda pode gerenciar túneis de forma independente para esse serviço em execução:
|
||||
|
||||
```bash
|
||||
openchamber tunnel start --port 3000
|
||||
openchamber tunnel stop --port 3000
|
||||
```
|
||||
|
||||
Parar o túnel não reinicia o serviço nem o app.
|
||||
|
||||
## "OpenCode is restarting"
|
||||
|
||||
Enquanto o servidor está iniciando ou reiniciando, o OpenChamber mostra um estado "OpenCode is restarting" e pausa as requisições até ele estar pronto. Isso é normal logo após a inicialização ou um reinício. Se nunca sair desse estado, veja [Conexão com o OpenCode](/pt-br/troubleshooting/opencode-connection/).
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
---
|
||||
title: Змінні середовища
|
||||
description: Налаштуйте інтеграцію OpenChamber і OpenCode через змінні середовища.
|
||||
---
|
||||
|
||||
# Змінні середовища
|
||||
|
||||
OpenChamber читає ці змінні під час запуску. Для startup-сервісів `openchamber startup enable` типово зберігає snapshot поточного середовища, тому після змін перезапустіть цю команду.
|
||||
|
||||
## Сервер OpenChamber
|
||||
|
||||
### `OPENCHAMBER_HOST`
|
||||
|
||||
Адреса, на якій слухає web-сервер OpenChamber. Використовуйте `0.0.0.0`, щоб відкрити доступ з інших машин.
|
||||
|
||||
### `OPENCHAMBER_UI_PASSWORD`
|
||||
|
||||
Пароль для browser UI. Використовуйте його для доступу не лише з localhost, тунелів або reverse proxy.
|
||||
|
||||
### `OPENCHAMBER_DATA_DIR`
|
||||
|
||||
Перевизначає директорію даних OpenChamber. Типово це `~/.config/openchamber`.
|
||||
|
||||
### `OPENCHAMBER_COMPRESS_API`
|
||||
|
||||
Керує стисненням API-відповідей. `true` або `1` вмикає, `false` або `0` вимикає.
|
||||
|
||||
### `OPENCHAMBER_SKIP_API_COMPRESSION`
|
||||
|
||||
Вимикає стиснення API-відповідей, якщо встановлено `true` або `1`. Має пріоритет над `OPENCHAMBER_COMPRESS_API`.
|
||||
|
||||
### `OPENCHAMBER_VERBOSE_REQUEST_LOGS`
|
||||
|
||||
Вмикає детальні HTTP request logs, якщо встановлено `true` або `1`.
|
||||
|
||||
### `OPENCHAMBER_UPDATE_API_URL`
|
||||
|
||||
Перевизначає endpoint для перевірки оновлень. Більшості користувачів це не потрібно.
|
||||
|
||||
### `OPENCHAMBER_PACKAGE_MANAGER`
|
||||
|
||||
Примусово задає package manager для оновлень, якщо auto-detection помилився.
|
||||
|
||||
## Сервер OpenCode
|
||||
|
||||
### `OPENCODE_HOST`
|
||||
|
||||
Підключає OpenChamber до вже запущеного OpenCode server. Значення має бути `http` або `https` origin з явним портом і без path, query або hash. `OPENCODE_HOST` має пріоритет над `OPENCODE_PORT`.
|
||||
|
||||
### `OPENCODE_PORT`
|
||||
|
||||
Задає порт OpenCode server. Для managed OpenCode це бажаний порт; з `OPENCODE_SKIP_START=true` OpenChamber підключається до зовнішнього сервера на цьому порту.
|
||||
|
||||
### `OPENCODE_SKIP_START`
|
||||
|
||||
Не дає OpenChamber запускати власний OpenCode server, якщо встановлено `true`.
|
||||
|
||||
### `OPENCHAMBER_OPENCODE_HOSTNAME`
|
||||
|
||||
Hostname для OpenCode server, яким керує OpenChamber. Типово `127.0.0.1`.
|
||||
|
||||
### `OPENCODE_BINARY`
|
||||
|
||||
Шлях до executable `opencode`, який має запускати OpenChamber.
|
||||
|
||||
### `OPENCODE_CONFIG`
|
||||
|
||||
Шлях до конкретного config-файлу OpenCode.
|
||||
|
||||
### `OPENCODE_CONFIG_DIR`
|
||||
|
||||
Шлях до директорії config OpenCode для agents, skills, snippets і config discovery.
|
||||
|
||||
### `OPENCODE_DATA_DIR`
|
||||
|
||||
Власна директорія даних для managed OpenCode server.
|
||||
|
||||
### `OPENCODE_WSL_DISTRO`
|
||||
|
||||
Вибирає WSL distro для інтеграції OpenCode на Windows.
|
||||
|
||||
### `OPENCHAMBER_OPENCODE_WSL_DISTRO`
|
||||
|
||||
OpenChamber-specific alias для вибору WSL distro. `OPENCODE_WSL_DISTRO` має пріоритет, якщо задані обидві змінні.
|
||||
|
||||
### `OPENCODE_JWT_SECRET`
|
||||
|
||||
Secret для підпису UI auth tokens. Для постійних service deployments використовуйте довге випадкове значення.
|
||||
|
||||
## Terminal і Git
|
||||
|
||||
### `OPENCHAMBER_TERMINAL_SHELL`
|
||||
|
||||
Shell executable для terminal sessions в OpenChamber.
|
||||
|
||||
### `OPENCHAMBER_GIT_BINARY`
|
||||
|
||||
Git executable для Git-функцій OpenChamber.
|
||||
|
||||
### `GIT_BINARY`
|
||||
|
||||
Альтернативний override для Git executable. Для OpenChamber-specific налаштувань краще використовуйте `OPENCHAMBER_GIT_BINARY`.
|
||||
|
||||
### `OPENCHAMBER_GIT_READ_CACHE_TTL_MS`
|
||||
|
||||
TTL у мілісекундах для cached Git-backed file reads. Встановіть `0`, щоб вимкнути cache під час debugging.
|
||||
|
||||
## Voice і tunnels
|
||||
|
||||
### `OPENAI_API_KEY`
|
||||
|
||||
API key для voice-функцій OpenChamber, які викликають OpenAI-compatible services.
|
||||
|
||||
### `OPENCHAMBER_ALLOW_REMOTE_OPENAI_COMPAT_URLS`
|
||||
|
||||
Дозволяє remote OpenAI-compatible base URLs для voice-функцій, якщо встановлено `true` або `1`.
|
||||
|
||||
### `NGROK_AUTHTOKEN`
|
||||
|
||||
ngrok auth token для tunnel-команд OpenChamber. Також можна налаштувати ngrok через `ngrok config add-authtoken <token>`.
|
||||
|
||||
## Runtime helpers
|
||||
|
||||
### `BUN_BINARY`
|
||||
|
||||
Bun executable, який OpenChamber має використовувати для запуску daemon processes.
|
||||
|
||||
### `BUN_INSTALL`
|
||||
|
||||
Корінь інсталяції Bun. OpenChamber використовує його, щоб знайти `bin/bun` для daemon startup і updates.
|
||||
|
||||
### `VITE_OPENCODE_URL`
|
||||
|
||||
Build-time API base URL для Vite-built web app. Для звичайного CLI або desktop usage більшості користувачів це не потрібно.
|
||||
@@ -44,6 +44,33 @@ openchamber stop
|
||||
|
||||
`openchamber` сам по собі запускає сервер у фоні. Додайте `--foreground`, щоб лишити його прив'язаним до вашого терміналу.
|
||||
|
||||
## Запуск OpenChamber при вході в систему
|
||||
|
||||
Скористайтеся `startup enable`, щоб встановити нативний користувацький сервіс. OpenChamber використовує `launchd` на macOS, `systemd --user` на Linux і Task Scheduler на Windows.
|
||||
|
||||
```bash
|
||||
openchamber startup enable
|
||||
openchamber startup status
|
||||
openchamber startup disable
|
||||
```
|
||||
|
||||
Щоб захистити UI, задайте пароль під час увімкнення сервісу:
|
||||
|
||||
```bash
|
||||
OPENCHAMBER_UI_PASSWORD='secret' openchamber startup enable
|
||||
```
|
||||
|
||||
`startup enable` зберігає знімок поточного середовища в сервісі, щоб запуск був ближчим до ручного запуску `openchamber` з тієї самої shell-сесії. Так зберігаються токени провайдерів, `PATH`, налаштування SSH agent та інші CLI-змінні для auth/config. Використайте `--no-env-snapshot`, якщо потрібне мінімальне середовище сервісу.
|
||||
|
||||
Тунелями для такого запущеного сервісу можна керувати окремо:
|
||||
|
||||
```bash
|
||||
openchamber tunnel start --port 3000
|
||||
openchamber tunnel stop --port 3000
|
||||
```
|
||||
|
||||
Зупинка тунелю не перезапускає сервіс або застосунок.
|
||||
|
||||
## «OpenCode перезапускається»
|
||||
|
||||
Поки сервер запускається або перезапускається, OpenChamber показує стан «OpenCode перезапускається» і призупиняє запити, доки той не буде готовий. Це нормально одразу після запуску чи перезапуску. Якщо стан так і не зникає, див. [Підключення до OpenCode](/uk/troubleshooting/opencode-connection/).
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
---
|
||||
title: 环境变量
|
||||
description: 使用环境变量配置 OpenChamber 和 OpenCode 集成。
|
||||
---
|
||||
|
||||
# 环境变量
|
||||
|
||||
OpenChamber 会在启动时读取这些变量。对于开机启动服务,`openchamber startup enable` 默认会保存当前环境的 snapshot,所以如果要让服务使用新的变量,请重新运行它。
|
||||
|
||||
## OpenChamber 服务器
|
||||
|
||||
### `OPENCHAMBER_HOST`
|
||||
|
||||
OpenChamber web 服务器监听的地址。使用 `0.0.0.0` 可允许其他机器访问。
|
||||
|
||||
### `OPENCHAMBER_UI_PASSWORD`
|
||||
|
||||
浏览器 UI 的密码。当你绑定到 localhost 之外、使用隧道或反向代理时,请设置它。
|
||||
|
||||
### `OPENCHAMBER_DATA_DIR`
|
||||
|
||||
覆盖 OpenChamber 数据目录。默认是 `~/.config/openchamber`。
|
||||
|
||||
### `OPENCHAMBER_COMPRESS_API`
|
||||
|
||||
控制 API 响应压缩。`true` 或 `1` 强制开启,`false` 或 `0` 强制关闭。
|
||||
|
||||
### `OPENCHAMBER_SKIP_API_COMPRESSION`
|
||||
|
||||
设置为 `true` 或 `1` 时关闭 API 响应压缩。它优先于 `OPENCHAMBER_COMPRESS_API`。
|
||||
|
||||
### `OPENCHAMBER_VERBOSE_REQUEST_LOGS`
|
||||
|
||||
设置为 `true` 或 `1` 时启用详细 HTTP request logs。
|
||||
|
||||
### `OPENCHAMBER_UPDATE_API_URL`
|
||||
|
||||
覆盖更新检查 API endpoint。大多数用户不需要设置它。
|
||||
|
||||
### `OPENCHAMBER_PACKAGE_MANAGER`
|
||||
|
||||
当自动检测错误时,强制指定更新操作使用的 package manager。
|
||||
|
||||
## OpenCode 服务器
|
||||
|
||||
### `OPENCODE_HOST`
|
||||
|
||||
将 OpenChamber 连接到已有的 OpenCode server。值必须是带明确端口、没有 path、query 或 hash 的 `http` 或 `https` origin。`OPENCODE_HOST` 优先于 `OPENCODE_PORT`。
|
||||
|
||||
### `OPENCODE_PORT`
|
||||
|
||||
设置 OpenCode server 端口。对于 managed OpenCode,它请求使用该端口;配合 `OPENCODE_SKIP_START=true` 时,它连接到该端口上的外部服务器。
|
||||
|
||||
### `OPENCODE_SKIP_START`
|
||||
|
||||
设置为 `true` 时阻止 OpenChamber 启动自己的 OpenCode server。
|
||||
|
||||
### `OPENCHAMBER_OPENCODE_HOSTNAME`
|
||||
|
||||
OpenChamber 管理的 OpenCode server 绑定 hostname。默认是 `127.0.0.1`。
|
||||
|
||||
### `OPENCODE_BINARY`
|
||||
|
||||
OpenChamber 应该运行的 `opencode` executable 路径。
|
||||
|
||||
### `OPENCODE_CONFIG`
|
||||
|
||||
指定 OpenCode config 文件路径。
|
||||
|
||||
### `OPENCODE_CONFIG_DIR`
|
||||
|
||||
指定 OpenCode config 目录,用于 agents、skills、snippets 和 config discovery。
|
||||
|
||||
### `OPENCODE_DATA_DIR`
|
||||
|
||||
managed OpenCode server 的自定义数据目录。
|
||||
|
||||
### `OPENCODE_WSL_DISTRO`
|
||||
|
||||
选择 Windows 上 OpenCode 集成使用的 WSL distro。
|
||||
|
||||
### `OPENCHAMBER_OPENCODE_WSL_DISTRO`
|
||||
|
||||
OpenChamber 专用的 WSL distro alias。如果两个变量都设置,`OPENCODE_WSL_DISTRO` 优先。
|
||||
|
||||
### `OPENCODE_JWT_SECRET`
|
||||
|
||||
用于签名 UI auth tokens 的 secret。持久服务部署应使用长的随机值。
|
||||
|
||||
## Terminal 和 Git
|
||||
|
||||
### `OPENCHAMBER_TERMINAL_SHELL`
|
||||
|
||||
OpenChamber terminal sessions 使用的 shell executable。
|
||||
|
||||
### `OPENCHAMBER_GIT_BINARY`
|
||||
|
||||
OpenChamber Git 功能使用的 Git executable。
|
||||
|
||||
### `GIT_BINARY`
|
||||
|
||||
Git executable 的备用 override。OpenChamber 专用配置建议使用 `OPENCHAMBER_GIT_BINARY`。
|
||||
|
||||
### `OPENCHAMBER_GIT_READ_CACHE_TTL_MS`
|
||||
|
||||
Git-backed file reads cache 的 TTL,单位为毫秒。调试时可设置为 `0` 禁用 cache。
|
||||
|
||||
## 语音和隧道
|
||||
|
||||
### `OPENAI_API_KEY`
|
||||
|
||||
OpenChamber 语音功能调用 OpenAI-compatible services 时使用的 API key。
|
||||
|
||||
### `OPENCHAMBER_ALLOW_REMOTE_OPENAI_COMPAT_URLS`
|
||||
|
||||
设置为 `true` 或 `1` 时,允许语音功能使用远程 OpenAI-compatible base URLs。
|
||||
|
||||
### `NGROK_AUTHTOKEN`
|
||||
|
||||
OpenChamber tunnel commands 使用的 ngrok auth token。也可以用 `ngrok config add-authtoken <token>` 配置。
|
||||
|
||||
## Runtime helpers
|
||||
|
||||
### `BUN_BINARY`
|
||||
|
||||
OpenChamber 启动 daemon processes 时使用的 Bun executable。
|
||||
|
||||
### `BUN_INSTALL`
|
||||
|
||||
Bun 安装根目录。OpenChamber 使用它查找 `bin/bun` 来启动 daemon 和执行更新。
|
||||
|
||||
### `VITE_OPENCODE_URL`
|
||||
|
||||
Vite-built web app 的 build-time API base URL。普通 CLI 或 desktop 使用通常不需要设置它。
|
||||
@@ -44,6 +44,33 @@ openchamber stop
|
||||
|
||||
单独运行 `openchamber` 会在后台启动服务器。添加 `--foreground` 可以让它保持附加在你的终端上。
|
||||
|
||||
## 登录时启动 OpenChamber
|
||||
|
||||
使用 `startup enable` 安装原生用户服务。OpenChamber 在 macOS 上使用 `launchd`,在 Linux 上使用 `systemd --user`,在 Windows 上使用 Task Scheduler。
|
||||
|
||||
```bash
|
||||
openchamber startup enable
|
||||
openchamber startup status
|
||||
openchamber startup disable
|
||||
```
|
||||
|
||||
要保护 UI,请在启用服务时设置密码:
|
||||
|
||||
```bash
|
||||
OPENCHAMBER_UI_PASSWORD='secret' openchamber startup enable
|
||||
```
|
||||
|
||||
`startup enable` 会把当前环境快照保存到服务中,让启动行为更接近你在同一个 shell 中手动运行 `openchamber`。这会保留提供商 token、`PATH`、SSH agent 设置以及其他 CLI auth/config 环境变量。如果你想要最小化的服务环境,请使用 `--no-env-snapshot`。
|
||||
|
||||
你仍然可以独立管理这个运行中服务的隧道:
|
||||
|
||||
```bash
|
||||
openchamber tunnel start --port 3000
|
||||
openchamber tunnel stop --port 3000
|
||||
```
|
||||
|
||||
停止隧道不会重启服务或应用。
|
||||
|
||||
## “OpenCode is restarting”
|
||||
|
||||
当服务器正在启动或重启时,OpenChamber 会显示 “OpenCode is restarting” 状态,并暂停请求直到服务器就绪。这在启动或重启后立即出现是正常的。如果它一直无法消失,请参阅 [OpenCode 连接](/zh-cn/troubleshooting/opencode-connection/)。
|
||||
|
||||
@@ -58,6 +58,18 @@
|
||||
"ko": "OpenCode 서버",
|
||||
"pl": "Serwer OpenCode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Environment Variables",
|
||||
"link": "/environment/",
|
||||
"translations": {
|
||||
"uk": "Змінні середовища",
|
||||
"zh-CN": "环境变量",
|
||||
"es": "Variables de entorno",
|
||||
"pt-BR": "Variáveis de ambiente",
|
||||
"ko": "환경 변수",
|
||||
"pl": "Zmienne środowiskowe"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
+83
-10
@@ -20,6 +20,24 @@ const isDev = process.env.OPENCHAMBER_ELECTRON_DEV === '1' || !app.isPackaged;
|
||||
|
||||
const DEEP_LINK_PROTOCOL = 'openchamber';
|
||||
const APP_USER_MODEL_ID = 'dev.openchamber.desktop';
|
||||
const BACKGROUND_START_ARG = '--background';
|
||||
|
||||
const readLoginItemSettings = () => {
|
||||
if (process.platform !== 'darwin') return null;
|
||||
try {
|
||||
return app.getLoginItemSettings();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const shouldStartInBackground = (loginItemSettings = readLoginItemSettings()) => {
|
||||
return (
|
||||
process.argv.includes(BACKGROUND_START_ARG) ||
|
||||
loginItemSettings?.wasOpenedAtLogin === true ||
|
||||
loginItemSettings?.wasOpenedAsHidden === true
|
||||
);
|
||||
};
|
||||
|
||||
if (!app.requestSingleInstanceLock()) {
|
||||
app.exit(0);
|
||||
@@ -1378,6 +1396,21 @@ const activateMainWindow = async (url, localOrigin, bootOutcome) => {
|
||||
return state.mainWindow;
|
||||
};
|
||||
|
||||
const openMainWindow = async () => {
|
||||
if (!state.localOrigin) {
|
||||
const { initialUrl, localOrigin, bootOutcome } = await resolveInitialUrl();
|
||||
return activateMainWindow(initialUrl, localOrigin, bootOutcome);
|
||||
}
|
||||
|
||||
const config = readDesktopHostsConfig();
|
||||
const localUiUrl = state.sidecarUrl || state.localOrigin;
|
||||
const host = config.defaultHostId && config.defaultHostId !== LOCAL_HOST_ID
|
||||
? config.hosts.find((entry) => entry.id === config.defaultHostId)
|
||||
: null;
|
||||
const targetUrl = host?.url && !state.unreachableHosts.has(host.url) ? host.url : localUiUrl;
|
||||
return activateMainWindow(targetUrl, state.localOrigin, state.bootOutcome);
|
||||
};
|
||||
|
||||
const createAdditionalWindow = async (url) => {
|
||||
if (!state.localOrigin) {
|
||||
return null;
|
||||
@@ -1869,6 +1902,24 @@ const handleInvoke = async (browserWindow, command, args = {}) => {
|
||||
case 'desktop_get_app_version':
|
||||
return APP_VERSION;
|
||||
|
||||
case 'desktop_get_launch_at_login': {
|
||||
if (process.platform !== 'darwin') return { supported: false, enabled: false };
|
||||
const settings = app.getLoginItemSettings();
|
||||
return { supported: true, enabled: settings.openAtLogin === true };
|
||||
}
|
||||
|
||||
case 'desktop_set_launch_at_login': {
|
||||
if (process.platform !== 'darwin') return { supported: false, enabled: false };
|
||||
const enabled = args.enabled === true;
|
||||
app.setLoginItemSettings({
|
||||
openAtLogin: enabled,
|
||||
openAsHidden: enabled,
|
||||
args: enabled ? [BACKGROUND_START_ARG] : [],
|
||||
});
|
||||
const settings = app.getLoginItemSettings();
|
||||
return { supported: true, enabled: settings.openAtLogin === true };
|
||||
}
|
||||
|
||||
case 'desktop_browser_capture_page': {
|
||||
const wcId = Number.isFinite(args.webContentsId) ? Math.trunc(args.webContentsId) : null;
|
||||
if (wcId === null || wcId < 0) throw new Error('webContentsId is required');
|
||||
@@ -2636,12 +2687,19 @@ app.on('second-instance', (_event, argv) => {
|
||||
? argv.filter((arg) => typeof arg === 'string' && arg.startsWith(`${DEEP_LINK_PROTOCOL}://`))
|
||||
: [];
|
||||
if (urls.length > 0) handleDeepLinks(urls);
|
||||
focusForegroundWindow();
|
||||
if (BrowserWindow.getAllWindows().length > 0) {
|
||||
focusForegroundWindow();
|
||||
} else {
|
||||
void openMainWindow();
|
||||
}
|
||||
});
|
||||
|
||||
app.on('open-url', (event, url) => {
|
||||
event.preventDefault();
|
||||
handleDeepLinks([url]);
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
void openMainWindow();
|
||||
}
|
||||
});
|
||||
|
||||
app.on('activate', async () => {
|
||||
@@ -2655,23 +2713,20 @@ app.on('activate', async () => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.localOrigin) {
|
||||
const config = readDesktopHostsConfig();
|
||||
const localUiUrl = state.sidecarUrl || state.localOrigin;
|
||||
const host = config.defaultHostId && config.defaultHostId !== LOCAL_HOST_ID
|
||||
? config.hosts.find((entry) => entry.id === config.defaultHostId)
|
||||
: null;
|
||||
const targetUrl = host?.url && !state.unreachableHosts.has(host.url) ? host.url : localUiUrl;
|
||||
await createAdditionalWindow(targetUrl);
|
||||
}
|
||||
await openMainWindow();
|
||||
});
|
||||
|
||||
app.whenReady().then(async () => {
|
||||
const loginItemSettings = readLoginItemSettings();
|
||||
const isBackgroundStart = shouldStartInBackground(loginItemSettings);
|
||||
log.info('[electron] app starting', {
|
||||
version: APP_VERSION,
|
||||
packaged: app.isPackaged,
|
||||
platform: process.platform,
|
||||
arch: process.arch,
|
||||
argv: process.argv,
|
||||
isBackgroundStart,
|
||||
loginItemSettings,
|
||||
});
|
||||
nativeTheme.themeSource = readThemeSource();
|
||||
setupAutoUpdater();
|
||||
@@ -2680,6 +2735,24 @@ app.whenReady().then(async () => {
|
||||
Menu.setApplicationMenu(buildMacMenu());
|
||||
}
|
||||
|
||||
if (process.platform === 'darwin' && app.isPackaged) {
|
||||
const openAtLogin = loginItemSettings?.openAtLogin === true;
|
||||
app.setLoginItemSettings({
|
||||
openAtLogin,
|
||||
openAsHidden: openAtLogin,
|
||||
args: openAtLogin ? [BACKGROUND_START_ARG] : [],
|
||||
});
|
||||
}
|
||||
|
||||
if (isBackgroundStart) {
|
||||
const { localOrigin, bootOutcome } = await resolveInitialUrl();
|
||||
state.localOrigin = localOrigin;
|
||||
state.bootOutcome = bootOutcome ?? null;
|
||||
state.initScript = buildInitScript(localOrigin, state.bootOutcome);
|
||||
log.info('[electron] started in background without window');
|
||||
return;
|
||||
}
|
||||
|
||||
state.mainWindow = createBrowserWindow({
|
||||
label: 'main',
|
||||
restoreGeometry: true,
|
||||
|
||||
@@ -3,7 +3,14 @@ import * as React from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { getDesktopLanAddress, isDesktopLocalOriginActive, isDesktopShell, restartDesktopApp } from '@/lib/desktop';
|
||||
import {
|
||||
getDesktopLanAddress,
|
||||
getDesktopLaunchAtLogin,
|
||||
isDesktopLocalOriginActive,
|
||||
isDesktopShell,
|
||||
restartDesktopApp,
|
||||
setDesktopLaunchAtLogin,
|
||||
} from '@/lib/desktop';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
|
||||
export const DesktopNetworkSettings: React.FC = () => {
|
||||
@@ -15,6 +22,9 @@ export const DesktopNetworkSettings: React.FC = () => {
|
||||
const [draftPassword, setDraftPassword] = React.useState('');
|
||||
const [isLoading, setIsLoading] = React.useState(true);
|
||||
const [isSaving, setIsSaving] = React.useState(false);
|
||||
const [launchAtLoginSupported, setLaunchAtLoginSupported] = React.useState(false);
|
||||
const [launchAtLoginEnabled, setLaunchAtLoginEnabled] = React.useState(false);
|
||||
const [isSavingLaunchAtLogin, setIsSavingLaunchAtLogin] = React.useState(false);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
const [lanAddress, setLanAddress] = React.useState<string | null>(null);
|
||||
|
||||
@@ -66,6 +76,27 @@ export const DesktopNetworkSettings: React.FC = () => {
|
||||
};
|
||||
}, [isLocalDesktop, t]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!isLocalDesktop) {
|
||||
setLaunchAtLoginSupported(false);
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
void (async () => {
|
||||
const status = await getDesktopLaunchAtLogin();
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
setLaunchAtLoginSupported(status?.supported === true);
|
||||
setLaunchAtLoginEnabled(status?.enabled === true);
|
||||
})();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [isLocalDesktop]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!isLocalDesktop || !draftValue) {
|
||||
setLanAddress(null);
|
||||
@@ -101,6 +132,30 @@ export const DesktopNetworkSettings: React.FC = () => {
|
||||
setDraftValue((current) => !current);
|
||||
}, []);
|
||||
|
||||
const handleLaunchAtLoginToggle = React.useCallback(async () => {
|
||||
if (!launchAtLoginSupported || isSavingLaunchAtLogin) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextValue = !launchAtLoginEnabled;
|
||||
setLaunchAtLoginEnabled(nextValue);
|
||||
setIsSavingLaunchAtLogin(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const status = await setDesktopLaunchAtLogin(nextValue);
|
||||
if (!status?.supported) {
|
||||
throw new Error(t('settings.openchamber.desktopNetwork.error.launchAtLoginUnsupported'));
|
||||
}
|
||||
setLaunchAtLoginEnabled(status.enabled);
|
||||
} catch (cause) {
|
||||
setLaunchAtLoginEnabled(!nextValue);
|
||||
setError(cause instanceof Error ? cause.message : t('settings.openchamber.desktopNetwork.error.launchAtLoginSaveFailed'));
|
||||
} finally {
|
||||
setIsSavingLaunchAtLogin(false);
|
||||
}
|
||||
}, [isSavingLaunchAtLogin, launchAtLoginEnabled, launchAtLoginSupported, t]);
|
||||
|
||||
const handleSaveAndRestart = React.useCallback(async () => {
|
||||
if (!isDirty) {
|
||||
return;
|
||||
@@ -150,6 +205,34 @@ export const DesktopNetworkSettings: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<section className="space-y-2 px-2 pb-2 pt-0">
|
||||
{launchAtLoginSupported ? (
|
||||
<div
|
||||
className="group flex cursor-pointer items-start gap-2 py-1.5"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={handleLaunchAtLoginToggle}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
handleLaunchAtLoginToggle();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
checked={launchAtLoginEnabled}
|
||||
onChange={handleLaunchAtLoginToggle}
|
||||
ariaLabel={t('settings.openchamber.desktopNetwork.field.launchAtLoginAria')}
|
||||
disabled={isSavingLaunchAtLogin}
|
||||
/>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="typography-ui-label text-foreground">{t('settings.openchamber.desktopNetwork.field.launchAtLogin')}</div>
|
||||
<div className="typography-micro text-muted-foreground/70">
|
||||
{t('settings.openchamber.desktopNetwork.field.launchAtLoginDescription')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="space-y-1 py-1.5">
|
||||
<label className="typography-ui-label text-foreground" htmlFor="desktop-ui-password">
|
||||
{t('settings.openchamber.desktopPassword.field.password')}
|
||||
|
||||
@@ -40,16 +40,16 @@ export const OpenChamberPage: React.FC<OpenChamberPageProps> = ({ section }) =>
|
||||
<div className="border-t border-border/40 pt-6">
|
||||
<DefaultsSettings />
|
||||
</div>
|
||||
{!isVSCode && (
|
||||
<div className="border-t border-border/40 pt-6">
|
||||
<OpenCodeCliSettings />
|
||||
</div>
|
||||
)}
|
||||
{showDesktopNetworkSettings && (
|
||||
<div className="border-t border-border/40 pt-6">
|
||||
<DesktopNetworkSettings />
|
||||
</div>
|
||||
)}
|
||||
{!isVSCode && (
|
||||
<div className="border-t border-border/40 pt-6">
|
||||
<OpenCodeCliSettings />
|
||||
</div>
|
||||
)}
|
||||
<div className="border-t border-border/40 pt-6">
|
||||
<SessionRetentionSettings />
|
||||
</div>
|
||||
@@ -139,16 +139,16 @@ const SessionsSectionContent: React.FC = () => {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<DefaultsSettings />
|
||||
{!isVSCode && (
|
||||
<div className="border-t border-border/40 pt-6">
|
||||
<OpenCodeCliSettings />
|
||||
</div>
|
||||
)}
|
||||
{showDesktopNetworkSettings && (
|
||||
<div className="border-t border-border/40 pt-6">
|
||||
<DesktopNetworkSettings />
|
||||
</div>
|
||||
)}
|
||||
{!isVSCode && (
|
||||
<div className="border-t border-border/40 pt-6">
|
||||
<OpenCodeCliSettings />
|
||||
</div>
|
||||
)}
|
||||
<div className="border-t border-border/40 pt-6">
|
||||
<SessionRetentionSettings />
|
||||
</div>
|
||||
|
||||
@@ -225,6 +225,45 @@ export const invokeDesktop = async <T = unknown>(command: string, args?: Record<
|
||||
return tauri.core.invoke(command, args ?? {}) as Promise<T>;
|
||||
};
|
||||
|
||||
type LaunchAtLoginStatus = {
|
||||
supported: boolean;
|
||||
enabled: boolean;
|
||||
};
|
||||
|
||||
export const getDesktopLaunchAtLogin = async (): Promise<LaunchAtLoginStatus | null> => {
|
||||
if (!canUseElectronDesktopIPC() || !isDesktopLocalOriginActive()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await invokeDesktop<LaunchAtLoginStatus>('desktop_get_launch_at_login');
|
||||
if (!result || typeof result.supported !== 'boolean' || typeof result.enabled !== 'boolean') {
|
||||
return null;
|
||||
}
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.warn('Failed to get launch at login status', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const setDesktopLaunchAtLogin = async (enabled: boolean): Promise<LaunchAtLoginStatus | null> => {
|
||||
if (!canUseElectronDesktopIPC() || !isDesktopLocalOriginActive()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await invokeDesktop<LaunchAtLoginStatus>('desktop_set_launch_at_login', { enabled });
|
||||
if (!result || typeof result.supported !== 'boolean' || typeof result.enabled !== 'boolean') {
|
||||
return null;
|
||||
}
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.warn('Failed to set launch at login status', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const normalizeOrigin = (raw: string): string | null => {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) return null;
|
||||
|
||||
@@ -758,6 +758,9 @@ export const settingsDict = {
|
||||
'settings.openchamber.sessionRetention.toast.failedArchiveCount': 'Failed to archive {count} session(s)',
|
||||
'settings.openchamber.sessionRetention.toast.failedDeleteCount': 'Failed to delete {count} session(s)',
|
||||
'settings.openchamber.desktopNetwork.title': 'Desktop Network Access',
|
||||
'settings.openchamber.desktopNetwork.field.launchAtLoginAria': 'Start OpenChamber at login',
|
||||
'settings.openchamber.desktopNetwork.field.launchAtLogin': 'Start OpenChamber when you log in',
|
||||
'settings.openchamber.desktopNetwork.field.launchAtLoginDescription': 'Starts the app in the background without opening a window. Click the Dock icon to open it.',
|
||||
'settings.openchamber.desktopNetwork.field.allowLanAccessAria': 'Allow LAN access to desktop sidecar',
|
||||
'settings.openchamber.desktopNetwork.field.allowLanAccess': 'Let other devices on your local network open this app',
|
||||
'settings.openchamber.desktopNetwork.field.allowLanAccessDescription': 'Restarts the app so phones, tablets, and other computers on your Wi-Fi can open it.',
|
||||
@@ -771,6 +774,8 @@ export const settingsDict = {
|
||||
'settings.openchamber.desktopNetwork.error.loadFailed': 'Failed to load desktop settings',
|
||||
'settings.openchamber.desktopNetwork.error.saveFailed': 'Failed to save desktop settings',
|
||||
'settings.openchamber.desktopNetwork.error.savedRestartFailed': 'Saved, but failed to restart app',
|
||||
'settings.openchamber.desktopNetwork.error.launchAtLoginUnsupported': 'Launch at login is not supported on this system',
|
||||
'settings.openchamber.desktopNetwork.error.launchAtLoginSaveFailed': 'Failed to update launch at login setting',
|
||||
'settings.openchamber.opencodeCli.title': 'OpenCode CLI',
|
||||
'settings.openchamber.opencodeCli.tooltipPrefix': 'Optional absolute path to the',
|
||||
'settings.openchamber.opencodeCli.tooltipSuffix': 'binary.',
|
||||
|
||||
@@ -725,6 +725,9 @@ export const settingsDict = {
|
||||
"settings.openchamber.sessionRetention.toast.failedArchiveCount": "No se pudo archivar {count} sesión(es)",
|
||||
"settings.openchamber.sessionRetention.toast.failedDeleteCount": "No se pudo eliminar {count} sesión(es)",
|
||||
"settings.openchamber.desktopNetwork.title": "Acceso de red de escritorio",
|
||||
"settings.openchamber.desktopNetwork.field.launchAtLoginAria": "Iniciar OpenChamber al iniciar sesión",
|
||||
"settings.openchamber.desktopNetwork.field.launchAtLogin": "Iniciar OpenChamber al iniciar sesión",
|
||||
"settings.openchamber.desktopNetwork.field.launchAtLoginDescription": "Inicia la app en segundo plano sin abrir una ventana. Haz clic en el icono del Dock para abrirla.",
|
||||
"settings.openchamber.desktopNetwork.field.allowLanAccessAria": "Permitir acceso LAN al lado de escritorio",
|
||||
"settings.openchamber.desktopNetwork.field.allowLanAccess": "Permitir que otros dispositivos en tu red local abran esta aplicación",
|
||||
"settings.openchamber.desktopNetwork.field.allowLanAccessDescription": "Reinicia la aplicación para que los teléfonos, tablets y otros ordenadores en tu Wi-Fi puedan abrirla.",
|
||||
@@ -738,6 +741,8 @@ export const settingsDict = {
|
||||
"settings.openchamber.desktopNetwork.error.loadFailed": "No se pudo cargar la configuración de escritorio",
|
||||
"settings.openchamber.desktopNetwork.error.saveFailed": "No se pudo guardar la configuración de escritorio",
|
||||
"settings.openchamber.desktopNetwork.error.savedRestartFailed": "Guardado, pero no se pudo reiniciar la aplicación",
|
||||
"settings.openchamber.desktopNetwork.error.launchAtLoginUnsupported": "El inicio al iniciar sesión no es compatible con este sistema",
|
||||
"settings.openchamber.desktopNetwork.error.launchAtLoginSaveFailed": "No se pudo actualizar la opción de inicio al iniciar sesión",
|
||||
"settings.openchamber.opencodeCli.title": "CLI de OpenCode",
|
||||
"settings.openchamber.opencodeCli.tooltipPrefix": "Ruta absoluta opcional al",
|
||||
"settings.openchamber.opencodeCli.tooltipSuffix": "ejecutable.",
|
||||
|
||||
@@ -725,6 +725,9 @@ export const settingsDict = {
|
||||
'settings.openchamber.sessionRetention.toast.failedArchiveCount': '세션 {count}개를 보관하지 못했습니다',
|
||||
'settings.openchamber.sessionRetention.toast.failedDeleteCount': '세션 {count}개를 삭제하지 못했습니다',
|
||||
'settings.openchamber.desktopNetwork.title': 'Desktop 네트워크 접속',
|
||||
'settings.openchamber.desktopNetwork.field.launchAtLoginAria': '로그인 시 OpenChamber 시작',
|
||||
'settings.openchamber.desktopNetwork.field.launchAtLogin': '로그인 시 OpenChamber 시작',
|
||||
'settings.openchamber.desktopNetwork.field.launchAtLoginDescription': '창을 열지 않고 백그라운드에서 앱을 시작합니다. Dock 아이콘을 클릭하면 열립니다.',
|
||||
'settings.openchamber.desktopNetwork.field.allowLanAccessAria': 'desktop sidecar에 LAN 접속 허용',
|
||||
'settings.openchamber.desktopNetwork.field.allowLanAccess': '로컬 네트워크의 다른 기기에서 이 앱 열기 허용',
|
||||
'settings.openchamber.desktopNetwork.field.allowLanAccessDescription': '휴대폰, 태블릿, Wi-Fi의 다른 컴퓨터에서 열 수 있도록 앱을 다시 시작합니다.',
|
||||
@@ -738,6 +741,8 @@ export const settingsDict = {
|
||||
'settings.openchamber.desktopNetwork.error.loadFailed': 'desktop 설정을 로드하지 못했습니다',
|
||||
'settings.openchamber.desktopNetwork.error.saveFailed': 'desktop 설정을 저장하지 못했습니다',
|
||||
'settings.openchamber.desktopNetwork.error.savedRestartFailed': '저장했지만 앱을 다시 시작하지 못했습니다',
|
||||
'settings.openchamber.desktopNetwork.error.launchAtLoginUnsupported': '이 시스템에서는 로그인 시 시작을 지원하지 않습니다',
|
||||
'settings.openchamber.desktopNetwork.error.launchAtLoginSaveFailed': '로그인 시 시작 설정을 업데이트하지 못했습니다',
|
||||
'settings.openchamber.opencodeCli.title': 'OpenCode CLI',
|
||||
'settings.openchamber.opencodeCli.tooltipPrefix': '선택적 절대 경로:',
|
||||
'settings.openchamber.opencodeCli.tooltipSuffix': 'binary.',
|
||||
|
||||
@@ -643,12 +643,17 @@ export const settingsDict = {
|
||||
'settings.openchamber.defaults.summaryPrefix': 'Nowe sesje będą rozpoczynać się od:',
|
||||
'settings.openchamber.defaults.title': 'Domyślne ustawienia sesji',
|
||||
'settings.openchamber.desktopNetwork.actions.saveAndRestart': 'Zapisz + Zrestartuj',
|
||||
'settings.openchamber.desktopNetwork.error.launchAtLoginUnsupported': 'Uruchamianie przy logowaniu nie jest obsługiwane w tym systemie',
|
||||
'settings.openchamber.desktopNetwork.error.launchAtLoginSaveFailed': 'Nie udało się zaktualizować ustawienia uruchamiania przy logowaniu',
|
||||
'settings.openchamber.desktopNetwork.error.loadFailed': 'Nie udało się załadować ustawień pulpitu',
|
||||
'settings.openchamber.desktopNetwork.error.saveFailed': 'Nie udało się zapisać ustawień pulpitu',
|
||||
'settings.openchamber.desktopNetwork.error.savedRestartFailed': 'Zapisano, ale restart aplikacji nie powiódł się',
|
||||
'settings.openchamber.desktopNetwork.field.allowLanAccess': 'Pozwól innym urządzeniom w sieci lokalnej otwierać tę aplikację',
|
||||
'settings.openchamber.desktopNetwork.field.allowLanAccessAria': 'Zezwól na dostęp LAN do sidecara pulpitu',
|
||||
'settings.openchamber.desktopNetwork.field.allowLanAccessDescription': 'Restartuje aplikację, aby telefony, tablety i inne komputery w Twojej sieci Wi-Fi mogły ją otworzyć.',
|
||||
'settings.openchamber.desktopNetwork.field.launchAtLogin': 'Uruchamiaj OpenChamber przy logowaniu',
|
||||
'settings.openchamber.desktopNetwork.field.launchAtLoginAria': 'Uruchamiaj OpenChamber przy logowaniu',
|
||||
'settings.openchamber.desktopNetwork.field.launchAtLoginDescription': 'Uruchamia aplikację w tle bez otwierania okna. Kliknij ikonę w Docku, aby ją otworzyć.',
|
||||
'settings.openchamber.desktopNetwork.field.warning': 'Ostrzeżenie: po włączeniu aplikacja jest dostępna dla każdego w tej samej sieci lokalnej.',
|
||||
'settings.openchamber.desktopPassword.field.password': 'Hasło UI pulpitu',
|
||||
'settings.openchamber.desktopPassword.field.passwordPlaceholder': 'Hasło nie jest wymagane',
|
||||
|
||||
@@ -725,6 +725,9 @@ export const settingsDict = {
|
||||
"settings.openchamber.sessionRetention.toast.failedArchiveCount": "Não foi possível arquivar {count} sessão(es)",
|
||||
"settings.openchamber.sessionRetention.toast.failedDeleteCount": "Não foi possível excluir {count} sessão(es)",
|
||||
"settings.openchamber.desktopNetwork.title": "Acesso de rede do desktop",
|
||||
"settings.openchamber.desktopNetwork.field.launchAtLoginAria": "Iniciar o OpenChamber ao fazer login",
|
||||
"settings.openchamber.desktopNetwork.field.launchAtLogin": "Iniciar o OpenChamber ao fazer login",
|
||||
"settings.openchamber.desktopNetwork.field.launchAtLoginDescription": "Inicia o app em segundo plano sem abrir uma janela. Clique no ícone do Dock para abrir.",
|
||||
"settings.openchamber.desktopNetwork.field.allowLanAccessAria": "Permitir acesso LAN ao lado de desktop",
|
||||
"settings.openchamber.desktopNetwork.field.allowLanAccess": "Permitir que outros dispositivos na sua rede local abram este aplicativo",
|
||||
"settings.openchamber.desktopNetwork.field.allowLanAccessDescription": "Reinicia o aplicativo para que os telefones, tablets e outros computadores em seu Wi-Fi possam abri-lo.",
|
||||
@@ -738,6 +741,8 @@ export const settingsDict = {
|
||||
"settings.openchamber.desktopNetwork.error.loadFailed": "Não foi possível carregar as configurações de desktop",
|
||||
"settings.openchamber.desktopNetwork.error.saveFailed": "Não foi possível salvar a configuração de desktop",
|
||||
"settings.openchamber.desktopNetwork.error.savedRestartFailed": "Salvo, mas não foi possível reiniciar o aplicativo",
|
||||
"settings.openchamber.desktopNetwork.error.launchAtLoginUnsupported": "Iniciar ao fazer login não é compatível com este sistema",
|
||||
"settings.openchamber.desktopNetwork.error.launchAtLoginSaveFailed": "Não foi possível atualizar a opção de iniciar ao fazer login",
|
||||
"settings.openchamber.opencodeCli.title": "CLI do OpenCode",
|
||||
"settings.openchamber.opencodeCli.tooltipPrefix": "Caminho absoluto opcional para o",
|
||||
"settings.openchamber.opencodeCli.tooltipSuffix": "executável.",
|
||||
|
||||
@@ -725,6 +725,9 @@ export const settingsDict = {
|
||||
"settings.openchamber.sessionRetention.toast.failedArchiveCount": "Не вдалося заархівувати сесій: {count}",
|
||||
"settings.openchamber.sessionRetention.toast.failedDeleteCount": "Не вдалося видалити сесій: {count}",
|
||||
"settings.openchamber.desktopNetwork.title": "Мережевий доступ десктопного застосунку",
|
||||
"settings.openchamber.desktopNetwork.field.launchAtLoginAria": "Запускати OpenChamber під час входу в систему",
|
||||
"settings.openchamber.desktopNetwork.field.launchAtLogin": "Запускати OpenChamber під час входу в систему",
|
||||
"settings.openchamber.desktopNetwork.field.launchAtLoginDescription": "Запускає застосунок у фоні без відкриття вікна. Щоб відкрити його, натисніть іконку в Dock.",
|
||||
"settings.openchamber.desktopNetwork.field.allowLanAccessAria": "Дозволити LAN-доступ до десктопного застосунку",
|
||||
"settings.openchamber.desktopNetwork.field.allowLanAccess": "Дозволити іншим пристроям у локальній мережі відкривати цей застосунок",
|
||||
"settings.openchamber.desktopNetwork.field.allowLanAccessDescription": "Перезапускає застосунок, щоб телефони, планшети та інші комп’ютери в мережі Wi-Fi могли його відкрити.",
|
||||
@@ -738,6 +741,8 @@ export const settingsDict = {
|
||||
"settings.openchamber.desktopNetwork.error.loadFailed": "Не вдалося завантажити налаштування десктопного застосунку",
|
||||
"settings.openchamber.desktopNetwork.error.saveFailed": "Не вдалося зберегти налаштування десктопного застосунку",
|
||||
"settings.openchamber.desktopNetwork.error.savedRestartFailed": "Збережено, але не вдалося перезапустити застосунок",
|
||||
"settings.openchamber.desktopNetwork.error.launchAtLoginUnsupported": "Запуск під час входу в систему не підтримується на цій системі",
|
||||
"settings.openchamber.desktopNetwork.error.launchAtLoginSaveFailed": "Не вдалося оновити налаштування запуску під час входу в систему",
|
||||
"settings.openchamber.opencodeCli.title": "OpenCode CLI",
|
||||
"settings.openchamber.opencodeCli.tooltipPrefix": "Додатковий абсолютний шлях до",
|
||||
"settings.openchamber.opencodeCli.tooltipSuffix": "бінарного файлу.",
|
||||
|
||||
@@ -725,6 +725,9 @@ export const settingsDict = {
|
||||
'settings.openchamber.sessionRetention.toast.failedArchiveCount': '归档 {count} 个会话失败',
|
||||
'settings.openchamber.sessionRetention.toast.failedDeleteCount': '删除 {count} 个会话失败',
|
||||
'settings.openchamber.desktopNetwork.title': '桌面端网络访问',
|
||||
'settings.openchamber.desktopNetwork.field.launchAtLoginAria': '登录时启动 OpenChamber',
|
||||
'settings.openchamber.desktopNetwork.field.launchAtLogin': '登录时启动 OpenChamber',
|
||||
'settings.openchamber.desktopNetwork.field.launchAtLoginDescription': '在后台启动应用且不打开窗口。点击 Dock 图标即可打开。',
|
||||
'settings.openchamber.desktopNetwork.field.allowLanAccessAria': '允许桌面 sidecar 局域网访问',
|
||||
'settings.openchamber.desktopNetwork.field.allowLanAccess': '允许你本地网络中的其他设备打开此应用',
|
||||
'settings.openchamber.desktopNetwork.field.allowLanAccessDescription': '会重启应用,以便手机、平板和同一 Wi‑Fi 下的其他电脑访问。',
|
||||
@@ -738,6 +741,8 @@ export const settingsDict = {
|
||||
'settings.openchamber.desktopNetwork.error.loadFailed': '加载桌面设置失败',
|
||||
'settings.openchamber.desktopNetwork.error.saveFailed': '保存桌面设置失败',
|
||||
'settings.openchamber.desktopNetwork.error.savedRestartFailed': '已保存,但重启应用失败',
|
||||
'settings.openchamber.desktopNetwork.error.launchAtLoginUnsupported': '此系统不支持登录时启动',
|
||||
'settings.openchamber.desktopNetwork.error.launchAtLoginSaveFailed': '更新登录时启动设置失败',
|
||||
'settings.openchamber.opencodeCli.title': 'OpenCode CLI',
|
||||
'settings.openchamber.opencodeCli.tooltipPrefix': '可选的',
|
||||
'settings.openchamber.opencodeCli.tooltipSuffix': '二进制绝对路径。',
|
||||
|
||||
@@ -24,6 +24,10 @@ Or install manually: `bun add -g @openchamber/web` (or npm, pnpm, yarn).
|
||||
openchamber # Start on port 3000
|
||||
openchamber --port 8080 # Custom port
|
||||
openchamber --ui-password secret # Password-protect UI
|
||||
openchamber startup enable # Start at login as a native service
|
||||
OPENCHAMBER_UI_PASSWORD=secret openchamber startup enable # Save service password env
|
||||
openchamber startup status # Show startup service status
|
||||
openchamber startup disable # Remove startup service
|
||||
openchamber tunnel help # Tunnel lifecycle commands
|
||||
openchamber tunnel providers # Show provider capabilities
|
||||
openchamber tunnel profile add --provider cloudflare --mode managed-remote --name prod-main --hostname app.example.com --token <token>
|
||||
@@ -39,6 +43,8 @@ openchamber stop # Stop server
|
||||
openchamber update # Update to latest version
|
||||
```
|
||||
|
||||
`startup enable` snapshots your current environment into the native service so startup behaves like you launched `openchamber` from the same shell. This preserves provider tokens, PATH, SSH agent settings, and other CLI auth/config env vars. Use `--no-env-snapshot` for a minimal service env.
|
||||
|
||||
### Tunnel behavior notes
|
||||
|
||||
- One active tunnel per running OpenChamber instance (port).
|
||||
|
||||
+443
-2
@@ -32,6 +32,7 @@ const DEFAULT_TAIL_LINES = 200;
|
||||
const DAEMON_READY_TIMEOUT_MS = 30000;
|
||||
const LOG_ROTATE_MAX_BYTES = 10 * 1024 * 1024;
|
||||
const LOG_ROTATE_KEEP = 5;
|
||||
const STARTUP_SERVICE_ID = 'dev.openchamber.web';
|
||||
const TUNNEL_PROFILES_VERSION = 1;
|
||||
const TUNNEL_PROFILES_FILE_NAME = 'tunnel-profiles.json';
|
||||
const LEGACY_CLOUDFLARE_MANAGED_REMOTE_FILE_NAME = 'cloudflare-managed-remote-tunnels.json';
|
||||
@@ -601,6 +602,7 @@ function parseArgs(argv = process.argv.slice(2)) {
|
||||
quiet: false,
|
||||
explicitPort: false,
|
||||
explicitUiPassword: false,
|
||||
envSnapshot: true,
|
||||
foreground: false,
|
||||
};
|
||||
|
||||
@@ -753,6 +755,9 @@ function parseArgs(argv = process.argv.slice(2)) {
|
||||
case 'no-follow':
|
||||
options.follow = false;
|
||||
break;
|
||||
case 'no-env-snapshot':
|
||||
options.envSnapshot = false;
|
||||
break;
|
||||
case 'lines': {
|
||||
const { value, nextIndex } = consumeValue(i, inlineValue);
|
||||
i = nextIndex;
|
||||
@@ -833,11 +838,13 @@ function parseArgs(argv = process.argv.slice(2)) {
|
||||
const command = positional[0] || 'serve';
|
||||
const subcommand = command === 'tunnel' ? (positional[1] || 'help') : null;
|
||||
const tunnelAction = command === 'tunnel' ? (positional[2] || null) : null;
|
||||
const startupAction = command === 'startup' ? (positional[1] || 'status') : null;
|
||||
|
||||
return {
|
||||
command,
|
||||
subcommand,
|
||||
tunnelAction,
|
||||
startupAction,
|
||||
options,
|
||||
removedFlagErrors,
|
||||
helpRequested,
|
||||
@@ -858,6 +865,7 @@ COMMANDS:
|
||||
restart Stop and start the server
|
||||
status Show server status
|
||||
tunnel Tunnel lifecycle commands
|
||||
startup Manage launch at system startup
|
||||
logs Tail OpenChamber logs
|
||||
update Check for and install updates
|
||||
|
||||
@@ -883,11 +891,39 @@ EXAMPLES:
|
||||
openchamber # Start in daemon mode on default port 3000 (or free port)
|
||||
openchamber --port 8080 # Start on port 8080 (daemon)
|
||||
openchamber serve --foreground # Start in foreground (for systemd Type=simple)
|
||||
openchamber startup enable # Start OpenChamber at user login
|
||||
openchamber tunnel help # Show tunnel lifecycle help
|
||||
openchamber logs # Follow logs for latest running instance
|
||||
`);
|
||||
}
|
||||
|
||||
function showStartupHelp() {
|
||||
console.log(`
|
||||
OpenChamber Startup Commands
|
||||
|
||||
USAGE:
|
||||
openchamber startup <SUBCOMMAND> [OPTIONS]
|
||||
|
||||
SUBCOMMANDS:
|
||||
status Show startup integration status
|
||||
enable Install and start native user startup integration
|
||||
disable Stop and remove native user startup integration
|
||||
|
||||
OPTIONS:
|
||||
-p, --port Web server port used by startup service
|
||||
--host Bind address used by startup service
|
||||
--ui-password Protect browser UI with single password
|
||||
--no-env-snapshot Do not save current environment for startup service
|
||||
--json Output machine-readable JSON
|
||||
-q, --quiet Suppress non-essential output
|
||||
|
||||
EXAMPLES:
|
||||
openchamber startup enable
|
||||
openchamber startup enable --port 3000
|
||||
openchamber startup status --json
|
||||
`);
|
||||
}
|
||||
|
||||
function showTunnelHelp() {
|
||||
console.log(`
|
||||
Tunnel Lifecycle Commands
|
||||
@@ -1702,6 +1738,352 @@ function getRunDir() {
|
||||
return dir;
|
||||
}
|
||||
|
||||
function getStartupServicePaths() {
|
||||
if (process.platform === 'darwin') {
|
||||
return {
|
||||
platform: 'macos',
|
||||
servicePath: path.join(os.homedir(), 'Library', 'LaunchAgents', `${STARTUP_SERVICE_ID}.plist`),
|
||||
};
|
||||
}
|
||||
if (process.platform === 'linux') {
|
||||
return {
|
||||
platform: 'linux',
|
||||
servicePath: path.join(os.homedir(), '.config', 'systemd', 'user', 'openchamber.service'),
|
||||
};
|
||||
}
|
||||
if (process.platform === 'win32') {
|
||||
return { platform: 'windows', servicePath: STARTUP_SERVICE_ID };
|
||||
}
|
||||
return { platform: process.platform, servicePath: null };
|
||||
}
|
||||
|
||||
function escapeXml(value) {
|
||||
return String(value)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function systemdEscapeArg(value) {
|
||||
return String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
||||
}
|
||||
|
||||
function startupShellQuote(value) {
|
||||
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
||||
}
|
||||
|
||||
function systemdUnitPath(value) {
|
||||
return String(value).replace(/\\/g, '\\\\').replace(/ /g, '\\x20');
|
||||
}
|
||||
|
||||
function powershellQuote(value) {
|
||||
return `'${String(value).replace(/'/g, "''")}'`;
|
||||
}
|
||||
|
||||
function startupEnvFileQuote(value) {
|
||||
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
||||
}
|
||||
|
||||
function systemdEnvFileQuote(value) {
|
||||
return `"${String(value)
|
||||
.replace(/\\/g, '\\\\')
|
||||
.replace(/"/g, '\\"')
|
||||
.replace(/`/g, '\\`')
|
||||
.replace(/\$/g, '\\$')}"`;
|
||||
}
|
||||
|
||||
function getStartupEnvFilePath() {
|
||||
return path.join(getDataDir(), 'startup.env');
|
||||
}
|
||||
|
||||
function getMacosStartupWrapperPath() {
|
||||
return path.join(getDataDir(), 'bin', 'OpenChamber');
|
||||
}
|
||||
|
||||
function collectStartupEnv(options = {}) {
|
||||
const env = options.envSnapshot === false ? {} : Object.fromEntries(
|
||||
Object.entries(process.env)
|
||||
.filter(([key, value]) => shouldPersistStartupEnv(key, value))
|
||||
.map(([key, value]) => [key, String(value)])
|
||||
);
|
||||
|
||||
if (options.envSnapshot !== false) {
|
||||
const opencodeBinary = process.env.OPENCODE_BINARY || searchPathFor('opencode');
|
||||
if (typeof opencodeBinary === 'string' && opencodeBinary.trim().length > 0) {
|
||||
env.OPENCODE_BINARY = opencodeBinary.trim();
|
||||
}
|
||||
}
|
||||
const uiPassword = hasUiPasswordConfigured(options.uiPassword) ? options.uiPassword : undefined;
|
||||
if (uiPassword) {
|
||||
env.OPENCHAMBER_UI_PASSWORD = uiPassword;
|
||||
}
|
||||
if (typeof process.env.OPENCHAMBER_DATA_DIR === 'string' && process.env.OPENCHAMBER_DATA_DIR.trim().length > 0) {
|
||||
env.OPENCHAMBER_DATA_DIR = path.resolve(process.env.OPENCHAMBER_DATA_DIR.trim());
|
||||
}
|
||||
return env;
|
||||
}
|
||||
|
||||
function shouldPersistStartupEnv(key, value) {
|
||||
if (typeof key !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) return false;
|
||||
if (typeof value !== 'string') return false;
|
||||
if (/[\r\n]/.test(value)) return false;
|
||||
|
||||
// These are shell/session implementation details, not app configuration.
|
||||
const volatileKeys = new Set([
|
||||
'_',
|
||||
'BASH_ENV',
|
||||
'COLUMNS',
|
||||
'CONDA_DEFAULT_ENV',
|
||||
'CONDA_PREFIX',
|
||||
'CONDA_PROMPT_MODIFIER',
|
||||
'CONDA_SHLVL',
|
||||
'ENV',
|
||||
'HISTFILE',
|
||||
'HISTFILESIZE',
|
||||
'HISTSIZE',
|
||||
'LINES',
|
||||
'OLDPWD',
|
||||
'PROMPT',
|
||||
'PROMPT_COMMAND',
|
||||
'PS1',
|
||||
'PS2',
|
||||
'PS3',
|
||||
'PS4',
|
||||
'PWD',
|
||||
'PYENV_VERSION',
|
||||
'SHLVL',
|
||||
'TERM',
|
||||
'TERM_PROGRAM',
|
||||
'TERM_PROGRAM_VERSION',
|
||||
'TTY',
|
||||
'VIRTUAL_ENV',
|
||||
'VIRTUAL_ENV_PROMPT',
|
||||
]);
|
||||
return !volatileKeys.has(key);
|
||||
}
|
||||
|
||||
function writeStartupEnvFile(options = {}, fileOptions = {}) {
|
||||
const envFilePath = getStartupEnvFilePath();
|
||||
const lines = [];
|
||||
const env = collectStartupEnv(options);
|
||||
const quoteValue = typeof fileOptions.quoteValue === 'function' ? fileOptions.quoteValue : startupEnvFileQuote;
|
||||
for (const [key, value] of Object.entries(env)) {
|
||||
lines.push(`${key}=${quoteValue(value)}`);
|
||||
}
|
||||
fs.mkdirSync(path.dirname(envFilePath), { recursive: true, mode: 0o700 });
|
||||
fs.writeFileSync(envFilePath, lines.length > 0 ? `${lines.join('\n')}\n` : '', { mode: 0o600 });
|
||||
return envFilePath;
|
||||
}
|
||||
|
||||
function removeStartupEnvFile() {
|
||||
try { fs.unlinkSync(getStartupEnvFilePath()); } catch {}
|
||||
}
|
||||
|
||||
function resolveCliEntrypoint() {
|
||||
const entry = typeof process.argv[1] === 'string' && process.argv[1].trim().length > 0
|
||||
? process.argv[1]
|
||||
: path.join(__dirname, 'cli.js');
|
||||
try {
|
||||
return fs.realpathSync(entry);
|
||||
} catch {
|
||||
return path.resolve(entry);
|
||||
}
|
||||
}
|
||||
|
||||
function buildStartupArgs(options = {}) {
|
||||
const args = [resolveCliEntrypoint(), 'serve', '--foreground', '--port', String(options.port || DEFAULT_PORT)];
|
||||
if (typeof options.host === 'string' && options.host.length > 0) {
|
||||
args.push('--host', options.host);
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
function writeMacosStartupWrapper(options = {}) {
|
||||
const wrapperPath = getMacosStartupWrapperPath();
|
||||
const args = buildStartupArgs(options).map(startupShellQuote).join(' ');
|
||||
const content = `#!/bin/sh
|
||||
exec ${startupShellQuote(process.execPath)} ${args}
|
||||
`;
|
||||
fs.mkdirSync(path.dirname(wrapperPath), { recursive: true, mode: 0o700 });
|
||||
fs.writeFileSync(wrapperPath, content, { mode: 0o700 });
|
||||
return wrapperPath;
|
||||
}
|
||||
|
||||
function buildMacosLaunchAgent(options = {}) {
|
||||
const wrapperPath = writeMacosStartupWrapper(options);
|
||||
const args = [wrapperPath];
|
||||
const env = collectStartupEnv(options);
|
||||
const logDir = path.join(os.homedir(), 'Library', 'Logs', 'OpenChamber');
|
||||
const argXml = args.map((arg) => ` <string>${escapeXml(arg)}</string>`).join('\n');
|
||||
const envXml = Object.entries(env).length > 0
|
||||
? ` <key>EnvironmentVariables</key>\n <dict>\n${Object.entries(env).map(([key, value]) => ` <key>${escapeXml(key)}</key>\n <string>${escapeXml(value)}</string>`).join('\n')}\n </dict>\n`
|
||||
: '';
|
||||
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>${STARTUP_SERVICE_ID}</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
${argXml}
|
||||
</array>
|
||||
${envXml} <key>ProcessType</key>
|
||||
<string>Background</string>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>KeepAlive</key>
|
||||
<true/>
|
||||
<key>WorkingDirectory</key>
|
||||
<string>${escapeXml(os.homedir())}</string>
|
||||
<key>StandardOutPath</key>
|
||||
<string>${escapeXml(path.join(logDir, 'startup.log'))}</string>
|
||||
<key>StandardErrorPath</key>
|
||||
<string>${escapeXml(path.join(logDir, 'startup.err.log'))}</string>
|
||||
</dict>
|
||||
</plist>
|
||||
`;
|
||||
}
|
||||
|
||||
function buildSystemdUserService(options = {}) {
|
||||
const args = buildStartupArgs(options).map((arg) => `"${systemdEscapeArg(arg)}"`).join(' ');
|
||||
const envFilePath = getStartupEnvFilePath();
|
||||
return `[Unit]
|
||||
Description=OpenChamber web server
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
EnvironmentFile=-${systemdEscapeArg(envFilePath)}
|
||||
ExecStart="${systemdEscapeArg(process.execPath)}" ${args}
|
||||
WorkingDirectory=${systemdUnitPath(os.homedir())}
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
`;
|
||||
}
|
||||
|
||||
function runStartupCommand(command, args, options = {}) {
|
||||
const result = spawnSync(command, args, {
|
||||
encoding: 'utf8',
|
||||
stdio: options.stdio || 'pipe',
|
||||
windowsHide: true,
|
||||
});
|
||||
if (result.error) {
|
||||
throw result.error;
|
||||
}
|
||||
if (result.status !== 0 && options.allowFailure !== true) {
|
||||
const detail = (result.stderr || result.stdout || '').trim();
|
||||
throw new Error(`${command} ${args.join(' ')} failed${detail ? `: ${detail}` : ''}`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function getStartupStatus() {
|
||||
const paths = getStartupServicePaths();
|
||||
if (!paths.servicePath) {
|
||||
return { supported: false, platform: paths.platform, enabled: false, servicePath: null };
|
||||
}
|
||||
if (paths.platform === 'windows') {
|
||||
const result = runStartupCommand('schtasks.exe', ['/Query', '/TN', STARTUP_SERVICE_ID], { allowFailure: true });
|
||||
return { supported: true, platform: paths.platform, enabled: result.status === 0, active: null, servicePath: paths.servicePath };
|
||||
}
|
||||
if (paths.platform === 'linux') {
|
||||
const enabledResult = runStartupCommand('systemctl', ['--user', 'is-enabled', 'openchamber.service'], { allowFailure: true });
|
||||
const activeResult = runStartupCommand('systemctl', ['--user', 'is-active', 'openchamber.service'], { allowFailure: true });
|
||||
const activeState = (activeResult.stdout || '').trim() || 'inactive';
|
||||
return {
|
||||
supported: true,
|
||||
platform: paths.platform,
|
||||
enabled: enabledResult.status === 0 || fs.existsSync(paths.servicePath),
|
||||
active: activeState === 'active',
|
||||
activeState,
|
||||
servicePath: paths.servicePath,
|
||||
};
|
||||
}
|
||||
return {
|
||||
supported: true,
|
||||
platform: paths.platform,
|
||||
enabled: fs.existsSync(paths.servicePath),
|
||||
active: null,
|
||||
servicePath: paths.servicePath,
|
||||
};
|
||||
}
|
||||
|
||||
function enableStartupService(options = {}) {
|
||||
const paths = getStartupServicePaths();
|
||||
if (!paths.servicePath) {
|
||||
throw new TunnelCliError(`Startup integration is not supported on ${paths.platform}.`, EXIT_CODE.USAGE_ERROR);
|
||||
}
|
||||
|
||||
if (paths.platform === 'macos') {
|
||||
removeStartupEnvFile();
|
||||
fs.mkdirSync(path.dirname(paths.servicePath), { recursive: true, mode: 0o700 });
|
||||
fs.mkdirSync(path.join(os.homedir(), 'Library', 'Logs', 'OpenChamber'), { recursive: true, mode: 0o700 });
|
||||
fs.writeFileSync(paths.servicePath, buildMacosLaunchAgent(options), { mode: 0o600 });
|
||||
runStartupCommand('/bin/launchctl', ['bootout', `gui/${process.getuid()}`, paths.servicePath], { allowFailure: true });
|
||||
runStartupCommand('/bin/launchctl', ['bootstrap', `gui/${process.getuid()}`, paths.servicePath]);
|
||||
runStartupCommand('/bin/launchctl', ['kickstart', '-k', `gui/${process.getuid()}/${STARTUP_SERVICE_ID}`], { allowFailure: true });
|
||||
return getStartupStatus();
|
||||
}
|
||||
|
||||
if (paths.platform === 'linux') {
|
||||
writeStartupEnvFile(options, { quoteValue: systemdEnvFileQuote });
|
||||
fs.mkdirSync(path.dirname(paths.servicePath), { recursive: true, mode: 0o700 });
|
||||
fs.writeFileSync(paths.servicePath, buildSystemdUserService(options), { mode: 0o600 });
|
||||
runStartupCommand('systemctl', ['--user', 'daemon-reload']);
|
||||
runStartupCommand('systemctl', ['--user', 'enable', '--now', 'openchamber.service']);
|
||||
return getStartupStatus();
|
||||
}
|
||||
|
||||
const envFilePath = writeStartupEnvFile(options);
|
||||
const startupArgs = buildStartupArgs(options).map(powershellQuote).join(', ');
|
||||
const powerShellCommand = [
|
||||
`$envFile=${powershellQuote(envFilePath)}`,
|
||||
`if (Test-Path $envFile) { Get-Content $envFile | ForEach-Object { if ($_ -match '^([^=]+)=(.*)$') { $v=$matches[2]; if ($v.StartsWith("'") -and $v.EndsWith("'")) { $v=$v.Substring(1,$v.Length-2).Replace("'\\''","'") }; [Environment]::SetEnvironmentVariable($matches[1], $v, 'Process') } } }`,
|
||||
`& ${powershellQuote(process.execPath)} ${startupArgs}`,
|
||||
].join('; ');
|
||||
const taskArgs = `powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "${powerShellCommand.replace(/"/g, '\\"')}"`;
|
||||
runStartupCommand('schtasks.exe', [
|
||||
'/Create',
|
||||
'/TN', STARTUP_SERVICE_ID,
|
||||
'/SC', 'ONLOGON',
|
||||
'/RL', 'LIMITED',
|
||||
'/F',
|
||||
'/TR', taskArgs,
|
||||
]);
|
||||
runStartupCommand('schtasks.exe', ['/Run', '/TN', STARTUP_SERVICE_ID], { allowFailure: true });
|
||||
return getStartupStatus();
|
||||
}
|
||||
|
||||
function disableStartupService() {
|
||||
const paths = getStartupServicePaths();
|
||||
if (!paths.servicePath) {
|
||||
throw new TunnelCliError(`Startup integration is not supported on ${paths.platform}.`, EXIT_CODE.USAGE_ERROR);
|
||||
}
|
||||
|
||||
if (paths.platform === 'macos') {
|
||||
runStartupCommand('/bin/launchctl', ['bootout', `gui/${process.getuid()}`, paths.servicePath], { allowFailure: true });
|
||||
try { fs.unlinkSync(paths.servicePath); } catch {}
|
||||
return getStartupStatus();
|
||||
}
|
||||
|
||||
if (paths.platform === 'linux') {
|
||||
runStartupCommand('systemctl', ['--user', 'disable', '--now', 'openchamber.service'], { allowFailure: true });
|
||||
try { fs.unlinkSync(paths.servicePath); } catch {}
|
||||
runStartupCommand('systemctl', ['--user', 'daemon-reload'], { allowFailure: true });
|
||||
return getStartupStatus();
|
||||
}
|
||||
|
||||
runStartupCommand('schtasks.exe', ['/End', '/TN', STARTUP_SERVICE_ID], { allowFailure: true });
|
||||
runStartupCommand('schtasks.exe', ['/Delete', '/TN', STARTUP_SERVICE_ID, '/F'], { allowFailure: true });
|
||||
return getStartupStatus();
|
||||
}
|
||||
|
||||
async function getPidFilePath(port) {
|
||||
return path.join(getRunDir(), `openchamber-${port}.pid`);
|
||||
}
|
||||
@@ -4719,6 +5101,58 @@ const commands = {
|
||||
});
|
||||
},
|
||||
|
||||
async startup(options, action = 'status') {
|
||||
const normalized = typeof action === 'string' ? action.trim().toLowerCase() : 'status';
|
||||
if (!['status', 'enable', 'disable'].includes(normalized)) {
|
||||
throw new TunnelCliError(
|
||||
`Unknown startup subcommand '${action}'. Use 'openchamber startup --help'.`,
|
||||
EXIT_CODE.USAGE_ERROR
|
||||
);
|
||||
}
|
||||
|
||||
let status;
|
||||
if (normalized === 'enable') {
|
||||
status = enableStartupService(options);
|
||||
} else if (normalized === 'disable') {
|
||||
status = disableStartupService();
|
||||
} else {
|
||||
status = getStartupStatus();
|
||||
}
|
||||
|
||||
const result = { action: normalized, ...status };
|
||||
if (!result.supported) {
|
||||
throw new TunnelCliError(
|
||||
`Startup integration is not supported on ${result.platform}.`,
|
||||
EXIT_CODE.USAGE_ERROR
|
||||
);
|
||||
}
|
||||
if (normalized === 'enable' && result.activeState === 'failed') {
|
||||
throw new TunnelCliError(
|
||||
'Startup service was installed but failed to start. Run `journalctl --user -u openchamber.service -n 80 --no-pager` for details.',
|
||||
EXIT_CODE.GENERAL_ERROR
|
||||
);
|
||||
}
|
||||
if (isJsonMode(options)) {
|
||||
printJson(result);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isQuietMode(options)) {
|
||||
process.stdout.write(`startup ${result.enabled ? 'enabled' : 'disabled'} platform:${result.platform} supported:${result.supported ? 'yes' : 'no'}${result.servicePath ? ` path:${result.servicePath}` : ''}\n`);
|
||||
return;
|
||||
}
|
||||
|
||||
clackIntro('OpenChamber Startup');
|
||||
logStatus(result.enabled ? 'success' : 'info', `startup ${result.enabled ? 'enabled' : 'disabled'}`, result.servicePath || undefined);
|
||||
if (typeof result.activeState === 'string') {
|
||||
logStatus(result.active ? 'success' : result.activeState === 'failed' ? 'error' : 'warning', `service ${result.activeState}`);
|
||||
}
|
||||
if (normalized === 'enable') {
|
||||
logStatus('info', 'service command', 'openchamber serve --foreground');
|
||||
}
|
||||
clackOutro(normalized === 'status' ? 'status complete' : `${normalized} complete`);
|
||||
},
|
||||
|
||||
async update(options = {}) {
|
||||
const showOutput = shouldRenderHumanOutput(options);
|
||||
const updateSpin = createSpinner(options);
|
||||
@@ -4843,7 +5277,7 @@ const commands = {
|
||||
|
||||
async function main() {
|
||||
const parsed = parseArgs();
|
||||
const { command, subcommand, tunnelAction, options, removedFlagErrors, helpRequested, versionRequested } = parsed;
|
||||
const { command, subcommand, tunnelAction, startupAction, options, removedFlagErrors, helpRequested, versionRequested } = parsed;
|
||||
activeCommandOptions = options;
|
||||
|
||||
if (versionRequested) {
|
||||
@@ -4875,6 +5309,8 @@ async function main() {
|
||||
if (helpRequested) {
|
||||
if (command === 'tunnel') {
|
||||
showTunnelHelp();
|
||||
} else if (command === 'startup') {
|
||||
showStartupHelp();
|
||||
} else {
|
||||
showHelp();
|
||||
}
|
||||
@@ -4886,8 +5322,13 @@ async function main() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (command === 'startup') {
|
||||
await commands.startup(options, startupAction);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!commands[command]) {
|
||||
const knownCommands = ['serve', 'stop', 'restart', 'status', 'tunnel', 'logs', 'update'];
|
||||
const knownCommands = ['serve', 'stop', 'restart', 'status', 'tunnel', 'startup', 'logs', 'update'];
|
||||
const suggestion = findClosestMatch(command, knownCommands);
|
||||
const hint = suggestion ? ` Did you mean '${suggestion}'?` : '';
|
||||
if (isJsonMode(options)) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
export const registerOpenChamberRoutes = (app, dependencies) => {
|
||||
const {
|
||||
fs,
|
||||
os,
|
||||
path,
|
||||
process,
|
||||
server,
|
||||
@@ -104,14 +103,15 @@ export const registerOpenChamberRoutes = (app, dependencies) => {
|
||||
}
|
||||
|
||||
const currentPort = server.address()?.port || 3000;
|
||||
const tmpDir = os.tmpdir();
|
||||
const instanceFilePath = path.join(tmpDir, `openchamber-${currentPort}.json`);
|
||||
const instanceFilePath = path.join(openchamberDataDir, 'run', `openchamber-${currentPort}.json`);
|
||||
let storedOptions = { port: currentPort, daemon: true };
|
||||
try {
|
||||
const content = await fs.promises.readFile(instanceFilePath, 'utf8');
|
||||
storedOptions = JSON.parse(content);
|
||||
} catch {
|
||||
}
|
||||
const launchMode = storedOptions.launchMode === 'foreground' ? 'foreground' : 'daemon';
|
||||
const isForegroundService = launchMode === 'foreground';
|
||||
|
||||
const isWindows = process.platform === 'win32';
|
||||
const quotePosix = (value) => `'${String(value).replace(/'/g, "'\\''")}'`;
|
||||
@@ -152,7 +152,7 @@ export const registerOpenChamberRoutes = (app, dependencies) => {
|
||||
restartCmdFallback += ` --ui-password '${escapedPw}'`;
|
||||
}
|
||||
}
|
||||
const restartCmd = `(${restartCmdPrimary}) || (${restartCmdFallback})`;
|
||||
const restartCmd = isForegroundService ? '' : `(${restartCmdPrimary}) || (${restartCmdFallback})`;
|
||||
const updateLogPath = path.join(openchamberDataDir, 'update-install.log');
|
||||
const logPreamble = [
|
||||
'',
|
||||
@@ -165,8 +165,9 @@ export const registerOpenChamberRoutes = (app, dependencies) => {
|
||||
`packagePath=${pmDetails.packagePath || 'unknown'}`,
|
||||
`globalNodeModulesRoot=${pmDetails.globalNodeModulesRoot || 'unknown'}`,
|
||||
`mode=${isContainer ? 'container' : 'restart'}`,
|
||||
`launchMode=${launchMode}`,
|
||||
`updateCommand=${updateCmd}`,
|
||||
`restartCommand=${restartCmd}`,
|
||||
`restartCommand=${restartCmd || 'service-manager'}`,
|
||||
`logPath=${updateLogPath}`,
|
||||
].join('\n');
|
||||
|
||||
@@ -176,6 +177,7 @@ export const registerOpenChamberRoutes = (app, dependencies) => {
|
||||
version: updateInfo.version,
|
||||
packageManager: pm,
|
||||
autoRestart: true,
|
||||
restartManager: isForegroundService ? 'service' : 'cli',
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
@@ -192,7 +194,7 @@ export const registerOpenChamberRoutes = (app, dependencies) => {
|
||||
${updateCmd}
|
||||
if %ERRORLEVEL% EQU 0 (
|
||||
echo Update successful, restarting OpenChamber...
|
||||
${restartCmd}
|
||||
${restartCmd || 'echo Service manager will restart OpenChamber.'}
|
||||
) else (
|
||||
echo Update failed
|
||||
exit /b 1
|
||||
@@ -204,7 +206,7 @@ export const registerOpenChamberRoutes = (app, dependencies) => {
|
||||
${updateCmd}
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "Update successful, restarting OpenChamber..."
|
||||
${restartCmd}
|
||||
${restartCmd || 'echo "Service manager will restart OpenChamber."'}
|
||||
else
|
||||
echo "Update failed"
|
||||
exit 1
|
||||
|
||||
Reference in New Issue
Block a user