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:
Lea Anthony 2025-12-06 20:03:02 +11:00
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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -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

View file

@ -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();
}

View file

@ -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;
}

View file

@ -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),
})
}

View file

@ -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"