All checks were successful
CI / build (push) Successful in 2m41s
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>
441 lines
14 KiB
Go
441 lines
14 KiB
Go
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"}
|
||
}
|