diff --git a/.gitea/workflows/ci-develop.yml b/.gitea/workflows/ci-develop.yml index 9b86164..91ce858 100644 --- a/.gitea/workflows/ci-develop.yml +++ b/.gitea/workflows/ci-develop.yml @@ -80,12 +80,13 @@ jobs: mkdir -p dist VERSION=${{ steps.version.outputs.version }} LDFLAGS="-s -w -X github.com/muyue/muyue/internal/version.Prerelease=${VERSION#v}" + WIN_LDFLAGS="$LDFLAGS -H=windowsgui" CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="$LDFLAGS" -o dist/muyue-linux-amd64 ./cmd/muyue/ CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="$LDFLAGS" -o dist/muyue-linux-arm64 ./cmd/muyue/ CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags="$LDFLAGS" -o dist/muyue-darwin-amd64 ./cmd/muyue/ CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags="$LDFLAGS" -o dist/muyue-darwin-arm64 ./cmd/muyue/ - CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags="$LDFLAGS" -o dist/muyue-windows-amd64.exe ./cmd/muyue/ - CGO_ENABLED=0 GOOS=windows GOARCH=arm64 go build -ldflags="$LDFLAGS" -o dist/muyue-windows-arm64.exe ./cmd/muyue/ + CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags="$WIN_LDFLAGS" -o dist/muyue-windows-amd64.exe ./cmd/muyue/ + CGO_ENABLED=0 GOOS=windows GOARCH=arm64 go build -ldflags="$WIN_LDFLAGS" -o dist/muyue-windows-arm64.exe ./cmd/muyue/ - name: Package archives run: | diff --git a/.gitea/workflows/ci-main.yml b/.gitea/workflows/ci-main.yml index 5fd9821..0f1fae8 100644 --- a/.gitea/workflows/ci-main.yml +++ b/.gitea/workflows/ci-main.yml @@ -75,12 +75,17 @@ jobs: run: | mkdir -p dist LDFLAGS="-s -w" + # Windows builds use -H=windowsgui so the binary registers as a GUI + # subsystem app: double-clicking from the Desktop shortcut does not + # spawn a console window (and huh's "This is a command line tool" + # banner can never appear). + WIN_LDFLAGS="$LDFLAGS -H=windowsgui" CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="$LDFLAGS" -o dist/muyue-linux-amd64 ./cmd/muyue/ CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="$LDFLAGS" -o dist/muyue-linux-arm64 ./cmd/muyue/ CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags="$LDFLAGS" -o dist/muyue-darwin-amd64 ./cmd/muyue/ CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags="$LDFLAGS" -o dist/muyue-darwin-arm64 ./cmd/muyue/ - CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags="$LDFLAGS" -o dist/muyue-windows-amd64.exe ./cmd/muyue/ - CGO_ENABLED=0 GOOS=windows GOARCH=arm64 go build -ldflags="$LDFLAGS" -o dist/muyue-windows-arm64.exe ./cmd/muyue/ + CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags="$WIN_LDFLAGS" -o dist/muyue-windows-amd64.exe ./cmd/muyue/ + CGO_ENABLED=0 GOOS=windows GOARCH=arm64 go build -ldflags="$WIN_LDFLAGS" -o dist/muyue-windows-arm64.exe ./cmd/muyue/ - name: Package archives run: | @@ -145,12 +150,13 @@ jobs: echo "sudo mv muyue-darwin-arm64 /usr/local/bin/muyue" echo "\`\`\`" echo "" - echo "**Windows (x86_64)** — sans privilèges admin, crée les raccourcis Bureau + Menu Démarrer :" + echo "**Windows (x86_64)** — sans privilèges admin, crée les raccourcis Bureau + Menu Démarrer + commande \`muyue\` dans la session courante :" echo "\`\`\`powershell" echo "\$dest = \"\$env:LOCALAPPDATA\\Muyue\"; New-Item -ItemType Directory -Force -Path \$dest | Out-Null" echo "Invoke-WebRequest -Uri \"${DL_URL}/muyue-windows-amd64.zip\" -OutFile \"\$env:TEMP\\muyue.zip\"" echo "Expand-Archive -Path \"\$env:TEMP\\muyue.zip\" -DestinationPath \$dest -Force" echo "& \"\$dest\\muyue-windows-amd64.exe\" install-shortcuts" + echo "\$env:Path += \";\$dest\"" echo "\`\`\`" } > /tmp/stable_changelog.md echo "path=/tmp/stable_changelog.md" >> $GITHUB_OUTPUT diff --git a/CHANGELOG.md b/CHANGELOG.md index a7920aa..c0cc971 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,32 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). +## v0.7.5 + +### Fix Windows : commande `muyue` reconnue après install + +Symptôme rapporté : après les commandes d'install, `muyue` retourne `n'est pas reconnu comme nom d'applet de commande`. Causes : +- Le binaire extrait s'appelle `muyue-windows-amd64.exe` — taper `muyue` ne résoud pas +- La PATH utilisateur a été mise à jour mais la session PowerShell courante n'en hérite que pour les NOUVEAUX processus + +Corrections dans `install-shortcuts` : +- **Copie canonique** : `muyue.exe` est créé à côté de `muyue-windows-amd64.exe` (copy, pas rename — le binaire en cours d'exécution est verrouillé sur Windows). Les raccourcis Bureau / Menu Démarrer ciblent désormais cette copie. +- **Hint de session** : la commande imprime `$env:Path += ';...'` à coller pour activer `muyue` dans le shell courant sans rouvrir un terminal. + +Snippet d'install passe à 5 lignes : la dernière (`$env:Path += ";$dest"`) rend la commande dispo immédiatement dans la session. + +### Fix Windows : double-clic du raccourci fonctionne enfin + +Symptôme rapporté : après installation, double-clic sur le raccourci Bureau → boîte de dialogue *"This is a command line tool. You need to open cmd.exe and run it from there."*. Cause : `charmbracelet/huh` (utilisé pour la TUI de premier lancement) détecte l'absence de TTY interactif quand le binaire est lancé via Explorer Windows et avorte avec ce message. + +Double correctif : + +1. **Skip de la TUI sans terminal interactif** (`cmd/muyue/commands/root.go::isInteractiveStdin`) — si `os.Stdin.Stat()` indique pas de `os.ModeCharDevice`, on saute `profiler.RunFirstTimeSetup` et on persiste un `config.Default()`. L'onboarding web (déjà existant) prend ensuite le relais dès l'ouverture du navigateur — aucune régression : avec un vrai terminal, la TUI continue de tourner comme avant. + +2. **Build Windows en GUI subsystem** (`-H=windowsgui` ajouté aux Windows builds dans `ci-main.yml` et `ci-develop.yml`) — le binaire ne demande plus de console, donc plus aucun flash de fenêtre noire au double-clic. + +Conséquence : les sous-commandes CLI (`muyue scan`, `muyue version`, `muyue install-shortcuts`) ne produiraient plus d'output quand lancées depuis cmd.exe. Mitigation : nouveau fichier `cmd/muyue/console_windows.go` qui appelle `kernel32!AttachConsole(ATTACH_PARENT_PROCESS)` au démarrage. Si un terminal parent existe, on s'y rattache et `os.Stdout` / `os.Stderr` / `os.Stdin` y sont rebindés ; sinon, on tourne silencieusement (cas double-clic). Compatible des deux usages sans deux binaires séparés. + ## v0.7.4 ### Logo Muyue intégré diff --git a/cmd/muyue/commands/install_shortcuts.go b/cmd/muyue/commands/install_shortcuts.go index 0ba3aab..2ba22f0 100644 --- a/cmd/muyue/commands/install_shortcuts.go +++ b/cmd/muyue/commands/install_shortcuts.go @@ -2,10 +2,12 @@ package commands import ( "fmt" + "io" "os" "os/exec" "path/filepath" "runtime" + "strings" "github.com/spf13/cobra" ) @@ -34,7 +36,20 @@ var installShortcutsCmd = &cobra.Command{ installDir := filepath.Dir(exe) fmt.Println("Installing Muyue shortcuts...") - fmt.Printf(" Executable : %s\n", exe) + fmt.Printf(" Source : %s\n", exe) + + // Provide a clean `muyue.exe` next to the platform-suffixed binary so + // users can type `muyue` once the install dir is on PATH. Copy (not + // rename) because the running .exe is locked on Windows. + canonicalExe := filepath.Join(installDir, "muyue.exe") + if !strings.EqualFold(exe, canonicalExe) { + if err := copyFile(exe, canonicalExe); err != nil { + fmt.Fprintf(os.Stderr, " Copy : warning — could not create muyue.exe: %v\n", err) + canonicalExe = exe + } else { + fmt.Printf(" Canonical : %s\n", canonicalExe) + } + } desktop, err := userShellFolder("Desktop") if err != nil { @@ -48,12 +63,12 @@ var installShortcutsCmd = &cobra.Command{ desktopLnk := filepath.Join(desktop, "Muyue.lnk") startLnk := filepath.Join(startMenu, "Muyue.lnk") - if err := createWindowsShortcut(desktopLnk, exe, installDir, "Muyue — AI-powered dev environment"); err != nil { + if err := createWindowsShortcut(desktopLnk, canonicalExe, installDir, "Muyue — AI-powered dev environment"); err != nil { return fmt.Errorf("create desktop shortcut: %w", err) } fmt.Printf(" Desktop : %s\n", desktopLnk) - if err := createWindowsShortcut(startLnk, exe, installDir, "Muyue — AI-powered dev environment"); err != nil { + if err := createWindowsShortcut(startLnk, canonicalExe, installDir, "Muyue — AI-powered dev environment"); err != nil { return fmt.Errorf("create Start Menu shortcut: %w", err) } fmt.Printf(" Start Menu : %s\n", startLnk) @@ -61,14 +76,37 @@ var installShortcutsCmd = &cobra.Command{ if err := addUserPATH(installDir); err != nil { fmt.Fprintf(os.Stderr, " PATH : warning — could not add %s to user PATH: %v\n", installDir, err) } else { - fmt.Printf(" PATH : added %s (open a new terminal to pick it up)\n", installDir) + fmt.Printf(" PATH : added %s\n", installDir) } fmt.Println("\nDone — double-click the Muyue icon on your Desktop to launch.") + fmt.Println("\nTo use 'muyue' from this PowerShell session right now, run:") + fmt.Printf(" $env:Path += ';%s'\n", installDir) + fmt.Println("(New terminals will pick up the user PATH automatically.)") return nil }, } +// copyFile duplicates src to dst, overwriting an existing dst (used to drop a +// `muyue.exe` next to the platform-suffixed binary so the command is callable +// as `muyue` from PATH). +func copyFile(src, dst string) error { + in, err := os.Open(src) + if err != nil { + return err + } + defer in.Close() + out, err := os.Create(dst) + if err != nil { + return err + } + defer out.Close() + if _, err := io.Copy(out, in); err != nil { + return err + } + return out.Sync() +} + func init() { rootCmd.AddCommand(installShortcutsCmd) } diff --git a/cmd/muyue/commands/root.go b/cmd/muyue/commands/root.go index e17451d..c37ce1f 100644 --- a/cmd/muyue/commands/root.go +++ b/cmd/muyue/commands/root.go @@ -24,30 +24,61 @@ 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() { - 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) - } + // 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 + 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) } - - fmt.Println("\nSetup complete! Starting muyue...") return cfg } diff --git a/cmd/muyue/console_windows.go b/cmd/muyue/console_windows.go new file mode 100644 index 0000000..276e0b8 --- /dev/null +++ b/cmd/muyue/console_windows.go @@ -0,0 +1,54 @@ +//go:build windows + +package main + +// Windows-only: with -H=windowsgui the binary is registered as a GUI +// subsystem app, so double-clicking from the Desktop shortcut does NOT +// spawn a console window (good for the desktop UX). The downside is that +// sub-commands like `muyue scan`, `muyue version`, `muyue install-shortcuts` +// produce no output when invoked from cmd.exe. +// +// Workaround: at process start, try to attach to the parent's console via +// kernel32!AttachConsole(ATTACH_PARENT_PROCESS). If the parent has a console +// (i.e. we were launched from cmd.exe / PowerShell), stdout/stderr/stdin are +// rebound to it. If not (Explorer double-click), the call fails silently and +// the binary runs without any console — exactly what we want. + +import ( + "log" + "os" + "syscall" +) + +const attachParentProcess = ^uint32(0) // -1 cast to DWORD + +func init() { + kernel32, err := syscall.LoadLibrary("kernel32.dll") + if err != nil { + return + } + defer syscall.FreeLibrary(kernel32) + attachConsole, err := syscall.GetProcAddress(kernel32, "AttachConsole") + if err != nil { + return + } + r0, _, _ := syscall.SyscallN(attachConsole, uintptr(attachParentProcess)) + if r0 == 0 { + return // parent has no console (Explorer launch) — stay silent + } + // Re-bind the standard streams to the freshly attached console so + // fmt.Println / log output appear in the parent terminal. + if h, err := syscall.GetStdHandle(syscall.STD_OUTPUT_HANDLE); err == nil && h != 0 { + os.Stdout = os.NewFile(uintptr(h), "stdout") + } + if h, err := syscall.GetStdHandle(syscall.STD_ERROR_HANDLE); err == nil && h != 0 { + os.Stderr = os.NewFile(uintptr(h), "stderr") + } + if h, err := syscall.GetStdHandle(syscall.STD_INPUT_HANDLE); err == nil && h != 0 { + os.Stdin = os.NewFile(uintptr(h), "stdin") + } + // log.Default() captured the original os.Stderr at init time — repoint it + // at the freshly attached console so log.Printf calls (e.g. desktop.Run) + // surface in the parent terminal. + log.SetOutput(os.Stderr) +} diff --git a/internal/version/version.go b/internal/version/version.go index e1936aa..2632125 100644 --- a/internal/version/version.go +++ b/internal/version/version.go @@ -7,7 +7,7 @@ import ( const ( Name = "muyue" - Version = "0.7.4" + Version = "0.7.5" Author = "La Légion de Muyue" )