diff --git a/internal/installer/installer.go b/internal/installer/installer.go index 40116a5..60fb54e 100644 --- a/internal/installer/installer.go +++ b/internal/installer/installer.go @@ -344,7 +344,14 @@ func (i *Installer) installUv() InstallResult { 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: diff --git a/internal/tui/app.go b/internal/tui/app.go index aff5c08..38d29f0 100644 --- a/internal/tui/app.go +++ b/internal/tui/app.go @@ -163,6 +163,11 @@ type aiResponseMsg struct{ content string } type aiErrMsg struct{ err error } type scanCompleteMsg struct{ result *scanner.ScanResult } type installCompleteMsg struct{ results []installer.InstallResult } +type installProgressMsg struct { + tool string + current int + total int +} type updateCheckMsg struct{ statuses []updater.UpdateStatus } type previewReadyMsg struct{ url string } type workflowPhaseMsg struct{ phase workflow.Phase } @@ -206,6 +211,11 @@ type Model struct { ctrlCCount int lastCtrlC time.Time + + installing bool + installCurrent int + installTotal int + installTool string } type keyMap struct { @@ -390,6 +400,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.viewport.SetContent(m.renderContent()) return m, nil case installCompleteMsg: + m.installing = false for _, r := range msg.results { status := itemOKStyle.Render("[OK]") if !r.Success { @@ -398,6 +409,35 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.installLog = append(m.installLog, fmt.Sprintf(" %s %s: %s", status, r.Tool, r.Message)) } m.scanResult = scanner.ScanSystem() + m.progressBar.SetPercent(1) + m.viewport.SetContent(m.renderContent()) + return m, nil + case installProgressMsg: + status := itemOKStyle.Render("[OK]") + m.installLog = append(m.installLog, fmt.Sprintf(" %s %s installed", status, msg.tool)) + m.installCurrent = msg.current + m.installTool = "" + pct := float64(msg.current) / float64(max(msg.total, 1)) + m.progressBar.SetPercent(pct) + m.viewport.SetContent(m.renderContent()) + return m, nil + case installBatchMsg: + status := itemOKStyle.Render("[OK]") + if !msg.result.Success { + status = itemMissingStyle.Render("[FAIL]") + } + m.installLog = append(m.installLog, fmt.Sprintf(" %s %s: %s", status, msg.result.Tool, msg.result.Message)) + m.installCurrent = msg.index + 1 + m.installTotal = len(msg.tools) + pct := float64(m.installCurrent) / float64(max(m.installTotal, 1)) + m.progressBar.SetPercent(pct) + if msg.index+1 < len(msg.tools) { + m.installTool = msg.tools[msg.index+1] + m.viewport.SetContent(m.renderContent()) + return m, startInstallCmd(msg.config, msg.tools, msg.index+1) + } + m.installing = false + m.scanResult = scanner.ScanSystem() m.viewport.SetContent(m.renderContent()) return m, nil case updateCheckMsg: @@ -577,27 +617,35 @@ func (m Model) handleChatSubmit() (tea.Model, tea.Cmd) { func (m Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { switch msg.String() { case "i": - return m, tea.Cmd(func() tea.Msg { - needsSudo := checkNeedsSudo(m.scanResult) - if needsSudo && !hasSudo() { - return installCompleteMsg{results: []installer.InstallResult{ - {Tool: "system", Success: false, Message: "some tools require sudo. Run: muyue install, or relaunch with sudo/pkexec"}, - }} - } - inst := installer.New(m.config) - var missing []string - if m.scanResult != nil { - for _, t := range m.scanResult.Tools { - if !t.Installed { - missing = append(missing, t.Name) - } + if m.installing { + return m, nil + } + var missing []string + if m.scanResult != nil { + for _, t := range m.scanResult.Tools { + if !t.Installed { + missing = append(missing, t.Name) } } - if len(missing) == 0 { - return installCompleteMsg{results: []installer.InstallResult{}} - } - return installCompleteMsg{results: inst.InstallAll(missing)} - }) + } + if len(missing) == 0 { + m.installLog = append(m.installLog, itemOKStyle.Render("All tools already installed!")) + m.viewport.SetContent(m.renderContent()) + return m, nil + } + needsSudo := checkNeedsSudo(m.scanResult) + if needsSudo && !hasSudo() { + m.installLog = append(m.installLog, errMsgStyle.Render("Some tools require sudo. Run: sudo muyue install")) + m.viewport.SetContent(m.renderContent()) + return m, nil + } + m.installing = true + m.installCurrent = 0 + m.installTotal = len(missing) + m.installTool = missing[0] + m.progressBar.SetPercent(0) + m.viewport.SetContent(m.renderContent()) + return m, startInstallCmd(m.config, missing, 0) case "u": return m, tea.Cmd(func() tea.Msg { result := scanner.ScanSystem() @@ -748,6 +796,30 @@ func hasSudo() bool { return false } +func startInstallCmd(cfg *config.MuyueConfig, tools []string, index int) tea.Cmd { + return tea.Cmd(func() tea.Msg { + inst := installer.New(cfg) + result := inst.InstallTool(tools[index]) + + if index+1 < len(tools) { + return installBatchMsg{ + result: result, + tools: tools, + index: index, + config: cfg, + } + } + return installCompleteMsg{results: []installer.InstallResult{result}} + }) +} + +type installBatchMsg struct { + result installer.InstallResult + tools []string + index int + config *config.MuyueConfig +} + func sendAIMessage(orch *orchestrator.Orchestrator, input string) tea.Cmd { return tea.Cmd(func() tea.Msg { if orch == nil { @@ -968,6 +1040,20 @@ func (m Model) renderDashboard() string { b.WriteString("\n") } + if m.installing { + b.WriteString(sectionStyle.Render("Installing...")) + b.WriteString("\n") + progBar := m.progressBar.View() + label := "" + if m.installTool != "" { + label = fmt.Sprintf(" %d/%d - %s", m.installCurrent+1, m.installTotal, m.installTool) + } else { + label = fmt.Sprintf(" %d/%d", m.installCurrent, m.installTotal) + } + b.WriteString(fmt.Sprintf(" %s%s\n", progBar, label)) + b.WriteString("\n") + } + if len(m.installLog) > 0 { b.WriteString(sectionStyle.Render("Install Log")) b.WriteString("\n") diff --git a/internal/updater/updater.go b/internal/updater/updater.go index 960f27f..76209f3 100644 --- a/internal/updater/updater.go +++ b/internal/updater/updater.go @@ -60,7 +60,7 @@ func getLatestVersion(tool string) (string, error) { "crush": "charmbracelet/crush", "gh": "cli/cli", "starship": "starship/starship", - "docker": "docker/compose", + "docker": "moby/moby", } repo, ok := repos[tool]