diff --git a/internal/config/config.go b/internal/config/config.go index b59d09a..8cdb941 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -156,6 +156,18 @@ func Default() *MuyueConfig { Model: "claude-sonnet-4-20250514", Active: false, }, + { + Name: "openai", + Model: "gpt-4o", + BaseURL: "https://api.openai.com/v1", + Active: false, + }, + { + Name: "ollama", + Model: "llama3", + BaseURL: "http://localhost:11434/api", + Active: false, + }, } cfg.BMAD.Global = true diff --git a/internal/profiler/profiler.go b/internal/profiler/profiler.go index 32c64cd..b95ca2f 100644 --- a/internal/profiler/profiler.go +++ b/internal/profiler/profiler.go @@ -2,15 +2,49 @@ package profiler import ( "fmt" - "strings" + "os/exec" + "sort" "github.com/charmbracelet/huh" "github.com/muyue/muyue/internal/config" + "github.com/muyue/muyue/internal/platform" "github.com/muyue/muyue/internal/scanner" ) +type editorCandidate struct { + key string + binary string + label string +} + +var allEditors = []editorCandidate{ + {"VS Code", "code", "VS Code"}, + {"Cursor", "cursor", "Cursor"}, + {"Zed", "zed", "Zed"}, + {"Neovim", "nvim", "Neovim"}, + {"Vim", "vim", "Vim"}, + {"Helix", "hx", "Helix"}, + {"Emacs", "emacs", "Emacs"}, + {"JetBrains", "jetbrains", "JetBrains (IntelliJ/GoLand/etc.)"}, + {"Sublime Text", "subl", "Sublime Text"}, +} + +type aiCandidate struct { + key string + label string +} + +var allAIProviders = []aiCandidate{ + {"minimax", "MiniMax M2.7"}, + {"zai", "Z.AI GLM"}, + {"anthropic", "Anthropic Claude"}, + {"openai", "OpenAI GPT"}, + {"ollama", "Ollama (local)"}, +} + func RunFirstTimeSetup() (*config.MuyueConfig, error) { cfg := config.Default() + info := platform.Detect() result := scanner.ScanSystem() fmt.Println("Welcome to muyue! Let's set up your environment.") @@ -35,46 +69,19 @@ func RunFirstTimeSetup() (*config.MuyueConfig, error) { Placeholder("you@example.com"). Value(&email) - langOptions := []huh.Option[string]{ - {Key: "Go", Value: "go"}, - {Key: "TypeScript/JavaScript", Value: "typescript"}, - {Key: "Python", Value: "python"}, - {Key: "Rust", Value: "rust"}, - {Key: "Java", Value: "java"}, - {Key: "C/C++", Value: "c"}, - {Key: "Ruby", Value: "ruby"}, - {Key: "PHP", Value: "php"}, - {Key: "Swift", Value: "swift"}, - {Key: "Kotlin", Value: "kotlin"}, - } - + langOptions := buildLanguageOptions(result) langSelect := huh.NewMultiSelect[string](). Title("Which languages do you work with?"). Options(langOptions...). Value(&languages) - editorOptions := []huh.Option[string]{ - {Key: "VS Code", Value: "code"}, - {Key: "Neovim", Value: "nvim"}, - {Key: "Vim", Value: "vim"}, - {Key: "Emacs", Value: "emacs"}, - {Key: "JetBrains", Value: "jetbrains"}, - {Key: "Helix", Value: "hx"}, - {Key: "Sublime Text", Value: "subl"}, - } - + editorOptions := buildEditorOptions(info) editorSelect := huh.NewSelect[string](). Title("Preferred editor?"). Options(editorOptions...). Value(&editor) - aiOptions := []huh.Option[string]{ - {Key: "MiniMax M2.7", Value: "minimax"}, - {Key: "Z.AI GLM", Value: "zai"}, - {Key: "Anthropic Claude", Value: "anthropic"}, - {Key: "OpenAI", Value: "openai"}, - } - + aiOptions := buildAIOptions(info) aiSelect := huh.NewSelect[string](). Title("Which AI provider for orchestration?"). Options(aiOptions...). @@ -97,16 +104,176 @@ func RunFirstTimeSetup() (*config.MuyueConfig, error) { cfg.Profile.Languages = languages cfg.Profile.Preferences.Editor = editor cfg.Profile.Preferences.DefaultAI = aiProvider + cfg.Profile.Preferences.Shell = info.Shell for i := range cfg.AI.Providers { cfg.AI.Providers[i].Active = cfg.AI.Providers[i].Name == aiProvider } - _ = result - return cfg, nil } +func buildEditorOptions(info platform.SystemInfo) []huh.Option[string] { + type scored struct { + option huh.Option[string] + score int + } + + var candidates []scored + for _, e := range allEditors { + installed := isInstalled(e.binary) + score := 0 + if installed { + score = 10 + } + + switch info.OS { + case platform.Linux: + if e.binary == "code" || e.binary == "nvim" || e.binary == "hx" || e.binary == "vim" { + score += 2 + } + case platform.MacOS: + if e.binary == "code" || e.binary == "cursor" || e.binary == "zed" { + score += 2 + } + case platform.Windows: + if e.binary == "code" || e.binary == "cursor" { + score += 2 + } + } + + label := e.label + if installed { + label = e.label + " (installed)" + } + + candidates = append(candidates, scored{ + option: huh.NewOption[string](label, e.key), + score: score, + }) + } + + sort.SliceStable(candidates, func(i, j int) bool { + return candidates[i].score > candidates[j].score + }) + + options := make([]huh.Option[string], len(candidates)) + for i, c := range candidates { + options[i] = c.option + } + return options +} + +func buildLanguageOptions(result *scanner.ScanResult) []huh.Option[string] { + type scored struct { + option huh.Option[string] + score int + } + + langs := []struct { + key string + label string + binaries []string + }{ + {"go", "Go", []string{"go"}}, + {"typescript", "TypeScript/JavaScript", []string{"node", "npm", "pnpm"}}, + {"python", "Python", []string{"python3", "pip3", "uv"}}, + {"rust", "Rust", []string{"rustc", "cargo"}}, + {"java", "Java", []string{"java", "javac"}}, + {"c", "C/C++", []string{"gcc", "g++", "clang"}}, + {"ruby", "Ruby", []string{"ruby"}}, + {"php", "PHP", []string{"php"}}, + {"swift", "Swift", []string{"swift"}}, + {"kotlin", "Kotlin", []string{"kotlin"}}, + {"zig", "Zig", []string{"zig"}}, + {"dart", "Dart/Flutter", []string{"dart", "flutter"}}, + } + + var candidates []scored + for _, l := range langs { + score := 0 + for _, bin := range l.binaries { + if isInstalled(bin) { + score = 5 + break + } + } + if result != nil { + for _, r := range result.Runtimes { + if r.Installed && r.Name == l.key { + score += 5 + break + } + } + } + candidates = append(candidates, scored{ + option: huh.NewOption[string](l.label, l.key), + score: score, + }) + } + + sort.SliceStable(candidates, func(i, j int) bool { + return candidates[i].score > candidates[j].score + }) + + options := make([]huh.Option[string], len(candidates)) + for i, c := range candidates { + options[i] = c.option + } + return options +} + +func buildAIOptions(info platform.SystemInfo) []huh.Option[string] { + type scored struct { + option huh.Option[string] + score int + } + + var candidates []scored + for _, ai := range allAIProviders { + score := 0 + + if ai.key == "ollama" { + if isInstalled("ollama") { + score = 15 + } else { + score = -5 + } + } + + switch info.OS { + case platform.Linux: + if ai.key == "ollama" || ai.key == "anthropic" { + score += 1 + } + case platform.MacOS: + if ai.key == "anthropic" || ai.key == "openai" { + score += 1 + } + } + + candidates = append(candidates, scored{ + option: huh.NewOption[string](ai.label, ai.key), + score: score, + }) + } + + sort.SliceStable(candidates, func(i, j int) bool { + return candidates[i].score > candidates[j].score + }) + + options := make([]huh.Option[string], len(candidates)) + for i, c := range candidates { + options[i] = c.option + } + return options +} + +func isInstalled(binary string) bool { + _, err := exec.LookPath(binary) + return err == nil +} + func AskAPIKey(providerName string) (string, error) { var apiKey string @@ -121,5 +288,5 @@ func AskAPIKey(providerName string) (string, error) { return "", err } - return strings.TrimSpace(apiKey), nil + return apiKey, nil }