All checks were successful
CI / build (push) Successful in 1m46s
💘 Generated with Crush Assisted-by: GLM-5.1 via Crush <crush@charm.land>
540 lines
13 KiB
Go
540 lines
13 KiB
Go
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 <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:
|
|
Alt+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 <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(), 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 <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")
|
|
}
|
|
}
|
|
|
|
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, ", ")
|
|
}
|