Files
MuyueWorkspace/internal/installer/installer.go
Augustin 44691225e7
All checks were successful
CI / build (push) Successful in 2m41s
refactor: modularize TUI, improve error handling, add CI caching and tests
Split monolithic app.go into focused modules (dashboard, chat, workflow,
config, agents, terminal, commands, handlers). Add proper error handling
for installer commands, proxy pipes, and MCP config parsing. Fix daemon
channel buffer, cap orchestrator history, compile think regex once, and
set HTTP timeouts on preview server. Improve CI with Go module caching,
dependency download step, and test stage with race detection.

😘 Generated with Crush

Assisted-by: GLM-5-Turbo via Crush <crush@charm.land>
2026-04-20 19:13:48 +02:00

441 lines
14 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package installer
import (
"fmt"
"os"
"os/exec"
"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 := 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 := 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) SetupPrompt() error {
starshipPath, err := exec.LookPath("starship")
if err != nil {
return fmt.Errorf("starship not found")
}
rcFile := i.getRCFile()
line := fmt.Sprintf("eval \"$(" + starshipPath + " init %s)\"", i.system.Shell)
appendLine(rcFile, line)
configDir, _ := config.ConfigDir()
starshipConfig := `format = """
$directory\
$git_branch\
$git_status\
$git_metrics\
$nodejs\
$python\
$golang\
$rust\
$cmd_duration\
$line_break\
$character"""
[character]
success_symbol = "[](bold green)"
error_symbol = "[](bold red)"
[git_branch]
format = "[$symbol$branch]($style) "
[git_status]
format = '([$all_status$ahead_behind]($style) )'
`
configPath := configDir + "/starship.toml"
os.MkdirAll(configDir, 0755)
os.WriteFile(configPath, []byte(starshipConfig), 0644)
return nil
}
func (i *Installer) getRCFile() string {
home, _ := os.UserHomeDir()
switch i.system.Shell {
case "zsh":
return home + "/.zshrc"
case "fish":
return home + "/.config/fish/config.fish"
default:
return 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="+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"}
}