From 1442b4fd8a7573da54d7023ee4910f5e46145460 Mon Sep 17 00:00:00 2001 From: Muyue Date: Mon, 27 Apr 2026 12:12:18 +0200 Subject: [PATCH] feat: onboarding 2-keys + Windows install w/o admin (v0.7.3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .gitea/workflows/ci-main.yml | 9 +- CHANGELOG.md | 24 ++++ cmd/muyue/commands/install_shortcuts.go | 151 ++++++++++++++++++++++++ internal/version/version.go | 2 +- web/src/components/OnboardingWizard.jsx | 125 ++++++++++++++++---- 5 files changed, 280 insertions(+), 31 deletions(-) create mode 100644 cmd/muyue/commands/install_shortcuts.go diff --git a/.gitea/workflows/ci-main.yml b/.gitea/workflows/ci-main.yml index 04e24bb..7d5e9d8 100644 --- a/.gitea/workflows/ci-main.yml +++ b/.gitea/workflows/ci-main.yml @@ -138,11 +138,12 @@ jobs: echo "sudo mv muyue-darwin-arm64 /usr/local/bin/muyue" echo "\`\`\`" echo "" - echo "**Windows (x86_64)**" + echo "**Windows (x86_64)** — sans privilèges admin, crée les raccourcis Bureau + Menu Démarrer :" echo "\`\`\`powershell" - echo "Invoke-WebRequest -Uri \"${DL_URL}/muyue-windows-amd64.zip\" -OutFile \"muyue.zip\"" - echo "Expand-Archive -Path \"muyue.zip\" -DestinationPath \".\"" - echo "Move-Item muyue-windows-amd64.exe C:\\Windows\\muyue.exe" + echo "\$dest = \"\$env:LOCALAPPDATA\\Muyue\"" + 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 "\`\`\`" } > /tmp/stable_changelog.md echo "path=/tmp/stable_changelog.md" >> $GITHUB_OUTPUT diff --git a/CHANGELOG.md b/CHANGELOG.md index 8bf28d0..f8d019f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,30 @@ 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.3 + +### Onboarding — focus MiniMax + MiMo + +- L'étape `apikey` du wizard de premier lancement propose désormais **les deux clés** (MiniMax + MiMo) côte à côte ; au moins une doit être validée pour continuer. +- Les autres fournisseurs (OpenAI, Anthropic, Z.AI, Ollama) ne sont plus proposés dans le wizard — l'utilisateur les configure ensuite via l'onglet **Configuration** s'il le souhaite. Justification : pour les nouveaux utilisateurs, deux choix simples > six choix qui ralentissent le démarrage. +- Si MiniMax est validé, il devient le provider actif. Sinon, c'est MiMo. Si les deux sont validés, MiniMax reste actif (peut être basculé via `/model change` plus tard). + +### Install Windows — pas d'admin + raccourcis automatiques + +- **Avant** : la 3ᵉ ligne du snippet d'install (`Move-Item ... C:\Windows\muyue.exe`) échouait avec `UnauthorizedAccessException` sur PowerShell sans élévation. +- **Maintenant** : 4 lignes, toutes exécutables sans admin : + ```powershell + $dest = "$env:LOCALAPPDATA\Muyue" + Invoke-WebRequest -Uri ".../muyue-windows-amd64.zip" -OutFile "$env:TEMP\muyue.zip" + Expand-Archive -Path "$env:TEMP\muyue.zip" -DestinationPath $dest -Force + & "$dest\muyue-windows-amd64.exe" install-shortcuts + ``` +- Nouvelle commande `muyue install-shortcuts` (Windows uniquement) : + - crée `Muyue.lnk` sur le Bureau et dans le Menu Démarrer (résolus via `[Environment]::GetFolderPath`, robuste OneDrive / profils non-standards) ; + - utilise WScript.Shell COM via PowerShell pour générer les `.lnk` (pas de dépendance Go ajoutée) ; + - ajoute le dossier d'install au `PATH` utilisateur (scope User, pas de modif système). +- Une icône custom pourra être branchée plus tard en remplaçant la ressource embed du `.exe` ; pour l'instant, l'icône Windows par défaut du binaire est utilisée. + ## v0.7.2 ### Amélioration diff --git a/cmd/muyue/commands/install_shortcuts.go b/cmd/muyue/commands/install_shortcuts.go new file mode 100644 index 0000000..0ba3aab --- /dev/null +++ b/cmd/muyue/commands/install_shortcuts.go @@ -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 +} diff --git a/internal/version/version.go b/internal/version/version.go index 600f85d..18e7098 100644 --- a/internal/version/version.go +++ b/internal/version/version.go @@ -7,7 +7,7 @@ import ( const ( Name = "muyue" - Version = "0.7.2" + Version = "0.7.3" Author = "La Légion de Muyue" ) diff --git a/web/src/components/OnboardingWizard.jsx b/web/src/components/OnboardingWizard.jsx index 7d7e9df..44f81a4 100644 --- a/web/src/components/OnboardingWizard.jsx +++ b/web/src/components/OnboardingWizard.jsx @@ -23,8 +23,12 @@ export default function OnboardingWizard({ api, onComplete }) { language: 'fr', keyboard: 'azerty', apikey: '', + apikey_mimo: '', editor: '', }) + const [keyValidMimo, setKeyValidMimo] = useState(false) + const [errorMimo, setErrorMimo] = useState(null) + const [validatingMimo, setValidatingMimo] = useState(false) const [editorList, setEditorList] = useState(BASE_EDITORS) const [saving, setSaving] = useState(false) const [error, setError] = useState(null) @@ -52,7 +56,7 @@ export default function OnboardingWizard({ api, onComplete }) { case 'name': return answers.name.trim().length > 0 case 'language': return !!answers.language case 'keyboard': return !!answers.keyboard - case 'apikey': return keyValid && !scanning + case 'apikey': return (keyValid || keyValidMimo) && !scanning case 'editor': return true case 'done': return true default: return true @@ -173,6 +177,33 @@ export default function OnboardingWizard({ api, onComplete }) { setValidating(false) } + const handleValidateKeyMimo = async () => { + if (!answers.apikey_mimo.trim()) return + setValidatingMimo(true) + setErrorMimo(null) + try { + await api.validateProvider({ + name: 'mimo', + api_key: answers.apikey_mimo, + model: 'mimo-v2.5-pro', + base_url: 'https://token-plan-ams.xiaomimimo.com/v1', + }) + setKeyValidMimo(true) + // Save MiMo. If MiniMax wasn't validated yet, MiMo becomes the active provider. + await api.saveProvider({ + name: 'mimo', + api_key: answers.apikey_mimo, + model: 'mimo-v2.5-pro', + base_url: 'https://token-plan-ams.xiaomimimo.com/v1', + active: !keyValid, + }) + } catch (err) { + setErrorMimo(err.message || 'Clé invalide') + setKeyValidMimo(false) + } + setValidatingMimo(false) + } + const handleSave = async () => { @@ -201,6 +232,15 @@ export default function OnboardingWizard({ api, onComplete }) { active: true, }) } + if (answers.apikey_mimo.trim()) { + await api.saveProvider({ + name: 'mimo', + api_key: answers.apikey_mimo, + model: 'mimo-v2.5-pro', + base_url: 'https://token-plan-ams.xiaomimimo.com/v1', + active: !answers.apikey.trim(), + }) + } onComplete() } catch (err) { setError(err.message || 'Erreur lors de la sauvegarde') @@ -283,38 +323,71 @@ export default function OnboardingWizard({ api, onComplete }) { {current.key === 'apikey' && (
-
Clé API MiniMax
+
Clés API
- Entrez votre clé API MiniMax pour activer l'assistant IA. La clé est obligatoire pour continuer. + Renseignez au moins l'une des deux clés pour activer l'assistant. Les autres fournisseurs (OpenAI, Anthropic, Ollama, Z.AI) se configurent plus tard depuis l'onglet Configuration.
- { setAnswers(a => ({ ...a, apikey: e.target.value })); setKeyValid(false); setError(null) }} - autoFocus - /> - {error && !keyValid &&
{error}
} - {keyValid && !scanning &&
Clé valide ✓ — Appuyez sur Entrée pour continuer
} + +
+ + { setAnswers(a => ({ ...a, apikey: e.target.value })); setKeyValid(false); setError(null) }} + autoFocus + /> +
+ + {keyValid && ✓ MiniMax OK} + {error && !keyValid && {error}} +
+
+ +
+ + { setAnswers(a => ({ ...a, apikey_mimo: e.target.value })); setKeyValidMimo(false); setErrorMimo(null) }} + /> +
+ + {keyValidMimo && ✓ MiMo OK} + {errorMimo && !keyValidMimo && {errorMimo}} +
+
+ {scanning && ( -
+
{scanMessage}
)} - {requiredError &&
Veuillez valider votre clé API pour continuer
} -
- -
- {!keyValid && !error && answers.apikey.trim() && ( -
Entrez votre clé puis cliquez "Valider la clé"
+ {requiredError && ( +
+ Veuillez valider au moins une clé (MiniMax ou MiMo) pour continuer. +
+ )} + {(keyValid || keyValidMimo) && !scanning && ( +
+ Au moins une clé est valide — appuyez sur Suivant pour continuer. +
)}
)}