mirror of
https://github.com/wailsapp/wails.git
synced 2026-03-14 14:45:49 +01:00
feat(setup): redesign wizard as classic installer with actionable install
- Redesign as classic Windows-style page-by-page wizard - Add install commands from doctor/packagemanager for missing deps - Show copyable commands for system package installs (pacman, apt, etc) - Add external links for npm (nodejs.org) and Docker (docker.com) - Add API endpoint to run install commands from wizard - Show breadcrumb navigation: Welcome > Dependencies > Docker > Complete - Add Cancel button and Back/Next navigation - Dependencies page shows Required/Optional sections with status - Docker page shows cross-compilation setup with image build option - Complete page has copyable next steps commands - Include InstallCommand and HelpURL fields in DependencyStatus 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
b8630ca7b5
commit
96aa27eb9b
10 changed files with 976 additions and 358 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
48
v3/internal/setupwizard/frontend/dist/assets/index-ChymCpvQ.js
vendored
Normal file
48
v3/internal/setupwizard/frontend/dist/assets/index-ChymCpvQ.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
v3/internal/setupwizard/frontend/dist/assets/index-D_hwfYDk.css
vendored
Normal file
1
v3/internal/setupwizard/frontend/dist/assets/index-D_hwfYDk.css
vendored
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -7,8 +7,8 @@
|
|||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
<script type="module" crossorigin src="/assets/index-BYm8n1Ze.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-5h4Dv4JW.css">
|
||||
<script type="module" crossorigin src="/assets/index-ChymCpvQ.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-D_hwfYDk.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -58,3 +58,18 @@ export async function saveWailsConfig(config: WailsConfig): Promise<{ status: st
|
|||
});
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export interface InstallResult {
|
||||
success: boolean;
|
||||
output: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export async function installDependency(command: string): Promise<InstallResult> {
|
||||
const response = await fetch(`${API_BASE}/dependencies/install`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ command }),
|
||||
});
|
||||
return response.json();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ export interface DependencyStatus {
|
|||
status: 'installed' | 'not_installed' | 'needs_update' | 'checking';
|
||||
required: boolean;
|
||||
message?: string;
|
||||
installCommand?: string;
|
||||
helpUrl?: string;
|
||||
}
|
||||
|
||||
export interface DockerStatus {
|
||||
|
|
@ -45,6 +47,8 @@ export interface SystemInfo {
|
|||
wailsVersion: string;
|
||||
goVersion: string;
|
||||
homeDir: string;
|
||||
osName?: string;
|
||||
osVersion?: string;
|
||||
gitName?: string;
|
||||
gitEmail?: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,12 +27,26 @@ var frontendFS embed.FS
|
|||
|
||||
// DependencyStatus represents the status of a dependency
|
||||
type DependencyStatus struct {
|
||||
Name string `json:"name"`
|
||||
Installed bool `json:"installed"`
|
||||
Version string `json:"version,omitempty"`
|
||||
Status string `json:"status"` // "installed", "not_installed", "needs_update"
|
||||
Required bool `json:"required"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Installed bool `json:"installed"`
|
||||
Version string `json:"version,omitempty"`
|
||||
Status string `json:"status"` // "installed", "not_installed", "needs_update"
|
||||
Required bool `json:"required"`
|
||||
Message string `json:"message,omitempty"`
|
||||
InstallCommand string `json:"installCommand,omitempty"`
|
||||
HelpURL string `json:"helpUrl,omitempty"`
|
||||
}
|
||||
|
||||
// DockerStatus represents Docker installation and image status
|
||||
type DockerStatus struct {
|
||||
Installed bool `json:"installed"`
|
||||
Running bool `json:"running"`
|
||||
Version string `json:"version,omitempty"`
|
||||
ImageBuilt bool `json:"imageBuilt"`
|
||||
ImageName string `json:"imageName"`
|
||||
PullProgress int `json:"pullProgress"`
|
||||
PullStatus string `json:"pullStatus"` // "idle", "pulling", "complete", "error"
|
||||
PullError string `json:"pullError,omitempty"`
|
||||
}
|
||||
|
||||
// WailsConfigInfo represents the info section of wails.yaml
|
||||
|
|
@ -71,11 +85,13 @@ type WizardState struct {
|
|||
|
||||
// Wizard is the setup wizard server
|
||||
type Wizard struct {
|
||||
server *http.Server
|
||||
state WizardState
|
||||
stateMu sync.RWMutex
|
||||
done chan struct{}
|
||||
shutdown chan struct{}
|
||||
server *http.Server
|
||||
state WizardState
|
||||
stateMu sync.RWMutex
|
||||
dockerStatus DockerStatus
|
||||
dockerMu sync.RWMutex
|
||||
done chan struct{}
|
||||
shutdown chan struct{}
|
||||
}
|
||||
|
||||
// New creates a new setup wizard
|
||||
|
|
@ -143,6 +159,9 @@ func (w *Wizard) setupRoutes(mux *http.ServeMux) {
|
|||
// API routes
|
||||
mux.HandleFunc("/api/state", w.handleState)
|
||||
mux.HandleFunc("/api/dependencies/check", w.handleCheckDependencies)
|
||||
mux.HandleFunc("/api/dependencies/install", w.handleInstallDependency)
|
||||
mux.HandleFunc("/api/docker/status", w.handleDockerStatus)
|
||||
mux.HandleFunc("/api/docker/build", w.handleDockerBuild)
|
||||
mux.HandleFunc("/api/wails-config", w.handleWailsConfig)
|
||||
mux.HandleFunc("/api/complete", w.handleComplete)
|
||||
mux.HandleFunc("/api/close", w.handleClose)
|
||||
|
|
@ -326,3 +345,150 @@ func commandExists(name string) bool {
|
|||
_, err := exec.LookPath(name)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (w *Wizard) handleDockerStatus(rw http.ResponseWriter, r *http.Request) {
|
||||
status := w.checkDocker()
|
||||
|
||||
w.dockerMu.Lock()
|
||||
w.dockerStatus = status
|
||||
w.dockerMu.Unlock()
|
||||
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(rw).Encode(status)
|
||||
}
|
||||
|
||||
func (w *Wizard) checkDocker() DockerStatus {
|
||||
status := DockerStatus{
|
||||
ImageName: "wails-cross",
|
||||
PullStatus: "idle",
|
||||
}
|
||||
|
||||
// Check if Docker is installed
|
||||
output, err := execCommand("docker", "--version")
|
||||
if err != nil {
|
||||
status.Installed = false
|
||||
return status
|
||||
}
|
||||
|
||||
status.Installed = true
|
||||
// Parse version from "Docker version 24.0.7, build afdd53b"
|
||||
parts := strings.Split(output, ",")
|
||||
if len(parts) > 0 {
|
||||
status.Version = strings.TrimPrefix(strings.TrimSpace(parts[0]), "Docker version ")
|
||||
}
|
||||
|
||||
// Check if Docker daemon is running
|
||||
if _, err := execCommand("docker", "info"); err != nil {
|
||||
status.Running = false
|
||||
return status
|
||||
}
|
||||
status.Running = true
|
||||
|
||||
// Check if wails-cross image exists
|
||||
imageOutput, err := execCommand("docker", "image", "inspect", "wails-cross")
|
||||
status.ImageBuilt = err == nil && len(imageOutput) > 0
|
||||
|
||||
return status
|
||||
}
|
||||
|
||||
func (w *Wizard) handleDockerBuild(rw http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
w.dockerMu.Lock()
|
||||
w.dockerStatus.PullStatus = "pulling"
|
||||
w.dockerStatus.PullProgress = 0
|
||||
w.dockerMu.Unlock()
|
||||
|
||||
// Build the Docker image in background
|
||||
go func() {
|
||||
// Run: wails3 task setup:docker
|
||||
cmd := exec.Command("wails3", "task", "setup:docker")
|
||||
err := cmd.Run()
|
||||
|
||||
w.dockerMu.Lock()
|
||||
if err != nil {
|
||||
w.dockerStatus.PullStatus = "error"
|
||||
w.dockerStatus.PullError = err.Error()
|
||||
} else {
|
||||
w.dockerStatus.PullStatus = "complete"
|
||||
w.dockerStatus.ImageBuilt = true
|
||||
}
|
||||
w.dockerStatus.PullProgress = 100
|
||||
w.dockerMu.Unlock()
|
||||
}()
|
||||
|
||||
// Simulate progress updates while building
|
||||
go func() {
|
||||
for i := 0; i < 90; i += 5 {
|
||||
time.Sleep(2 * time.Second)
|
||||
w.dockerMu.Lock()
|
||||
if w.dockerStatus.PullStatus != "pulling" {
|
||||
w.dockerMu.Unlock()
|
||||
return
|
||||
}
|
||||
w.dockerStatus.PullProgress = i
|
||||
w.dockerMu.Unlock()
|
||||
}
|
||||
}()
|
||||
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(rw).Encode(map[string]string{"status": "started"})
|
||||
}
|
||||
|
||||
// InstallRequest represents a request to install a dependency
|
||||
type InstallRequest struct {
|
||||
Command string `json:"command"`
|
||||
}
|
||||
|
||||
// InstallResponse represents the result of an install attempt
|
||||
type InstallResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Output string `json:"output"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func (w *Wizard) handleInstallDependency(rw http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
var req InstallRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(rw, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
|
||||
// Execute the install command
|
||||
// Split the command into parts
|
||||
parts := strings.Fields(req.Command)
|
||||
if len(parts) == 0 {
|
||||
json.NewEncoder(rw).Encode(InstallResponse{
|
||||
Success: false,
|
||||
Error: "Empty command",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
cmd := exec.Command(parts[0], parts[1:]...)
|
||||
output, err := cmd.CombinedOutput()
|
||||
|
||||
if err != nil {
|
||||
json.NewEncoder(rw).Encode(InstallResponse{
|
||||
Success: false,
|
||||
Output: string(output),
|
||||
Error: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
json.NewEncoder(rw).Encode(InstallResponse{
|
||||
Success: true,
|
||||
Output: string(output),
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,8 +34,9 @@ func (w *Wizard) checkAllDependencies() []DependencyStatus {
|
|||
} else {
|
||||
status.Installed = false
|
||||
status.Status = "not_installed"
|
||||
status.InstallCommand = dep.InstallCommand
|
||||
if dep.InstallCommand != "" {
|
||||
status.Message = "Install with: " + dep.InstallCommand
|
||||
status.Message = "Run the install command to install this dependency"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -55,14 +56,16 @@ func (w *Wizard) checkAllDependencies() []DependencyStatus {
|
|||
func checkNpm() DependencyStatus {
|
||||
dep := DependencyStatus{
|
||||
Name: "npm",
|
||||
Required: true,
|
||||
Required: false, // Optional - not strictly required for Go-only projects
|
||||
}
|
||||
|
||||
version, err := execCommand("npm", "-v")
|
||||
if err != nil {
|
||||
dep.Status = "not_installed"
|
||||
dep.Installed = false
|
||||
dep.Message = "npm is required. Install Node.js from https://nodejs.org/"
|
||||
dep.Message = "Required for frontend development"
|
||||
dep.HelpURL = "https://nodejs.org/"
|
||||
dep.InstallCommand = "Install Node.js from https://nodejs.org/"
|
||||
return dep
|
||||
}
|
||||
|
||||
|
|
@ -75,7 +78,8 @@ func checkNpm() DependencyStatus {
|
|||
if major < 7 {
|
||||
dep.Status = "needs_update"
|
||||
dep.Installed = true
|
||||
dep.Message = "npm 7.0.0 or higher is required"
|
||||
dep.Message = "npm 7.0.0 or higher recommended"
|
||||
dep.HelpURL = "https://nodejs.org/"
|
||||
return dep
|
||||
}
|
||||
}
|
||||
|
|
@ -95,7 +99,9 @@ func checkDocker() DependencyStatus {
|
|||
if err != nil {
|
||||
dep.Status = "not_installed"
|
||||
dep.Installed = false
|
||||
dep.Message = "Optional - for cross-compilation"
|
||||
dep.Message = "Enables cross-platform builds"
|
||||
dep.HelpURL = "https://docs.docker.com/get-docker/"
|
||||
dep.InstallCommand = "Install Docker from https://docs.docker.com/get-docker/"
|
||||
return dep
|
||||
}
|
||||
|
||||
|
|
@ -110,7 +116,7 @@ func checkDocker() DependencyStatus {
|
|||
if err != nil {
|
||||
dep.Installed = true
|
||||
dep.Status = "installed"
|
||||
dep.Message = "Daemon not running"
|
||||
dep.Message = "Start Docker to enable cross-compilation"
|
||||
return dep
|
||||
}
|
||||
|
||||
|
|
@ -119,7 +125,7 @@ func checkDocker() DependencyStatus {
|
|||
if imageCheck == "" || strings.Contains(imageCheck, "Error") {
|
||||
dep.Installed = true
|
||||
dep.Status = "installed"
|
||||
dep.Message = "wails-cross image not built"
|
||||
dep.Message = "Run 'wails3 task setup:docker' to build cross-compilation image"
|
||||
} else {
|
||||
dep.Installed = true
|
||||
dep.Status = "installed"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue