@@ -117,7 +117,7 @@ func main() {
|
||||
OnAuthURL: func(u string) {
|
||||
fmt.Fprintln(os.Stderr, "gitea-mcp: opening browser for Gitea OAuth login:")
|
||||
fmt.Fprintln(os.Stderr, " ", u)
|
||||
if err := browser.Open(u); err != nil {
|
||||
if err := browser.OpenWith(svc.OAuthBrowser, u); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "gitea-mcp: open browser:", err)
|
||||
fmt.Fprintln(os.Stderr, "(visit the URL above manually)")
|
||||
}
|
||||
|
||||
@@ -114,7 +114,7 @@ func main() {
|
||||
OnAuthURL: func(u string) {
|
||||
fmt.Fprintln(os.Stderr, "grafana-mcp: opening browser for Grafana OAuth login:")
|
||||
fmt.Fprintln(os.Stderr, " ", u)
|
||||
if err := browser.Open(u); err != nil {
|
||||
if err := browser.OpenWith(svc.OAuthBrowser, u); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "grafana-mcp: open browser:", err)
|
||||
fmt.Fprintln(os.Stderr, "(visit the URL above manually)")
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ func main() {
|
||||
OnAuthURL: func(u string) {
|
||||
fmt.Fprintln(os.Stderr, "gssh-mcp: opening browser for Gssh OAuth login:")
|
||||
fmt.Fprintln(os.Stderr, " ", u)
|
||||
if err := browser.Open(u); err != nil {
|
||||
if err := browser.OpenWith(svc.OAuthBrowser, u); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "gssh-mcp: open browser:", err)
|
||||
fmt.Fprintln(os.Stderr, "(visit the URL above manually)")
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ func main() {
|
||||
OnAuthURL: func(u string) {
|
||||
fmt.Fprintln(os.Stderr, "searxng-mcp: opening browser for SearXNG OAuth login:")
|
||||
fmt.Fprintln(os.Stderr, " ", u)
|
||||
if err := browser.Open(u); err != nil {
|
||||
if err := browser.OpenWith(svc.OAuthBrowser, u); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "searxng-mcp: open browser:", err)
|
||||
fmt.Fprintln(os.Stderr, "(visit the URL above manually)")
|
||||
}
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# sherlock conifugration - lives in $XDG_CONFIG_HOME/sherlock/config.toml
|
||||
|
||||
# ── OAuth login ───────────────────────────────────────────────────────
|
||||
# Optional browser executable/name for OAuth login URLs. Leave unset to
|
||||
# use the OS default browser (xdg-open/open/rundll32).
|
||||
# [oauth]
|
||||
# browser = "firefox"
|
||||
|
||||
# ── Providers ────────────────────────────────────────────────────────
|
||||
# A [providers.<name>] block is a reusable OAuth/OIDC client. Services
|
||||
# point at one via `provider = "<name>"`. Define a provider once and
|
||||
|
||||
@@ -19,6 +19,10 @@ Resolved in order:
|
||||
|
||||
## Shape
|
||||
|
||||
`[oauth]` is optional. Set `browser = "<executable>"` to open OAuth login URLs
|
||||
with a specific browser, for example `browser = "firefox"`. Leave it unset to
|
||||
use the OS default browser.
|
||||
|
||||
`[providers.<name>]` defines a reusable OAuth/OIDC identity: `issuer`,
|
||||
`client_id`, optional `client_secret`.
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ package browser
|
||||
import (
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Open starts the OS's default browser pointed at url, then returns
|
||||
@@ -15,9 +16,29 @@ import (
|
||||
// itself reports — there's no portable way to know whether the user
|
||||
// actually loaded the page.
|
||||
func Open(url string) error {
|
||||
return OpenWith("", url)
|
||||
}
|
||||
|
||||
// OpenWith starts browser pointed at url, then returns immediately. If
|
||||
// browser is empty, it falls back to the OS default helper used by Open.
|
||||
func OpenWith(browser, url string) error {
|
||||
bin, args := commandFor(browser, url, runtime.GOOS)
|
||||
cmd := exec.Command(bin, args...)
|
||||
if err := cmd.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
go func() { _ = cmd.Wait() }()
|
||||
return nil
|
||||
}
|
||||
|
||||
func commandFor(browser, url, goos string) (string, []string) {
|
||||
if browser = strings.TrimSpace(browser); browser != "" {
|
||||
return browser, []string{url}
|
||||
}
|
||||
|
||||
var bin string
|
||||
var args []string
|
||||
switch runtime.GOOS {
|
||||
switch goos {
|
||||
case "darwin":
|
||||
bin = "open"
|
||||
args = []string{url}
|
||||
@@ -28,10 +49,5 @@ func Open(url string) error {
|
||||
bin = "xdg-open"
|
||||
args = []string{url}
|
||||
}
|
||||
cmd := exec.Command(bin, args...)
|
||||
if err := cmd.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
go func() { _ = cmd.Wait() }()
|
||||
return nil
|
||||
return bin, args
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package browser
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCommandFor_Defaults(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
goos string
|
||||
wantBin string
|
||||
wantArgs []string
|
||||
}{
|
||||
{name: "linux", goos: "linux", wantBin: "xdg-open", wantArgs: []string{"https://example.com"}},
|
||||
{name: "darwin", goos: "darwin", wantBin: "open", wantArgs: []string{"https://example.com"}},
|
||||
{name: "windows", goos: "windows", wantBin: "rundll32", wantArgs: []string{"url.dll,FileProtocolHandler", "https://example.com"}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotBin, gotArgs := commandFor("", "https://example.com", tt.goos)
|
||||
if gotBin != tt.wantBin || !reflect.DeepEqual(gotArgs, tt.wantArgs) {
|
||||
t.Fatalf("commandFor = %q %v, want %q %v", gotBin, gotArgs, tt.wantBin, tt.wantArgs)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommandFor_CustomBrowser(t *testing.T) {
|
||||
gotBin, gotArgs := commandFor(" firefox ", "https://example.com", "linux")
|
||||
wantArgs := []string{"https://example.com"}
|
||||
if gotBin != "firefox" || !reflect.DeepEqual(gotArgs, wantArgs) {
|
||||
t.Fatalf("commandFor = %q %v, want firefox %v", gotBin, gotArgs, wantArgs)
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,9 @@
|
||||
//
|
||||
// Shape:
|
||||
//
|
||||
// [oauth]
|
||||
// browser = "firefox" # optional; default OS browser when empty
|
||||
//
|
||||
// [providers.sherlock-cli] # a reusable OAuth provider
|
||||
// issuer = "https://id.example/application/o/sherlock-cli/"
|
||||
// client_id = "abc123"
|
||||
@@ -66,8 +69,16 @@ type Service struct {
|
||||
BaseURL string `toml:"base_url"`
|
||||
}
|
||||
|
||||
// OAuth contains global OAuth-flow preferences.
|
||||
type OAuth struct {
|
||||
// Browser is an optional executable name or path used to open OAuth
|
||||
// login URLs. Empty means the OS default browser.
|
||||
Browser string `toml:"browser"`
|
||||
}
|
||||
|
||||
// Config is the whole parsed file.
|
||||
type Config struct {
|
||||
OAuth OAuth `toml:"oauth"`
|
||||
Providers map[string]Provider `toml:"providers"`
|
||||
Services map[string]Service `toml:"services"`
|
||||
|
||||
@@ -83,6 +94,7 @@ type Resolved struct {
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
BaseURL string
|
||||
OAuthBrowser string
|
||||
}
|
||||
|
||||
// DefaultPath returns the config-file path sherlock reads, honouring
|
||||
@@ -155,6 +167,7 @@ func (c *Config) Service(name string) (Resolved, error) {
|
||||
ClientID: svc.ClientID,
|
||||
ClientSecret: svc.ClientSecret,
|
||||
BaseURL: svc.BaseURL,
|
||||
OAuthBrowser: c.OAuth.Browser,
|
||||
}
|
||||
|
||||
if svc.Provider != "" {
|
||||
|
||||
@@ -63,6 +63,26 @@ base_url = "https://gitea.example"
|
||||
}
|
||||
}
|
||||
|
||||
func TestService_OAuthBrowser(t *testing.T) {
|
||||
path := writeConfig(t, `
|
||||
[oauth]
|
||||
browser = "firefox"
|
||||
|
||||
[services.gitea]
|
||||
issuer = "https://gitea.example"
|
||||
client_id = "gitea-client"
|
||||
base_url = "https://gitea.example"
|
||||
`)
|
||||
c, _ := LoadFrom(path)
|
||||
got, err := c.Service("gitea")
|
||||
if err != nil {
|
||||
t.Fatalf("Service: %v", err)
|
||||
}
|
||||
if got.OAuthBrowser != "firefox" {
|
||||
t.Fatalf("OAuthBrowser = %q", got.OAuthBrowser)
|
||||
}
|
||||
}
|
||||
|
||||
func TestService_UnknownService(t *testing.T) {
|
||||
path := writeConfig(t, `
|
||||
[services.gitea]
|
||||
|
||||
Reference in New Issue
Block a user