Compare commits

...

3 Commits

Author SHA1 Message Date
J
9eda64fdf9 docs(self-host): note setup self-host honors MULTICA_SERVER_URL / MULTICA_APP_URL
Document that `setup self-host` reads the env vars when the matching flag
is omitted (flag wins), and that MULTICA_SERVER_URL accepts the ws://…/ws
daemon form. Added to en/zh/ja/ko quickstart for parity.

Co-authored-by: multica-agent <github@multica.ai>
2026-06-09 14:01:07 +08:00
J
2c79cc0c8d fix(cli): normalize MULTICA_SERVER_URL in setup self-host
MULTICA_SERVER_URL is documented as a ws:// daemon address
(ws://localhost:8080/ws) and every other command normalizes it via
NormalizeServerBaseURL before use. setup self-host consumed the resolved
value raw and probed <url>/health, so a self-hoster who set the
documented ws:// form would still fail the reachability check.

Run the flag/env value through normalizeAPIBaseURL (ws->http, wss->https,
strip /ws) so the documented form works and the stored server_url stays a
clean http(s) base. Add a normalization test case and a focused test for
the MULTICA_APP_URL env path (review nit).

Co-authored-by: multica-agent <github@multica.ai>
2026-06-09 13:57:07 +08:00
J
54ca3220c5 fix(cli): honor MULTICA_SERVER_URL in setup self-host
`multica setup self-host` resolved the backend URL only from the
--server-url flag, falling back to http://localhost:8080 when the flag
was absent. It never consulted MULTICA_SERVER_URL, even though that env
var is documented on the root --server-url flag and in `multica --help`,
and is honored by every other command via resolveServerURL. A self-host
user who set the env var instead of the flag still hit localhost and got
"Server at http://localhost:8080 is not reachable".

Route server-url and app-url through cli.FlagOrEnv so the documented env
vars (MULTICA_SERVER_URL / MULTICA_APP_URL) are honored when the matching
flag is not set, with the flag still taking precedence. userProvided now
reflects flag-or-env, so an env-sourced remote URL still triggers the
explicit app_url prompt. Not platform-specific despite the report.

Fixes GitHub #3912.

Co-authored-by: multica-agent <github@multica.ai>
2026-06-09 13:44:47 +08:00
6 changed files with 141 additions and 10 deletions

View File

@@ -162,6 +162,10 @@ multica setup self-host \
--app-url https://<your-domain>
```
<Callout type="info">
フラグより環境変数を使いたい場合は、対応するフラグを省略すると `setup self-host` が `MULTICA_SERVER_URL` と `MULTICA_APP_URL` を読み取ります(両方設定した場合はフラグが優先されます)。`MULTICA_SERVER_URL` は[環境変数](/environment-variables)で示される `ws://…/ws` というデーモン形式も受け付け、HTTP ベース URL に正規化します。
</Callout>
単一のホスト名でフロントエンドとバックエンドの両方を前段に置く(デーモンと Web アプリの両方に必要な WebSocket サポートを含む)最小限の Caddyfile は次のとおりです。
```nginx

View File

@@ -162,6 +162,10 @@ multica setup self-host \
--app-url https://<your-domain>
```
<Callout type="info">
플래그 대신 환경 변수를 선호한다면, 해당 플래그를 생략할 때 `setup self-host`가 `MULTICA_SERVER_URL`과 `MULTICA_APP_URL`을 읽습니다(둘 다 설정하면 플래그가 우선합니다). `MULTICA_SERVER_URL`은 [환경 변수](/environment-variables)에 나오는 `ws://…/ws` 데몬 형식도 허용하며 HTTP 기본 URL로 정규화합니다.
</Callout>
단일 호스트네임에서 프런트엔드와 백엔드를 모두 앞단에 두는(데몬과 웹 앱 모두에 필요한 WebSocket 지원 포함) 최소 Caddyfile은 다음과 같습니다.
```nginx

View File

@@ -163,6 +163,10 @@ multica setup self-host \
--app-url https://<your-domain>
```
<Callout type="info">
Prefer environment variables over flags? `setup self-host` reads `MULTICA_SERVER_URL` and `MULTICA_APP_URL` when the matching flag is omitted — a flag still takes precedence over the env var. `MULTICA_SERVER_URL` also accepts the `ws://…/ws` daemon form from [Environment variables](/environment-variables) and normalizes it to the HTTP base.
</Callout>
A minimal Caddyfile that fronts both the frontend and the backend (with WebSocket support, which the daemon and the web app both need) on a single hostname:
```nginx

View File

@@ -162,6 +162,10 @@ multica setup self-host \
--app-url https://<你的域名>
```
<Callout type="info">
更习惯用环境变量?省略对应 flag 时,`setup self-host` 会读取 `MULTICA_SERVER_URL` 和 `MULTICA_APP_URL`(同时设置时 flag 优先)。`MULTICA_SERVER_URL` 也接受[环境变量](/environment-variables)里那种 `ws://…/ws` 的 daemon 写法,并自动归一化为 HTTP 地址。
</Callout>
最小可用的 Caddyfile单域名同时挂前后端带 WebSocket 转发daemon 和网页端都依赖):
```nginx

View File

@@ -60,8 +60,8 @@ Examples:
}
func init() {
setupSelfHostCmd.Flags().String("server-url", "", "Backend server URL (e.g. https://api.internal.co)")
setupSelfHostCmd.Flags().String("app-url", "", "Frontend app URL (e.g. https://app.internal.co)")
setupSelfHostCmd.Flags().String("server-url", "", "Backend server URL (e.g. https://api.internal.co) (env: MULTICA_SERVER_URL)")
setupSelfHostCmd.Flags().String("app-url", "", "Frontend app URL (e.g. https://app.internal.co) (env: MULTICA_APP_URL)")
setupSelfHostCmd.Flags().Int("port", 8080, "Backend server port (used when --server-url is not set)")
setupSelfHostCmd.Flags().Int("frontend-port", 3000, "Frontend port (used when --app-url is not set)")
setupSelfHostCmd.Flags().String(callbackHostFlag, "", "Host the OAuth callback URL points at (auto-detected when empty). Use this for reverse-proxy / FQDN setups.")
@@ -162,16 +162,16 @@ func runSetupSelfHost(cmd *cobra.Command, args []string) error {
return nil
}
serverURL, _ := cmd.Flags().GetString("server-url")
appURL, _ := cmd.Flags().GetString("app-url")
port, _ := cmd.Flags().GetInt("port")
// Honor MULTICA_SERVER_URL / MULTICA_APP_URL when the matching flag is not
// set — consistent with the rest of the CLI (resolveServerURL) and with the
// env vars documented on the root --server-url flag and in `multica --help`.
// Before this, setup self-host read only the flags, so a self-hoster who set
// MULTICA_SERVER_URL still got the localhost default and an "unreachable"
// error (GitHub #3912).
serverURL, userProvidedServerURL := resolveSelfHostServerURL(cmd)
appURL := cli.FlagOrEnv(cmd, "app-url", "MULTICA_APP_URL", "")
frontendPort, _ := cmd.Flags().GetInt("frontend-port")
userProvidedServerURL := serverURL != ""
// If custom URLs provided, use them; otherwise default to localhost with ports.
if serverURL == "" {
serverURL = fmt.Sprintf("http://localhost:%d", port)
}
if appURL == "" {
if userProvidedServerURL && !serverHostIsLocal(serverURL) {
// We can't guess the frontend URL for a remote server: api.x.co
@@ -246,6 +246,26 @@ func persistSelfHostConfigIfReachable(serverURL, appURL, profile string, probe f
return true, nil
}
// resolveSelfHostServerURL picks the backend URL for `setup self-host`: the
// --server-url flag wins, then the MULTICA_SERVER_URL env var (consistent with
// the rest of the CLI and the env var documented on the root flag), then the
// localhost default built from --port. userProvided is true when the URL came
// from the user (flag or env) rather than the localhost fallback — the caller
// uses it to decide whether a remote host needs an explicit app_url.
//
// A user-supplied URL is run through normalizeAPIBaseURL, the same path
// resolveServerURL uses: MULTICA_SERVER_URL is documented as a ws:// daemon
// address (e.g. ws://localhost:8080/ws), so the ws/wss form and a trailing /ws
// are accepted and converted to the http(s) base that the reachability probe
// and the stored server_url expect.
func resolveSelfHostServerURL(cmd *cobra.Command) (serverURL string, userProvided bool) {
if v := cli.FlagOrEnv(cmd, "server-url", "MULTICA_SERVER_URL", ""); v != "" {
return normalizeAPIBaseURL(v), true
}
port, _ := cmd.Flags().GetInt("port")
return fmt.Sprintf("http://localhost:%d", port), false
}
// serverHostIsLocal reports whether serverURL points at the same machine as
// the CLI (loopback literal or "localhost"). Used to decide whether to infer
// app_url from server_url or fall back to the local-dev default.

View File

@@ -3,6 +3,8 @@ package main
import (
"testing"
"github.com/spf13/cobra"
"github.com/multica-ai/multica/server/internal/cli"
)
@@ -70,6 +72,99 @@ func TestPersistSelfHostConfigIfReachable(t *testing.T) {
})
}
// TestResolveSelfHostServerURL covers GitHub #3912: `setup self-host` must
// honor MULTICA_SERVER_URL when --server-url is not passed, instead of always
// defaulting to localhost (which left self-hosters stuck on an "unreachable"
// error). The flag still wins over the env var.
func TestResolveSelfHostServerURL(t *testing.T) {
newCmd := func() *cobra.Command {
c := &cobra.Command{}
c.Flags().String("server-url", "", "")
c.Flags().Int("port", 8080, "")
return c
}
t.Run("env var honored when flag absent", func(t *testing.T) {
t.Setenv("MULTICA_SERVER_URL", "https://api.internal.co")
serverURL, userProvided := resolveSelfHostServerURL(newCmd())
if serverURL != "https://api.internal.co" {
t.Fatalf("server_url: want env value, got %q", serverURL)
}
if !userProvided {
t.Fatalf("userProvided: want true for env-sourced URL")
}
})
t.Run("flag wins over env", func(t *testing.T) {
t.Setenv("MULTICA_SERVER_URL", "https://env.example")
cmd := newCmd()
if err := cmd.Flags().Set("server-url", "https://flag.example"); err != nil {
t.Fatalf("set flag: %v", err)
}
serverURL, userProvided := resolveSelfHostServerURL(cmd)
if serverURL != "https://flag.example" {
t.Fatalf("server_url: want flag value, got %q", serverURL)
}
if !userProvided {
t.Fatalf("userProvided: want true for flag-sourced URL")
}
})
t.Run("falls back to localhost with --port when neither set", func(t *testing.T) {
t.Setenv("MULTICA_SERVER_URL", "")
cmd := newCmd()
if err := cmd.Flags().Set("port", "9090"); err != nil {
t.Fatalf("set flag: %v", err)
}
serverURL, userProvided := resolveSelfHostServerURL(cmd)
if serverURL != "http://localhost:9090" {
t.Fatalf("server_url: want localhost default, got %q", serverURL)
}
if userProvided {
t.Fatalf("userProvided: want false for localhost fallback")
}
})
// MULTICA_SERVER_URL is documented as a ws:// daemon address; the probe and
// stored config need an http(s) base, so the ws/wss + /ws form must be
// normalized just like every other command does.
t.Run("normalizes the documented ws:// daemon form", func(t *testing.T) {
t.Setenv("MULTICA_SERVER_URL", "wss://api.internal.co/ws")
serverURL, userProvided := resolveSelfHostServerURL(newCmd())
if serverURL != "https://api.internal.co" {
t.Fatalf("server_url: want normalized https base, got %q", serverURL)
}
if !userProvided {
t.Fatalf("userProvided: want true for env-sourced URL")
}
})
}
// TestSelfHostAppURLHonorsEnv pins the app-url half of the GitHub #3912 fix:
// setup self-host resolves --app-url through the same FlagOrEnv path, so
// MULTICA_APP_URL is honored when the flag is absent.
func TestSelfHostAppURLHonorsEnv(t *testing.T) {
cmd := &cobra.Command{}
cmd.Flags().String("app-url", "", "")
t.Run("env honored when flag absent", func(t *testing.T) {
t.Setenv("MULTICA_APP_URL", "https://app.internal.co")
if got := cli.FlagOrEnv(cmd, "app-url", "MULTICA_APP_URL", ""); got != "https://app.internal.co" {
t.Fatalf("app_url: want env value, got %q", got)
}
})
t.Run("flag wins over env", func(t *testing.T) {
t.Setenv("MULTICA_APP_URL", "https://env.example")
if err := cmd.Flags().Set("app-url", "https://flag.example"); err != nil {
t.Fatalf("set flag: %v", err)
}
if got := cli.FlagOrEnv(cmd, "app-url", "MULTICA_APP_URL", ""); got != "https://flag.example" {
t.Fatalf("app_url: want flag value, got %q", got)
}
})
}
func TestServerHostIsLocal(t *testing.T) {
cases := []struct {
name string