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>
520 lines
13 KiB
Go
520 lines
13 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
"github.com/muyue/muyue/internal/config"
|
|
"github.com/muyue/muyue/internal/installer"
|
|
"github.com/muyue/muyue/internal/lsp"
|
|
"github.com/muyue/muyue/internal/mcp"
|
|
"github.com/muyue/muyue/internal/orchestrator"
|
|
"github.com/muyue/muyue/internal/profiler"
|
|
"github.com/muyue/muyue/internal/scanner"
|
|
"github.com/muyue/muyue/internal/skills"
|
|
"github.com/muyue/muyue/internal/tui"
|
|
"github.com/muyue/muyue/internal/updater"
|
|
"github.com/muyue/muyue/internal/version"
|
|
)
|
|
|
|
func main() {
|
|
if len(os.Args) > 1 {
|
|
handleCommand(os.Args[1:])
|
|
return
|
|
}
|
|
|
|
runTUI()
|
|
}
|
|
|
|
func handleCommand(args []string) {
|
|
if len(args) == 0 {
|
|
runTUI()
|
|
return
|
|
}
|
|
|
|
switch args[0] {
|
|
case "version", "-v", "--version":
|
|
fmt.Println(version.FullVersion())
|
|
case "scan":
|
|
runScan()
|
|
case "install":
|
|
runInstall(args[1:])
|
|
case "update":
|
|
runUpdate()
|
|
case "setup":
|
|
runSetup()
|
|
case "config":
|
|
showConfig()
|
|
case "lsp":
|
|
runLSP(args[1:])
|
|
case "mcp":
|
|
runMCP(args[1:])
|
|
case "skills":
|
|
runSkills(args[1:])
|
|
case "help", "-h", "--help":
|
|
printHelp()
|
|
default:
|
|
fmt.Printf("Unknown command: %s\n", args[0])
|
|
printHelp()
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func printHelp() {
|
|
fmt.Printf(`%s - AI-powered development environment assistant
|
|
|
|
Usage:
|
|
muyue Start the interactive TUI
|
|
muyue <command> Run a specific command
|
|
|
|
Commands:
|
|
version Show version
|
|
scan Scan your system for tools and runtimes
|
|
install [tools] Install missing tools (needs sudo for some tools)
|
|
update Check and apply updates for all tools
|
|
setup Run first-time setup wizard
|
|
config Show current configuration
|
|
lsp [scan|install] Scan or install LSP servers
|
|
mcp [config|scan] Configure MCP servers for Crush and Claude Code
|
|
skills [list|generate|deploy|init|delete] Manage AI coding skills
|
|
help Show this help
|
|
|
|
TUI Controls:
|
|
Ctrl+T Open tab switcher (navigate with arrows, select with enter)
|
|
Tab / Shift+Tab Cycle tabs
|
|
Ctrl+C Show quit confirmation (press twice quickly to force quit)
|
|
|
|
Chat Commands:
|
|
/plan <goal> Start a structured Plan→Execute workflow
|
|
|
|
Workflow Controls:
|
|
[a] Approve plan
|
|
[r] Reject plan (type feedback)
|
|
[g] Generate plan (after answering questions)
|
|
[n] Execute next step
|
|
[x] Cancel/reset workflow
|
|
|
|
Note:
|
|
Some tools (docker, gh, etc.) require elevated privileges.
|
|
Run 'sudo muyue install' or use 'pkexec muyue install' if needed.
|
|
`, version.FullVersion())
|
|
}
|
|
|
|
func runTUI() {
|
|
cfg := loadOrSetupConfig()
|
|
result := scanner.ScanSystem()
|
|
|
|
model := tui.NewModel(cfg, result)
|
|
p := tea.NewProgram(model, tea.WithAltScreen())
|
|
|
|
if _, err := p.Run(); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func loadOrSetupConfig() *config.MuyueConfig {
|
|
if !config.Exists() {
|
|
fmt.Println("First time setup detected!")
|
|
cfg, err := profiler.RunFirstTimeSetup()
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Setup error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
for i := range cfg.AI.Providers {
|
|
if cfg.AI.Providers[i].Active && cfg.AI.Providers[i].APIKey == "" {
|
|
key, err := profiler.AskAPIKey(cfg.AI.Providers[i].Name)
|
|
if err == nil && key != "" {
|
|
cfg.AI.Providers[i].APIKey = key
|
|
}
|
|
}
|
|
}
|
|
|
|
if err := config.Save(cfg); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Save error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
fmt.Println("\nSetup complete! Starting muyue...")
|
|
return cfg
|
|
}
|
|
|
|
cfg, err := config.Load()
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Config load error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
return cfg
|
|
}
|
|
|
|
func runScan() {
|
|
fmt.Println("Scanning system...")
|
|
result := scanner.ScanSystem()
|
|
fmt.Println(result.Summary())
|
|
}
|
|
|
|
func runInstall(tools []string) {
|
|
cfg := loadOrSetupConfig()
|
|
inst := installer.New(cfg)
|
|
|
|
if len(tools) == 0 {
|
|
result := scanner.ScanSystem()
|
|
var missing []string
|
|
for _, t := range result.Tools {
|
|
if !t.Installed {
|
|
missing = append(missing, t.Name)
|
|
}
|
|
}
|
|
|
|
if len(missing) == 0 {
|
|
fmt.Println("All tools are installed!")
|
|
return
|
|
}
|
|
|
|
fmt.Printf("Missing tools: %v\nInstalling...\n", missing)
|
|
tools = missing
|
|
}
|
|
|
|
if needsSudo(tools) && os.Geteuid() != 0 {
|
|
fmt.Println("Some tools require elevated privileges.")
|
|
if path, err := exec.LookPath("sudo"); err == nil {
|
|
fmt.Printf("Re-running with sudo...\n")
|
|
cmd := exec.Command(path, append([]string{os.Args[0], "install"}, tools...)...)
|
|
cmd.Stdin = os.Stdin
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
if err := cmd.Run(); err != nil {
|
|
fmt.Fprintf(os.Stderr, "sudo install failed: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
config.Save(cfg)
|
|
return
|
|
}
|
|
if path, err := exec.LookPath("pkexec"); err == nil {
|
|
fmt.Printf("Re-running with pkexec...\n")
|
|
cmd := exec.Command(path, append([]string{os.Args[0], "install"}, tools...)...)
|
|
cmd.Stdin = os.Stdin
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
if err := cmd.Run(); err != nil {
|
|
fmt.Fprintf(os.Stderr, "pkexec install failed: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
config.Save(cfg)
|
|
return
|
|
}
|
|
fmt.Println("Neither sudo nor pkexec found. Some installs may fail.")
|
|
fmt.Println("Try running: sudo muyue install")
|
|
}
|
|
|
|
results := inst.InstallAll(tools)
|
|
for _, r := range results {
|
|
status := "[OK]"
|
|
if !r.Success {
|
|
status = "[FAIL]"
|
|
}
|
|
fmt.Printf(" %s %s: %s\n", status, r.Tool, r.Message)
|
|
}
|
|
|
|
config.Save(cfg)
|
|
}
|
|
|
|
func needsSudo(tools []string) bool {
|
|
sudoTools := map[string]bool{
|
|
"docker": true, "git": true, "gh": true, "node": true, "python": true,
|
|
}
|
|
for _, t := range tools {
|
|
if sudoTools[t] {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func runUpdate() {
|
|
fmt.Println("Checking for updates...")
|
|
result := scanner.ScanSystem()
|
|
statuses := updater.CheckUpdates(result)
|
|
|
|
needsUpdate := false
|
|
for _, s := range statuses {
|
|
if s.NeedsUpdate {
|
|
fmt.Printf(" [!] %s: %s -> %s\n", s.Tool, s.Current, s.Latest)
|
|
needsUpdate = true
|
|
} else if s.Error == "" {
|
|
fmt.Printf(" [v] %s: up to date (%s)\n", s.Tool, s.Current)
|
|
} else {
|
|
fmt.Printf(" [?] %s: %s\n", s.Tool, s.Error)
|
|
}
|
|
}
|
|
|
|
if needsUpdate {
|
|
fmt.Println("\nApplying updates...")
|
|
results := updater.RunAutoUpdate(statuses)
|
|
for _, r := range results {
|
|
fmt.Printf(" %s: %s\n", r.Tool, r.Message)
|
|
}
|
|
}
|
|
}
|
|
|
|
func runSetup() {
|
|
cfg, err := profiler.RunFirstTimeSetup()
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Setup error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
for i := range cfg.AI.Providers {
|
|
if cfg.AI.Providers[i].Active && cfg.AI.Providers[i].APIKey == "" {
|
|
key, err := profiler.AskAPIKey(cfg.AI.Providers[i].Name)
|
|
if err == nil && key != "" {
|
|
cfg.AI.Providers[i].APIKey = key
|
|
}
|
|
}
|
|
}
|
|
|
|
if err := config.Save(cfg); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Save error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
fmt.Println("Setup complete!")
|
|
}
|
|
|
|
func showConfig() {
|
|
cfg, err := config.Load()
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Config not found. Run `muyue setup` first.\n")
|
|
os.Exit(1)
|
|
}
|
|
|
|
fmt.Printf("Profile: %s (%s)\n", cfg.Profile.Name, cfg.Profile.Pseudo)
|
|
fmt.Printf("Email: %s\n", cfg.Profile.Email)
|
|
fmt.Printf("Editor: %s\n", cfg.Profile.Preferences.Editor)
|
|
fmt.Printf("Default AI: %s\n", cfg.Profile.Preferences.DefaultAI)
|
|
fmt.Printf("Languages: %v\n", cfg.Profile.Languages)
|
|
|
|
for _, p := range cfg.AI.Providers {
|
|
active := ""
|
|
if p.Active {
|
|
active = " (active)"
|
|
}
|
|
keyStatus := "no key"
|
|
if p.APIKey != "" {
|
|
keyStatus = "configured"
|
|
}
|
|
fmt.Printf(" %s: model=%s, key=%s%s\n", p.Name, p.Model, keyStatus, active)
|
|
}
|
|
|
|
fmt.Printf("BMAD: installed=%v, global=%v\n", cfg.BMAD.Installed, cfg.BMAD.Global)
|
|
fmt.Printf("Custom Prompt: %v\n", cfg.Terminal.CustomPrompt)
|
|
}
|
|
|
|
func runLSP(args []string) {
|
|
if len(args) == 0 {
|
|
args = []string{"scan"}
|
|
}
|
|
|
|
switch args[0] {
|
|
case "scan":
|
|
fmt.Println("Scanning LSP servers...")
|
|
servers := lsp.ScanServers()
|
|
installed := 0
|
|
for _, s := range servers {
|
|
if s.Installed {
|
|
installed++
|
|
fmt.Printf(" [v] %-35s (%s)\n", s.Name, s.Language)
|
|
} else {
|
|
fmt.Printf(" [ ] %-35s (%s)\n", s.Name, s.Language)
|
|
}
|
|
}
|
|
fmt.Printf("\nInstalled: %d/%d\n", installed, len(servers))
|
|
case "install":
|
|
if len(args) < 2 {
|
|
cfg := loadOrSetupConfig()
|
|
fmt.Printf("Installing LSP servers for: %v\n", cfg.Profile.Languages)
|
|
results := lsp.InstallForLanguages(cfg.Profile.Languages)
|
|
for _, r := range results {
|
|
if r.Installed {
|
|
fmt.Printf(" [OK] %s (%s)\n", r.Name, r.Language)
|
|
} else {
|
|
fmt.Printf(" [FAIL] %s (%s)\n", r.Name, r.Language)
|
|
}
|
|
}
|
|
} else {
|
|
for _, name := range args[1:] {
|
|
fmt.Printf("Installing %s...\n", name)
|
|
if err := lsp.InstallServer(name); err != nil {
|
|
fmt.Printf(" [FAIL] %s: %s\n", name, err)
|
|
} else {
|
|
fmt.Printf(" [OK] %s\n", name)
|
|
}
|
|
}
|
|
}
|
|
default:
|
|
fmt.Printf("Unknown lsp subcommand: %s (scan, install)\n", args[0])
|
|
}
|
|
}
|
|
|
|
func runMCP(args []string) {
|
|
if len(args) == 0 {
|
|
args = []string{"config"}
|
|
}
|
|
|
|
switch args[0] {
|
|
case "config":
|
|
cfg := loadOrSetupConfig()
|
|
fmt.Println("Configuring MCP servers for Crush and Claude Code...")
|
|
if err := mcp.ConfigureAll(cfg); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
fmt.Println("Done! MCP servers configured.")
|
|
case "scan":
|
|
fmt.Println("Scanning MCP servers...")
|
|
servers := mcp.ScanServers()
|
|
available := 0
|
|
for _, s := range servers {
|
|
if s.Installed {
|
|
available++
|
|
fmt.Printf(" [v] %-30s (%s)\n", s.Name, s.Category)
|
|
} else {
|
|
fmt.Printf(" [ ] %-30s (%s)\n", s.Name, s.Category)
|
|
}
|
|
}
|
|
fmt.Printf("\nAvailable: %d/%d\n", available, len(servers))
|
|
default:
|
|
fmt.Printf("Unknown mcp subcommand: %s (config, scan)\n", args[0])
|
|
}
|
|
}
|
|
|
|
func runSkills(args []string) {
|
|
if len(args) == 0 {
|
|
args = []string{"list"}
|
|
}
|
|
|
|
switch args[0] {
|
|
case "list", "ls":
|
|
skillsList, err := skills.List()
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
if len(skillsList) == 0 {
|
|
fmt.Println("No skills found. Run `muyue skills init` to install built-in skills.")
|
|
return
|
|
}
|
|
fmt.Printf("Skills (%d):\n", len(skillsList))
|
|
for _, s := range skillsList {
|
|
target := s.Target
|
|
if target == "" {
|
|
target = "both"
|
|
}
|
|
fmt.Printf(" %-20s %-8s %s\n", s.Name, target, s.Description)
|
|
}
|
|
|
|
case "init":
|
|
fmt.Println("Installing built-in skills...")
|
|
if err := skills.InstallBuiltinSkills(); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
fmt.Println("Deploying to Crush and Claude Code...")
|
|
if err := skills.DeployAll(); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Deploy error: %v\n", err)
|
|
}
|
|
fmt.Println("Done! Built-in skills installed and deployed.")
|
|
|
|
case "show":
|
|
if len(args) < 2 {
|
|
fmt.Println("Usage: muyue skills show <name>")
|
|
return
|
|
}
|
|
skill, err := skills.Get(args[1])
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
fmt.Printf("Name: %s\n", skill.Name)
|
|
fmt.Printf("Description: %s\n", skill.Description)
|
|
fmt.Printf("Author: %s\n", skill.Author)
|
|
fmt.Printf("Version: %s\n", skill.Version)
|
|
fmt.Printf("Target: %s\n", skill.Target)
|
|
fmt.Printf("Tags: %v\n", skill.Tags)
|
|
fmt.Printf("Path: %s\n", skill.FilePath)
|
|
fmt.Printf("\n--- Content ---\n%s\n", skill.Content)
|
|
|
|
case "generate":
|
|
if len(args) < 3 {
|
|
fmt.Println("Usage: muyue skills generate <name> <description> [crush|claude|both]")
|
|
fmt.Println("Example: muyue skills generate docker-setup \"Set up Docker for a project\" both")
|
|
return
|
|
}
|
|
name := args[1]
|
|
description := args[2]
|
|
target := "both"
|
|
if len(args) > 3 {
|
|
target = args[3]
|
|
}
|
|
|
|
cfg := loadOrSetupConfig()
|
|
orch, err := orchestrator.New(cfg)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "AI not configured: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
fmt.Printf("Generating skill '%s'...\n", name)
|
|
prompt := skills.BuildAIGeneratePrompt(name, description, target)
|
|
resp, err := orch.Send(prompt)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Generation error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
skill := &skills.Skill{
|
|
Name: name,
|
|
Description: description,
|
|
Content: resp,
|
|
Author: "muyue-generated",
|
|
Version: "0.1.0",
|
|
Target: target,
|
|
Tags: []string{"generated"},
|
|
}
|
|
|
|
if err := skills.Create(skill); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Save error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
fmt.Printf("Skill '%s' created and deployed!\n", name)
|
|
|
|
case "deploy":
|
|
fmt.Println("Deploying all skills to Crush and Claude Code...")
|
|
if err := skills.DeployAll(); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
fmt.Println("Done!")
|
|
|
|
case "delete":
|
|
if len(args) < 2 {
|
|
fmt.Println("Usage: muyue skills delete <name>")
|
|
return
|
|
}
|
|
if err := skills.Delete(args[1]); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
fmt.Printf("Skill '%s' deleted.\n", args[1])
|
|
|
|
default:
|
|
fmt.Printf("Unknown skills subcommand: %s\n", args[0])
|
|
fmt.Println("Available: list, show, generate, deploy, init, delete")
|
|
}
|
|
}
|