diff --git a/.gitignore b/.gitignore index 36e0ef5..3426056 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ # Binaries -muyue +/muyue dist/ # IDE diff --git a/cmd/muyue/main.go b/cmd/muyue/main.go new file mode 100644 index 0000000..30ab8a9 --- /dev/null +++ b/cmd/muyue/main.go @@ -0,0 +1,470 @@ +package main + +import ( + "fmt" + "os" + + 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 (crush, claude, bmad, starship, go, node, python, git) + 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 Cycle to next tab + q / Ctrl+C 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 +`, 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 + } + + 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 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: "1.0.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") + } +}