fix(terminal): resolve PTY shell exec error, simplify CLI, unify Config tabs, restore Studio CSS
- Fix detectShell() to return full paths via LookPath (was returning bare names causing exec error on some systems) - Add shell path validation before pty.Start to prevent crashes - Simplify CLI: remove all subcommands, keep only desktop launch with --port - Restore missing Studio shared CSS (code blocks, input area, animations) - Replace Config vertical sidebar with horizontal nav-tabs matching main layout 💘 Generated with Crush Assisted-by: GLM-5.1 via Crush <crush@charm.land>
This commit is contained in:
@@ -3,103 +3,15 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"github.com/muyue/muyue/internal/config"
|
||||
"github.com/muyue/muyue/internal/desktop"
|
||||
"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/updater"
|
||||
"github.com/muyue/muyue/internal/version"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) > 1 {
|
||||
if isCommand(os.Args[1]) {
|
||||
handleCommand(os.Args[1:])
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
runDesktop(os.Args[1:])
|
||||
}
|
||||
|
||||
func isCommand(arg string) bool {
|
||||
switch arg {
|
||||
case "version", "-v", "--version",
|
||||
"scan", "install", "update", "setup",
|
||||
"config", "doctor", "lsp", "mcp", "skills",
|
||||
"help", "-h", "--help":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func handleCommand(args []string) {
|
||||
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 "doctor":
|
||||
runDoctor()
|
||||
case "lsp":
|
||||
runLSP(args[1:])
|
||||
case "mcp":
|
||||
runMCP(args[1:])
|
||||
case "skills":
|
||||
runSkills(args[1:])
|
||||
case "help", "-h", "--help":
|
||||
printHelp()
|
||||
}
|
||||
}
|
||||
|
||||
func printHelp() {
|
||||
fmt.Printf(`%s - AI-powered development environment assistant
|
||||
|
||||
Usage:
|
||||
muyue Launch desktop app (opens browser)
|
||||
muyue <command> Run a specific command
|
||||
|
||||
Options:
|
||||
--port=PORT Specify port (default: auto)
|
||||
--no-open Don't open browser automatically
|
||||
|
||||
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
|
||||
doctor Check that everything is properly configured
|
||||
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
|
||||
|
||||
Note:
|
||||
Some tools (docker, gh, etc.) require elevated privileges.
|
||||
Run 'sudo muyue install' or use 'pkexec muyue install' if needed.
|
||||
`, version.FullVersion())
|
||||
}
|
||||
|
||||
func runDesktop(args []string) {
|
||||
cfg := loadOrSetupConfig()
|
||||
if err := desktop.Run(cfg, args); err != nil {
|
||||
if err := desktop.Run(cfg, os.Args[1:]); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -140,453 +52,3 @@ func loadOrSetupConfig() *config.MuyueConfig {
|
||||
|
||||
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 runDoctor() {
|
||||
ok := true
|
||||
fmt.Println("Running diagnostics...")
|
||||
fmt.Println()
|
||||
|
||||
fmt.Println("Configuration:")
|
||||
if !config.Exists() {
|
||||
fmt.Println(" [FAIL] Config file not found. Run 'muyue setup' first.")
|
||||
ok = false
|
||||
} else {
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
fmt.Printf(" [FAIL] Config load error: %v\n", err)
|
||||
ok = false
|
||||
} else {
|
||||
fmt.Println(" [OK] Config file present")
|
||||
hasKey := false
|
||||
for _, p := range cfg.AI.Providers {
|
||||
if p.Active && p.APIKey != "" {
|
||||
hasKey = true
|
||||
}
|
||||
}
|
||||
if hasKey {
|
||||
fmt.Println(" [OK] API key configured")
|
||||
} else {
|
||||
fmt.Println(" [FAIL] No API key set for active provider")
|
||||
ok = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("\nTools:")
|
||||
result := scanner.ScanSystem()
|
||||
installed := 0
|
||||
for _, t := range result.Tools {
|
||||
if t.Installed {
|
||||
installed++
|
||||
fmt.Printf(" [OK] %s\n", t.Name)
|
||||
} else {
|
||||
fmt.Printf(" [FAIL] %s (not installed)\n", t.Name)
|
||||
}
|
||||
}
|
||||
fmt.Printf(" Installed: %d/%d\n", installed, len(result.Tools))
|
||||
|
||||
fmt.Println("\nLSP Servers:")
|
||||
servers := lsp.ScanServers()
|
||||
lspOK := 0
|
||||
for _, s := range servers {
|
||||
if s.Installed {
|
||||
lspOK++
|
||||
fmt.Printf(" [OK] %s (%s)\n", s.Name, s.Language)
|
||||
}
|
||||
}
|
||||
fmt.Printf(" Available: %d/%d\n", lspOK, len(servers))
|
||||
|
||||
fmt.Println("\nMCP Servers:")
|
||||
mcpServers := mcp.ScanServers()
|
||||
mcpOK := 0
|
||||
for _, s := range mcpServers {
|
||||
if s.Installed {
|
||||
mcpOK++
|
||||
}
|
||||
}
|
||||
fmt.Printf(" Available: %d/%d\n", mcpOK, len(mcpServers))
|
||||
|
||||
fmt.Println("\nSkills:")
|
||||
skillList, err := skills.List()
|
||||
if err != nil || len(skillList) == 0 {
|
||||
fmt.Println(" [FAIL] No skills. Run 'muyue skills init'.")
|
||||
ok = false
|
||||
} else {
|
||||
fmt.Printf(" [OK] %d skills installed\n", len(skillList))
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
if ok {
|
||||
fmt.Println("All checks passed!")
|
||||
} else {
|
||||
fmt.Println("Some checks failed. Review the output above.")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user