Break down the 627-line handlers.go into specialized modules: - handlers_chat.go: chat and streaming endpoints - handlers_config.go: configuration endpoints - handlers_common.go: shared utilities - handlers_info.go: info and status endpoints - handlers_terminal.go: terminal/shell endpoints - handlers_tools.go: tool-related endpoints Also includes config improvements, orchestrator enhancements, and web component updates. 💘 Generated with Crush Assisted-by: MiniMax-M2.7 via Crush <crush@charm.land>
293 lines
6.5 KiB
Go
293 lines
6.5 KiB
Go
package profiler
|
|
|
|
import (
|
|
"fmt"
|
|
"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.")
|
|
|
|
var name, pseudo, email string
|
|
var languages []string
|
|
var editor string
|
|
var aiProvider string
|
|
|
|
nameField := huh.NewInput().
|
|
Title("What's your name?").
|
|
Placeholder("Augustin").
|
|
Value(&name)
|
|
|
|
pseudoField := huh.NewInput().
|
|
Title("Your pseudo?").
|
|
Placeholder("muyue").
|
|
Value(&pseudo)
|
|
|
|
emailField := huh.NewInput().
|
|
Title("Your email (for git config)?").
|
|
Placeholder("you@example.com").
|
|
Value(&email)
|
|
|
|
langOptions := buildLanguageOptions(result)
|
|
langSelect := huh.NewMultiSelect[string]().
|
|
Title("Which languages do you work with?").
|
|
Options(langOptions...).
|
|
Value(&languages)
|
|
|
|
editorOptions := buildEditorOptions(info)
|
|
editorSelect := huh.NewSelect[string]().
|
|
Title("Preferred editor?").
|
|
Options(editorOptions...).
|
|
Value(&editor)
|
|
|
|
aiOptions := buildAIOptions(info)
|
|
aiSelect := huh.NewSelect[string]().
|
|
Title("Which AI provider for orchestration?").
|
|
Options(aiOptions...).
|
|
Value(&aiProvider)
|
|
|
|
form := huh.NewForm(
|
|
huh.NewGroup(nameField, pseudoField, emailField),
|
|
huh.NewGroup(langSelect),
|
|
huh.NewGroup(editorSelect),
|
|
huh.NewGroup(aiSelect),
|
|
)
|
|
|
|
if err := form.Run(); err != nil {
|
|
return nil, fmt.Errorf("profile form: %w", err)
|
|
}
|
|
|
|
cfg.Profile.Name = name
|
|
cfg.Profile.Pseudo = pseudo
|
|
cfg.Profile.Email = email
|
|
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
|
|
}
|
|
|
|
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
|
|
|
|
field := huh.NewInput().
|
|
Title(fmt.Sprintf("Enter your %s API key:", providerName)).
|
|
Description("The key will be stored locally in ~/.config/muyue/config.yaml").
|
|
EchoMode(huh.EchoModePassword).
|
|
Value(&apiKey)
|
|
|
|
form := huh.NewForm(huh.NewGroup(field))
|
|
if err := form.Run(); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return apiKey, nil
|
|
}
|