@@ -147,6 +147,50 @@ function Need-Value([string]$Current, [string]$Prompt, [string]$VarName, [switch
147147 return (Read-Host - Prompt $Prompt )
148148}
149149
150+ # Unicode scalar count — matches stitch-panel's chars().count() (not UTF-16 .Length).
151+ function Get-PasswordCharCount ([string ]$Password ) {
152+ $n = 0
153+ for ($i = 0 ; $i -lt $Password.Length ; $i ++ ) {
154+ $n ++
155+ if (
156+ [char ]::IsHighSurrogate($Password [$i ]) -and
157+ ($i + 1 ) -lt $Password.Length -and
158+ [char ]::IsLowSurrogate($Password [$i + 1 ])
159+ ) {
160+ $i ++
161+ }
162+ }
163+ return $n
164+ }
165+
166+ function Need-Password ([string ]$Current , [string ]$Prompt , [string ]$VarName ) {
167+ if ($Current ) {
168+ $len = Get-PasswordCharCount $Current
169+ if ($len -lt 12 ) {
170+ Die " $VarName must be at least 12 characters (got $len )"
171+ }
172+ return $Current
173+ }
174+ if (-not [Environment ]::UserInteractive) {
175+ Die " $VarName is not set and there is no interactive session to ask on. Set it and re-run."
176+ }
177+ while ($true ) {
178+ $pw = Read-Secret $Prompt
179+ if (-not $pw ) { Die ' a panel password is required' }
180+ $len = Get-PasswordCharCount $pw
181+ if ($len -lt 12 ) {
182+ Write-Warn " need at least 12 characters (got $len ). Try again."
183+ continue
184+ }
185+ $again = Read-Secret ' Again'
186+ if ($pw -cne $again ) {
187+ Write-Warn " those didn't match. Try again."
188+ continue
189+ }
190+ return $pw
191+ }
192+ }
193+
150194function Fetch-File ([string ]$Url , [string ]$Dest ) {
151195 try {
152196 Invoke-WebRequest - Uri $Url - OutFile $Dest - UseBasicParsing
@@ -156,14 +200,61 @@ function Fetch-File([string]$Url, [string]$Dest) {
156200}
157201
158202# ---------------------------------------------------------------- preflight ---
203+ # Plain-language Docker help — many Windows operators have never used containers.
204+ # Use Write-Host (not Write-Error): $ErrorActionPreference=Stop would otherwise
205+ # abort before the "what to do" steps print.
206+ function Explain-Docker ([ValidateSet (' missing' , ' compose' , ' not_running' )][string ]$Reason ) {
207+ Write-Host ' '
208+ switch ($Reason ) {
209+ ' missing' {
210+ Write-Host ' error: Docker is not installed on this computer.'
211+ Write-Host ' '
212+ Write-Host ' Stitch runs inside Docker — a free app that hosts the web panel.'
213+ Write-Host ' Install it once, then re-run the same install command.'
214+ Write-Host ' '
215+ Write-Host ' What to do:'
216+ Write-Host ' 1. Download and install Docker Desktop for Windows:'
217+ Write-Host ' https://docs.docker.com/desktop/setup/install/windows-install/'
218+ Write-Host ' 2. Open Docker Desktop from the Start menu and wait until it says'
219+ Write-Host ' Docker is running (whale icon in the system tray is steady).'
220+ Write-Host ' 3. Open a new PowerShell window and re-run:'
221+ Write-Host ' irm https://raw.githubusercontent.com/textile-protocol/textile-stitch/main/install-panel.ps1 | iex'
222+ }
223+ ' compose' {
224+ Write-Host ' error: Docker Compose v2 is missing.'
225+ Write-Host ' '
226+ Write-Host ' The docker command is present, but docker compose does not work.'
227+ Write-Host ' Stitch needs Compose v2. Update or reinstall Docker Desktop, then re-run.'
228+ Write-Host ' https://docs.docker.com/desktop/setup/install/windows-install/'
229+ }
230+ ' not_running' {
231+ Write-Host ' error: Docker is installed but not running.'
232+ Write-Host ' '
233+ Write-Host ' The panel cannot start until Docker Desktop is up.'
234+ Write-Host ' '
235+ Write-Host ' What to do:'
236+ Write-Host ' 1. Open Docker Desktop from the Start menu.'
237+ Write-Host ' 2. Wait until it finishes starting (system-tray whale icon steady /'
238+ Write-Host ' Docker Desktop is running).'
239+ Write-Host ' 3. Re-run the install command in PowerShell.'
240+ }
241+ }
242+ Write-Host ' '
243+ exit 1
244+ }
245+
159246Write-Step ' Checking Docker'
160247if (-not (Get-Command docker - ErrorAction SilentlyContinue)) {
161- Die ' Docker is not installed. Install Docker Desktop for Windows: https://docs.docker.com/desktop/setup/install/windows-install/ '
248+ Explain - Docker missing
162249}
163250docker compose version | Out-Null
164- Assert-LastExitOk ' Docker Compose v2 is missing. `docker compose version` has to work.'
251+ if ($null -ne $LASTEXITCODE -and $LASTEXITCODE -ne 0 ) {
252+ Explain- Docker compose
253+ }
165254docker info | Out-Null
166- Assert-LastExitOk ' the Docker daemon is not reachable. Start Docker Desktop and re-run.'
255+ if ($null -ne $LASTEXITCODE -and $LASTEXITCODE -ne 0 ) {
256+ Explain- Docker not_running
257+ }
167258Write-Say ' Docker and Compose v2 are ready.'
168259
169260# Windows Docker Desktop runs Linux containers. Tailscale server compose needs
@@ -287,11 +378,29 @@ if (-not $ReuseEnv) {
287378}
288379
289380function Add-Password ([string ]$Password , [string ]$Label = ' password' ) {
290- $hash = $Password | docker run -- rm - i $PanelImage hash- password 2> $null
291- Assert-LastExitOk ' hashing the password failed'
292- if (-not $hash ) { Die ' hashing the password failed' }
293- $hash = ($hash | Select-Object - Last 1 ).ToString().Trim()
294- if (-not $hash ) { Die ' the panel returned an empty password hash' }
381+ $len = Get-PasswordCharCount $Password
382+ if ($len -lt 12 ) {
383+ Die " password must be at least 12 characters (got $len )"
384+ }
385+
386+ # Keep stderr: the binary's real reason used to disappear into $null.
387+ $errFile = [System.IO.Path ]::GetTempFileName()
388+ try {
389+ $hash = $Password | docker run -- rm - i $PanelImage hash- password 2> $errFile
390+ if ($null -ne $LASTEXITCODE -and $LASTEXITCODE -ne 0 ) {
391+ $msg = @ (Get-Content - LiteralPath $errFile - ErrorAction SilentlyContinue |
392+ Where-Object { $_.Trim () } |
393+ ForEach-Object { $_ -replace ' ^Error:\s*' , ' ' } |
394+ Select-Object - Last 1 ) -join ' '
395+ if ($msg ) { Die " hashing the password failed: $msg " }
396+ Die ' hashing the password failed'
397+ }
398+ $hashLine = @ ($hash ) | Where-Object { $_ -match ' ^\$argon2' } | Select-Object - First 1
399+ if (-not $hashLine ) { Die ' the panel returned no argon2 password hash' }
400+ $hash = $hashLine.ToString ().Trim()
401+ } finally {
402+ Remove-Item - LiteralPath $errFile - Force - ErrorAction SilentlyContinue
403+ }
295404
296405 $lines = @ ()
297406 if (Test-Path - LiteralPath $EnvFile ) {
@@ -313,8 +422,7 @@ if (-not (Get-EnvFileValue $EnvFile 'PANEL_PASSWORD_HASH')) {
313422 if (-not $env: PANEL_PASSWORD ) {
314423 Write-Say ' This install is loopback-only. You log in with a password you choose now.'
315424 }
316- $password = Need- Value $env: PANEL_PASSWORD ' Panel password (12+ characters, not shown)' ' PANEL_PASSWORD' - Secret
317- if (-not $password ) { Die ' a panel password is required for a local install' }
425+ $password = Need- Password $env: PANEL_PASSWORD ' Panel password (12+ characters, not shown)' ' PANEL_PASSWORD'
318426 Add-Password $password ' panel password'
319427} elseif ($env: PANEL_PASSWORD ) {
320428 Write-Step ' Updating the panel password'
0 commit comments