package main import ( "fmt" "os" "os/exec" "strings" 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 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: 1-5 Switch tabs (Dashboard/Chat/Workflow/Agents/Config) Tab / Shift+Tab Cycle tabs Ctrl+C Show quit confirmation (press twice quickly to force quit) Chat Commands: /plan 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(), tea.WithMouseCellMotion()) 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 ") 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 [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 ") 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") } } func checkConfigProviders(cfg *config.MuyueConfig) { for i := range cfg.AI.Providers { if cfg.AI.Providers[i].Active { return } } if len(cfg.AI.Providers) > 0 { cfg.AI.Providers[0].Active = true } } func joinWithQuotes(items []string) string { quoted := make([]string, len(items)) for i, item := range items { quoted[i] = fmt.Sprintf("%q", item) } return strings.Join(quoted, ", ") }