Add byre install-tool and a Homebrew row to the Server tab

This commit is contained in:
Ian Gulliver
2026-10-22 21:17:36 -07:00
parent 7556135879
commit f70ee02af4
8 changed files with 208 additions and 26 deletions
+29 -15
View File
@@ -5,7 +5,6 @@ import (
"fmt"
"os"
"os/exec"
"strings"
)
const (
@@ -19,38 +18,48 @@ type check struct {
Need string `json:"need"`
For string `json:"for"`
OK bool `json:"ok"`
Command string `json:"command"`
Tool string `json:"tool"`
Fix string `json:"fix"`
Blocking bool `json:"blocking"`
}
func checks() []check {
brew := command("Homebrew", "brew", needOptional, "installing codex, agy, node and Google Chrome", "brew")
return []check{
brew,
gitCheck(),
command("claude", needAgent, "Claude models", "curl -fsSL https://claude.ai/install.sh | bash", "Then run claude once to sign in."),
command("codex", needAgent, "GPT models", "brew install --cask codex", "Then run codex once to sign in."),
command("agy", needAgent, "Gemini models", "brew install --cask antigravity-cli", "Then run agy once to sign in."),
command("npx", needOptional, "reviewers' Playwright browser tools", "brew install node", ""),
appCheck("Google Chrome", needOptional, "browser tools for agents and reviewers", "brew install --cask google-chrome"),
command("claude", "claude", needAgent, "Claude models", "claude"),
withBrew(brew, command("codex", "codex", needAgent, "GPT models", "codex")),
withBrew(brew, command("agy", "agy", needAgent, "Gemini models", "agy")),
withBrew(brew, command("npx", "npx", needOptional, "reviewers' Playwright browser tools", "node")),
withBrew(brew, appCheck("Google Chrome", needOptional, "browser tools for agents and reviewers", "chrome")),
}
}
func gitCheck() check {
c := check{Name: "git", Need: needRequired, For: "worktrees, snapshots and merges", Command: "xcode-select --install"}
c := check{Name: "git", Need: needRequired, For: "worktrees, snapshots and merges", Tool: "git"}
path, err := exec.LookPath("git")
// /usr/bin/git is a stub that offers to install the Command Line Tools when they're missing.
c.OK = err == nil && (path != "/usr/bin/git" || exec.Command("xcode-select", "-p").Run() == nil)
return c
}
func command(name, need, purpose, install, fix string) check {
_, err := exec.LookPath(name)
return check{Name: name, Need: need, For: purpose, OK: err == nil, Command: install, Fix: fix}
func command(name, binary, need, purpose, tool string) check {
_, err := exec.LookPath(binary)
return check{Name: name, Need: need, For: purpose, OK: err == nil, Tool: tool}
}
func appCheck(name, need, purpose, install string) check {
func appCheck(name, need, purpose, tool string) check {
_, err := os.Stat("/Applications/" + name + ".app")
return check{Name: name, Need: need, For: purpose, OK: err == nil, Command: install}
return check{Name: name, Need: need, For: purpose, OK: err == nil, Tool: tool}
}
func withBrew(brew, c check) check {
if !c.OK && !brew.OK {
c.Tool = ""
c.Fix = "Install Homebrew first."
}
return c
}
func markBlocking(cs []check) bool {
@@ -87,9 +96,14 @@ func requireChecks() {
}
fmt.Println("byre can't run on this Mac yet. It needs git and at least one of claude, codex and agy:")
for _, c := range cs {
if c.Blocking {
fmt.Printf(" %s: %s\n", c.Name, strings.TrimSpace(c.Command+" "+c.Fix))
if !c.Blocking {
continue
}
if c.Tool == "" {
fmt.Printf(" %s: %s %s\n", c.Name, c.Fix, installCommand("brew"))
continue
}
fmt.Printf(" %s: %s\n", c.Name, installCommand(c.Tool))
}
os.Exit(1)
}
+1
View File
@@ -96,6 +96,7 @@ func installCmd() {
func run(name string, args ...string) {
cmd := exec.Command(name, args...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()
+3 -1
View File
@@ -18,6 +18,8 @@ func main() {
installCmd()
case "check":
checkCmd()
case "install-tool":
installToolCmd(os.Args[2:])
case "run":
runCmd(os.Args[2:])
case "signing":
@@ -36,7 +38,7 @@ func main() {
}
func usage() {
fmt.Fprintln(os.Stderr, "usage: byre serve | byre install | byre check | byre run [--resume ID] | byre signing CHECKOUT | byre grep [--glob GLOB] [--ignore-case] [--files-only] PATTERN PATH | byre list PATH | byre client-exec [--client NAME] [--timeout SECS] -- CMD... | byre screen")
fmt.Fprintln(os.Stderr, "usage: byre serve | byre install | byre check | byre install-tool NAME |byre run [--resume ID] | byre signing CHECKOUT | byre grep [--glob GLOB] [--ignore-case] [--files-only] PATTERN PATH | byre list PATH | byre client-exec [--client NAME] [--timeout SECS] -- CMD... | byre screen")
os.Exit(2)
}
+105
View File
@@ -0,0 +1,105 @@
package main
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"syscall"
)
type brewTool struct {
args []string
signIn []string
}
var brewTools = map[string]brewTool{
"codex": {args: []string{"--cask", "codex"}, signIn: []string{"codex", "login"}},
"agy": {args: []string{"--cask", "antigravity-cli"}, signIn: []string{"agy"}},
"node": {args: []string{"node"}},
"chrome": {args: []string{"--cask", "google-chrome"}},
}
const toolNames = "brew, git, claude, codex, agy, node or chrome"
func installToolCmd(args []string) {
if len(args) != 1 {
fmt.Fprintln(os.Stderr, "usage: byre install-tool NAME, where NAME is "+toolNames)
os.Exit(2)
}
switch args[0] {
case "brew":
run("/bin/sh", "-c", `/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"`)
addToProfile(".zprofile", `eval "$(/opt/homebrew/bin/brew shellenv)"`)
case "git":
run("xcode-select", "--install")
fmt.Println("macOS is showing its own dialog to install the Command Line Tools: click Install there.")
case "claude":
run("/bin/sh", "-c", "curl -fsSL https://claude.ai/install.sh | bash")
addToProfile(".zshrc", `export PATH="$HOME/.local/bin:$PATH"`)
signIn(filepath.Join(homeDir(), ".local", "bin", "claude"))
default:
t, ok := brewTools[args[0]]
if !ok {
fmt.Fprintln(os.Stderr, "unknown tool "+args[0]+": NAME is "+toolNames)
os.Exit(2)
}
brew, err := exec.LookPath("brew")
if err != nil {
fmt.Println("Homebrew isn't installed. Install it first:", installCommand("brew"))
os.Exit(1)
}
run(brew, append([]string{"install"}, t.args...)...)
if t.signIn != nil {
signIn(filepath.Join(filepath.Dir(brew), t.signIn[0]), t.signIn[1:]...)
}
}
}
func installCommand(name string) string {
exe, err := os.Executable()
if err != nil {
fatal("find executable", "err", err)
}
return "'" + strings.ReplaceAll(exe, "'", `'\''`) + "' install-tool " + name
}
func homeDir() string {
home, err := os.UserHomeDir()
if err != nil {
fatal("home dir", "err", err)
}
return home
}
func addToProfile(name, line string) {
path := filepath.Join(homeDir(), name)
data, err := os.ReadFile(path)
if err != nil && !os.IsNotExist(err) {
fatal("read profile", "path", path, "err", err)
}
if strings.Contains(string(data), line) {
return
}
text := line + "\n"
if len(data) > 0 && !strings.HasSuffix(string(data), "\n") {
text = "\n" + text
}
f, err := os.OpenFile(path, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0o644)
if err != nil {
fatal("open profile", "path", path, "err", err)
}
defer f.Close()
_, err = f.WriteString(text)
if err != nil {
fatal("write profile", "path", path, "err", err)
}
fmt.Println("Added", line, "to", path)
}
func signIn(path string, args ...string) {
fmt.Println("Starting", filepath.Base(path), "so you can sign in.")
err := syscall.Exec(path, append([]string{path}, args...), os.Environ())
fatal("start "+filepath.Base(path), "err", err)
}