feat: smart setup wizard - sort choices by system detection
All checks were successful
CI / build (push) Successful in 1m45s
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:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user