feat(setup): Preserve existing setup wizard design

This commit is contained in:
Lea Anthony 2025-12-06 16:47:51 +11:00
commit 5bac6d7d19
22 changed files with 4209 additions and 0 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Wails Setup Wizard</title>
<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">
</head>
<body>
<div id="root"></div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

View file

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Wails Setup Wizard</title>
<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">
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,26 @@
{
"name": "wails-setup-wizard",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
},
"dependencies": {
"framer-motion": "^12.23.25",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.3.3",
"autoprefixer": "^10.4.20",
"postcss": "^8.4.47",
"tailwindcss": "^3.4.14",
"typescript": "~5.6.2",
"vite": "^5.4.10"
}
}

View file

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

View file

@ -0,0 +1,327 @@
import { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import type { DependencyStatus, SystemInfo } from './types';
import { checkDependencies, getState } from './api';
import WailsLogo from './components/WailsLogo';
type CheckState = 'idle' | 'checking' | 'complete';
interface DependencyWithState extends DependencyStatus {
checkState: CheckState;
}
export default function App() {
const [dependencies, setDependencies] = useState<DependencyWithState[]>([]);
const [system, setSystem] = useState<SystemInfo | null>(null);
const [started, setStarted] = useState(false);
const [allComplete, setAllComplete] = useState(false);
// Load system info on mount
useEffect(() => {
getState().then((s) => {
setSystem(s.system);
});
}, []);
// Start checking after logo animation
useEffect(() => {
const timer = setTimeout(() => {
setStarted(true);
startDependencyCheck();
}, 1500);
return () => clearTimeout(timer);
}, []);
const startDependencyCheck = async () => {
// First get the list of dependencies
const deps = await checkDependencies();
// Initialize all as 'idle'
const depsWithState: DependencyWithState[] = deps.map(d => ({
...d,
checkState: 'idle'
}));
setDependencies(depsWithState);
// Animate each dependency check one by one with staggered timing
for (let i = 0; i < depsWithState.length; i++) {
await new Promise(resolve => setTimeout(resolve, 300));
setDependencies(prev => prev.map((d, idx) =>
idx === i ? { ...d, checkState: 'checking' } : d
));
// Simulate check time (the actual check is already done)
await new Promise(resolve => setTimeout(resolve, 400 + Math.random() * 300));
setDependencies(prev => prev.map((d, idx) =>
idx === i ? { ...d, checkState: 'complete' } : d
));
}
// All complete
await new Promise(resolve => setTimeout(resolve, 500));
setAllComplete(true);
};
const allInstalled = dependencies.length > 0 &&
dependencies.every(d => d.installed || !d.required);
const getStatusIcon = (dep: DependencyWithState) => {
if (dep.checkState === 'idle') {
return (
<div className="w-5 h-5 rounded-full border-2 border-gray-600" />
);
}
if (dep.checkState === 'checking') {
return (
<motion.div
className="w-5 h-5 rounded-full border-2 border-red-500 border-t-transparent"
animate={{ rotate: 360 }}
transition={{ duration: 1, repeat: Infinity, ease: 'linear' }}
/>
);
}
// Complete
if (dep.installed) {
return (
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
className="w-5 h-5 rounded-full bg-green-500 flex items-center justify-center"
>
<motion.svg
className="w-3 h-3 text-white"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
initial={{ pathLength: 0 }}
animate={{ pathLength: 1 }}
transition={{ duration: 0.3 }}
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
</motion.svg>
</motion.div>
);
} else {
return (
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
className="w-5 h-5 rounded-full bg-red-500 flex items-center justify-center"
>
<svg className="w-3 h-3 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M6 18L18 6M6 6l12 12" />
</svg>
</motion.div>
);
}
};
return (
<div className="min-h-screen bg-[#0f0f0f] grid-bg relative overflow-hidden">
{/* Radial glow background */}
<div className="absolute inset-0 radial-glow pointer-events-none" />
{/* Main content */}
<div className="relative z-10 min-h-screen flex flex-col items-center justify-center px-8">
{/* Logo */}
<motion.div
initial={{ scale: 0.8, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ duration: 0.6, ease: 'easeOut' }}
>
<WailsLogo size={160} />
</motion.div>
{/* Title */}
<motion.div
className="mt-6 text-center"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3, duration: 0.5 }}
>
<h1 className="text-3xl font-bold text-white">
Wails Setup
</h1>
<p className="text-gray-400 mt-2">
{system?.os && `${system.os}/${system.arch}`}
{system?.wailsVersion && ` • v${system.wailsVersion}`}
</p>
</motion.div>
{/* Dependencies section */}
<motion.div
className="mt-10 w-full max-w-md"
initial={{ opacity: 0 }}
animate={{ opacity: started ? 1 : 0 }}
transition={{ duration: 0.4 }}
>
{/* Header */}
<motion.div
className="flex items-center gap-2 mb-4"
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.2 }}
>
<span className="text-sm font-medium text-gray-400 uppercase tracking-wider">
Checking Dependencies
</span>
{!allComplete && dependencies.length > 0 && (
<motion.span
className="text-xs text-gray-500"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
>
({dependencies.filter(d => d.checkState === 'complete').length}/{dependencies.length})
</motion.span>
)}
</motion.div>
{/* Dependency list */}
<div className="space-y-2">
<AnimatePresence mode="popLayout">
{dependencies.map((dep, index) => (
<motion.div
key={dep.name}
className="flex items-center gap-4 p-4 rounded-xl bg-gray-800/50 border border-gray-700/50"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{
delay: index * 0.1,
duration: 0.3,
ease: 'easeOut'
}}
layout
>
{/* Status icon */}
<div className="flex-shrink-0">
{getStatusIcon(dep)}
</div>
{/* Name and version */}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="text-white font-medium">{dep.name}</span>
{dep.checkState === 'complete' && dep.version && (
<motion.span
className="text-xs text-gray-500 font-mono"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
>
v{dep.version}
</motion.span>
)}
</div>
{/* Message for missing deps */}
{dep.checkState === 'complete' && !dep.installed && dep.message && (
<motion.p
className="text-xs text-red-400 mt-1 truncate"
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
>
{dep.message}
</motion.p>
)}
</div>
{/* Required badge */}
{dep.required && dep.checkState === 'complete' && !dep.installed && (
<motion.span
className="text-xs px-2 py-1 rounded bg-red-500/20 text-red-400"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
>
Required
</motion.span>
)}
</motion.div>
))}
</AnimatePresence>
</div>
{/* Summary */}
<AnimatePresence>
{allComplete && (
<motion.div
className="mt-6"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
>
{allInstalled ? (
<div className="text-center p-4 rounded-xl bg-green-500/10 border border-green-500/30">
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ type: 'spring', stiffness: 200, damping: 15 }}
>
<div className="text-3xl mb-2"></div>
</motion.div>
<p className="text-green-400 font-medium">
All dependencies installed!
</p>
<p className="text-gray-400 text-sm mt-1">
You're ready to build Wails applications.
</p>
</div>
) : (
<div className="text-center p-4 rounded-xl bg-yellow-500/10 border border-yellow-500/30">
<div className="text-3xl mb-2"></div>
<p className="text-yellow-400 font-medium">
Some dependencies are missing
</p>
<p className="text-gray-400 text-sm mt-1">
Install the missing dependencies to continue.
</p>
</div>
)}
{/* Close button */}
<motion.button
className="mt-6 w-full btn-primary"
onClick={() => window.close()}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.4 }}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
Close
</motion.button>
</motion.div>
)}
</AnimatePresence>
</motion.div>
{/* Loading dots while fetching deps */}
{started && dependencies.length === 0 && (
<motion.div
className="mt-10 flex space-x-2"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
>
{[0, 1, 2].map((i) => (
<motion.div
key={i}
className="w-2 h-2 rounded-full bg-red-500/50"
animate={{
scale: [1, 1.3, 1],
opacity: [0.5, 1, 0.5]
}}
transition={{
duration: 1,
repeat: Infinity,
delay: i * 0.15
}}
/>
))}
</motion.div>
)}
</div>
</div>
);
}

View file

@ -0,0 +1,60 @@
import type { WizardState, DependencyStatus, DockerStatus, UserConfig, WailsConfig } from './types';
const API_BASE = '/api';
export async function getState(): Promise<WizardState> {
const response = await fetch(`${API_BASE}/state`);
return response.json();
}
export async function checkDependencies(): Promise<DependencyStatus[]> {
const response = await fetch(`${API_BASE}/dependencies/check`);
return response.json();
}
export async function getDockerStatus(): Promise<DockerStatus> {
const response = await fetch(`${API_BASE}/docker/status`);
return response.json();
}
export async function buildDockerImage(): Promise<{ status: string }> {
const response = await fetch(`${API_BASE}/docker/build`, { method: 'POST' });
return response.json();
}
export async function detectConfig(): Promise<Partial<UserConfig>> {
const response = await fetch(`${API_BASE}/config/detect`);
return response.json();
}
export async function saveConfig(config: UserConfig): Promise<{ status: string }> {
const response = await fetch(`${API_BASE}/config/save`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(config),
});
return response.json();
}
export async function complete(): Promise<{ status: string; duration: string }> {
const response = await fetch(`${API_BASE}/complete`);
return response.json();
}
export async function close(): Promise<void> {
await fetch(`${API_BASE}/close`);
}
export async function getWailsConfig(): Promise<WailsConfig | null> {
const response = await fetch(`${API_BASE}/wails-config`);
return response.json();
}
export async function saveWailsConfig(config: WailsConfig): Promise<{ status: string }> {
const response = await fetch(`${API_BASE}/wails-config`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(config),
});
return response.json();
}

View file

@ -0,0 +1,19 @@
interface WailsLogoProps {
className?: string;
size?: number;
}
export default function WailsLogo({ className = '', size = 240 }: WailsLogoProps) {
return (
<img
src="/wails-logo.png"
alt="Wails"
width={size}
height={size}
className={`object-contain ${className}`}
style={{
filter: 'drop-shadow(0 0 60px rgba(239, 68, 68, 0.4))',
}}
/>
);
}

View file

@ -0,0 +1,112 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--wails-red: #ef4444;
--wails-red-dark: #dc2626;
--wails-red-light: #f87171;
--bg-primary: #0f0f0f;
--bg-secondary: #1f2937;
--bg-tertiary: #374151;
}
* {
box-sizing: border-box;
}
html, body, #root {
margin: 0;
padding: 0;
min-height: 100vh;
background: var(--bg-primary);
color: white;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* Gradient text utility */
.gradient-text {
background: linear-gradient(135deg, #ffffff 0%, #ef4444 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* Glass morphism card */
.glass-card {
background: rgba(31, 41, 55, 0.8);
backdrop-filter: blur(10px);
border: 1px solid rgba(55, 65, 81, 0.5);
}
/* Subtle grid background */
.grid-bg {
background-image:
linear-gradient(rgba(239, 68, 68, 0.03) 1px, transparent 1px),
linear-gradient(90deg, rgba(239, 68, 68, 0.03) 1px, transparent 1px);
background-size: 40px 40px;
}
/* Radial glow */
.radial-glow {
background: radial-gradient(ellipse at center, rgba(239, 68, 68, 0.1) 0%, transparent 70%);
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: var(--bg-primary);
}
::-webkit-scrollbar-thumb {
background: var(--bg-tertiary);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #4b5563;
}
/* Button hover effect */
.btn-primary {
@apply bg-gradient-to-r from-red-500 to-red-600 text-white font-semibold
py-3 px-8 rounded-xl shadow-lg transition-all duration-300
hover:scale-105 hover:shadow-xl hover:shadow-red-500/30
active:scale-95;
}
.btn-secondary {
@apply bg-transparent border border-gray-600 text-gray-300 font-medium
py-3 px-8 rounded-xl transition-all duration-300
hover:border-gray-500 hover:text-white hover:bg-gray-800/50;
}
/* Status badge animations */
@keyframes spin {
to { transform: rotate(360deg); }
}
.spinner {
animation: spin 1s linear infinite;
}
/* Draw check animation for success */
.check-path {
stroke-dasharray: 100;
stroke-dashoffset: 100;
animation: drawCheck 0.5s ease-out forwards;
}
/* Reduced motion support */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}

View file

@ -0,0 +1,10 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)

View file

@ -0,0 +1,61 @@
export interface DependencyStatus {
name: string;
installed: boolean;
version?: string;
path?: string;
status: 'installed' | 'not_installed' | 'needs_update' | 'checking';
required: boolean;
message?: string;
}
export interface DockerStatus {
installed: boolean;
running: boolean;
version?: string;
imageBuilt: boolean;
imageName: string;
pullProgress: number;
pullStatus: 'idle' | 'pulling' | 'complete' | 'error';
pullError?: string;
}
export interface UserConfig {
developerName: string;
email: string;
defaultFramework: string;
projectDirectory: string;
editor: string;
}
export interface WailsConfig {
info: {
companyName: string;
productName: string;
productIdentifier: string;
description: string;
copyright: string;
comments: string;
version: string;
};
}
export interface SystemInfo {
os: string;
arch: string;
wailsVersion: string;
goVersion: string;
homeDir: string;
gitName?: string;
gitEmail?: string;
}
export interface WizardState {
currentStep: number;
dependencies: DependencyStatus[];
docker: DockerStatus;
config: UserConfig;
system: SystemInfo;
startTime: string;
}
export type Step = 'splash' | 'welcome' | 'dependencies' | 'docker' | 'config' | 'wails-config' | 'complete';

View file

@ -0,0 +1,56 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {
colors: {
'wails-red': {
DEFAULT: '#ef4444',
dark: '#dc2626',
light: '#f87171',
},
},
fontFamily: {
sans: ['Inter', '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'sans-serif'],
},
animation: {
'fade-in': 'fadeIn 0.5s ease-out',
'slide-up': 'slideUp 0.5s ease-out',
'scale-in': 'scaleIn 0.3s ease-out',
'pulse-glow': 'pulseGlow 2s ease-in-out infinite',
'draw-check': 'drawCheck 0.5s ease-out forwards',
'shimmer': 'shimmer 2s linear infinite',
},
keyframes: {
fadeIn: {
'0%': { opacity: '0' },
'100%': { opacity: '1' },
},
slideUp: {
'0%': { opacity: '0', transform: 'translateY(20px)' },
'100%': { opacity: '1', transform: 'translateY(0)' },
},
scaleIn: {
'0%': { opacity: '0', transform: 'scale(0.9)' },
'100%': { opacity: '1', transform: 'scale(1)' },
},
pulseGlow: {
'0%, 100%': { boxShadow: '0 0 20px rgba(239, 68, 68, 0.4)' },
'50%': { boxShadow: '0 0 40px rgba(239, 68, 68, 0.6)' },
},
drawCheck: {
'0%': { strokeDashoffset: '100' },
'100%': { strokeDashoffset: '0' },
},
shimmer: {
'0%': { backgroundPosition: '-200% 0' },
'100%': { backgroundPosition: '200% 0' },
},
},
},
},
plugins: [],
}

View file

@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}

View file

@ -0,0 +1,10 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
build: {
outDir: 'dist',
emptyOutDir: true,
},
})

View file

@ -0,0 +1,328 @@
package setupwizard
import (
"context"
"embed"
"encoding/json"
"fmt"
"io/fs"
"net"
"net/http"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"sync"
"time"
"github.com/pkg/browser"
"github.com/wailsapp/wails/v3/internal/operatingsystem"
"github.com/wailsapp/wails/v3/internal/version"
"gopkg.in/yaml.v3"
)
//go:embed frontend/dist/*
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"`
}
// WailsConfigInfo represents the info section of wails.yaml
type WailsConfigInfo struct {
CompanyName string `json:"companyName" yaml:"companyName"`
ProductName string `json:"productName" yaml:"productName"`
ProductIdentifier string `json:"productIdentifier" yaml:"productIdentifier"`
Description string `json:"description" yaml:"description"`
Copyright string `json:"copyright" yaml:"copyright"`
Comments string `json:"comments,omitempty" yaml:"comments,omitempty"`
Version string `json:"version" yaml:"version"`
}
// WailsConfig represents the wails.yaml configuration
type WailsConfig struct {
Info WailsConfigInfo `json:"info" yaml:"info"`
}
// SystemInfo contains detected system information
type SystemInfo struct {
OS string `json:"os"`
Arch string `json:"arch"`
WailsVersion string `json:"wailsVersion"`
GoVersion string `json:"goVersion"`
HomeDir string `json:"homeDir"`
OSName string `json:"osName,omitempty"`
OSVersion string `json:"osVersion,omitempty"`
}
// WizardState represents the complete wizard state
type WizardState struct {
Dependencies []DependencyStatus `json:"dependencies"`
System SystemInfo `json:"system"`
StartTime time.Time `json:"startTime"`
}
// Wizard is the setup wizard server
type Wizard struct {
server *http.Server
state WizardState
stateMu sync.RWMutex
done chan struct{}
shutdown chan struct{}
}
// New creates a new setup wizard
func New() *Wizard {
return &Wizard{
done: make(chan struct{}),
shutdown: make(chan struct{}),
state: WizardState{
StartTime: time.Now(),
},
}
}
// Run starts the wizard and opens it in the browser
func (w *Wizard) Run() error {
// Initialize system info
w.initSystemInfo()
// Find an available port
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return fmt.Errorf("failed to find available port: %w", err)
}
port := listener.Addr().(*net.TCPAddr).Port
url := fmt.Sprintf("http://127.0.0.1:%d", port)
// Set up HTTP routes
mux := http.NewServeMux()
w.setupRoutes(mux)
w.server = &http.Server{
Handler: mux,
}
// Start server in goroutine
go func() {
if err := w.server.Serve(listener); err != nil && err != http.ErrServerClosed {
fmt.Fprintf(os.Stderr, "Server error: %v\n", err)
}
}()
fmt.Printf("Setup wizard running at %s\n", url)
// Open browser
if err := browser.OpenURL(url); err != nil {
fmt.Printf("Please open %s in your browser\n", url)
}
// Wait for completion or shutdown
select {
case <-w.done:
fmt.Println("\nSetup completed successfully!")
case <-w.shutdown:
fmt.Println("\nSetup wizard closed.")
}
// Shutdown server
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
return w.server.Shutdown(ctx)
}
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/wails-config", w.handleWailsConfig)
mux.HandleFunc("/api/complete", w.handleComplete)
mux.HandleFunc("/api/close", w.handleClose)
// Serve frontend
frontendDist, err := fs.Sub(frontendFS, "frontend/dist")
if err != nil {
panic(err)
}
fileServer := http.FileServer(http.FS(frontendDist))
mux.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) {
// Try to serve the file
path := r.URL.Path
if path == "/" {
path = "/index.html"
}
// Check if file exists
if _, err := fs.Stat(frontendDist, strings.TrimPrefix(path, "/")); err != nil {
// Serve index.html for SPA routing
r.URL.Path = "/"
}
fileServer.ServeHTTP(rw, r)
})
}
func (w *Wizard) initSystemInfo() {
w.stateMu.Lock()
defer w.stateMu.Unlock()
homeDir, _ := os.UserHomeDir()
w.state.System = SystemInfo{
OS: runtime.GOOS,
Arch: runtime.GOARCH,
WailsVersion: version.String(),
GoVersion: runtime.Version(),
HomeDir: homeDir,
}
// Get OS details
if info, err := operatingsystem.Info(); err == nil {
w.state.System.OSName = info.Name
w.state.System.OSVersion = info.Version
}
}
func (w *Wizard) handleState(rw http.ResponseWriter, r *http.Request) {
w.stateMu.RLock()
defer w.stateMu.RUnlock()
rw.Header().Set("Content-Type", "application/json")
json.NewEncoder(rw).Encode(w.state)
}
func (w *Wizard) handleCheckDependencies(rw http.ResponseWriter, r *http.Request) {
deps := w.checkAllDependencies()
w.stateMu.Lock()
w.state.Dependencies = deps
w.stateMu.Unlock()
rw.Header().Set("Content-Type", "application/json")
json.NewEncoder(rw).Encode(deps)
}
func (w *Wizard) handleWailsConfig(rw http.ResponseWriter, r *http.Request) {
rw.Header().Set("Content-Type", "application/json")
// Find wails.yaml in current directory or parent directories
configPath := findWailsConfig()
switch r.Method {
case http.MethodGet:
if configPath == "" {
json.NewEncoder(rw).Encode(nil)
return
}
data, err := os.ReadFile(configPath)
if err != nil {
json.NewEncoder(rw).Encode(nil)
return
}
var config WailsConfig
if err := yaml.Unmarshal(data, &config); err != nil {
json.NewEncoder(rw).Encode(nil)
return
}
json.NewEncoder(rw).Encode(config)
case http.MethodPost:
var config WailsConfig
if err := json.NewDecoder(r.Body).Decode(&config); err != nil {
http.Error(rw, err.Error(), http.StatusBadRequest)
return
}
if configPath == "" {
configPath = "wails.yaml"
}
data, err := yaml.Marshal(&config)
if err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
if err := os.WriteFile(configPath, data, 0644); err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
json.NewEncoder(rw).Encode(map[string]string{"status": "saved", "path": configPath})
default:
http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed)
}
}
func findWailsConfig() string {
dir, err := os.Getwd()
if err != nil {
return ""
}
for {
configPath := filepath.Join(dir, "wails.yaml")
if _, err := os.Stat(configPath); err == nil {
return configPath
}
parent := filepath.Dir(dir)
if parent == dir {
break
}
dir = parent
}
return ""
}
func (w *Wizard) handleComplete(rw http.ResponseWriter, r *http.Request) {
w.stateMu.RLock()
state := w.state
w.stateMu.RUnlock()
duration := time.Since(state.StartTime)
response := map[string]interface{}{
"status": "complete",
"duration": duration.String(),
}
rw.Header().Set("Content-Type", "application/json")
json.NewEncoder(rw).Encode(response)
close(w.done)
}
func (w *Wizard) handleClose(rw http.ResponseWriter, r *http.Request) {
rw.Header().Set("Content-Type", "application/json")
json.NewEncoder(rw).Encode(map[string]string{"status": "closing"})
close(w.shutdown)
}
// execCommand runs a command and returns its output
func execCommand(name string, args ...string) (string, error) {
cmd := exec.Command(name, args...)
output, err := cmd.Output()
return strings.TrimSpace(string(output)), err
}
// commandExists checks if a command exists in PATH
func commandExists(name string) bool {
_, err := exec.LookPath(name)
return err == nil
}

View file

@ -0,0 +1,135 @@
//go:build darwin
package setupwizard
import (
"os/exec"
"strconv"
"strings"
)
func (w *Wizard) checkAllDependencies() []DependencyStatus {
var deps []DependencyStatus
// Check Xcode Command Line Tools
deps = append(deps, checkXcode())
// Check npm (common dependency)
deps = append(deps, checkNpm())
// Check Docker (optional)
deps = append(deps, checkDocker())
return deps
}
func checkXcode() DependencyStatus {
dep := DependencyStatus{
Name: "Xcode Command Line Tools",
Required: true,
}
path, err := execCommand("xcode-select", "-p")
if err != nil {
dep.Status = "not_installed"
dep.Installed = false
dep.Message = "Run: xcode-select --install"
return dep
}
dep.Installed = true
dep.Status = "installed"
// Try to get version
cmd := exec.Command("pkgutil", "--pkg-info=com.apple.pkg.CLTools_Executables")
output, err := cmd.Output()
if err == nil {
lines := strings.Split(string(output), "\n")
for _, line := range lines {
if strings.HasPrefix(line, "version:") {
dep.Version = strings.TrimSpace(strings.TrimPrefix(line, "version:"))
break
}
}
}
_ = path // suppress unused warning
return dep
}
func checkNpm() DependencyStatus {
dep := DependencyStatus{
Name: "npm",
Required: true,
}
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/"
return dep
}
dep.Version = version
// Check minimum version (7.0.0)
parts := strings.Split(version, ".")
if len(parts) > 0 {
major, _ := strconv.Atoi(parts[0])
if major < 7 {
dep.Status = "needs_update"
dep.Installed = true
dep.Message = "npm 7.0.0 or higher is required"
return dep
}
}
dep.Installed = true
dep.Status = "installed"
return dep
}
func checkDocker() DependencyStatus {
dep := DependencyStatus{
Name: "docker",
Required: false, // Optional for cross-compilation
}
version, err := execCommand("docker", "--version")
if err != nil {
dep.Status = "not_installed"
dep.Installed = false
dep.Message = "Optional - for cross-compilation"
return dep
}
// Parse version from "Docker version 24.0.7, build afdd53b"
parts := strings.Split(version, ",")
if len(parts) > 0 {
dep.Version = strings.TrimPrefix(strings.TrimSpace(parts[0]), "Docker version ")
}
// Check if daemon is running
_, err = execCommand("docker", "info")
if err != nil {
dep.Installed = true
dep.Status = "installed"
dep.Message = "Daemon not running"
return dep
}
// Check for wails-cross image
imageCheck, _ := execCommand("docker", "image", "inspect", "wails-cross")
if imageCheck == "" || strings.Contains(imageCheck, "Error") {
dep.Installed = true
dep.Status = "installed"
dep.Message = "wails-cross image not built"
} else {
dep.Installed = true
dep.Status = "installed"
dep.Message = "Cross-compilation ready"
}
return dep
}

View file

@ -0,0 +1,130 @@
//go:build linux
package setupwizard
import (
"strconv"
"strings"
"github.com/wailsapp/wails/v3/internal/doctor/packagemanager"
"github.com/wailsapp/wails/v3/internal/operatingsystem"
)
func (w *Wizard) checkAllDependencies() []DependencyStatus {
var deps []DependencyStatus
// Get OS info for package manager detection
info, _ := operatingsystem.Info()
// Find the package manager
pm := packagemanager.Find(info.ID)
if pm != nil {
// Get platform dependencies from the doctor package
platformDeps, _ := packagemanager.Dependencies(pm)
for _, dep := range platformDeps {
status := DependencyStatus{
Name: dep.Name,
Required: !dep.Optional,
}
if dep.Installed {
status.Installed = true
status.Status = "installed"
status.Version = dep.Version
} else {
status.Installed = false
status.Status = "not_installed"
if dep.InstallCommand != "" {
status.Message = "Install with: " + dep.InstallCommand
}
}
deps = append(deps, status)
}
}
// Check npm (common dependency)
deps = append(deps, checkNpm())
// Check Docker (optional)
deps = append(deps, checkDocker())
return deps
}
func checkNpm() DependencyStatus {
dep := DependencyStatus{
Name: "npm",
Required: true,
}
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/"
return dep
}
dep.Version = version
// Check minimum version (7.0.0)
parts := strings.Split(version, ".")
if len(parts) > 0 {
major, _ := strconv.Atoi(parts[0])
if major < 7 {
dep.Status = "needs_update"
dep.Installed = true
dep.Message = "npm 7.0.0 or higher is required"
return dep
}
}
dep.Installed = true
dep.Status = "installed"
return dep
}
func checkDocker() DependencyStatus {
dep := DependencyStatus{
Name: "docker",
Required: false, // Optional for cross-compilation
}
version, err := execCommand("docker", "--version")
if err != nil {
dep.Status = "not_installed"
dep.Installed = false
dep.Message = "Optional - for cross-compilation"
return dep
}
// Parse version from "Docker version 24.0.7, build afdd53b"
parts := strings.Split(version, ",")
if len(parts) > 0 {
dep.Version = strings.TrimPrefix(strings.TrimSpace(parts[0]), "Docker version ")
}
// Check if daemon is running
_, err = execCommand("docker", "info")
if err != nil {
dep.Installed = true
dep.Status = "installed"
dep.Message = "Daemon not running"
return dep
}
// Check for wails-cross image
imageCheck, _ := execCommand("docker", "image", "inspect", "wails-cross")
if imageCheck == "" || strings.Contains(imageCheck, "Error") {
dep.Installed = true
dep.Status = "installed"
dep.Message = "wails-cross image not built"
} else {
dep.Installed = true
dep.Status = "installed"
dep.Message = "Cross-compilation ready"
}
return dep
}

View file

@ -0,0 +1,142 @@
//go:build windows
package setupwizard
import (
"os"
"path/filepath"
"strconv"
"strings"
)
func (w *Wizard) checkAllDependencies() []DependencyStatus {
var deps []DependencyStatus
// Check WebView2 Runtime
deps = append(deps, checkWebView2())
// Check npm (common dependency)
deps = append(deps, checkNpm())
// Check Docker (optional)
deps = append(deps, checkDocker())
return deps
}
func checkWebView2() DependencyStatus {
dep := DependencyStatus{
Name: "WebView2 Runtime",
Required: true,
}
// Check common installation paths
paths := []string{
filepath.Join(os.Getenv("PROGRAMFILES(X86)"), "Microsoft", "EdgeWebView", "Application"),
filepath.Join(os.Getenv("LOCALAPPDATA"), "Microsoft", "EdgeWebView", "Application"),
filepath.Join(os.Getenv("PROGRAMFILES"), "Microsoft", "EdgeWebView", "Application"),
}
for _, path := range paths {
if info, err := os.Stat(path); err == nil && info.IsDir() {
dep.Installed = true
dep.Status = "installed"
// Try to get version from directory name
entries, _ := os.ReadDir(path)
for _, entry := range entries {
if entry.IsDir() {
name := entry.Name()
// Version directories look like "120.0.2210.91"
if len(name) > 0 && name[0] >= '0' && name[0] <= '9' {
dep.Version = name
break
}
}
}
return dep
}
}
dep.Status = "not_installed"
dep.Installed = false
dep.Message = "Download from Microsoft Edge WebView2"
return dep
}
func checkNpm() DependencyStatus {
dep := DependencyStatus{
Name: "npm",
Required: true,
}
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/"
return dep
}
dep.Version = version
// Check minimum version (7.0.0)
parts := strings.Split(version, ".")
if len(parts) > 0 {
major, _ := strconv.Atoi(parts[0])
if major < 7 {
dep.Status = "needs_update"
dep.Installed = true
dep.Message = "npm 7.0.0 or higher is required"
return dep
}
}
dep.Installed = true
dep.Status = "installed"
return dep
}
func checkDocker() DependencyStatus {
dep := DependencyStatus{
Name: "docker",
Required: false, // Optional for cross-compilation
}
version, err := execCommand("docker", "--version")
if err != nil {
dep.Status = "not_installed"
dep.Installed = false
dep.Message = "Optional - for cross-compilation"
return dep
}
// Parse version from "Docker version 24.0.7, build afdd53b"
parts := strings.Split(version, ",")
if len(parts) > 0 {
dep.Version = strings.TrimPrefix(strings.TrimSpace(parts[0]), "Docker version ")
}
// Check if daemon is running
_, err = execCommand("docker", "info")
if err != nil {
dep.Installed = true
dep.Status = "installed"
dep.Message = "Daemon not running"
return dep
}
// Check for wails-cross image
imageCheck, _ := execCommand("docker", "image", "inspect", "wails-cross")
if imageCheck == "" || strings.Contains(imageCheck, "Error") {
dep.Installed = true
dep.Status = "installed"
dep.Message = "wails-cross image not built"
} else {
dep.Installed = true
dep.Status = "installed"
dep.Message = "Cross-compilation ready"
}
return dep
}