feat: onboarding 2-keys + Windows install w/o admin (v0.7.3)
All checks were successful
PR Check / check (pull_request) Successful in 56s

Two user-reported pain points:

1. First-run setup proposed only MiniMax (no MiMo step). Onboarding
   now offers both keys side-by-side under a single "apikey" step,
   with per-key Validate buttons. At least one must be valid to
   proceed; the rest of the providers (OpenAI/Anthropic/Z.AI/Ollama)
   are not shown in the wizard — they're configured later via the
   Config tab. Active provider = MiniMax if valid, else MiMo.

2. Windows install instructions failed: Move-Item to C:\Windows
   requires admin. Replaced with a no-admin 4-line snippet that
   installs to %LOCALAPPDATA%\Muyue and calls a new subcommand
   `muyue install-shortcuts` to create Desktop + Start Menu .lnk
   files and add the install dir to the user PATH. Shortcut creation
   uses WScript.Shell COM via PowerShell — keeps Go binary
   dependency-free. Folder paths resolved through
   [Environment]::GetFolderPath so OneDrive/redirected profiles
   work too.

- cmd/muyue/commands/install_shortcuts.go: new file
- web/src/components/OnboardingWizard.jsx: 2-key apikey step
- .gitea/workflows/ci-main.yml: updated install snippet
- internal/version/version.go: 0.7.2 → 0.7.3
- CHANGELOG.md: v0.7.3 entry
This commit is contained in:
Muyue
2026-04-27 12:12:18 +02:00
parent a1da9da3db
commit 1442b4fd8a
5 changed files with 280 additions and 31 deletions

View File

@@ -0,0 +1,151 @@
package commands
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"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(" Executable : %s\n", exe)
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, exe, 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 {
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 (open a new terminal to pick it up)\n", installDir)
}
fmt.Println("\nDone — double-click the Muyue icon on your Desktop to launch.")
return nil
},
}
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
}