Files
MuyueWorkspace/cmd/muyue/commands/root.go
Muyue 79e467c32a
All checks were successful
PR Check / check (pull_request) Successful in 58s
fix(windows): GUI subsystem + parent-console attach + canonical muyue.exe (v0.7.5)
Three Windows install/launch issues reported by the user:

1. Double-click on Desktop shortcut → dialog "This is a command line
   tool. You need to open cmd.exe and run it from there."
   Cause: charmbracelet/huh detects no TTY when launched via Explorer
   and aborts. Fix:
   - cmd/muyue/commands/root.go: skip RunFirstTimeSetup when
     os.Stdin is not a character device; persist config.Default()
     and let the React onboarding wizard handle first-run UX.
   - ci-{main,develop}.yml: build Windows binaries with
     -ldflags="-H=windowsgui" so the .exe is a GUI subsystem app —
     no console window flashes on double-click.

2. CLI sub-commands (`muyue scan`, `muyue install-shortcuts`, etc.)
   would lose all output under -H=windowsgui when launched from
   cmd.exe / PowerShell. Mitigation:
   - cmd/muyue/console_windows.go (new, build-tagged): on init(),
     call kernel32!AttachConsole(ATTACH_PARENT_PROCESS). If the
     parent has a console, rebind os.Stdout/os.Stderr/os.Stdin to
     it and call log.SetOutput(os.Stderr) so existing log.Printf
     calls surface. If no parent console (Explorer), exit silently.

3. After install, `muyue` not recognized in PowerShell.
   Causes: (a) the extracted binary is muyue-windows-amd64.exe, not
   muyue.exe; (b) the user PATH update by install-shortcuts doesn't
   propagate to the existing PowerShell session.
   Fix in install-shortcuts:
   - Copy self to <installDir>/muyue.exe (rename impossible — the
     running .exe is locked on Windows) so `muyue` resolves once
     PATH is set.
   - Update Desktop + Start Menu .lnk to target the canonical
     muyue.exe rather than the platform-suffixed binary.
   - Print the line `$env:Path += ';<installDir>'` for the user to
     paste, refreshing the current session immediately.
   - ci-main.yml install snippet bumps to 5 lines, last being
     `$env:Path += ";$dest"`.

- internal/version/version.go: 0.7.4 → 0.7.5
- CHANGELOG.md: v0.7.5 entry covers all three fixes
2026-04-27 13:39:22 +02:00

97 lines
2.8 KiB
Go

package commands
import (
"fmt"
"os"
"github.com/muyue/muyue/internal/config"
"github.com/muyue/muyue/internal/desktop"
"github.com/muyue/muyue/internal/profiler"
"github.com/spf13/cobra"
)
var rootCmd = &cobra.Command{
Use: "muyue",
Short: "Muyue is your AI-powered development companion",
Long: `Muyue - A modern development environment with AI assistance, tool management, and seamless desktop integration.`,
RunE: func(cmd *cobra.Command, args []string) error {
cfg := loadOrSetupConfig()
return desktop.Run(cfg, os.Args[1:])
},
}
func Execute() error {
return rootCmd.Execute()
}
// isInteractiveStdin reports whether os.Stdin is connected to a real terminal.
// Used to decide between the TUI first-time setup (huh forms) and a no-op
// fallback that defers onboarding to the web wizard. Returns false when the
// binary is launched by a double-click on Windows (Explorer attaches a pseudo
// console without a usable TTY) — which is the exact case where huh prints
// "This is a command line tool. You need to open cmd.exe and run it from there."
// and exits.
func isInteractiveStdin() bool {
stat, err := os.Stdin.Stat()
if err != nil {
return false
}
return (stat.Mode() & os.ModeCharDevice) != 0
}
func loadOrSetupConfig() *config.MuyueConfig {
if !config.Exists() {
// No config yet. If we have a real terminal, run the rich TUI setup
// (huh forms). Otherwise — typically when the user double-clicked the
// shortcut on Windows — write defaults silently and let the React
// onboarding wizard handle the real first-run flow once the browser
// opens. This avoids huh aborting with "This is a command line tool".
if isInteractiveStdin() {
fmt.Println("First time setup detected!")
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("\nSetup complete! Starting muyue...")
return cfg
}
// Non-interactive — skip the TUI, persist defaults, web onboarding
// will fill in the profile / API keys.
cfg := config.Default()
if err := config.Save(cfg); err != nil {
fmt.Fprintf(os.Stderr, "Save error: %v\n", err)
os.Exit(1)
}
return cfg
}
cfg, err := config.Load()
if err != nil {
fmt.Fprintf(os.Stderr, "Config load error: %v\n", err)
os.Exit(1)
}
return cfg
}
func init() {
rootCmd.PersistentFlags().Int("port", 8080, "HTTP port for the desktop server")
rootCmd.PersistentFlags().Bool("no-open", false, "Don't open browser on startup")
}