feat(setup): add real-time Docker pull progress via Docker Engine API

- Use Docker Engine API for accurate download progress with byte counts
- Add SSE streaming for smooth real-time UI updates (replaces polling)
- Show toast notification when Docker completes in background mode
- Fix progress calculation to use bytes instead of layer count
- Fix framework selection border clipping with padding
- Pull pre-built image from ghcr.io/wailsapp/wails-cross instead of building locally
This commit is contained in:
Lea Anthony 2026-01-06 20:52:23 +11:00
commit f763cf3e2f
16 changed files with 2377 additions and 487 deletions

Binary file not shown.

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-CEPUVERO.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BKINrEPO.css">
<script type="module" crossorigin src="/assets/index-Ced0vwAS.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CGNSfeb6.css">
</head>
<body>
<div id="root"></div>

View file

@ -0,0 +1,319 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Wails Project Defaults (Mock)</title>
<style>
:root{
--bg0:#05070b;
--bg1:#070a11;
--panel:#0b0f18;
--panel2:#0a0e16;
--text: rgba(255,255,255,.92);
--text2: rgba(255,255,255,.62);
--line: rgba(255,255,255,.08);
--hover: rgba(255,255,255,.06);
--shadow: rgba(0,0,0,.55);
--accent:#e0242a;
}
*{ box-sizing:border-box; }
html,body{ height:100%; }
body{
margin:0;
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji","Segoe UI Emoji";
background:
radial-gradient(900px 600px at 60% 40%, rgba(255,0,40,.08), transparent 55%),
radial-gradient(1100px 900px at 65% 55%, rgba(255,255,255,.06), transparent 60%),
radial-gradient(900px 600px at 30% 60%, rgba(255,255,255,.04), transparent 60%),
linear-gradient(180deg, var(--bg0), var(--bg1));
color:var(--text);
display:grid;
place-items:center;
}
/* Window */
.window{
width:min(1160px, 96vw);
height:min(720px, 92vh);
border-radius:18px;
overflow:hidden;
position:relative;
background: linear-gradient(180deg, rgba(255,255,255,.06), rgba(255,255,255,.03));
box-shadow:
0 30px 80px rgba(0,0,0,.65),
0 0 0 1px rgba(255,255,255,.06) inset;
}
.layout{
height:100%;
display:grid;
grid-template-columns: 320px 1fr;
}
/* Sidebar */
.sidebar{
position:relative;
background:
radial-gradient(1000px 600px at 40% 20%, rgba(255,0,50,.10), transparent 55%),
linear-gradient(180deg, rgba(5,7,12,.92), rgba(5,7,12,.74));
box-shadow: 1px 0 0 rgba(255,255,255,.06) inset;
padding:28px 22px 18px;
}
.brand{
display:flex;
flex-direction:column;
align-items:flex-start;
gap:10px;
padding-bottom:18px;
}
/* Placeholder "dragon" */
.logo{
width:90px; height:64px;
position:relative;
filter: drop-shadow(0 10px 18px rgba(224,36,42,.35));
}
.logo:before, .logo:after{
content:"";
position:absolute;
inset:0;
background: conic-gradient(from 210deg, #ff4b4f, #b90f15, #ff4b4f);
clip-path: polygon(8% 65%, 40% 20%, 58% 30%, 78% 8%, 84% 30%, 64% 44%, 92% 52%, 70% 66%, 78% 88%, 55% 74%, 42% 92%, 34% 66%, 10% 82%);
border-radius:10px;
}
.logo:after{
inset:6px;
opacity:.55;
filter: blur(.2px);
}
.wordmark{
letter-spacing:.46em;
font-weight:700;
font-size:14px;
color: rgba(255,255,255,.88);
margin-left:4px;
}
.nav{
margin-top:8px;
display:flex;
flex-direction:column;
gap:10px;
}
.nav a{
text-decoration:none;
color: rgba(255,255,255,.62);
font-size:18px;
padding:14px 14px;
border-radius:12px;
display:flex;
align-items:center;
gap:12px;
transition: background-color .12s ease, color .12s ease;
}
.nav a.active{
background: rgba(255,255,255,.08);
color: rgba(255,255,255,.92);
box-shadow: 0 0 0 1px rgba(255,255,255,.06) inset;
}
.dot{
width:12px; height:12px; border-radius:999px;
background: rgba(255,255,255,.15);
flex:0 0 auto;
}
/* Main panel */
.main{
position:relative;
padding:48px 56px;
background:
radial-gradient(1200px 900px at 55% 40%, rgba(255,255,255,.05), transparent 60%),
linear-gradient(180deg, rgba(5,7,12,.40), rgba(5,7,12,.20));
}
.content{
max-width: 760px;
margin: 0 auto;
text-align:center;
padding-top: 6px;
}
h1{
margin:0;
font-size: 38px;
line-height: 44px;
letter-spacing: .01em;
font-weight: 650;
color: rgba(255,255,255,.92);
}
.sub{
margin:10px 0 26px 0;
font-size: 18px;
line-height: 26px;
color: rgba(255,255,255,.62);
}
/* Preference group */
.group{
width: 640px;
margin: 0 auto;
border-radius: 12px;
background: rgba(255,255,255,.12);
box-shadow:
0 12px 30px rgba(0,0,0,.35),
0 0 0 1px rgba(255,255,255,.06) inset;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
overflow:hidden;
text-align:left;
}
.row{
height: 52px;
display:flex;
align-items:center;
justify-content:space-between;
padding: 0 20px;
cursor:pointer;
user-select:none;
transition: background-color .12s ease;
}
.row:hover{ background: var(--hover); }
.row + .row{ border-top: 1px solid var(--line); }
.label{
font-size: 13px;
font-weight: 550;
color: rgba(255,255,255,.90);
}
.value{
display:flex;
align-items:center;
gap: 8px;
font-size: 13px;
font-weight: 400;
color: rgba(255,255,255,.65);
max-width: 360px;
}
.value span{
overflow:hidden;
text-overflow:ellipsis;
white-space:nowrap;
text-align:right;
display:block;
max-width: 330px;
}
.chev{
font-size: 12px;
color: rgba(255,255,255,.40);
transition: color .12s ease;
transform: translateY(-.5px);
}
.row:hover .chev{ color: rgba(255,255,255,.65); }
/* Continue button */
.actions{
margin-top: 86px;
display:flex;
justify-content:center;
}
button{
height: 44px;
padding: 0 34px;
border-radius: 12px;
border: 0;
background: linear-gradient(180deg, rgba(224,36,42,.95), rgba(184,16,22,.95));
color: rgba(255,255,255,.92);
font-size: 18px;
font-weight: 650;
letter-spacing: .01em;
box-shadow:
0 14px 30px rgba(224,36,42,.22),
0 0 0 1px rgba(255,255,255,.12) inset;
cursor:pointer;
transition: transform .08s ease, filter .12s ease;
}
button:active{ transform: translateY(1px); filter: brightness(.98); }
/* Optional: subtle film grain */
.grain{
pointer-events:none;
position:absolute;
inset:0;
opacity:.10;
mix-blend-mode: overlay;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='220' height='220'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='220' height='220' filter='url(%23n)' opacity='.35'/%3E%3C/svg%3E");
background-size: 220px 220px;
}
</style>
</head>
<body>
<div class="window" role="application" aria-label="Wails onboarding wizard">
<div class="layout">
<aside class="sidebar">
<div class="brand">
<div class="logo" aria-hidden="true"></div>
<div class="wordmark">WAILS</div>
</div>
<nav class="nav" aria-label="Setup steps">
<a href="#" aria-current="false"><span class="dot"></span>Welcome</a>
<a href="#" aria-current="false"><span class="dot"></span>Dependencies</a>
<a href="#" aria-current="false"><span class="dot"></span>Platform</a>
<a class="active" href="#" aria-current="page"><span class="dot"></span>Defaults</a>
<a href="#" aria-current="false"><span class="dot"></span>Complete</a>
</nav>
</aside>
<main class="main">
<div class="content">
<h1>Project Defaults</h1>
<div class="sub">Choose how new Wails apps are created by default.</div>
<section class="group" aria-label="Project defaults">
<div class="row" tabindex="0" role="button" aria-label="Edit bundle identifier">
<div class="label">Bundle identifier</div>
<div class="value"><span>com.example</span><div class="chev"></div></div>
</div>
<div class="row" tabindex="0" role="button" aria-label="Edit version">
<div class="label">Version</div>
<div class="value"><span>0.1.0</span><div class="chev"></div></div>
</div>
<div class="row" tabindex="0" role="button" aria-label="Choose template">
<div class="label">Template</div>
<div class="value"><span>Vanilla (JavaScript)</span><div class="chev"></div></div>
</div>
</section>
<div class="actions">
<button type="button">Continue</button>
</div>
</div>
</main>
</div>
<div class="grain"></div>
</div>
<script>
// Tiny affordance: allow Enter/Space to activate rows (no actual editing yet).
document.querySelectorAll(".row").forEach(row => {
row.addEventListener("keydown", (e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
row.click();
}
});
row.addEventListener("click", () => {
// Stub: replace with your routing / inline edit (Step 3).
console.log("Row activated:", row.querySelector(".label").textContent.trim());
});
});
</script>
</body>
</html>

File diff suppressed because it is too large Load diff

View file

@ -17,6 +17,40 @@ export async function getDockerStatus(): Promise<DockerStatus> {
return response.json();
}
export function subscribeDockerStatus(onUpdate: (status: DockerStatus) => void): () => void {
let eventSource: EventSource | null = null;
let closed = false;
const connect = () => {
if (closed) return;
eventSource = new EventSource(`${API_BASE}/docker/status/stream`);
eventSource.onmessage = (event) => {
try {
const status = JSON.parse(event.data) as DockerStatus;
onUpdate(status);
} catch (e) {
console.error('Failed to parse docker status:', e);
}
};
eventSource.onerror = () => {
eventSource?.close();
if (!closed) {
setTimeout(connect, 1000);
}
};
};
connect();
return () => {
closed = true;
eventSource?.close();
};
}
export async function buildDockerImage(): Promise<{ status: string }> {
const response = await fetch(`${API_BASE}/docker/build`, { method: 'POST' });
return response.json();

View file

@ -187,7 +187,6 @@ html, body, #root {
animation: scrollBackground 60s linear infinite;
}
/* Reduced motion support - but preserve loading spinners */
@media (prefers-reduced-motion: reduce) {
*:not(.animate-spin):not(.spinner), *:not(.animate-spin):not(.spinner)::before, *:not(.animate-spin):not(.spinner)::after {
animation-duration: 0.01ms !important;
@ -195,3 +194,20 @@ html, body, #root {
transition-duration: 0.01ms !important;
}
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.settings-row:focus-visible {
outline: 2px solid #ef4444;
outline-offset: -2px;
}

View file

@ -19,8 +19,13 @@ export interface DockerStatus {
imageName: string;
imageSize?: string;
pullProgress: number;
pullMessage?: string;
pullStatus: 'idle' | 'pulling' | 'complete' | 'error';
pullError?: string;
bytesTotal?: number;
bytesDone?: number;
layerCount?: number;
layersDone?: number;
}
export interface UserConfig {

View file

@ -0,0 +1,212 @@
package setupwizard
import (
"strings"
"testing"
)
func TestParseSize(t *testing.T) {
tests := []struct {
input string
expected float64
}{
{"10B", 10},
{"10KB", 10 * 1024},
{"10MB", 10 * 1024 * 1024},
{"10GB", 10 * 1024 * 1024 * 1024},
{"1.5MB", 1.5 * 1024 * 1024},
{"100", 100},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
got := parseSize(tt.input)
if got != tt.expected {
t.Errorf("parseSize(%q) = %v, want %v", tt.input, got, tt.expected)
}
})
}
}
func TestPullParser(t *testing.T) {
dockerOutput := `latest: Pulling from wailsapp/wails-cross
1074353eec0d: Already exists
665f62578bce: Already exists
61d7d5b4f231: Pulling fs layer
202b93a508bb: Pulling fs layer
61d7d5b4f231: Downloading 1.5MB/10MB
202b93a508bb: Downloading 2MB/20MB
61d7d5b4f231: Downloading 5MB/10MB
202b93a508bb: Downloading 10MB/20MB
61d7d5b4f231: Verifying Checksum
61d7d5b4f231: Download complete
61d7d5b4f231: Extracting 1MB/10MB
61d7d5b4f231: Extracting 10MB/10MB
61d7d5b4f231: Pull complete
202b93a508bb: Verifying Checksum
202b93a508bb: Download complete
202b93a508bb: Extracting 5MB/20MB
202b93a508bb: Extracting 20MB/20MB
202b93a508bb: Pull complete
Digest: sha256:abc123
Status: Downloaded newer image`
parser := newPullParser()
lines := strings.Split(dockerOutput, "\n")
type checkpoint struct {
lineContains string
minProgress int
maxProgress int
stage string
}
checkpoints := []checkpoint{
{"Pulling from", 0, 0, "Connecting"},
{"Pulling fs layer", 0, 0, "Downloading"},
{"Downloading 1.5MB/10MB", 1, 20, "Downloading"},
{"Downloading 5MB/10MB", 20, 60, "Downloading"},
{"Download complete", 30, 70, "Extracting"},
{"Pull complete", 30, 100, "Extracting"},
}
checkIdx := 0
for _, line := range lines {
progress := parser.ParseLine(line)
if checkIdx < len(checkpoints) && strings.Contains(line, checkpoints[checkIdx].lineContains) {
cp := checkpoints[checkIdx]
if progress.Progress < cp.minProgress || progress.Progress > cp.maxProgress {
t.Errorf("After %q: progress=%d, want between %d and %d",
cp.lineContains, progress.Progress, cp.minProgress, cp.maxProgress)
}
if progress.Stage != cp.stage {
t.Errorf("After %q: stage=%q, want %q", cp.lineContains, progress.Stage, cp.stage)
}
checkIdx++
}
}
finalProgress := parser.ParseLine("")
if finalProgress.Progress != 100 {
t.Errorf("Final progress = %d, want 100", finalProgress.Progress)
}
}
func TestPullParserRealOutput(t *testing.T) {
realOutput := `latest: Pulling from wailsapp/wails-cross
1074353eec0d: Already exists
665f62578bce: Already exists
61d7d5b4f231: Pulling fs layer
202b93a508bb: Pulling fs layer
604349a0d76e: Pulling fs layer
61d7d5b4f231: Downloading 1MB/10MB
202b93a508bb: Downloading 2MB/20MB
604349a0d76e: Downloading 1MB/15MB
61d7d5b4f231: Downloading 5MB/10MB
202b93a508bb: Downloading 10MB/20MB
604349a0d76e: Downloading 8MB/15MB
61d7d5b4f231: Verifying Checksum
61d7d5b4f231: Download complete
61d7d5b4f231: Pull complete
202b93a508bb: Verifying Checksum
202b93a508bb: Download complete
202b93a508bb: Pull complete
604349a0d76e: Verifying Checksum
604349a0d76e: Download complete
604349a0d76e: Pull complete
Digest: sha256:abc123
Status: Downloaded newer image`
parser := newPullParser()
lines := strings.Split(realOutput, "\n")
var lastProgress PullProgress
for _, line := range lines {
lastProgress = parser.ParseLine(line)
t.Logf("Line: %q -> Stage: %s, Progress: %d%%", line, lastProgress.Stage, lastProgress.Progress)
}
if lastProgress.Stage == "Connecting" {
t.Errorf("Stage should not be 'Connecting' after parsing layers, got: %s", lastProgress.Stage)
}
if lastProgress.Progress != 100 {
t.Errorf("Final progress should be 100%%, got: %d%%", lastProgress.Progress)
}
}
func TestPullParserWithANSI(t *testing.T) {
lines := []string{
"latest: Pulling from wailsapp/wails-cross",
"[1A[2K1074353eec0d: Pulling fs layer [1B",
"[1A[2K665f62578bce: Pulling fs layer [1B",
"[22A[2K665f62578bce: Downloading [==> ] 16.38kB/296.1kB[22B",
"[21A[2K5c445a0e108b: Downloading [> ] 538.1kB/60.15MB[21B",
"[23A[2K1074353eec0d: Downloading [=================================================> ] 3.811MB/3.86MB[23B",
"[23A[2K1074353eec0d: Verifying Checksum [23B",
"[23A[2K1074353eec0d: Download complete [23B",
"[23A[2K1074353eec0d: Extracting [> ] 65.54kB/3.86MB[23B",
"[23A[2K1074353eec0d: Pull complete [23B",
"[22A[2K665f62578bce: Pull complete [22B",
}
parser := newPullParser()
var lastProgress PullProgress
for _, line := range lines {
lastProgress = parser.ParseLine(line)
t.Logf("Line: %.60q -> Stage: %s, Progress: %d%%", line, lastProgress.Stage, lastProgress.Progress)
}
if lastProgress.Stage == "Connecting" {
t.Errorf("Stage should not be 'Connecting', got: %s", lastProgress.Stage)
}
if len(parser.layerSizes) == 0 {
t.Errorf("Should have parsed layer sizes, got none")
}
if lastProgress.Progress == 0 {
t.Errorf("Progress should not be 0 after Pull complete")
}
}
func TestPullParserNoSizeInfo(t *testing.T) {
output := `latest: Pulling from wailsapp/wails-cross
layer1: Pulling fs layer
layer2: Pulling fs layer
layer3: Pulling fs layer
layer4: Pulling fs layer
layer1: Verifying Checksum
layer1: Download complete
layer1: Pull complete
layer2: Verifying Checksum
layer2: Download complete
layer2: Pull complete
layer3: Verifying Checksum
layer3: Download complete
layer3: Pull complete
layer4: Verifying Checksum
layer4: Download complete
layer4: Pull complete`
parser := newPullParser()
lines := strings.Split(output, "\n")
progressHistory := []int{}
for _, line := range lines {
progress := parser.ParseLine(line)
progressHistory = append(progressHistory, progress.Progress)
t.Logf("Line: %q -> Stage: %s, Progress: %d%%", line, progress.Stage, progress.Progress)
}
finalProgress := progressHistory[len(progressHistory)-1]
if finalProgress != 100 {
t.Errorf("Final progress should be 100%%, got: %d%%", finalProgress)
}
for i := 1; i < len(progressHistory); i++ {
if progressHistory[i] < progressHistory[i-1] {
t.Errorf("Progress should not decrease: %d -> %d", progressHistory[i-1], progressHistory[i])
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -11,6 +11,9 @@ import (
func (w *Wizard) checkAllDependencies() []DependencyStatus {
var deps []DependencyStatus
// Check Go (required)
deps = append(deps, checkGo())
// Check Xcode Command Line Tools
deps = append(deps, checkXcode())
@ -23,6 +26,44 @@ func (w *Wizard) checkAllDependencies() []DependencyStatus {
return deps
}
func checkGo() DependencyStatus {
dep := DependencyStatus{
Name: "Go",
Required: true,
}
version, err := execCommand("go", "version")
if err != nil {
dep.Status = "not_installed"
dep.Installed = false
dep.Message = "Go 1.25+ is required"
dep.HelpURL = "https://go.dev/dl/"
return dep
}
dep.Installed = true
dep.Status = "installed"
parts := strings.Split(version, " ")
if len(parts) >= 3 {
versionStr := strings.TrimPrefix(parts[2], "go")
dep.Version = versionStr
versionParts := strings.Split(versionStr, ".")
if len(versionParts) >= 2 {
major, _ := strconv.Atoi(versionParts[0])
minor, _ := strconv.Atoi(versionParts[1])
if major < 1 || (major == 1 && minor < 25) {
dep.Status = "needs_update"
dep.Message = "Go 1.25+ is required (found " + versionStr + ")"
dep.HelpURL = "https://go.dev/dl/"
}
}
}
return dep
}
func checkXcode() DependencyStatus {
dep := DependencyStatus{
Name: "Xcode Command Line Tools",

View file

@ -14,13 +14,12 @@ func (w *Wizard) checkAllDependencies() []DependencyStatus {
var deps []DependencyStatus
hasNpm := false
// Get OS info for package manager detection
deps = append(deps, checkGo())
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 {
if dep.Name == "npm" {
@ -45,17 +44,54 @@ func (w *Wizard) checkAllDependencies() []DependencyStatus {
}
}
// Check npm (common dependency) - only if not already added by package manager
if !hasNpm {
deps = append(deps, checkNpm())
}
// Check Docker (optional)
deps = append(deps, checkDocker())
return deps
}
func checkGo() DependencyStatus {
dep := DependencyStatus{
Name: "Go",
Required: true,
}
version, err := execCommand("go", "version")
if err != nil {
dep.Status = "not_installed"
dep.Installed = false
dep.Message = "Go 1.25+ is required"
dep.HelpURL = "https://go.dev/dl/"
dep.InstallCommand = "Download from https://go.dev/dl/"
return dep
}
dep.Installed = true
dep.Status = "installed"
parts := strings.Split(version, " ")
if len(parts) >= 3 {
versionStr := strings.TrimPrefix(parts[2], "go")
dep.Version = versionStr
versionParts := strings.Split(versionStr, ".")
if len(versionParts) >= 2 {
major, _ := strconv.Atoi(versionParts[0])
minor, _ := strconv.Atoi(versionParts[1])
if major < 1 || (major == 1 && minor < 25) {
dep.Status = "needs_update"
dep.Message = "Go 1.25+ is required (found " + versionStr + ")"
dep.HelpURL = "https://go.dev/dl/"
}
}
}
return dep
}
func checkNpm() DependencyStatus {
dep := DependencyStatus{
Name: "npm",

View file

@ -60,18 +60,52 @@ func execCommandRefreshed(name string, args ...string) (string, error) {
func (w *Wizard) checkAllDependencies() []DependencyStatus {
var deps []DependencyStatus
// Check WebView2 Runtime
deps = append(deps, checkGo())
deps = append(deps, checkWebView2())
// Check npm (common dependency)
deps = append(deps, checkNpm())
// Check Docker (optional)
deps = append(deps, checkDocker())
return deps
}
func checkGo() DependencyStatus {
dep := DependencyStatus{
Name: "Go",
Required: true,
}
version, err := execCommandRefreshed("go", "version")
if err != nil {
dep.Status = "not_installed"
dep.Installed = false
dep.Message = "Go 1.25+ is required"
dep.HelpURL = "https://go.dev/dl/"
return dep
}
dep.Installed = true
dep.Status = "installed"
parts := strings.Split(version, " ")
if len(parts) >= 3 {
versionStr := strings.TrimPrefix(parts[2], "go")
dep.Version = versionStr
versionParts := strings.Split(versionStr, ".")
if len(versionParts) >= 2 {
major, _ := strconv.Atoi(versionParts[0])
minor, _ := strconv.Atoi(versionParts[1])
if major < 1 || (major == 1 && minor < 25) {
dep.Status = "needs_update"
dep.Message = "Go 1.25+ is required (found " + versionStr + ")"
dep.HelpURL = "https://go.dev/dl/"
}
}
}
return dep
}
func checkWebView2() DependencyStatus {
dep := DependencyStatus{
Name: "WebView2 Runtime",