package installer import ( "fmt" "os" "os/exec" "path/filepath" "runtime" "strings" "github.com/muyue/muyue/internal/config" "github.com/muyue/muyue/internal/platform" ) type InstallResult struct { Tool string Success bool Message string } type Installer struct { config *config.MuyueConfig system platform.SystemInfo } func New(cfg *config.MuyueConfig) *Installer { return &Installer{ config: cfg, system: platform.Detect(), } } func (i *Installer) InstallTool(name string) InstallResult { switch name { case "crush": return i.installCrush() case "claude": return i.installClaudeCode() case "bmad": return i.installBMAD() case "starship": return i.installStarship() case "go": return i.installGo() case "node": return i.installNode() case "python": return i.installPython() case "git": return i.installGit() case "pnpm": return i.installPnpm() case "uv": return i.installUv() case "docker": return i.installDocker() case "gh": return i.installGh() default: return InstallResult{Tool: name, Success: false, Message: "unknown tool"} } } func (i *Installer) InstallAll(missing []string) []InstallResult { var results []InstallResult for _, name := range missing { results = append(results, i.InstallTool(name)) } return results } func (i *Installer) installCrush() InstallResult { if _, err := exec.LookPath("crush"); err == nil { return InstallResult{Tool: "crush", Success: true, Message: "already installed"} } var cmd *exec.Cmd switch i.system.OS { case platform.Linux: cmd = exec.Command("bash", "-c", "curl -fsSL https://github.com/charmbracelet/crush/releases/latest/download/crush_"+runtime.GOOS+"_"+runtime.GOARCH+".tar.gz | tar xz -C /usr/local/bin") case platform.MacOS: cmd = exec.Command("bash", "-c", "brew install charmbracelet/tap/crush") case platform.Windows: cmd = exec.Command("powershell", "-Command", "winget install charmbracelet.crush") default: return InstallResult{Tool: "crush", Success: false, Message: "unsupported OS"} } if output, err := cmd.CombinedOutput(); err != nil { return InstallResult{ Tool: "crush", Success: false, Message: fmt.Sprintf("install failed: %s: %s", err, string(output)), } } return InstallResult{Tool: "crush", Success: true, Message: "installed"} } func (i *Installer) installClaudeCode() InstallResult { if _, err := exec.LookPath("claude"); err == nil { return InstallResult{Tool: "claude", Success: true, Message: "already installed"} } cmd := exec.Command("npm", "install", "-g", "@anthropic-ai/claude-code") if output, err := cmd.CombinedOutput(); err != nil { return InstallResult{ Tool: "claude", Success: false, Message: fmt.Sprintf("install failed: %s: %s", err, string(output)), } } return InstallResult{Tool: "claude", Success: true, Message: "installed"} } func (i *Installer) installBMAD() InstallResult { if _, err := exec.LookPath("npx"); err != nil { return InstallResult{Tool: "bmad", Success: false, Message: "npx not found, install node first"} } configDir, err := config.ConfigDir() if err != nil { return InstallResult{Tool: "bmad", Success: false, Message: err.Error()} } bmadDir := filepath.Join(configDir, "bmad") os.MkdirAll(bmadDir, 0755) cmd := exec.Command("npx", "bmad-method@latest", "install", "--directory", bmadDir, "--yes") cmd.Env = append(os.Environ(), "npm_config_yes=true") if output, err := cmd.CombinedOutput(); err != nil { return InstallResult{ Tool: "bmad", Success: false, Message: fmt.Sprintf("install failed: %s: %s", err, string(output)), } } i.config.BMAD.Installed = true i.config.BMAD.Global = true return InstallResult{Tool: "bmad", Success: true, Message: "installed globally in ~/.muyue/bmad"} } func (i *Installer) installStarship() InstallResult { if _, err := exec.LookPath("starship"); err == nil { return InstallResult{Tool: "starship", Success: true, Message: "already installed"} } var cmd *exec.Cmd switch i.system.OS { case platform.Linux, platform.MacOS: cmd = exec.Command("bash", "-c", "curl -sS https://starship.rs/install.sh | sh -s -- -y") case platform.Windows: cmd = exec.Command("powershell", "-Command", "winget install Starship.Starship") default: return InstallResult{Tool: "starship", Success: false, Message: "unsupported OS"} } if output, err := cmd.CombinedOutput(); err != nil { return InstallResult{ Tool: "starship", Success: false, Message: fmt.Sprintf("install failed: %s: %s", err, string(output)), } } return InstallResult{Tool: "starship", Success: true, Message: "installed"} } func (i *Installer) installGo() InstallResult { if _, err := exec.LookPath("go"); err == nil { return InstallResult{Tool: "go", Success: true, Message: "already installed"} } home, _ := os.UserHomeDir() goDir := filepath.Join(home, ".local", "go") cmd := exec.Command("bash", "-c", fmt.Sprintf( "curl -sL https://go.dev/dl/go1.24.3.%s-%s.tar.gz | tar -C %s/.local -xzf -", runtime.GOOS, runtime.GOARCH, home, )) if output, err := cmd.CombinedOutput(); err != nil { return InstallResult{ Tool: "go", Success: false, Message: fmt.Sprintf("install failed: %s: %s", err, string(output)), } } rcFile := i.getRCFile() appendLine(rcFile, "export PATH="+goDir+"/bin:$PATH") return InstallResult{Tool: "go", Success: true, Message: "installed in ~/.local/go"} } func (i *Installer) installNode() InstallResult { if _, err := exec.LookPath("node"); err == nil { return InstallResult{Tool: "node", Success: true, Message: "already installed"} } switch i.system.OS { case platform.Linux: cmd := exec.Command("bash", "-c", "curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && apt-get install -y nodejs") if output, err := cmd.CombinedOutput(); err != nil { return InstallResult{Tool: "node", Success: false, Message: fmt.Sprintf("install failed: %s", string(output))} } case platform.MacOS: cmd := exec.Command("brew", "install", "node") if output, err := cmd.CombinedOutput(); err != nil { return InstallResult{Tool: "node", Success: false, Message: fmt.Sprintf("install failed: %s: %s", err, string(output))} } case platform.Windows: cmd := exec.Command("winget", "install", "OpenJS.NodeJS.LTS") if output, err := cmd.CombinedOutput(); err != nil { return InstallResult{Tool: "node", Success: false, Message: fmt.Sprintf("install failed: %s: %s", err, string(output))} } default: return InstallResult{Tool: "node", Success: false, Message: "unsupported OS"} } return InstallResult{Tool: "node", Success: true, Message: "installed"} } func (i *Installer) installPython() InstallResult { if _, err := exec.LookPath("python3"); err == nil { return InstallResult{Tool: "python", Success: true, Message: "already installed"} } switch i.system.PackageManager { case "apt": if output, err := exec.Command("apt", "install", "-y", "python3", "python3-pip").CombinedOutput(); err != nil { return InstallResult{Tool: "python", Success: false, Message: fmt.Sprintf("install failed: %s: %s", err, string(output))} } case "brew": if output, err := exec.Command("brew", "install", "python3").CombinedOutput(); err != nil { return InstallResult{Tool: "python", Success: false, Message: fmt.Sprintf("install failed: %s: %s", err, string(output))} } case "winget": if output, err := exec.Command("winget", "install", "Python.Python.3.12").CombinedOutput(); err != nil { return InstallResult{Tool: "python", Success: false, Message: fmt.Sprintf("install failed: %s: %s", err, string(output))} } default: return InstallResult{Tool: "python", Success: false, Message: fmt.Sprintf("install via '%s' not supported", i.system.PackageManager)} } return InstallResult{Tool: "python", Success: true, Message: "installed"} } func (i *Installer) installGit() InstallResult { if _, err := exec.LookPath("git"); err == nil { return InstallResult{Tool: "git", Success: true, Message: "already installed"} } switch i.system.PackageManager { case "apt": if output, err := exec.Command("apt", "install", "-y", "git").CombinedOutput(); err != nil { return InstallResult{Tool: "git", Success: false, Message: fmt.Sprintf("install failed: %s: %s", err, string(output))} } case "brew": if output, err := exec.Command("brew", "install", "git").CombinedOutput(); err != nil { return InstallResult{Tool: "git", Success: false, Message: fmt.Sprintf("install failed: %s: %s", err, string(output))} } case "winget": if output, err := exec.Command("winget", "install", "Git.Git").CombinedOutput(); err != nil { return InstallResult{Tool: "git", Success: false, Message: fmt.Sprintf("install failed: %s: %s", err, string(output))} } default: return InstallResult{Tool: "git", Success: false, Message: fmt.Sprintf("install via '%s' not supported", i.system.PackageManager)} } if i.config.Profile.Name != "" { exec.Command("git", "config", "--global", "user.name", i.config.Profile.Name).Run() } if i.config.Profile.Email != "" { exec.Command("git", "config", "--global", "user.email", i.config.Profile.Email).Run() } return InstallResult{Tool: "git", Success: true, Message: "installed and configured"} } func (i *Installer) getRCFile() string { home, _ := os.UserHomeDir() switch i.system.Shell { case "zsh": return filepath.Join(home, ".zshrc") case "fish": return filepath.Join(home, ".config", "fish", "config.fish") default: return filepath.Join(home, ".bashrc") } } func appendLine(file, line string) { data, _ := os.ReadFile(file) if strings.Contains(string(data), line) { return } f, err := os.OpenFile(file, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644) if err != nil { return } defer f.Close() f.WriteString("\n" + line + "\n") } func (i *Installer) installPnpm() InstallResult { if _, err := exec.LookPath("pnpm"); err == nil { return InstallResult{Tool: "pnpm", Success: true, Message: "already installed"} } cmd := exec.Command("npm", "install", "-g", "pnpm") if output, err := cmd.CombinedOutput(); err != nil { return InstallResult{Tool: "pnpm", Success: false, Message: fmt.Sprintf("install failed: %s: %s", err, string(output))} } return InstallResult{Tool: "pnpm", Success: true, Message: "installed"} } func (i *Installer) installUv() InstallResult { if _, err := exec.LookPath("uv"); err == nil { return InstallResult{Tool: "uv", Success: true, Message: "already installed"} } var cmd *exec.Cmd switch i.system.OS { case platform.Linux, platform.MacOS: home, _ := os.UserHomeDir() cmd = exec.Command("bash", "-c", "curl -LsSf https://astral.sh/uv/install.sh | sh") if output, err := cmd.CombinedOutput(); err != nil { return InstallResult{Tool: "uv", Success: false, Message: fmt.Sprintf("install failed: %s: %s", err, string(output))} } rcFile := i.getRCFile() appendLine(rcFile, "export PATH="+filepath.Join(home, ".local", "bin")+":$PATH") return InstallResult{Tool: "uv", Success: true, Message: "installed (added ~/.local/bin to PATH)"} case platform.Windows: cmd = exec.Command("powershell", "-Command", "irm https://astral.sh/uv/install.ps1 | iex") default: return InstallResult{Tool: "uv", Success: false, Message: "unsupported OS"} } if output, err := cmd.CombinedOutput(); err != nil { return InstallResult{Tool: "uv", Success: false, Message: fmt.Sprintf("install failed: %s: %s", err, string(output))} } return InstallResult{Tool: "uv", Success: true, Message: "installed"} } func (i *Installer) installDocker() InstallResult { if _, err := exec.LookPath("docker"); err == nil { return InstallResult{Tool: "docker", Success: true, Message: "already installed"} } var cmd *exec.Cmd switch i.system.PackageManager { case "apt": cmd = exec.Command("bash", "-c", "apt-get update && apt-get install -y docker.io && systemctl start docker && systemctl enable docker") case "pacman": cmd = exec.Command("bash", "-c", "pacman -S --noconfirm docker && systemctl start docker && systemctl enable docker") case "dnf": cmd = exec.Command("bash", "-c", "dnf install -y docker && systemctl start docker && systemctl enable docker") case "brew": cmd = exec.Command("bash", "-c", "brew install docker") default: return InstallResult{Tool: "docker", Success: false, Message: fmt.Sprintf("install via '%s' not supported, install manually", i.system.PackageManager)} } if output, err := cmd.CombinedOutput(); err != nil { return InstallResult{Tool: "docker", Success: false, Message: fmt.Sprintf("install failed: %s: %s", err, string(output))} } return InstallResult{Tool: "docker", Success: true, Message: "installed"} } func (i *Installer) installGh() InstallResult { if _, err := exec.LookPath("gh"); err == nil { return InstallResult{Tool: "gh", Success: true, Message: "already installed"} } var cmd *exec.Cmd switch i.system.PackageManager { case "apt": cmd = exec.Command("bash", "-c", "curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg && echo 'deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main' | tee /etc/apt/sources.list.d/github-cli.list > /dev/null && apt update && apt install gh -y") case "pacman": cmd = exec.Command("pacman", "-S", "--noconfirm", "github-cli") case "dnf": cmd = exec.Command("dnf", "install", "-y", "gh") case "brew": cmd = exec.Command("brew", "install", "gh") default: return InstallResult{Tool: "gh", Success: false, Message: fmt.Sprintf("install via '%s' not supported, install manually", i.system.PackageManager)} } if output, err := cmd.CombinedOutput(); err != nil { return InstallResult{Tool: "gh", Success: false, Message: fmt.Sprintf("install failed: %s: %s", err, string(output))} } return InstallResult{Tool: "gh", Success: true, Message: "installed"} }