open-code-review/cmd/opencodereview/viewer_cmd.go
Tao Xin 24edcbd3b7
feat(cmd, viewer): open viewer URL in default browser (#1108)
* feat(viewer): open URL in default browser

* chore: write tests (AI)

* fix: TCP listener created by net.Listen is not explicitly closed via defer

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* fix: no interactive prompt in non-interactives

* fixes qwq

* fix: no new line

* refactor

* chore: write test (AI)

* apply suggestions & new example

* docs: update (AI)

* fix(viewer): address review feedback on browser auto-open

- displayURL takes the host from the requested addr and only the port from
  the listener. net.Listen resolves a hostname to an IP literal while
  hostGuard's allowlist is built from the requested addr, so
  `--addr box.local:5483` auto-opened a URL its own host guard answered with
  403 forbidden host. Taking only the port keeps `--addr :0` reporting the
  port the kernel assigned.

- runBrowserCmd reads cmd.Wait() within a bounded window instead of trusting
  Start(). xdg-open exits non-zero when no handler is registered and open
  exits non-zero when no application claims the scheme, both after a
  successful fork/exec, so those failures left the user with neither a
  browser nor a warning. An opener that outlives the window is treated as
  success because it became the browser, but a later failure still warns.
  The child's stderr is folded into the error, and every candidate's failure
  is reported rather than only the first.

- --open=auto|always|never replaces --no-open, matching the existing --color.
  The boolean could only suppress opening, never force it, leaving no way out
  when the heuristic declines wrongly. The auto-mode suppression reason is
  appended to the ready line so it cannot be mistaken for a broken open,
  $BROWSER is honored on Unix, and SSH is judged together with the display
  variables so `ssh -X` opens without an override.

- ASSURANCE_CASE.md no longer claims every exec.Command call runs git. It
  already had four kinds of exception before this one.

Browser handling moves to internal/viewer/browser.go, covered through a
helper-process test that exercises the exec paths without launching a real
browser. Docs updated across en/ja/ko/ru/zh.

* fix(viewer): correct $BROWSER attribution and harden open-mode test

browserCandidates credited the colon-separated list and %s placeholder to
the Go toolchain, but cmd/internal/browser treats $BROWSER as a single
executable path with neither. Attribute the parsing to the freedesktop
convention as Python's webbrowser implements it, and note the divergence so
the richer parsing reads as this package's choice rather than something
inherited. Move the Go reference to browserWaitWindow, which is what is
actually borrowed from it (appearsSuccessful, also 3s).

TestViewerCmd_RejectsInvalidOpenMode called RunE directly. Dropping the
ValidateOpenMode guard does not make RunE return an error there: it falls
through to StartServer, which serves until the listener fails and so never
returns, turning the regression into a suite-wide timeout panic that blames
whichever test the runner happened to be on. Run RunE under a deadline and
bind port 0 so a regression reports the actual cause instead of hanging, and
restore addr alongside open. Confirmed by deleting the guard: the test now
fails in 5s with "--open is not validated before StartServer binds".

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: kite <lizhengfeng.lzf@alibaba-inc.com>
2026-09-01 20:42:40 +08:00

41 lines
1.3 KiB
Go

// SPDX-License-Identifier: Apache-2.0
// Copyright 2026 alibaba/open-code-review Contributors
package main
import (
"github.com/alibaba/open-code-review/internal/viewer"
"github.com/spf13/cobra"
)
type viewerOptions struct {
addr string
open string
}
var viewerOpts viewerOptions
var viewerCmd = &cobra.Command{
Use: "viewer [flags]",
Aliases: []string{"v"},
Short: "Start the WebUI session viewer",
Long: "Session history WebUI viewer.",
Args: cobra.NoArgs,
Example: ` ocr viewer # start and open the browser
ocr viewer --addr :3000 # bind to all interfaces on port 3000
ocr viewer --open=never # just print the URL
ocr viewer --open=always # force it when auto declines (piped output, WSL)`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := viewer.ValidateOpenMode(viewerOpts.open); err != nil {
return err
}
return viewer.StartServer(viewerOpts.addr, viewerOpts.open)
},
}
func init() {
viewerCmd.Flags().StringVar(&viewerOpts.addr, "addr", "localhost:5483", "listen address")
viewerCmd.Flags().StringVar(&viewerOpts.open, "open", viewer.OpenAuto,
"when to open the browser: auto (only on a local terminal with a display), always, or never")
viewerCmd.RegisterFlagCompletionFunc("open", completeEnum(viewer.OpenAuto, viewer.OpenAlways, viewer.OpenNever))
}