Skip to content

Commit 4bc5f01

Browse files
committed
Add Windows CLI install support
1 parent b028aac commit 4bc5f01

21 files changed

Lines changed: 426 additions & 33 deletions

File tree

.github/workflows/release.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,11 @@ jobs:
9090
echo "Expected 4 CLI release archives, found ${cli_asset_count}" >&2
9191
exit 1
9292
fi
93+
windows_cli_asset_count="$(find dist/cli -maxdepth 1 -type f -name '*.zip' | wc -l | tr -d ' ')"
94+
if [ "${windows_cli_asset_count}" -ne 2 ]; then
95+
echo "Expected 2 Windows CLI release archives, found ${windows_cli_asset_count}" >&2
96+
exit 1
97+
fi
9398
chrome_extension_zip="$(find dist/chrome-extension -maxdepth 1 -type f -name '*.zip' -print -quit)"
9499
if [ -z "${chrome_extension_zip}" ]; then
95100
echo "Chrome extension zip not found" >&2
@@ -131,6 +136,7 @@ jobs:
131136
dist/repo-metadata.tgz
132137
dist/release-manifest.json
133138
dist/cli/*.tar.gz
139+
dist/cli/*.zip
134140
dist/chrome-extension/*.zip
135141
dist/chrome-extension/*.crx
136142
dist/chrome-extension/*.json
@@ -147,6 +153,7 @@ jobs:
147153
dist/chrome-extension/*.zip
148154
dist/chrome-extension/*.crx
149155
dist/cli/*.tar.gz
156+
dist/cli/*.zip
150157
dist/skills/*.zip
151158
dist/skills/*.skill
152159
@@ -158,6 +165,7 @@ jobs:
158165
run: |
159166
assets=(
160167
dist/cli/*.tar.gz
168+
dist/cli/*.zip
161169
dist/chrome-extension/*.zip
162170
dist/chrome-extension/*.crx
163171
dist/skills/*.zip

README.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,22 +26,21 @@ https://github.com/user-attachments/assets/bcfba878-f6a8-44b9-b84b-29c7e0285687
2626
## Quick Start
2727

2828
```bash
29-
brew tap iFurySt/open-browser-use
30-
brew install open-browser-use
29+
npm i -g open-browser-use
3130
open-browser-use setup
3231
```
3332

3433
### Install the CLI
3534

3635
```bash
37-
# npm
36+
# npm (macOS, Linux, Windows)
3837
npm i -g open-browser-use
3938

40-
# Homebrew
39+
# Homebrew (macOS, Linux)
4140
brew tap iFurySt/open-browser-use && brew install open-browser-use
4241

4342
# Upgrade
44-
brew upgrade open-browser-use
43+
npm update -g open-browser-use
4544
```
4645

4746
### Set Up Chrome

README.zh-CN.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,21 +20,20 @@ https://github.com/user-attachments/assets/bcfba878-f6a8-44b9-b84b-29c7e0285687
2020
## Quick Start
2121
第一次安装可以无脑运行:
2222
```bash
23-
brew tap iFurySt/open-browser-use
24-
brew install open-browser-use
23+
npm i -g open-browser-use
2524
open-browser-use setup
2625
```
2726

2827
### 安装 CLI
2928
```bash
30-
# npm安装
29+
# npm安装(macOS、Linux、Windows)
3130
npm i -g open-browser-use
3231

33-
# Homebrew安装
32+
# Homebrew安装(macOS、Linux)
3433
brew tap iFurySt/open-browser-use && brew install open-browser-use
3534

3635
# 升级
37-
brew upgrade open-browser-use
36+
npm update -g open-browser-use
3837
```
3938

4039
### 配置 Chrome

apps/chrome-extension/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"manifest_version": 3,
33
"name": "Open Browser Use",
44
"description": "Open Browser Use Chrome automation extension.",
5-
"version": "0.1.36",
5+
"version": "0.1.37",
66
"icons": {
77
"16": "icons/icon-16.png",
88
"32": "icons/icon-32.png",

cmd/open-browser-use/main.go

Lines changed: 129 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import (
2727
"github.com/spf13/cobra"
2828
)
2929

30-
const version = "0.1.36"
30+
const version = "0.1.37"
3131
const defaultChromeExtensionID = "bgjoihaepiejlfjinojjfgokghnodnhd"
3232
const defaultCLISessionID = "obu-cli"
3333
const defaultMCPSessionID = "obu-mcp"
@@ -47,16 +47,24 @@ func main() {
4747
}
4848

4949
func run(args []string) error {
50-
if len(args) > 0 && isNativeMessagingLaunch(args[0]) {
50+
if isNativeMessagingLaunch(args) {
5151
return runHost(host.DefaultSocketDir, "")
5252
}
5353
cmd := newRootCommand()
5454
cmd.SetArgs(args)
5555
return cmd.Execute()
5656
}
5757

58-
func isNativeMessagingLaunch(arg string) bool {
59-
return strings.HasPrefix(arg, "chrome-extension://")
58+
func isNativeMessagingLaunch(args []string) bool {
59+
for _, arg := range args {
60+
if strings.HasPrefix(arg, "chrome-extension://") {
61+
return true
62+
}
63+
if runtime.GOOS == "windows" && strings.HasPrefix(arg, "--parent-window=") {
64+
return true
65+
}
66+
}
67+
return false
6068
}
6169

6270
func newRootCommand() *cobra.Command {
@@ -325,6 +333,12 @@ func installNativeManifestForBrowser(extensionID string, binaryPath string, outp
325333
if err := os.WriteFile(path, append(payload, '\n'), 0o600); err != nil {
326334
return "", err
327335
}
336+
if outputPath == "" && runtime.GOOS == "windows" {
337+
key := `HKCU\Software\Google\Chrome\NativeMessagingHosts\` + host.NativeHostName
338+
if err := regAddDefaultString(key, path); err != nil {
339+
return "", fmt.Errorf("failed to register native messaging host %q: %w", key, err)
340+
}
341+
}
328342
return path, nil
329343
}
330344

@@ -892,6 +906,12 @@ func defaultChromeUserDataDir() (string, error) {
892906
return filepath.Join(home, "Library/Application Support/Google/Chrome"), nil
893907
case "linux":
894908
return filepath.Join(home, ".config/google-chrome"), nil
909+
case "windows":
910+
localAppData := os.Getenv("LOCALAPPDATA")
911+
if strings.TrimSpace(localAppData) == "" {
912+
localAppData = filepath.Join(home, "AppData", "Local")
913+
}
914+
return filepath.Join(localAppData, "Google", "Chrome", "User Data"), nil
895915
default:
896916
return "", fmt.Errorf("Chrome extension detection is not implemented for %s", runtime.GOOS)
897917
}
@@ -1033,6 +1053,13 @@ func installChromeExternalExtensionForBrowser(extensionID string, outputPath str
10331053
if allowedExtensionID == "" {
10341054
allowedExtensionID = defaultChromeExtensionID
10351055
}
1056+
if runtime.GOOS == "windows" && outputPath == "" {
1057+
key := `HKCU\Software\Google\Chrome\Extensions\` + allowedExtensionID
1058+
if err := regAddString(key, "update_url", chromeWebStoreUpdateURL); err != nil {
1059+
return "", fmt.Errorf("failed to register Chrome external extension %q: %w", key, err)
1060+
}
1061+
return "registry:" + key, nil
1062+
}
10361063
path := outputPath
10371064
if path == "" {
10381065
var err error
@@ -1521,6 +1548,23 @@ func supportedBrowserProfileRoots() ([]browserProfileRoot, error) {
15211548
BrowserName: "Google Chrome",
15221549
Root: filepath.Join(home, ".config/google-chrome"),
15231550
}}, nil
1551+
case "windows":
1552+
localAppData := os.Getenv("LOCALAPPDATA")
1553+
if strings.TrimSpace(localAppData) == "" {
1554+
localAppData = filepath.Join(home, "AppData", "Local")
1555+
}
1556+
return []browserProfileRoot{
1557+
{
1558+
BrowserID: "chrome",
1559+
BrowserName: "Google Chrome",
1560+
Root: filepath.Join(localAppData, "Google", "Chrome", "User Data"),
1561+
},
1562+
{
1563+
BrowserID: "chrome-beta",
1564+
BrowserName: "Google Chrome Beta",
1565+
Root: filepath.Join(localAppData, "Google", "Chrome Beta", "User Data"),
1566+
},
1567+
}, nil
15241568
default:
15251569
return nil, fmt.Errorf("browser profile detection is not implemented for %s", runtime.GOOS)
15261570
}
@@ -2728,7 +2772,7 @@ func resolveNativeHostTarget(binaryPath string) (string, error) {
27282772
if info.IsDir() {
27292773
return "", fmt.Errorf("native host binary target is a directory: %s", absolutePath)
27302774
}
2731-
if info.Mode()&0o111 == 0 {
2775+
if runtime.GOOS != "windows" && info.Mode()&0o111 == 0 {
27322776
return "", fmt.Errorf("native host binary target is not executable: %s", absolutePath)
27332777
}
27342778
return absolutePath, nil
@@ -2744,6 +2788,12 @@ func stableNativeHostPath() (string, error) {
27442788
return filepath.Join(home, "Library/Application Support/OpenBrowserUse/native-host/open-browser-use"), nil
27452789
case "linux":
27462790
return filepath.Join(home, ".local/share/open-browser-use/native-host/open-browser-use"), nil
2791+
case "windows":
2792+
localAppData := os.Getenv("LOCALAPPDATA")
2793+
if strings.TrimSpace(localAppData) == "" {
2794+
localAppData = filepath.Join(home, "AppData", "Local")
2795+
}
2796+
return filepath.Join(localAppData, "OpenBrowserUse", "native-host", "open-browser-use.exe"), nil
27472797
default:
27482798
return "", fmt.Errorf("stable native host link path is not implemented for %s", runtime.GOOS)
27492799
}
@@ -2770,6 +2820,11 @@ func defaultChromeExternalExtensionPathForBrowser(extensionID string, browserSel
27702820
return filepath.Join(root.Root, "External Extensions", filename), nil
27712821
case "linux":
27722822
return filepath.Join("/opt/google/chrome/extensions", filename), nil
2823+
case "windows":
2824+
if strings.TrimSpace(browserSelector) != "" && !browserSelectorMatches(browserSelector, connectedProfileInfo{Browser: "chrome", BrowserName: "Google Chrome"}) {
2825+
return "", fmt.Errorf("Chrome external extension setup is not implemented for browser selector %q on %s", browserSelector, runtime.GOOS)
2826+
}
2827+
return `HKCU\Software\Google\Chrome\Extensions\` + strings.TrimSpace(extensionID), nil
27732828
default:
27742829
return "", fmt.Errorf("Chrome external extension setup is not implemented for %s", runtime.GOOS)
27752830
}
@@ -2779,12 +2834,45 @@ func installStableNativeHostLink(targetPath string, linkPath string) error {
27792834
if err := os.MkdirAll(filepath.Dir(linkPath), 0o700); err != nil {
27802835
return err
27812836
}
2837+
if runtime.GOOS == "windows" {
2838+
if samePath(targetPath, linkPath) {
2839+
return nil
2840+
}
2841+
return copyFile(targetPath, linkPath, 0o700)
2842+
}
27822843
if err := os.Remove(linkPath); err != nil && !errors.Is(err, os.ErrNotExist) {
27832844
return err
27842845
}
27852846
return os.Symlink(targetPath, linkPath)
27862847
}
27872848

2849+
func samePath(left string, right string) bool {
2850+
leftAbs, leftErr := filepath.Abs(left)
2851+
rightAbs, rightErr := filepath.Abs(right)
2852+
if leftErr != nil || rightErr != nil {
2853+
return false
2854+
}
2855+
return strings.EqualFold(filepath.Clean(leftAbs), filepath.Clean(rightAbs))
2856+
}
2857+
2858+
func copyFile(sourcePath string, targetPath string, mode os.FileMode) error {
2859+
source, err := os.Open(sourcePath)
2860+
if err != nil {
2861+
return err
2862+
}
2863+
defer source.Close()
2864+
target, err := os.OpenFile(targetPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, mode)
2865+
if err != nil {
2866+
return err
2867+
}
2868+
_, copyErr := io.Copy(target, source)
2869+
closeErr := target.Close()
2870+
if copyErr != nil {
2871+
return copyErr
2872+
}
2873+
return closeErr
2874+
}
2875+
27882876
func defaultNativeHostManifestPath() (string, error) {
27892877
return defaultNativeHostManifestPathForBrowser("")
27902878
}
@@ -2804,6 +2892,12 @@ func defaultNativeHostManifestPathForBrowser(browserSelector string) (string, er
28042892
return filepath.Join(root.Root, "NativeMessagingHosts", filename), nil
28052893
case "linux":
28062894
return filepath.Join(home, ".config/google-chrome/NativeMessagingHosts", filename), nil
2895+
case "windows":
2896+
localAppData := os.Getenv("LOCALAPPDATA")
2897+
if strings.TrimSpace(localAppData) == "" {
2898+
localAppData = filepath.Join(home, "AppData", "Local")
2899+
}
2900+
return filepath.Join(localAppData, "OpenBrowserUse", "NativeMessagingHosts", filename), nil
28072901
default:
28082902
return "", fmt.Errorf("default manifest install path is not implemented for %s; pass --output", runtime.GOOS)
28092903
}
@@ -3019,6 +3113,12 @@ func defaultUnpackedExtensionDir() (string, error) {
30193113
return filepath.Join(home, "Library/Application Support/OpenBrowserUse/chrome-extension/release"), nil
30203114
case "linux":
30213115
return filepath.Join(home, ".local/share/open-browser-use/chrome-extension/release"), nil
3116+
case "windows":
3117+
localAppData := os.Getenv("LOCALAPPDATA")
3118+
if strings.TrimSpace(localAppData) == "" {
3119+
localAppData = filepath.Join(home, "AppData", "Local")
3120+
}
3121+
return filepath.Join(localAppData, "OpenBrowserUse", "chrome-extension", "release"), nil
30223122
default:
30233123
return "", fmt.Errorf("release extension setup is not implemented for %s", runtime.GOOS)
30243124
}
@@ -3254,6 +3354,8 @@ func openChromeExtensionsPage() error {
32543354
cmd = exec.Command("open", "-a", "Google Chrome", "chrome://extensions/")
32553355
case "linux":
32563356
cmd = exec.Command("xdg-open", "chrome://extensions/")
3357+
case "windows":
3358+
cmd = exec.Command("rundll32", "url.dll,FileProtocolHandler", "chrome://extensions/")
32573359
default:
32583360
return fmt.Errorf("opening Chrome extensions page is not implemented for %s", runtime.GOOS)
32593361
}
@@ -3281,6 +3383,28 @@ func openChromeWebStorePage() error {
32813383
return cmd.Process.Release()
32823384
}
32833385

3386+
func regAddDefaultString(key string, value string) error {
3387+
return regAddString(key, "", value)
3388+
}
3389+
3390+
func regAddString(key string, name string, value string) error {
3391+
if runtime.GOOS != "windows" {
3392+
return fmt.Errorf("registry setup is not implemented for %s", runtime.GOOS)
3393+
}
3394+
args := []string{"ADD", key}
3395+
if strings.TrimSpace(name) == "" {
3396+
args = append(args, "/ve")
3397+
} else {
3398+
args = append(args, "/v", name)
3399+
}
3400+
args = append(args, "/t", "REG_SZ", "/d", value, "/f")
3401+
output, err := exec.Command("reg.exe", args...).CombinedOutput()
3402+
if err != nil {
3403+
return fmt.Errorf("%w: %s", err, strings.TrimSpace(string(output)))
3404+
}
3405+
return nil
3406+
}
3407+
32843408
func numberAsInt(value any) (int, bool) {
32853409
switch typed := value.(type) {
32863410
case int:

cmd/open-browser-use/main_test.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,13 @@ func TestNativeHostNameIsChromeCompatible(t *testing.T) {
2929
}
3030

3131
func TestNativeMessagingLaunchArg(t *testing.T) {
32-
if !isNativeMessagingLaunch("chrome-extension://nfjjgckfgejeofdcmaepbapclmldcflf/") {
32+
if !isNativeMessagingLaunch([]string{"--parent-window=123", "chrome-extension://nfjjgckfgejeofdcmaepbapclmldcflf/"}) {
3333
t.Fatal("expected Chrome extension origin to launch host mode")
3434
}
35-
if isNativeMessagingLaunch("host") {
35+
if runtime.GOOS == "windows" && !isNativeMessagingLaunch([]string{"--parent-window=123"}) {
36+
t.Fatal("expected Windows parent window arg to launch host mode")
37+
}
38+
if isNativeMessagingLaunch([]string{"host"}) {
3639
t.Fatal("expected CLI subcommand not to be treated as native messaging launch")
3740
}
3841
}

0 commit comments

Comments
 (0)