feat: smart setup wizard - sort choices by system detection
All checks were successful
CI / build (push) Successful in 1m45s

- Editors sorted by install status + platform relevance
- Languages sorted by detected runtimes/tools on system
- AI providers sorted by platform + local ollama detection
- Added Cursor, Zed editors; Dart/Flutter, Zig languages
- Added Ollama and OpenAI as provider options
- Installed tools show "(installed)" label in editor list

💘 Generated with Crush

Assisted-by: GLM-5.1 via Crush <crush@charm.land>
This commit is contained in:
Augustin
2026-04-20 00:02:20 +02:00
parent 825b429042
commit 1be4fc061f
2 changed files with 213 additions and 34 deletions

View File

@@ -156,6 +156,18 @@ func Default() *MuyueConfig {
Model: "claude-sonnet-4-20250514", Model: "claude-sonnet-4-20250514",
Active: false, 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 cfg.BMAD.Global = true

View File

@@ -2,15 +2,49 @@ package profiler
import ( import (
"fmt" "fmt"
"strings" "os/exec"
"sort"
"github.com/charmbracelet/huh" "github.com/charmbracelet/huh"
"github.com/muyue/muyue/internal/config" "github.com/muyue/muyue/internal/config"
"github.com/muyue/muyue/internal/platform"
"github.com/muyue/muyue/internal/scanner" "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) { func RunFirstTimeSetup() (*config.MuyueConfig, error) {
cfg := config.Default() cfg := config.Default()
info := platform.Detect()
result := scanner.ScanSystem() result := scanner.ScanSystem()
fmt.Println("Welcome to muyue! Let's set up your environment.") fmt.Println("Welcome to muyue! Let's set up your environment.")
@@ -35,46 +69,19 @@ func RunFirstTimeSetup() (*config.MuyueConfig, error) {
Placeholder("you@example.com"). Placeholder("you@example.com").
Value(&email) Value(&email)
langOptions := []huh.Option[string]{ langOptions := buildLanguageOptions(result)
{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"},
}
langSelect := huh.NewMultiSelect[string](). langSelect := huh.NewMultiSelect[string]().
Title("Which languages do you work with?"). Title("Which languages do you work with?").
Options(langOptions...). Options(langOptions...).
Value(&languages) Value(&languages)
editorOptions := []huh.Option[string]{ editorOptions := buildEditorOptions(info)
{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"},
}
editorSelect := huh.NewSelect[string](). editorSelect := huh.NewSelect[string]().
Title("Preferred editor?"). Title("Preferred editor?").
Options(editorOptions...). Options(editorOptions...).
Value(&editor) Value(&editor)
aiOptions := []huh.Option[string]{ aiOptions := buildAIOptions(info)
{Key: "MiniMax M2.7", Value: "minimax"},
{Key: "Z.AI GLM", Value: "zai"},
{Key: "Anthropic Claude", Value: "anthropic"},
{Key: "OpenAI", Value: "openai"},
}
aiSelect := huh.NewSelect[string](). aiSelect := huh.NewSelect[string]().
Title("Which AI provider for orchestration?"). Title("Which AI provider for orchestration?").
Options(aiOptions...). Options(aiOptions...).
@@ -97,16 +104,176 @@ func RunFirstTimeSetup() (*config.MuyueConfig, error) {
cfg.Profile.Languages = languages cfg.Profile.Languages = languages
cfg.Profile.Preferences.Editor = editor cfg.Profile.Preferences.Editor = editor
cfg.Profile.Preferences.DefaultAI = aiProvider cfg.Profile.Preferences.DefaultAI = aiProvider
cfg.Profile.Preferences.Shell = info.Shell
for i := range cfg.AI.Providers { for i := range cfg.AI.Providers {
cfg.AI.Providers[i].Active = cfg.AI.Providers[i].Name == aiProvider cfg.AI.Providers[i].Active = cfg.AI.Providers[i].Name == aiProvider
} }
_ = result
return cfg, nil 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) { func AskAPIKey(providerName string) (string, error) {
var apiKey string var apiKey string
@@ -121,5 +288,5 @@ func AskAPIKey(providerName string) (string, error) {
return "", err return "", err
} }
return strings.TrimSpace(apiKey), nil return apiKey, nil
} }