Skip to content

Commit eb98890

Browse files
committed
Make setup guidance idempotent
1 parent d2b9e21 commit eb98890

6 files changed

Lines changed: 224 additions & 42 deletions

File tree

cmd/open-browser-use/main.go

Lines changed: 99 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -136,14 +136,15 @@ func newSetupCommand() *cobra.Command {
136136
if err != nil {
137137
return err
138138
}
139-
if !noOpen {
139+
status := detectBrowserExtension(host.DefaultSocketDir, 700*time.Millisecond)
140+
shouldOpenStore := shouldOpenStoreSetup(status, noOpen)
141+
if shouldOpenStore {
140142
if err := openChromeWebStorePage(); err != nil {
141143
result.StoreOpenError = err.Error()
142144
} else {
143145
result.OpenedStore = true
144146
}
145147
}
146-
status := detectBrowserExtension(host.DefaultSocketDir, 700*time.Millisecond)
147148
return renderStoreSetupResult(cmd.OutOrStdout(), result, status)
148149
},
149150
}
@@ -198,24 +199,27 @@ func newSetupBetaCommand() *cobra.Command {
198199
if err != nil {
199200
return err
200201
}
201-
if !noOpen {
202+
status := detectBrowserExtension(host.DefaultSocketDir, 700*time.Millisecond)
203+
status.InstallCommand = "open-browser-use setup beta"
204+
status.UpgradeCommand = "open-browser-use setup beta"
205+
shouldOpen := shouldOpenManualSetup(status, noOpen)
206+
if shouldOpen {
202207
if err := openChromeExtensionsPage(); err != nil {
203208
return err
204209
}
205210
if err := revealFile(installZIPPath); err != nil {
206211
return err
207212
}
208213
}
209-
status := detectBrowserExtension(host.DefaultSocketDir, 700*time.Millisecond)
210-
status.InstallCommand = "open-browser-use setup beta"
211-
status.UpgradeCommand = "open-browser-use setup beta"
214+
skillUpdate := maybeUpdateInstalledSkill()
212215
return renderManualSetupResult(cmd.OutOrStdout(), manualSetupResult{
213216
NativeManifestPath: manifestPath,
214217
ExtensionID: effectiveExtensionID,
215218
ZIPPath: installZIPPath,
216219
UnpackedPath: unpackedPath,
217-
OpenedChrome: !noOpen,
218-
OpenedFileManager: !noOpen,
220+
OpenedChrome: shouldOpen,
221+
OpenedFileManager: shouldOpen,
222+
SkillUpdate: skillUpdate,
219223
}, status)
220224
},
221225
}
@@ -311,6 +315,7 @@ type setupResult struct {
311315
StoreURL string
312316
OpenedStore bool
313317
StoreOpenError string
318+
SkillUpdate skillUpdateStatus
314319
}
315320

316321
type manualSetupResult struct {
@@ -320,6 +325,7 @@ type manualSetupResult struct {
320325
UnpackedPath string
321326
OpenedChrome bool
322327
OpenedFileManager bool
328+
SkillUpdate skillUpdateStatus
323329
}
324330

325331
type browserExtensionStatus struct {
@@ -334,6 +340,13 @@ type browserExtensionStatus struct {
334340
Error string
335341
}
336342

343+
type skillUpdateStatus struct {
344+
Checked bool
345+
Attempted bool
346+
Updated bool
347+
Error string
348+
}
349+
337350
func setupChrome(extensionID string, binaryPath string, externalExtensionOutput string) (setupResult, error) {
338351
manifestPath, err := installNativeManifest(extensionID, binaryPath, "")
339352
if err != nil {
@@ -347,6 +360,7 @@ func setupChrome(extensionID string, binaryPath string, externalExtensionOutput
347360
NativeManifestPath: manifestPath,
348361
ExternalExtensionPath: extensionPath,
349362
StoreURL: chromeWebStoreExtensionURL,
363+
SkillUpdate: maybeUpdateInstalledSkill(),
350364
}, nil
351365
}
352366

@@ -391,19 +405,25 @@ func renderStoreSetupResult(writer io.Writer, result setupResult, status browser
391405
fmt.Fprintf(writer, "3. Open Chrome Web Store\n %s\n", result.StoreURL)
392406
}
393407
fmt.Fprintf(writer, "4. 🧩 Browser extension\n %s\n", status.summaryForSetup("Not installed yet. Install or enable it from the Chrome Web Store page."))
408+
renderSkillUpdateResult(writer, result.SkillUpdate)
394409
fmt.Fprintln(writer)
395410
if status.isReady() {
396-
fmt.Fprintln(writer, "All set. The browser extension is installed, connected, and on the expected version.")
411+
fmt.Fprintln(writer, "All set. Browser extension is installed, connected, and on the expected version.")
397412
return nil
398413
}
399414
if status.needsUpgrade() {
400415
fmt.Fprintf(writer, "Next: upgrade the browser extension with `%s`, then run `open-browser-use info`.\n", status.UpgradeCommand)
401416
return nil
402417
}
403-
fmt.Fprintln(writer, "Next:")
404-
fmt.Fprintln(writer, " 1. Install or enable Open Browser Use from the Chrome Web Store page.")
405-
fmt.Fprintln(writer, " 2. Restart Chrome if Chrome asks or the extension does not appear immediately.")
406-
fmt.Fprintln(writer, " 3. Verify the connection: open-browser-use info")
418+
if result.OpenedStore {
419+
fmt.Fprintln(writer, "Next: the Chrome Web Store page is open; install or enable the extension, then run `open-browser-use info`.")
420+
return nil
421+
}
422+
if result.StoreOpenError != "" {
423+
fmt.Fprintln(writer, "Next: open the Chrome Web Store page manually, install or enable the extension, then run `open-browser-use info`.")
424+
return nil
425+
}
426+
fmt.Fprintln(writer, "Next: restart Chrome if needed, approve or enable the extension if Chrome asks, then run `open-browser-use info`.")
407427
return nil
408428
}
409429

@@ -413,29 +433,35 @@ func renderManualSetupResult(writer io.Writer, result manualSetupResult, status
413433
fmt.Fprintf(writer, "2. ✅ Prepared browser extension package\n Extension id: %s\n ZIP: %s\n Includes stable extension key for this id.\n", result.ExtensionID, result.ZIPPath)
414434
fmt.Fprintf(writer, "3. ✅ Prepared unpacked extension directory\n %s\n", result.UnpackedPath)
415435
fmt.Fprintf(writer, "4. 🧩 Browser extension\n %s\n", status.summaryForSetup("Not installed yet. Finish the ZIP install below."))
436+
renderSkillUpdateResult(writer, result.SkillUpdate)
416437
fmt.Fprintln(writer)
417438
if status.isReady() {
418-
fmt.Fprintln(writer, "All set. The browser extension is installed, connected, and on the expected version.")
439+
fmt.Fprintln(writer, "All set. Browser extension is installed, connected, and on the expected version.")
419440
return nil
420441
}
421-
if result.OpenedChrome {
422-
fmt.Fprintln(writer, "Opened chrome://extensions for you.")
423-
} else {
424-
fmt.Fprintln(writer, "Open chrome://extensions manually.")
425-
}
426-
if result.OpenedFileManager {
427-
fmt.Fprintln(writer, "Opened the extension package in Finder/file manager.")
428-
} else {
429-
fmt.Fprintf(writer, "Open the folder containing the package: %s\n", filepath.Dir(result.ZIPPath))
442+
if result.OpenedChrome && result.OpenedFileManager {
443+
fmt.Fprintln(writer, "Next: chrome://extensions and Finder/file manager are open; enable Developer mode, drag the ZIP into Chrome, then run `open-browser-use info`.")
444+
return nil
430445
}
431-
fmt.Fprintln(writer, "Next:")
432-
fmt.Fprintln(writer, " 1. Turn on Developer mode in chrome://extensions.")
433-
fmt.Fprintln(writer, " 2. Drag the ZIP file into the Chrome extensions page to install it manually.")
434-
fmt.Fprintln(writer, " 3. Approve or enable the Open Browser Use extension if Chrome asks.")
435-
fmt.Fprintln(writer, " 4. Verify the connection: open-browser-use info")
446+
fmt.Fprintf(writer, "Next: open chrome://extensions, enable Developer mode, drag in %s, then run `open-browser-use info`.\n", result.ZIPPath)
436447
return nil
437448
}
438449

450+
func renderSkillUpdateResult(writer io.Writer, status skillUpdateStatus) {
451+
if status.Updated {
452+
fmt.Fprintln(writer, "5. ✅ Agent skill\n Updated existing open-browser-use skill.")
453+
return
454+
}
455+
if status.Attempted && status.Error != "" {
456+
fmt.Fprintf(writer, "5. ⚠️ Agent skill\n Existing skill update failed: %s\n", status.Error)
457+
return
458+
}
459+
if !status.Checked {
460+
return
461+
}
462+
fmt.Fprintln(writer, "5. ℹ️ Agent skill install commands\n Codex: npx skills add iFurySt/open-codex-browser-use -g -a codex --skill open-browser-use --copy -y\n Claude Code: npx skills add iFurySt/open-codex-browser-use -g -a claude-code --skill open-browser-use --copy -y")
463+
}
464+
439465
func detectBrowserExtension(socketDir string, timeout time.Duration) browserExtensionStatus {
440466
status := browserExtensionStatus{
441467
ExpectedVersion: version,
@@ -607,6 +633,51 @@ func (status browserExtensionStatus) needsUpgrade() bool {
607633
return status.Installed && status.Version != "" && compareChromeVersions(status.Version, status.ExpectedVersion) < 0
608634
}
609635

636+
func shouldOpenManualSetup(status browserExtensionStatus, noOpen bool) bool {
637+
if noOpen {
638+
return false
639+
}
640+
return status.needsInstall() || status.needsUpgrade()
641+
}
642+
643+
func shouldOpenStoreSetup(status browserExtensionStatus, noOpen bool) bool {
644+
if noOpen {
645+
return false
646+
}
647+
return status.needsInstall() || status.needsUpgrade()
648+
}
649+
650+
func maybeUpdateInstalledSkill() skillUpdateStatus {
651+
npxPath, err := exec.LookPath("npx")
652+
if err != nil {
653+
return skillUpdateStatus{}
654+
}
655+
status := skillUpdateStatus{Checked: true}
656+
if err := runSilentCommand(20*time.Second, npxPath, "skills"); err != nil {
657+
return status
658+
}
659+
status.Attempted = true
660+
if err := runSilentCommand(2*time.Minute, npxPath, "skills", "update", "open-browser-use", "-g", "-y"); err != nil {
661+
status.Error = err.Error()
662+
return status
663+
}
664+
status.Updated = true
665+
return status
666+
}
667+
668+
func runSilentCommand(timeout time.Duration, name string, args ...string) error {
669+
ctx, cancel := context.WithTimeout(context.Background(), timeout)
670+
defer cancel()
671+
cmd := exec.CommandContext(ctx, name, args...)
672+
cmd.Stdout = io.Discard
673+
cmd.Stderr = io.Discard
674+
err := cmd.Run()
675+
if ctx.Err() == context.DeadlineExceeded {
676+
return fmt.Errorf("%s timed out", strings.Join(append([]string{name}, args...), " "))
677+
}
678+
return err
679+
}
680+
610681
func compareChromeVersions(left string, right string) int {
611682
leftParts := strings.Split(left, ".")
612683
rightParts := strings.Split(right, ".")

cmd/open-browser-use/main_test.go

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ func TestCobraSetupWritesNativeAndExternalManifests(t *testing.T) {
213213
}
214214
home := t.TempDir()
215215
t.Setenv("HOME", home)
216+
t.Setenv("PATH", t.TempDir())
216217
targetPath := filepath.Join(t.TempDir(), "open-browser-use")
217218
if err := os.WriteFile(targetPath, []byte("#!/bin/sh\nexit 0\n"), 0o755); err != nil {
218219
t.Fatal(err)
@@ -252,6 +253,7 @@ func TestCobraSetupBetaUsesProvidedZIP(t *testing.T) {
252253
}
253254
home := t.TempDir()
254255
t.Setenv("HOME", home)
256+
t.Setenv("PATH", t.TempDir())
255257
targetPath := filepath.Join(t.TempDir(), "open-browser-use")
256258
if err := os.WriteFile(targetPath, []byte("#!/bin/sh\nexit 0\n"), 0o755); err != nil {
257259
t.Fatal(err)
@@ -282,7 +284,7 @@ func TestCobraSetupBetaUsesProvidedZIP(t *testing.T) {
282284
if !strings.Contains(got, "Extension id: "+expectedExtensionID) {
283285
t.Fatalf("expected setup beta output to mention unpacked extension id, got %q", got)
284286
}
285-
if !strings.Contains(got, "Drag the ZIP file into the Chrome extensions page") && !strings.Contains(got, "All set.") {
287+
if !strings.Contains(got, "drag in "+zipPath) && !strings.Contains(got, "All set.") {
286288
t.Fatalf("expected setup beta output to mention manual install or connected status, got %q", got)
287289
}
288290
manifestPath := filepath.Join(home, "Library/Application Support/Google/Chrome/NativeMessagingHosts", host.NativeHostName+".json")
@@ -386,6 +388,71 @@ func TestBrowserExtensionStatusSummaries(t *testing.T) {
386388
}
387389
}
388390

391+
func TestShouldOpenManualSetup(t *testing.T) {
392+
tests := []struct {
393+
name string
394+
status browserExtensionStatus
395+
noOpen bool
396+
want bool
397+
}{
398+
{
399+
name: "missing extension opens guidance",
400+
status: browserExtensionStatus{
401+
ExpectedVersion: version,
402+
},
403+
want: true,
404+
},
405+
{
406+
name: "outdated extension opens guidance",
407+
status: browserExtensionStatus{
408+
Installed: true,
409+
Version: "0.1.0",
410+
ExpectedVersion: version,
411+
},
412+
want: true,
413+
},
414+
{
415+
name: "current installed extension skips guidance even when disconnected",
416+
status: browserExtensionStatus{
417+
Installed: true,
418+
Version: version,
419+
ExpectedVersion: version,
420+
},
421+
},
422+
{
423+
name: "no-open suppresses guidance",
424+
status: browserExtensionStatus{
425+
ExpectedVersion: version,
426+
},
427+
noOpen: true,
428+
},
429+
}
430+
for _, test := range tests {
431+
got := shouldOpenManualSetup(test.status, test.noOpen)
432+
if got != test.want {
433+
t.Fatalf("%s: shouldOpenManualSetup() = %v, want %v", test.name, got, test.want)
434+
}
435+
}
436+
}
437+
438+
func TestShouldOpenStoreSetup(t *testing.T) {
439+
current := browserExtensionStatus{
440+
Installed: true,
441+
Version: version,
442+
ExpectedVersion: version,
443+
}
444+
if shouldOpenStoreSetup(current, false) {
445+
t.Fatal("expected current store extension to skip store page")
446+
}
447+
missing := browserExtensionStatus{ExpectedVersion: version}
448+
if !shouldOpenStoreSetup(missing, false) {
449+
t.Fatal("expected missing store extension to open store page")
450+
}
451+
if shouldOpenStoreSetup(missing, true) {
452+
t.Fatal("expected --no-open to suppress store page")
453+
}
454+
}
455+
389456
func TestCobraUnknownCommand(t *testing.T) {
390457
cmd := newRootCommand()
391458
cmd.SetArgs([]string{"does-not-exist"})

docs/ARCHITECTURE.md

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -96,17 +96,21 @@ dot;hyphen 版本 `com.ifuryst.open-computer-use.extension` 会被
9696
写入 Chrome 默认位置,或通过 `--output` 写到指定路径。默认 extension id
9797
是 Chrome Web Store 版 `bgjoihaepiejlfjinojjfgokghnodnhd`
9898
- `open-browser-use setup`:显式安装流程,先调用 native host manifest
99-
注册,再写入 Chrome External Extensions JSON,并打开 Chrome Web Store
100-
正式扩展页,引导用户手动安装或启用扩展。macOS/Windows 仍可能需要用户在
101-
Chrome 中确认启用扩展并重启;Linux 的 External Extensions 写入默认使用
102-
Chrome 官方系统路径,可能需要更高权限。
99+
注册,再写入 Chrome External Extensions JSON。只有检测到浏览器插件未安装或
100+
版本低于当前 CLI 期望版本时,CLI 才打开 Chrome Web Store 正式扩展页,引导
101+
用户手动安装或启用扩展。macOS/Windows 仍可能需要用户在 Chrome 中确认启用
102+
扩展并重启;Linux 的 External Extensions 写入默认使用 Chrome 官方系统路径,
103+
可能需要更高权限。
103104
- `open-browser-use setup beta`:Chrome Web Store 临时不可用时的备用路径,
104105
注册 native host 后从 GitHub Releases 下载最新
105106
`open-browser-use-chrome-extension-*.zip`。CLI 会在本地 unpacked 目录和待拖入
106107
Chrome 的 ZIP 中写入稳定 beta public key,用该 id 注册 native host allowed
107-
origin,并打开 `chrome://extensions/` 和 Finder/文件管理器,引导用户把这个 ZIP
108-
拖到扩展页面手动安装;GitHub Release 中的正式 zip 本身保持为 Chrome Web
109-
Store 上传包,不再预写 beta key。
108+
origin;只有检测到浏览器插件未安装或版本低于当前 CLI 期望版本时,才打开
109+
`chrome://extensions/` 和 Finder/文件管理器,引导用户把这个 ZIP 拖到扩展页面
110+
手动安装;GitHub Release 中的正式 zip 本身保持为 Chrome Web Store 上传包,
111+
不再预写 beta key。setup 过程中如果本机已有可用的 `npx skills`,会
112+
best-effort 执行 `npx skills update open-browser-use -g -y` 更新已有 agent
113+
skill;未检测到 skills 时只展示 Codex 和 Claude Code 的安装命令。
110114
- manifest 的 `path` 默认统一写入稳定 native host link:
111115
macOS 为
112116
`~/Library/Application Support/OpenBrowserUse/native-host/open-browser-use`

docs/CHROME_WEB_STORE_RELEASE.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@ open-browser-use setup
1818
```
1919

2020
`setup` 会调用 native host manifest 注册,写入 Chrome External Extensions
21-
JSON,并打开 Chrome Web Store 正式扩展页。用户需要在商店页手动安装或启用扩展;
22-
Chrome 仍可能要求用户重启并确认启用扩展。需要只修复 native host 时,仍可运行:
21+
JSON。只有检测到浏览器插件未安装或版本低于当前 CLI 期望版本时,CLI 才打开
22+
Chrome Web Store 正式扩展页,引导用户手动安装或启用扩展;Chrome 仍可能要求用户
23+
重启并确认启用扩展。需要只修复 native host 时,仍可运行:
2324

2425
```bash
2526
open-browser-use install-manifest
@@ -34,9 +35,12 @@ open-browser-use setup beta
3435

3536
`setup beta` 会下载最新 release zip,在本机 unpacked 目录和待拖入 Chrome 的
3637
ZIP 中写入稳定 beta public key。CLI 会用该 beta id 注册 native host allowed
37-
origin,并打开 `chrome://extensions/`,同时在 Finder 或系统文件管理器中定位
38-
这个 zip。用户需要打开 Developer mode,把这个 ZIP 拖到 Chrome 扩展页面完成
39-
手动安装。
38+
origin。只有检测到浏览器插件未安装或版本低于当前 CLI 期望版本时,CLI 才会打开
39+
`chrome://extensions/`,同时在 Finder 或系统文件管理器中定位这个 zip。用户需要
40+
打开 Developer mode,把这个 ZIP 拖到 Chrome 扩展页面完成手动安装。setup 还会在
41+
本机已有可用 `npx skills` 时 best-effort 执行
42+
`npx skills update open-browser-use -g -y`;未检测到 skills 时只展示 Codex 和
43+
Claude Code 的 skill 安装命令。
4044

4145
这条 fallback 会安装为 beta extension id,而不是 Chrome Web Store 的正式
4246
extension id。它适合“CI 已发新版 release,但 Chrome Web Store 仍在审核新版”的

0 commit comments

Comments
 (0)