package commands import ( "fmt" "io" "os" "os/exec" "path/filepath" "runtime" "strings" "github.com/spf13/cobra" ) // installShortcutsCmd creates desktop + Start Menu shortcuts on Windows so // non-technical users can launch Muyue without opening a terminal. It also // adds the install directory to the user's PATH (per-user, no admin). // // Implementation note: shortcut (.lnk) creation on Windows is most reliable // via WScript.Shell COM. We invoke it via PowerShell — keeps the Go binary // dependency-free and works on any Windows 10+ host. var installShortcutsCmd = &cobra.Command{ Use: "install-shortcuts", Short: "Create Desktop + Start Menu shortcuts (Windows only) and add Muyue to PATH", RunE: func(cmd *cobra.Command, args []string) error { if runtime.GOOS != "windows" { fmt.Println("install-shortcuts is a Windows-only command (no-op on this platform)") return nil } exe, err := os.Executable() if err != nil { return fmt.Errorf("locate executable: %w", err) } exe, _ = filepath.Abs(exe) installDir := filepath.Dir(exe) fmt.Println("Installing Muyue shortcuts...") 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 { return fmt.Errorf("locate Desktop folder: %w", err) } startMenu, err := userShellFolder("Programs") if err != nil { return fmt.Errorf("locate Start Menu Programs folder: %w", err) } desktopLnk := filepath.Join(desktop, "Muyue.lnk") startLnk := filepath.Join(startMenu, "Muyue.lnk") 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, 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) 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\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) } // userShellFolder asks Windows for a user shell folder via PowerShell — // resilient to OneDrive redirection and non-default profile locations. // `which` is one of: Desktop, Programs (Start Menu Programs), StartMenu. func userShellFolder(which string) (string, error) { ps := fmt.Sprintf(`[Environment]::GetFolderPath('%s')`, which) out, err := exec.Command("powershell", "-NoLogo", "-NoProfile", "-Command", ps).Output() if err != nil { return "", err } path := stripTrailingWhitespace(string(out)) if path == "" { return "", fmt.Errorf("empty path for %s", which) } return path, nil } func stripTrailingWhitespace(s string) string { for len(s) > 0 && (s[len(s)-1] == '\n' || s[len(s)-1] == '\r' || s[len(s)-1] == ' ' || s[len(s)-1] == '\t') { s = s[:len(s)-1] } return s } // createWindowsShortcut generates a .lnk via WScript.Shell COM. The arguments // are passed through PowerShell variables (not interpolated into the script // body) to avoid quoting issues with paths containing spaces or special chars. func createWindowsShortcut(lnkPath, target, workingDir, description string) error { script := ` $lnk = $env:MUYUE_LNK $target = $env:MUYUE_TARGET $workdir = $env:MUYUE_WORKDIR $desc = $env:MUYUE_DESC $wsh = New-Object -ComObject WScript.Shell $sc = $wsh.CreateShortcut($lnk) $sc.TargetPath = $target $sc.WorkingDirectory = $workdir $sc.Description = $desc $sc.IconLocation = "$target,0" $sc.Save() ` cmd := exec.Command("powershell", "-NoLogo", "-NoProfile", "-Command", script) cmd.Env = append(os.Environ(), "MUYUE_LNK="+lnkPath, "MUYUE_TARGET="+target, "MUYUE_WORKDIR="+workingDir, "MUYUE_DESC="+description, ) out, err := cmd.CombinedOutput() if err != nil { return fmt.Errorf("powershell: %v: %s", err, string(out)) } return nil } // addUserPATH appends installDir to the user's PATH if not already present. // Uses PowerShell to read/write the User-scope environment via .NET API, // which broadcasts WM_SETTINGCHANGE so new processes pick it up. func addUserPATH(installDir string) error { script := ` $dir = $env:MUYUE_INSTALL_DIR $current = [Environment]::GetEnvironmentVariable('Path', 'User') if ($current -eq $null) { $current = '' } $parts = $current -split ';' | Where-Object { $_ -ne '' } if ($parts -notcontains $dir) { $new = if ($current -eq '') { $dir } else { "$current;$dir" } [Environment]::SetEnvironmentVariable('Path', $new, 'User') } ` cmd := exec.Command("powershell", "-NoLogo", "-NoProfile", "-Command", script) cmd.Env = append(os.Environ(), "MUYUE_INSTALL_DIR="+installDir) out, err := cmd.CombinedOutput() if err != nil { return fmt.Errorf("powershell: %v: %s", err, string(out)) } return nil }