@plusonelabs/cue 0.0.91 → 0.0.93

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1113 @@
1
+ param(
2
+ [string]$Distro = "Ubuntu",
3
+ [switch]$InstallCue,
4
+ [switch]$RequireSystemd = $true,
5
+ [switch]$Json,
6
+ [string]$RuntimeUrl = "",
7
+ [string]$RuntimeSha256 = ""
8
+ )
9
+
10
+ $ErrorActionPreference = "Stop"
11
+
12
+ $BootstrapReport = [ordered]@{
13
+ ok = $false
14
+ distro = $Distro
15
+ requireSystemd = [bool]$RequireSystemd
16
+ installCue = [bool]$InstallCue
17
+ checks = @()
18
+ warnings = @()
19
+ steps = @()
20
+ runtime = $null
21
+ host = [ordered]@{
22
+ version = $null
23
+ }
24
+ cue = [ordered]@{
25
+ installed = $false
26
+ version = $null
27
+ }
28
+ runtimeArtifact = [ordered]@{
29
+ url = $null
30
+ sha256 = $null
31
+ source = $null
32
+ downloaded = $false
33
+ extractedPath = $null
34
+ installMode = $null
35
+ }
36
+ }
37
+
38
+ if ($RuntimeUrl) {
39
+ $BootstrapReport.runtimeArtifact.url = $RuntimeUrl
40
+ }
41
+ if ($RuntimeSha256) {
42
+ $BootstrapReport.runtimeArtifact.sha256 = $RuntimeSha256
43
+ }
44
+
45
+ function Add-Check {
46
+ param(
47
+ [Parameter(Mandatory = $true)]
48
+ [string]$Name,
49
+ [Parameter(Mandatory = $true)]
50
+ [ValidateSet("ok", "warn", "fail")]
51
+ [string]$Status,
52
+ [Parameter(Mandatory = $true)]
53
+ [string]$Message
54
+ )
55
+ $BootstrapReport.checks += [pscustomobject]@{
56
+ name = $Name
57
+ status = $Status
58
+ message = $Message
59
+ }
60
+ }
61
+
62
+ function Add-Warning {
63
+ param([Parameter(Mandatory = $true)][string]$Message)
64
+ $BootstrapReport.warnings += $Message
65
+ }
66
+
67
+ function Add-Step {
68
+ param([Parameter(Mandatory = $true)][string]$Message)
69
+ $BootstrapReport.steps += $Message
70
+ }
71
+
72
+ function Complete-Bootstrap {
73
+ param(
74
+ [int]$ExitCode = 0,
75
+ [bool]$Ok = $true
76
+ )
77
+ $BootstrapReport.ok = $Ok
78
+ if ($Json) {
79
+ $BootstrapReport | ConvertTo-Json -Depth 6
80
+ }
81
+ exit $ExitCode
82
+ }
83
+
84
+ function Write-Step($Message) {
85
+ if (-not $Json) {
86
+ Write-Host "==> $Message"
87
+ }
88
+ }
89
+
90
+ function Write-WarnMessage($Message) {
91
+ Add-Warning $Message
92
+ if (-not $Json) {
93
+ Write-Host "WARN: $Message" -ForegroundColor Yellow
94
+ }
95
+ }
96
+
97
+ function Write-Ok($Message) {
98
+ if (-not $Json) {
99
+ Write-Host "OK: $Message" -ForegroundColor Green
100
+ }
101
+ }
102
+
103
+ function Normalize-WslOutputLine {
104
+ param([string]$Value)
105
+ if ($null -eq $Value) {
106
+ return ""
107
+ }
108
+
109
+ $normalized = [string]$Value
110
+ $normalized = $normalized.Replace([string][char]0, "")
111
+ return $normalized.Trim()
112
+ }
113
+
114
+ function Quote-BashSingle {
115
+ param([string]$Value)
116
+ if ($null -eq $Value) {
117
+ return "''"
118
+ }
119
+ $bashSingleQuoteEscape = "'" + '"' + "'" + '"' + "'"
120
+ return "'" + ($Value -replace "'", $bashSingleQuoteEscape) + "'"
121
+ }
122
+
123
+ function Convert-WindowsPathToWslPath {
124
+ param([string]$WindowsPath)
125
+
126
+ if ([string]::IsNullOrWhiteSpace($WindowsPath)) {
127
+ return $null
128
+ }
129
+
130
+ $fullPath = [System.IO.Path]::GetFullPath($WindowsPath)
131
+ if ($fullPath -notmatch '^[A-Za-z]:\\') {
132
+ return $null
133
+ }
134
+
135
+ $drive = $fullPath.Substring(0, 1).ToLowerInvariant()
136
+ $rest = $fullPath.Substring(3).Replace('\', '/')
137
+ if ([string]::IsNullOrEmpty($rest)) {
138
+ return "/mnt/$drive"
139
+ }
140
+ return "/mnt/$drive/$rest"
141
+ }
142
+
143
+ function Read-FileBytesSafe {
144
+ param([string]$Path)
145
+ if (-not (Test-Path -LiteralPath $Path)) {
146
+ return [byte[]]@()
147
+ }
148
+ return [System.IO.File]::ReadAllBytes($Path)
149
+ }
150
+
151
+ function Get-DecodedTextScore {
152
+ param([string]$Text)
153
+ if ($null -eq $Text) {
154
+ return -100000
155
+ }
156
+
157
+ $score = 0
158
+ foreach ($char in $Text.ToCharArray()) {
159
+ $code = [int][char]$char
160
+
161
+ if ($char -eq [char]0) {
162
+ $score -= 8
163
+ continue
164
+ }
165
+
166
+ if ($char -eq [char]0xFFFD) {
167
+ $score -= 6
168
+ continue
169
+ }
170
+
171
+ if ([char]::IsControl($char)) {
172
+ if ($char -eq "`r" -or $char -eq "`n" -or $char -eq "`t") {
173
+ $score += 0
174
+ } else {
175
+ $score -= 4
176
+ }
177
+ continue
178
+ }
179
+
180
+ if ([char]::IsLetterOrDigit($char) -or [char]::IsWhiteSpace($char)) {
181
+ $score += 2
182
+ continue
183
+ }
184
+
185
+ # Allow punctuation/symbols/CJK without over-penalizing.
186
+ $score += 1
187
+ }
188
+
189
+ return $score
190
+ }
191
+
192
+ function Test-BytesLikelyUtf16Le {
193
+ param([byte[]]$Bytes)
194
+ if ($null -eq $Bytes -or $Bytes.Length -lt 4) {
195
+ return $false
196
+ }
197
+
198
+ $pairs = 0
199
+ $oddZero = 0
200
+ for ($i = 1; $i -lt $Bytes.Length; $i += 2) {
201
+ $pairs += 1
202
+ if ($Bytes[$i] -eq 0) {
203
+ $oddZero += 1
204
+ }
205
+ }
206
+
207
+ if ($pairs -eq 0) {
208
+ return $false
209
+ }
210
+
211
+ return (($oddZero / $pairs) -ge 0.30)
212
+ }
213
+
214
+ function Test-BytesLikelyUtf16Be {
215
+ param([byte[]]$Bytes)
216
+ if ($null -eq $Bytes -or $Bytes.Length -lt 4) {
217
+ return $false
218
+ }
219
+
220
+ $pairs = 0
221
+ $evenZero = 0
222
+ for ($i = 0; $i -lt $Bytes.Length; $i += 2) {
223
+ $pairs += 1
224
+ if ($Bytes[$i] -eq 0) {
225
+ $evenZero += 1
226
+ }
227
+ }
228
+
229
+ if ($pairs -eq 0) {
230
+ return $false
231
+ }
232
+
233
+ return (($evenZero / $pairs) -ge 0.30)
234
+ }
235
+
236
+ function Decode-WslOutputBytes {
237
+ param([byte[]]$Bytes)
238
+
239
+ if ($null -eq $Bytes -or $Bytes.Length -eq 0) {
240
+ return ""
241
+ }
242
+
243
+ # BOM handling first
244
+ if ($Bytes.Length -ge 3 -and $Bytes[0] -eq 0xEF -and $Bytes[1] -eq 0xBB -and $Bytes[2] -eq 0xBF) {
245
+ return [System.Text.Encoding]::UTF8.GetString($Bytes, 3, $Bytes.Length - 3)
246
+ }
247
+ if ($Bytes.Length -ge 2 -and $Bytes[0] -eq 0xFF -and $Bytes[1] -eq 0xFE) {
248
+ return [System.Text.Encoding]::Unicode.GetString($Bytes, 2, $Bytes.Length - 2)
249
+ }
250
+ if ($Bytes.Length -ge 2 -and $Bytes[0] -eq 0xFE -and $Bytes[1] -eq 0xFF) {
251
+ return [System.Text.Encoding]::BigEndianUnicode.GetString($Bytes, 2, $Bytes.Length - 2)
252
+ }
253
+
254
+ # `wsl -l -q` frequently writes UTF-16 text without BOM when redirected.
255
+ if (Test-BytesLikelyUtf16Le $Bytes) {
256
+ return [System.Text.Encoding]::Unicode.GetString($Bytes)
257
+ }
258
+ if (Test-BytesLikelyUtf16Be $Bytes) {
259
+ return [System.Text.Encoding]::BigEndianUnicode.GetString($Bytes)
260
+ }
261
+
262
+ $candidates = New-Object System.Collections.Generic.List[object]
263
+
264
+ $unicodeText = [System.Text.Encoding]::Unicode.GetString($Bytes)
265
+ $candidates.Add([pscustomobject]@{ Name = "utf16le"; Text = $unicodeText; Score = (Get-DecodedTextScore $unicodeText) })
266
+
267
+ $beUnicodeText = [System.Text.Encoding]::BigEndianUnicode.GetString($Bytes)
268
+ $candidates.Add([pscustomobject]@{ Name = "utf16be"; Text = $beUnicodeText; Score = (Get-DecodedTextScore $beUnicodeText) })
269
+
270
+ $utf8Text = [System.Text.Encoding]::UTF8.GetString($Bytes)
271
+ $candidates.Add([pscustomobject]@{ Name = "utf8"; Text = $utf8Text; Score = (Get-DecodedTextScore $utf8Text) })
272
+
273
+ try {
274
+ $consoleEncoding = [Console]::OutputEncoding
275
+ if ($consoleEncoding) {
276
+ $consoleText = $consoleEncoding.GetString($Bytes)
277
+ $candidates.Add([pscustomobject]@{
278
+ Name = "console"
279
+ Text = $consoleText
280
+ Score = (Get-DecodedTextScore $consoleText)
281
+ })
282
+ }
283
+ } catch {
284
+ # Ignore console encoding errors and fall back to other candidates.
285
+ }
286
+
287
+ try {
288
+ $ansiEncoding = [System.Text.Encoding]::GetEncoding([System.Globalization.CultureInfo]::CurrentCulture.TextInfo.ANSICodePage)
289
+ if ($ansiEncoding) {
290
+ $ansiText = $ansiEncoding.GetString($Bytes)
291
+ $candidates.Add([pscustomobject]@{
292
+ Name = "ansi"
293
+ Text = $ansiText
294
+ Score = (Get-DecodedTextScore $ansiText)
295
+ })
296
+ }
297
+ } catch {
298
+ # Ignore ANSI code page lookup errors.
299
+ }
300
+
301
+ $best = $candidates | Sort-Object -Property Score -Descending | Select-Object -First 1
302
+ if ($null -eq $best) {
303
+ return ""
304
+ }
305
+
306
+ return [string]$best.Text
307
+ }
308
+
309
+ function Invoke-WslCommandRawCapture {
310
+ param(
311
+ [Parameter(Mandatory = $true)]
312
+ [string[]]$Arguments
313
+ )
314
+
315
+ $stdoutPath = [System.IO.Path]::GetTempFileName()
316
+ $stderrPath = [System.IO.Path]::GetTempFileName()
317
+ try {
318
+ $process = Start-Process -FilePath "wsl.exe" `
319
+ -ArgumentList $Arguments `
320
+ -NoNewWindow `
321
+ -Wait `
322
+ -PassThru `
323
+ -RedirectStandardOutput $stdoutPath `
324
+ -RedirectStandardError $stderrPath
325
+ $code = $process.ExitCode
326
+
327
+ $stdoutText = Decode-WslOutputBytes (Read-FileBytesSafe $stdoutPath)
328
+ $stderrText = Decode-WslOutputBytes (Read-FileBytesSafe $stderrPath)
329
+ } finally {
330
+ Remove-Item -LiteralPath $stdoutPath -ErrorAction SilentlyContinue
331
+ Remove-Item -LiteralPath $stderrPath -ErrorAction SilentlyContinue
332
+ }
333
+
334
+ $lines = @()
335
+
336
+ foreach ($textBlock in @($stdoutText, $stderrText)) {
337
+ if ([string]::IsNullOrWhiteSpace($textBlock)) {
338
+ continue
339
+ }
340
+ foreach ($item in ($textBlock -split "(`r`n|`n|`r)")) {
341
+ $line = Normalize-WslOutputLine ([string]$item)
342
+ if ($line) {
343
+ $lines += $line
344
+ }
345
+ }
346
+ }
347
+
348
+ return [pscustomobject]@{
349
+ ExitCode = $code
350
+ Output = ($lines -join [Environment]::NewLine)
351
+ }
352
+ }
353
+
354
+ function Invoke-WslCommand {
355
+ param(
356
+ [Parameter(Mandatory = $true)]
357
+ [string[]]$Arguments
358
+ )
359
+
360
+ $previousErrorActionPreference = $ErrorActionPreference
361
+ $nativeCommandPreferenceVar = Get-Variable -Name PSNativeCommandUseErrorActionPreference -ErrorAction SilentlyContinue
362
+ $previousNativeCommandPreference = $null
363
+ if ($nativeCommandPreferenceVar) {
364
+ $previousNativeCommandPreference = [bool]$nativeCommandPreferenceVar.Value
365
+ }
366
+
367
+ try {
368
+ $ErrorActionPreference = "Continue"
369
+ if ($nativeCommandPreferenceVar) {
370
+ $script:PSNativeCommandUseErrorActionPreference = $false
371
+ }
372
+ $rawOutput = & wsl.exe @Arguments 2>&1
373
+ $code = $LASTEXITCODE
374
+ } finally {
375
+ $ErrorActionPreference = $previousErrorActionPreference
376
+ if ($nativeCommandPreferenceVar) {
377
+ $script:PSNativeCommandUseErrorActionPreference = $previousNativeCommandPreference
378
+ }
379
+ }
380
+
381
+ $lines = @()
382
+ foreach ($item in @($rawOutput)) {
383
+ if ($null -eq $item) {
384
+ continue
385
+ }
386
+ if ($item -is [System.Management.Automation.ErrorRecord]) {
387
+ $line = Normalize-WslOutputLine ($item.ToString())
388
+ if ($line) {
389
+ $lines += $line
390
+ }
391
+ continue
392
+ }
393
+ $line = Normalize-WslOutputLine ([string]$item)
394
+ if ($line) {
395
+ $lines += $line
396
+ }
397
+ }
398
+
399
+ return [pscustomobject]@{
400
+ ExitCode = $code
401
+ Output = ($lines -join [Environment]::NewLine)
402
+ }
403
+ }
404
+
405
+ function Resolve-RuntimeArtifactRoot {
406
+ param([Parameter(Mandatory = $true)][string]$ExtractDir)
407
+
408
+ $directBundle = Join-Path $ExtractDir "cli-release.mjs"
409
+ $directInstaller = Join-Path $ExtractDir "install-in-wsl-user.sh"
410
+ if ((Test-Path -LiteralPath $directBundle) -and (Test-Path -LiteralPath $directInstaller)) {
411
+ return $ExtractDir
412
+ }
413
+
414
+ $wslRuntimeDir = Join-Path $ExtractDir "wsl-runtime"
415
+ $nestedBundle = Join-Path $wslRuntimeDir "cli-release.mjs"
416
+ $nestedInstaller = Join-Path $wslRuntimeDir "install-in-wsl-user.sh"
417
+ if ((Test-Path -LiteralPath $nestedBundle) -and (Test-Path -LiteralPath $nestedInstaller)) {
418
+ return $wslRuntimeDir
419
+ }
420
+
421
+ $bundleFile = Get-ChildItem -LiteralPath $ExtractDir -Recurse -File -Filter "cli-release.mjs" -ErrorAction SilentlyContinue | Select-Object -First 1
422
+ if ($bundleFile) {
423
+ $candidateDir = $bundleFile.Directory.FullName
424
+ if (Test-Path -LiteralPath (Join-Path $candidateDir "install-in-wsl-user.sh")) {
425
+ return $candidateDir
426
+ }
427
+ }
428
+
429
+ return $null
430
+ }
431
+
432
+ function Resolve-ConfiguredRuntimeArtifactSource {
433
+ $envUrl = [string]$env:CUE_WSL_RUNTIME_URL
434
+ $envSha256 = [string]$env:CUE_WSL_RUNTIME_SHA256
435
+ if (-not [string]::IsNullOrWhiteSpace($envUrl)) {
436
+ $resolvedEnvSha256 = $null
437
+ if (-not [string]::IsNullOrWhiteSpace($envSha256)) {
438
+ $resolvedEnvSha256 = $envSha256
439
+ }
440
+ return [pscustomobject]@{
441
+ Url = $envUrl
442
+ Sha256 = $resolvedEnvSha256
443
+ Source = "env"
444
+ }
445
+ }
446
+
447
+ $manifestPath = Join-Path $PSScriptRoot "windows-runtime-artifact.json"
448
+ if (-not (Test-Path -LiteralPath $manifestPath)) {
449
+ return $null
450
+ }
451
+
452
+ try {
453
+ $manifestJson = Get-Content -LiteralPath $manifestPath -Raw | ConvertFrom-Json
454
+ } catch {
455
+ Add-Check -Name "runtime_artifact_source" -Status "warn" -Message "Runtime artifact manifest could not be parsed"
456
+ Add-Step "Verify windows-runtime-artifact.json is valid JSON or rerun bootstrap with -RuntimeUrl"
457
+ return $null
458
+ }
459
+
460
+ $entry = $null
461
+ if ($manifestJson.PSObject.Properties.Name -contains "runtimeArtifact") {
462
+ $entry = $manifestJson.runtimeArtifact
463
+ } else {
464
+ $entry = $manifestJson
465
+ }
466
+
467
+ if ($null -eq $entry) {
468
+ return $null
469
+ }
470
+
471
+ $manifestUrl = [string]$entry.url
472
+ if ([string]::IsNullOrWhiteSpace($manifestUrl)) {
473
+ return $null
474
+ }
475
+
476
+ $manifestSha256 = $null
477
+ if ($entry.PSObject.Properties.Name -contains "sha256") {
478
+ $manifestSha256 = [string]$entry.sha256
479
+ if ([string]::IsNullOrWhiteSpace($manifestSha256)) {
480
+ $manifestSha256 = $null
481
+ }
482
+ }
483
+
484
+ return [pscustomobject]@{
485
+ Url = $manifestUrl
486
+ Sha256 = $manifestSha256
487
+ Source = "manifest"
488
+ }
489
+ }
490
+
491
+ function Resolve-LocalRuntimeArtifactPath {
492
+ param([Parameter(Mandatory = $true)][string]$ArtifactPathOrUrl)
493
+
494
+ if ([string]::IsNullOrWhiteSpace($ArtifactPathOrUrl)) {
495
+ return $null
496
+ }
497
+
498
+ $candidates = New-Object System.Collections.Generic.List[string]
499
+ $candidates.Add($ArtifactPathOrUrl)
500
+
501
+ if (-not [System.IO.Path]::IsPathRooted($ArtifactPathOrUrl)) {
502
+ $candidates.Add((Join-Path $PSScriptRoot $ArtifactPathOrUrl))
503
+
504
+ $scriptParent = Split-Path -Parent $PSScriptRoot
505
+ $scriptGrandparent = Split-Path -Parent $scriptParent
506
+ $artifactBaseName = [System.IO.Path]::GetFileName($ArtifactPathOrUrl)
507
+ if (-not [string]::IsNullOrWhiteSpace($artifactBaseName)) {
508
+ # Fallbacks for test-kit deployments where artifacts may be copied to C:\cue-test
509
+ # instead of C:\cue-test\artifacts.
510
+ $candidates.Add((Join-Path $scriptGrandparent $artifactBaseName))
511
+ $candidates.Add((Join-Path (Join-Path $scriptGrandparent "artifacts") $artifactBaseName))
512
+ }
513
+ }
514
+
515
+ foreach ($candidate in $candidates) {
516
+ if ([string]::IsNullOrWhiteSpace($candidate)) {
517
+ continue
518
+ }
519
+ if (Test-Path -LiteralPath $candidate) {
520
+ return [System.IO.Path]::GetFullPath($candidate)
521
+ }
522
+ }
523
+
524
+ return $null
525
+ }
526
+
527
+ function Install-CueFromRuntimeArtifact {
528
+ param(
529
+ [Parameter(Mandatory = $true)][string]$DistroName,
530
+ [Parameter(Mandatory = $true)][string]$ArtifactUrl,
531
+ [string]$ExpectedSha256
532
+ )
533
+
534
+ $downloadDir = Join-Path ([System.IO.Path]::GetTempPath()) ("cue-wsl-runtime-" + [Guid]::NewGuid().ToString("N"))
535
+ New-Item -ItemType Directory -Path $downloadDir -Force | Out-Null
536
+ $zipPath = Join-Path $downloadDir "cue-wsl-runtime.zip"
537
+ $extractDir = Join-Path $downloadDir "extract"
538
+
539
+ $artifactLocalPath = $null
540
+ $isUriLike = ($ArtifactUrl -match '^[a-zA-Z][a-zA-Z0-9+.-]*://')
541
+ if (-not $isUriLike) {
542
+ $artifactLocalPath = Resolve-LocalRuntimeArtifactPath -ArtifactPathOrUrl $ArtifactUrl
543
+ }
544
+
545
+ if ($artifactLocalPath) {
546
+ Write-Step "Using local WSL runtime artifact"
547
+ Copy-Item -LiteralPath $artifactLocalPath -Destination $zipPath -Force
548
+ $BootstrapReport.runtimeArtifact.downloaded = $false
549
+ $BootstrapReport.runtimeArtifact.localPath = $artifactLocalPath
550
+ Add-Check -Name "runtime_artifact_source" -Status "ok" -Message "Using local WSL runtime artifact: $artifactLocalPath"
551
+ } else {
552
+ Write-Step "Downloading WSL runtime artifact"
553
+ try {
554
+ Invoke-WebRequest -Uri $ArtifactUrl -OutFile $zipPath
555
+ } catch {
556
+ Add-Check -Name "runtime_artifact_download" -Status "fail" -Message "Failed to download WSL runtime artifact"
557
+ $BootstrapReport.runtimeArtifact.downloadError = $_.Exception.Message
558
+ throw
559
+ }
560
+ $BootstrapReport.runtimeArtifact.downloaded = $true
561
+ Add-Check -Name "runtime_artifact_download" -Status "ok" -Message "Downloaded WSL runtime artifact"
562
+ }
563
+
564
+ if ($ExpectedSha256) {
565
+ Write-Step "Verifying WSL runtime artifact checksum"
566
+ $actualSha256 = (Get-FileHash -LiteralPath $zipPath -Algorithm SHA256).Hash.ToLowerInvariant()
567
+ $BootstrapReport.runtimeArtifact.actualSha256 = $actualSha256
568
+ if ($actualSha256 -ne $ExpectedSha256.ToLowerInvariant()) {
569
+ Add-Check -Name "runtime_artifact_sha256" -Status "fail" -Message "WSL runtime artifact SHA256 mismatch"
570
+ Add-Step "Re-download the runtime artifact and verify the URL/version"
571
+ throw "Runtime artifact SHA256 mismatch. Expected $ExpectedSha256, got $actualSha256"
572
+ }
573
+ Add-Check -Name "runtime_artifact_sha256" -Status "ok" -Message "WSL runtime artifact SHA256 verified"
574
+ }
575
+
576
+ Write-Step "Extracting WSL runtime artifact"
577
+ New-Item -ItemType Directory -Path $extractDir -Force | Out-Null
578
+ Expand-Archive -Path $zipPath -DestinationPath $extractDir -Force
579
+ $runtimeRoot = Resolve-RuntimeArtifactRoot -ExtractDir $extractDir
580
+ if (-not $runtimeRoot) {
581
+ Add-Check -Name "runtime_artifact_extract" -Status "fail" -Message "WSL runtime artifact layout not recognized"
582
+ Add-Step "Verify artifact contains cli-release.mjs and install-in-wsl-user.sh"
583
+ throw "WSL runtime artifact layout not recognized after extract."
584
+ }
585
+ $BootstrapReport.runtimeArtifact.extractedPath = $runtimeRoot
586
+ Add-Check -Name "runtime_artifact_extract" -Status "ok" -Message "Extracted WSL runtime artifact"
587
+
588
+ $runtimeRootWsl = Convert-WindowsPathToWslPath -WindowsPath $runtimeRoot
589
+ if (-not $runtimeRootWsl) {
590
+ Add-Check -Name "runtime_artifact_path_map" -Status "fail" -Message "Could not map runtime artifact path into WSL"
591
+ throw "Could not map runtime artifact path to WSL: $runtimeRoot"
592
+ }
593
+
594
+ Write-Step "Installing Cue runtime in WSL from artifact"
595
+ $quotedRoot = Quote-BashSingle $runtimeRootWsl
596
+ $installCmd = "cd $quotedRoot && chmod +x ./cli-release.mjs ./install-in-wsl-user.sh && ./install-in-wsl-user.sh"
597
+ $installResult = Invoke-WslCommand -Arguments @("-d", $DistroName, "--", "bash", "-lc", $installCmd)
598
+ $BootstrapReport.runtimeArtifact.installMode = "artifact-user-script"
599
+ if ($installResult.Output) {
600
+ $BootstrapReport.installOutput = $installResult.Output
601
+ if (-not $Json) {
602
+ Write-Host $installResult.Output
603
+ }
604
+ }
605
+ if ($installResult.ExitCode -ne 0) {
606
+ Add-Check -Name "cue_install_artifact" -Status "fail" -Message "Cue runtime artifact install failed in WSL"
607
+ Add-Step "Run the installer script manually inside WSL to inspect errors"
608
+ throw "Cue runtime artifact install failed in WSL."
609
+ }
610
+ Add-Check -Name "cue_install_artifact" -Status "ok" -Message "Cue runtime artifact installed in WSL"
611
+ }
612
+
613
+ function Get-WslCuePath {
614
+ param([Parameter(Mandatory = $true)][string]$DistroName)
615
+
616
+ $cuePathResult = Invoke-WslCommand -Arguments @(
617
+ "-d", $DistroName, "--", "bash", "-lc",
618
+ 'if [ -d "$HOME/.cue/bin" ]; then export PATH="$HOME/.cue/bin:$PATH"; fi; command -v cue || true'
619
+ )
620
+
621
+ if ($cuePathResult.ExitCode -ne 0) {
622
+ return $null
623
+ }
624
+
625
+ $cuePath = $cuePathResult.Output.Trim()
626
+ if ([string]::IsNullOrWhiteSpace($cuePath)) {
627
+ return $null
628
+ }
629
+
630
+ return $cuePath
631
+ }
632
+
633
+ function Test-IsWindowsShimPath {
634
+ param([string]$Value)
635
+ if ([string]::IsNullOrWhiteSpace($Value)) {
636
+ return $false
637
+ }
638
+ return ($Value -match '^/mnt/' -or $Value -match '\.(exe|cmd)$')
639
+ }
640
+
641
+ function Get-WslUserCuePath {
642
+ return '$HOME/.cue/bin/cue'
643
+ }
644
+
645
+ function Get-HostPackageVersion {
646
+ $candidatePaths = @(
647
+ (Join-Path $PSScriptRoot "..\package.json"),
648
+ (Join-Path $PSScriptRoot "..\..\package.json")
649
+ )
650
+
651
+ foreach ($candidate in $candidatePaths) {
652
+ try {
653
+ if (-not (Test-Path -LiteralPath $candidate)) {
654
+ continue
655
+ }
656
+ $raw = Get-Content -LiteralPath $candidate -Raw
657
+ $pkg = $raw | ConvertFrom-Json
658
+ if ($pkg -and $pkg.version) {
659
+ return [string]$pkg.version
660
+ }
661
+ } catch {
662
+ continue
663
+ }
664
+ }
665
+
666
+ return $null
667
+ }
668
+
669
+ function Get-CueSemver {
670
+ param([string]$VersionText)
671
+ if ([string]::IsNullOrWhiteSpace($VersionText)) {
672
+ return $null
673
+ }
674
+ $match = [regex]::Match($VersionText, '\b(\d+\.\d+\.\d+)\b')
675
+ if (-not $match.Success) {
676
+ return $null
677
+ }
678
+ return $match.Groups[1].Value
679
+ }
680
+
681
+ function Add-CueVersionCompatibilityChecks {
682
+ param(
683
+ [string]$HostVersionText,
684
+ [string]$WslCueVersionText,
685
+ [switch]$AfterInstall
686
+ )
687
+
688
+ if ([string]::IsNullOrWhiteSpace($HostVersionText)) {
689
+ Add-Check -Name "cue_version_compat" -Status "warn" -Message "Could not compare host/WSL versions because host package version is unavailable"
690
+ return
691
+ }
692
+
693
+ if ([string]::IsNullOrWhiteSpace($WslCueVersionText)) {
694
+ Add-Check -Name "cue_version_compat" -Status "warn" -Message "Could not compare host/WSL versions because WSL Cue version is unavailable"
695
+ return
696
+ }
697
+
698
+ $hostSemver = Get-CueSemver $HostVersionText
699
+ $wslSemver = Get-CueSemver $WslCueVersionText
700
+ if ([string]::IsNullOrWhiteSpace($hostSemver) -or [string]::IsNullOrWhiteSpace($wslSemver)) {
701
+ Add-Check -Name "cue_version_compat" -Status "warn" -Message "Could not parse host/WSL Cue versions for compatibility check"
702
+ return
703
+ }
704
+
705
+ $BootstrapReport.host.version = $HostVersionText
706
+ $BootstrapReport.cue.hostVersion = $HostVersionText
707
+ $BootstrapReport.cue.hostSemver = $hostSemver
708
+ $BootstrapReport.cue.wslSemver = $wslSemver
709
+
710
+ if ($hostSemver -eq $wslSemver) {
711
+ Add-Check -Name "cue_version_compat" -Status "ok" -Message "Host and WSL Cue versions match ($hostSemver)"
712
+ return
713
+ }
714
+
715
+ if ($AfterInstall) {
716
+ Add-Check -Name "cue_version_compat" -Status "warn" -Message "Host and WSL Cue versions differ after install (host $hostSemver, WSL $wslSemver)"
717
+ Add-Step "Rerun bootstrap with -InstallCue to refresh WSL runtime, or update the Windows host package"
718
+ } else {
719
+ Add-Check -Name "cue_version_compat" -Status "warn" -Message "Host and WSL Cue versions differ (host $hostSemver, WSL $wslSemver)"
720
+ Add-Step "Run 'cue --windows-bootstrap --install-cue' to align the WSL runtime with the host package"
721
+ }
722
+ }
723
+
724
+ Write-Step "Cue Windows (WSL2) bootstrap checks"
725
+
726
+ if (-not (Get-Command wsl.exe -ErrorAction SilentlyContinue)) {
727
+ Add-Check -Name "wsl_exe" -Status "fail" -Message "wsl.exe not found"
728
+ Add-Step "Install WSL2 first, then rerun bootstrap"
729
+ if ($Json) { Complete-Bootstrap -ExitCode 1 -Ok $false }
730
+ Write-Error "wsl.exe not found. Install WSL2 first, then rerun."
731
+ }
732
+ Add-Check -Name "wsl_exe" -Status "ok" -Message "wsl.exe found"
733
+
734
+ $wslStatus = Invoke-WslCommandRawCapture -Arguments @("--status")
735
+ if ($wslStatus.ExitCode -ne 0) {
736
+ Add-Check -Name "wsl_status" -Status "warn" -Message "wsl --status returned non-zero"
737
+ Write-WarnMessage "wsl --status returned non-zero. Continuing with additional checks."
738
+ } else {
739
+ Add-Check -Name "wsl_status" -Status "ok" -Message "wsl --status succeeded"
740
+ }
741
+ if ($wslStatus.Output) {
742
+ $BootstrapReport.wslStatus = $wslStatus.Output
743
+ if (-not $Json) {
744
+ Write-Host $wslStatus.Output
745
+ }
746
+ }
747
+
748
+ Write-Step "Checking WSL distros"
749
+ $distroListResult = Invoke-WslCommandRawCapture -Arguments @("-l", "-q")
750
+ if ($distroListResult.ExitCode -ne 0) {
751
+ $detail = $distroListResult.Output
752
+ Add-Check -Name "wsl_distro_list" -Status "fail" -Message "Failed to list WSL distros"
753
+ Add-Step "Run 'wsl -l -q' in PowerShell to diagnose WSL installation"
754
+ $BootstrapReport.wslDistroListError = $detail
755
+ if ($Json) { Complete-Bootstrap -ExitCode 1 -Ok $false }
756
+ Write-Error "Failed to list WSL distros. Output:`n$detail"
757
+ }
758
+
759
+ $distros = @()
760
+ foreach ($line in ($distroListResult.Output -split "(`r`n|`n|`r)")) {
761
+ $trimmed = Normalize-WslOutputLine $line
762
+ if ($trimmed) {
763
+ $distros += $trimmed
764
+ }
765
+ }
766
+ $BootstrapReport.installedDistros = $distros
767
+
768
+ if ($distros.Count -eq 0) {
769
+ Add-Check -Name "wsl_distro_list" -Status "fail" -Message "No WSL distro installed"
770
+ Write-WarnMessage "No WSL distro is installed."
771
+ Add-Step "Install a distro (recommended): wsl --install -d $Distro"
772
+ if (-not $Json) {
773
+ Write-Host "Install one (recommended):"
774
+ Write-Host " wsl --install -d $Distro"
775
+ }
776
+ Complete-Bootstrap -ExitCode 1 -Ok $false
777
+ }
778
+ Add-Check -Name "wsl_distro_list" -Status "ok" -Message "Found $($distros.Count) installed distro(s)"
779
+
780
+ if (-not $Json) {
781
+ Write-Host "Installed distros:"
782
+ foreach ($name in $distros) {
783
+ Write-Host " - $name"
784
+ }
785
+ }
786
+
787
+ if (-not ($distros -contains $Distro)) {
788
+ Add-Check -Name "supported_distro" -Status "fail" -Message "Supported distro '$Distro' not found"
789
+ Write-WarnMessage "Supported distro '$Distro' not found."
790
+ Add-Step "Install recommended distro: wsl --install -d $Distro"
791
+ if (-not $Json) {
792
+ Write-Host "Recommended install command:"
793
+ Write-Host " wsl --install -d $Distro"
794
+ }
795
+ Complete-Bootstrap -ExitCode 1 -Ok $false
796
+ }
797
+ Add-Check -Name "supported_distro" -Status "ok" -Message "Found supported distro: $Distro"
798
+ Write-Ok "Found supported distro: $Distro"
799
+
800
+ Write-Step "Checking bash in WSL"
801
+ $bashCheck = Invoke-WslCommand -Arguments @("-d", $Distro, "--", "bash", "-lc", "command -v bash >/dev/null && echo bash-ok")
802
+ if ($bashCheck.ExitCode -ne 0 -or -not ($bashCheck.Output -match "bash-ok")) {
803
+ Add-Check -Name "bash" -Status "fail" -Message "bash not available in WSL distro '$Distro'"
804
+ Add-Step "Open WSL distro '$Distro' and ensure bash is installed/runnable"
805
+ if ($Json) { Complete-Bootstrap -ExitCode 1 -Ok $false }
806
+ Write-Error "bash not available in WSL distro '$Distro'."
807
+ }
808
+ Add-Check -Name "bash" -Status "ok" -Message "bash available in WSL distro '$Distro'"
809
+ Write-Ok "bash available"
810
+
811
+ if ($RequireSystemd) {
812
+ Write-Step "Checking systemd"
813
+ $systemdCheck = Invoke-WslCommand -Arguments @("-d", $Distro, "--", "bash", "-lc", "test -d /run/systemd/system && echo systemd-ok")
814
+ if ($systemdCheck.ExitCode -ne 0 -or -not ($systemdCheck.Output -match "systemd-ok")) {
815
+ Add-Check -Name "systemd" -Status "warn" -Message "systemd not detected in '$Distro'"
816
+ Write-WarnMessage "systemd does not appear to be enabled in '$Distro'."
817
+ Add-Step "Enable systemd in WSL ($Distro): edit /etc/wsl.conf with [boot] systemd=true"
818
+ Add-Step "Run from PowerShell: wsl --shutdown"
819
+ Add-Step "Reopen WSL and verify: systemctl --user status"
820
+ if (-not $Json) {
821
+ Write-Host "Cue manager/service parity may be incomplete until systemd is enabled in WSL."
822
+ Write-Host "See docs/windows/support.md for supported environment requirements."
823
+ }
824
+ } else {
825
+ Add-Check -Name "systemd" -Status "ok" -Message "systemd enabled in '$Distro'"
826
+ Write-Ok "systemd enabled"
827
+ }
828
+ }
829
+
830
+ Write-Step "Checking runtime (bun or npm) in WSL"
831
+ $nodePathCheck = Invoke-WslCommand -Arguments @("-d", $Distro, "--", "bash", "-lc", "command -v node || true")
832
+ if ($nodePathCheck.ExitCode -ne 0) {
833
+ Add-Check -Name "node" -Status "fail" -Message "Failed to check node runtime in WSL distro '$Distro'"
834
+ Add-Step "Open WSL and verify Linux Node.js is installed"
835
+ if ($Json) { Complete-Bootstrap -ExitCode 1 -Ok $false }
836
+ Write-Error "Failed to check node runtime in WSL distro '$Distro'."
837
+ }
838
+
839
+ $nodePath = $nodePathCheck.Output.Trim()
840
+ if ([string]::IsNullOrWhiteSpace($nodePath)) {
841
+ Add-Check -Name "node" -Status "fail" -Message "Linux node runtime not found in WSL"
842
+ Write-WarnMessage "Node.js is not installed inside WSL (Linux runtime required to run Cue)."
843
+ Add-Step "Install Linux Node.js/npm in Ubuntu (example): sudo apt update && sudo apt install -y nodejs npm"
844
+ Add-Step "Or install Bun and ensure Linux node is available before rerunning bootstrap"
845
+ if (-not $Json) {
846
+ Write-Host "Install Linux Node.js inside WSL, then rerun bootstrap."
847
+ }
848
+ Complete-Bootstrap -ExitCode 1 -Ok $false
849
+ }
850
+
851
+ if ($nodePath -match '^/mnt/' -or $nodePath -match '\.exe$') {
852
+ Add-Check -Name "node" -Status "fail" -Message "WSL node resolves to Windows path: $nodePath"
853
+ Write-WarnMessage "WSL node resolves to a Windows executable. Cue requires a Linux Node.js runtime inside WSL."
854
+ Add-Step "Install Linux Node.js/npm in Ubuntu (example): sudo apt update && sudo apt install -y nodejs npm"
855
+ Add-Step "Ensure 'command -v node' returns a Linux path (e.g. /usr/bin/node), then rerun bootstrap"
856
+ if (-not $Json) {
857
+ Write-Host "WSL node currently resolves to a Windows path:"
858
+ Write-Host " $nodePath"
859
+ }
860
+ Complete-Bootstrap -ExitCode 1 -Ok $false
861
+ }
862
+
863
+ Add-Check -Name "node" -Status "ok" -Message "Linux node runtime found in WSL: $nodePath"
864
+ Write-Ok "Linux node runtime found"
865
+
866
+ $runtimeCheck = Invoke-WslCommand -Arguments @("-d", $Distro, "--", "bash", "-lc", "if command -v bun >/dev/null; then echo bun; elif command -v npm >/dev/null; then echo npm; else echo none; fi")
867
+ if ($runtimeCheck.ExitCode -ne 0) {
868
+ Add-Check -Name "runtime" -Status "fail" -Message "Failed to check runtime in WSL distro '$Distro'"
869
+ Add-Step "Open WSL and verify Bun or Node/npm is installed"
870
+ if ($Json) { Complete-Bootstrap -ExitCode 1 -Ok $false }
871
+ Write-Error "Failed to check runtime in WSL distro '$Distro'."
872
+ }
873
+
874
+ $runtime = $runtimeCheck.Output.Trim()
875
+ $BootstrapReport.runtime = $runtime
876
+ if ($runtime -eq "none" -or [string]::IsNullOrWhiteSpace($runtime)) {
877
+ Add-Check -Name "runtime" -Status "fail" -Message "Neither bun nor npm found in WSL"
878
+ Write-WarnMessage "Neither bun nor npm was found in WSL."
879
+ Add-Step "Install Bun (recommended) or Node/npm inside WSL, then rerun bootstrap"
880
+ Add-Step "Bun install guide: https://bun.sh/"
881
+ if (-not $Json) {
882
+ Write-Host "Install Bun (recommended) or Node/npm inside WSL, then rerun."
883
+ Write-Host " Bun: https://bun.sh/"
884
+ }
885
+ Complete-Bootstrap -ExitCode 1 -Ok $false
886
+ }
887
+ Add-Check -Name "runtime" -Status "ok" -Message "Found runtime in WSL: $runtime"
888
+ Write-Ok "Found runtime in WSL: $runtime"
889
+
890
+ $hostVersion = Get-HostPackageVersion
891
+ if ($hostVersion) {
892
+ $BootstrapReport.host.version = $hostVersion
893
+ Add-Check -Name "host_version" -Status "ok" -Message "Host Cue package version: $hostVersion"
894
+ } else {
895
+ Add-Check -Name "host_version" -Status "warn" -Message "Could not detect host Cue package version"
896
+ }
897
+
898
+ $resolvedRuntimeArtifact = $null
899
+ if (-not [string]::IsNullOrWhiteSpace($RuntimeUrl)) {
900
+ $resolvedArgSha256 = $null
901
+ if (-not [string]::IsNullOrWhiteSpace($RuntimeSha256)) {
902
+ $resolvedArgSha256 = $RuntimeSha256
903
+ }
904
+ $resolvedRuntimeArtifact = [pscustomobject]@{
905
+ Url = $RuntimeUrl
906
+ Sha256 = $resolvedArgSha256
907
+ Source = "argument"
908
+ }
909
+ } elseif ($InstallCue) {
910
+ $resolvedRuntimeArtifact = Resolve-ConfiguredRuntimeArtifactSource
911
+ }
912
+
913
+ if ($resolvedRuntimeArtifact) {
914
+ $RuntimeUrl = [string]$resolvedRuntimeArtifact.Url
915
+ if ([string]::IsNullOrWhiteSpace($RuntimeSha256) -and $resolvedRuntimeArtifact.Sha256) {
916
+ $RuntimeSha256 = [string]$resolvedRuntimeArtifact.Sha256
917
+ }
918
+ $BootstrapReport.runtimeArtifact.url = $RuntimeUrl
919
+ if ($RuntimeSha256) {
920
+ $BootstrapReport.runtimeArtifact.sha256 = $RuntimeSha256
921
+ }
922
+ $BootstrapReport.runtimeArtifact.source = [string]$resolvedRuntimeArtifact.Source
923
+ Add-Check -Name "runtime_artifact_source" -Status "ok" -Message "Resolved runtime artifact source ($($resolvedRuntimeArtifact.Source))"
924
+ if (-not $Json) {
925
+ Write-Host "Resolved runtime artifact source ($($resolvedRuntimeArtifact.Source)): $RuntimeUrl"
926
+ }
927
+ }
928
+
929
+ Write-Step "Checking Cue in WSL"
930
+ $userCuePath = Get-WslUserCuePath
931
+ $userCueVersionProbe = Invoke-WslCommand -Arguments @("-d", $Distro, "--", "bash", "-lc", 'if [ -e "$HOME/.cue/bin/cue" ]; then "$HOME/.cue/bin/cue" --version; else exit 3; fi')
932
+ $cueUserPathRunnable = $false
933
+ $cueVersionText = $null
934
+ if ($userCueVersionProbe.ExitCode -eq 0) {
935
+ $cueUserPathRunnable = $true
936
+ if (-not [string]::IsNullOrWhiteSpace($userCueVersionProbe.Output)) {
937
+ $cueVersionText = $userCueVersionProbe.Output.Trim()
938
+ }
939
+ Add-Check -Name "cue_user_path" -Status "ok" -Message "Cue runtime found at ~/.cue/bin/cue"
940
+ } else {
941
+ $userCueNodeProbe = Invoke-WslCommand -Arguments @("-d", $Distro, "--", "bash", "-lc", 'if [ -e "$HOME/.cue/bin/cue" ]; then node "$HOME/.cue/bin/cue" --version; else exit 3; fi')
942
+ if ($userCueNodeProbe.ExitCode -eq 0) {
943
+ $cueUserPathRunnable = $true
944
+ if (-not [string]::IsNullOrWhiteSpace($userCueNodeProbe.Output)) {
945
+ $cueVersionText = $userCueNodeProbe.Output.Trim()
946
+ }
947
+ Add-Check -Name "cue_user_path" -Status "ok" -Message "Cue runtime found at ~/.cue/bin/cue (node fallback)"
948
+ }
949
+ }
950
+
951
+ $cuePath = Get-WslCuePath -DistroName $Distro
952
+ $cuePathIsWindowsShim = Test-IsWindowsShimPath -Value $cuePath
953
+ if ($cuePath) {
954
+ if ($cuePathIsWindowsShim) {
955
+ Add-Check -Name "cue_path" -Status "warn" -Message "WSL cue resolves to a Windows command: $cuePath"
956
+ Write-WarnMessage "WSL cue resolves to a Windows command: $cuePath"
957
+ Add-Step "Install Linux Cue in WSL or use bootstrap runtime artifact install; prefer ~/.cue/bin on PATH"
958
+ } else {
959
+ Add-Check -Name "cue_path" -Status "ok" -Message "Linux cue found in WSL: $cuePath"
960
+ }
961
+ }
962
+
963
+ if ([string]::IsNullOrWhiteSpace($cueVersionText) -and $cuePath -and -not $cuePathIsWindowsShim) {
964
+ $cueVersion = Invoke-WslCommand -Arguments @("-d", $Distro, "--", "bash", "-lc", 'export PATH="$HOME/.cue/bin:$PATH"; cue --version')
965
+ if ($cueVersion.ExitCode -eq 0 -and -not [string]::IsNullOrWhiteSpace($cueVersion.Output)) {
966
+ $cueVersionText = $cueVersion.Output.Trim()
967
+ }
968
+ }
969
+
970
+ $shouldInstallFromArtifact = [bool]$InstallCue -and (-not [string]::IsNullOrWhiteSpace($RuntimeUrl))
971
+ if ($cueUserPathRunnable -or -not [string]::IsNullOrWhiteSpace($cueVersionText)) {
972
+ if ($shouldInstallFromArtifact) {
973
+ Add-Check -Name "cue" -Status "ok" -Message "Cue already installed in WSL (will install/update from runtime artifact)"
974
+ } else {
975
+ if ([string]::IsNullOrWhiteSpace($cueVersionText)) {
976
+ Add-Check -Name "cue" -Status "ok" -Message "Cue already installed in WSL (version text unavailable)"
977
+ } else {
978
+ Add-Check -Name "cue" -Status "ok" -Message "Cue already installed in WSL"
979
+ }
980
+ }
981
+ $BootstrapReport.cue.installed = $true
982
+ $BootstrapReport.cue.version = $cueVersionText
983
+ Add-CueVersionCompatibilityChecks -HostVersionText $hostVersion -WslCueVersionText $cueVersionText
984
+ if ($shouldInstallFromArtifact) {
985
+ $BootstrapReport.cue.previousVersion = $cueVersionText
986
+ Write-Ok "Cue already installed in WSL (runtime artifact install/update requested)"
987
+ Add-Step "Proceeding with runtime artifact install/update because -InstallCue and a runtime artifact source are available"
988
+ if (-not $Json) {
989
+ Write-Host "Proceeding with runtime artifact install/update..."
990
+ }
991
+ } else {
992
+ Write-Ok "Cue already installed in WSL"
993
+ }
994
+ if (-not $Json -and $cueVersionText) {
995
+ Write-Host $cueVersionText
996
+ }
997
+ if (-not $shouldInstallFromArtifact) {
998
+ Complete-Bootstrap -ExitCode 0 -Ok $true
999
+ }
1000
+ }
1001
+
1002
+ if (-not $InstallCue) {
1003
+ Add-Check -Name "cue" -Status "fail" -Message "Cue not installed in WSL"
1004
+ Write-WarnMessage "Cue is not installed in WSL yet."
1005
+ Add-Step "Install Cue inside WSL and rerun bootstrap"
1006
+ if ($RuntimeUrl) {
1007
+ Add-Step "Or rerun bootstrap with -InstallCue (runtime artifact source is already configured)"
1008
+ Add-Step "Or rerun bootstrap with -InstallCue -RuntimeUrl <artifact-url>"
1009
+ if (-not $Json) {
1010
+ Write-Host "Install via runtime artifact:"
1011
+ Write-Host " cue --windows-bootstrap --install-cue"
1012
+ Write-Host " # or explicit override:"
1013
+ Write-Host " cue --windows-bootstrap --install-cue --runtime-url <artifact-url>"
1014
+ Write-Host ""
1015
+ }
1016
+ } else {
1017
+ Add-Step "Or rerun bootstrap with -InstallCue to auto-install using the configured runtime source (if present)"
1018
+ }
1019
+ if ($runtime -eq "bun") {
1020
+ $installCmd = "wsl -d $Distro -- bash -lc 'bun add -g @plusonelabs/cue'"
1021
+ } else {
1022
+ $installCmd = "wsl -d $Distro -- bash -lc 'npm install -g @plusonelabs/cue'"
1023
+ }
1024
+ Add-Step $installCmd
1025
+ if (-not $Json) {
1026
+ Write-Host "Install inside WSL and rerun:"
1027
+ Write-Host " $installCmd"
1028
+ Write-Host ""
1029
+ Write-Host "Or run this script with -InstallCue to install automatically."
1030
+ }
1031
+ Add-Step "Or rerun bootstrap with -InstallCue"
1032
+ Complete-Bootstrap -ExitCode 1 -Ok $false
1033
+ }
1034
+
1035
+ Write-Step "Installing Cue in WSL"
1036
+ if ($RuntimeUrl) {
1037
+ try {
1038
+ Install-CueFromRuntimeArtifact -DistroName $Distro -ArtifactUrl $RuntimeUrl -ExpectedSha256 $RuntimeSha256
1039
+ } catch {
1040
+ if (-not $Json) {
1041
+ Write-Error $_
1042
+ }
1043
+ if ($Json) { Complete-Bootstrap -ExitCode 1 -Ok $false }
1044
+ exit 1
1045
+ }
1046
+ } else {
1047
+ if ($runtime -eq "bun") {
1048
+ $installResult = Invoke-WslCommand -Arguments @("-d", $Distro, "--", "bash", "-lc", "bun add -g @plusonelabs/cue")
1049
+ } else {
1050
+ $installResult = Invoke-WslCommand -Arguments @("-d", $Distro, "--", "bash", "-lc", "npm install -g @plusonelabs/cue")
1051
+ }
1052
+
1053
+ if ($installResult.Output) {
1054
+ $BootstrapReport.installOutput = $installResult.Output
1055
+ if (-not $Json) {
1056
+ Write-Host $installResult.Output
1057
+ }
1058
+ }
1059
+ if ($installResult.ExitCode -ne 0) {
1060
+ Add-Check -Name "cue_install" -Status "fail" -Message "Cue install failed in WSL"
1061
+ Add-Step "Run the install command manually inside WSL to inspect errors"
1062
+ if ($Json) { Complete-Bootstrap -ExitCode 1 -Ok $false }
1063
+ Write-Error "Cue install failed in WSL."
1064
+ }
1065
+ Add-Check -Name "cue_install" -Status "ok" -Message "Cue install command succeeded in WSL"
1066
+ }
1067
+
1068
+ $userCueVersionAfter = Invoke-WslCommand -Arguments @("-d", $Distro, "--", "bash", "-lc", "$userCuePath --version")
1069
+ if ($userCueVersionAfter.ExitCode -ne 0) {
1070
+ Add-Check -Name "cue_verify_user_path" -Status "fail" -Message "Installed Cue runtime not runnable at ~/.cue/bin/cue after install"
1071
+ Add-Step "Inspect ~/.cue/bin/cue and ~/.cue/runtime in WSL"
1072
+ if ($Json) { Complete-Bootstrap -ExitCode 1 -Ok $false }
1073
+ Write-Error "Installed Cue runtime not runnable at ~/.cue/bin/cue after install."
1074
+ }
1075
+ Add-Check -Name "cue_verify_user_path" -Status "ok" -Message "Installed Cue runtime runnable at ~/.cue/bin/cue"
1076
+
1077
+ $cuePathAfter = Get-WslCuePath -DistroName $Distro
1078
+ if ([string]::IsNullOrWhiteSpace($cuePathAfter)) {
1079
+ Add-Check -Name "cue_verify_path" -Status "warn" -Message "Cue not found on default WSL PATH after install"
1080
+ Add-Step "Add ~/.cue/bin to PATH in WSL shell rc for direct WSL terminal usage"
1081
+ Write-WarnMessage "Cue is installed at ~/.cue/bin/cue but not on default WSL PATH."
1082
+ } elseif (Test-IsWindowsShimPath -Value $cuePathAfter) {
1083
+ Add-Check -Name "cue_verify_path" -Status "warn" -Message "Default WSL PATH resolves cue to Windows command: $cuePathAfter"
1084
+ Add-Step "Add ~/.cue/bin to PATH before Windows-inherited paths in WSL for direct WSL terminal usage"
1085
+ Add-Step "Windows wrapper still works because it prefers ~/.cue/bin automatically"
1086
+ Write-WarnMessage "Default WSL PATH resolves cue to a Windows command after install: $cuePathAfter"
1087
+ } else {
1088
+ Add-Check -Name "cue_verify_path" -Status "ok" -Message "Default WSL PATH resolves cue to Linux path after install: $cuePathAfter"
1089
+ }
1090
+
1091
+ $cueVersionAfter = Invoke-WslCommand -Arguments @("-d", $Distro, "--", "bash", "-lc", 'export PATH="$HOME/.cue/bin:$PATH"; cue --version')
1092
+ if ($cueVersionAfter.ExitCode -ne 0) {
1093
+ Add-Check -Name "cue_verify" -Status "fail" -Message "Cue not runnable in WSL after install"
1094
+ if ($Json) { Complete-Bootstrap -ExitCode 1 -Ok $false }
1095
+ Write-Error "Cue still not runnable in WSL after install."
1096
+ }
1097
+
1098
+ Add-Check -Name "cue_verify" -Status "ok" -Message "Cue installed and runnable in WSL"
1099
+ $BootstrapReport.cue.installed = $true
1100
+ $resolvedInstalledCueVersion = $cueVersionAfter.Output.Trim()
1101
+ if ([string]::IsNullOrWhiteSpace($resolvedInstalledCueVersion) -and -not [string]::IsNullOrWhiteSpace($userCueVersionAfter.Output)) {
1102
+ $resolvedInstalledCueVersion = $userCueVersionAfter.Output.Trim()
1103
+ }
1104
+ $BootstrapReport.cue.version = $resolvedInstalledCueVersion
1105
+ Add-CueVersionCompatibilityChecks -HostVersionText $hostVersion -WslCueVersionText $resolvedInstalledCueVersion -AfterInstall
1106
+ Write-Ok "Cue installed and runnable in WSL"
1107
+ Add-Step "Next: run 'cue' from Windows (wrapper) or inside WSL"
1108
+ if (-not $Json) {
1109
+ Write-Host $cueVersionAfter.Output
1110
+ Write-Host ""
1111
+ Write-Host "Next: run 'cue' from Windows (wrapper) or inside WSL."
1112
+ }
1113
+ Complete-Bootstrap -ExitCode 0 -Ok $true