# --- Author: zetod1ce (github.com/ztd38f) --- # # --- DISCLAIMER: Provided as-is, without warranties. For educational and testing use only in controlled environments. Use at your own risk. --- # # -- ANSI Colors -- # $ESC = [char]27 $C_RESET = "$ESC[0m" $C_BOLD = "$ESC[1m" $C_CYAN = "$ESC[96m" $C_BLUE = "$ESC[94m" $C_MAG = "$ESC[95m" $C_GREEN = "$ESC[92m" $C_YEL = "$ESC[93m" $C_RED = "$ESC[91m" $C_GRAY = "$ESC[90m" $C_WHITE = "$ESC[97m" # -- Cursor Helpers -- # function Cursor.Set([int]$x, [int]$y) { try {[Console]::SetCursorPosition($x, $y)} catch {} } function Cursor.Hide { try {[Console]::CursorVisible = $false} catch {} } function Cursor.Show { try {[Console]::CursorVisible = $true} catch {} } # -- Set Console Encoding -- # [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 $OutputEncoding = [System.Text.Encoding]::UTF8 try {[Console]::InputEncoding = [System.Text.Encoding]::UTF8} catch {} # -- Set CLI UI -- # iex (irm -useb psui.pages.dev) PS.UI "CF-PagesRoute [github.com/ztd38f]" # -- Helper to Draw Beautiful Framed Boxes -- # function UI.Box([string]$text, [string]$color = "Magenta", [int]$width = 38) { $ansi = switch ($color) { "Cyan" {$C_CYAN}; "Blue" {$C_BLUE}; "Green" {$C_GREEN} "Yellow" {$C_YEL}; "Red" {$C_RED}; "Magenta" {$C_MAG} default {$C_CYAN} } $width = [math]::Max($width, $text.Length + 4) $spaces = $width - $text.Length $padded = " $text" + (" " * ($spaces - 1)) $border = "─" * $width Write-Host "" Write-Host " ${ansi}╭${border}╮${C_RESET}" Write-Host " ${ansi}│${C_RESET}${C_BOLD}${padded}${C_RESET}${ansi}│${C_RESET}" Write-Host " ${ansi}╰${border}╯${C_RESET}" Write-Host "" } function UI.BoxGradient([string]$text, [int]$offset, [int]$width = 38, [string]$theme = "Cyan") { $width = [math]::Max($width, $text.Length + 4) $total = $width + 2 + 1 + $width + 2 + 1 function Get-Color([double]$p, [string]$theme) { $t = if ($p -lt 0.5) {$p * 2.0} else {($p - 0.5) * 2.0} if ($theme -eq "Red") { if ($p -lt 0.5) {$c1 = 255,77,77; $c2 = 153,27,27} else {$c1 = 153,27,27; $c2 = 255,77,77} } elseif ($theme -eq "Green") { if ($p -lt 0.5) {$c1 = 34,197,94; $c2 = 20,184,166} else {$c1 = 20,184,166; $c2 = 34,197,94} } elseif ($theme -eq "Yellow") { if ($p -lt 0.5) {$c1 = 250,204,21; $c2 = 249,115,22} else {$c1 = 249,115,22; $c2 = 250,204,21} } else { if ($p -lt 0.5) {$c1 = 34,211,238; $c2 = 37,99,235} else {$c1 = 37,99,235; $c2 = 34,211,238} } $r = [int]($c1[0] + $t * ($c2[0] - $c1[0])) $g = [int]($c1[1] + $t * ($c2[1] - $c1[1])) $b = [int]($c1[2] + $t * ($c2[2] - $c1[2])) return "$([char]27)[38;2;$r;$g;${b}m" } $sbTop = [System.Text.StringBuilder]::new(" ") $sbBot = [System.Text.StringBuilder]::new(" ") for ($k = 0; $k -lt ($width + 2); $k++) { $pTop = (($k + $offset) % $total) / $total $cTop = if ($k -eq 0) {"╭"} elseif ($k -eq $width + 1) {"╮"} else {"─"} $null = $sbTop.Append("$(Get-Color $pTop $theme)$cTop") $pBot = ((($total - 2 - $k) + $offset) % $total) / $total $cBot = if ($k -eq 0) {"╰"} elseif ($k -eq $width + 1) {"╯"} else {"─"} $null = $sbBot.Append("$(Get-Color $pBot $theme)$cBot") } $pLeft = ((( $total - 1 ) + $offset) % $total) / $total $midLeft = "$(Get-Color $pLeft $theme)│$([char]27)[0m" $pRight = ((( $width + 2 ) + $offset) % $total) / $total $midRight = "$(Get-Color $pRight $theme)│$([char]27)[0m" $null = $sbTop.Append("$([char]27)[0m") $null = $sbBot.Append("$([char]27)[0m") $spaces = $width - $text.Length $padded = " $text" + (" " * ($spaces - 1)) $frame = $sbTop.ToString() + "`n ${midLeft}$([char]27)[1m${padded}$([char]27)[0m${midRight}`n" + $sbBot.ToString() + "`n" [Console]::Write($frame) } function Show-Box([string]$text, [string]$color = "Magenta", [int]$width = 38) { UI.Box $text $color $width } # -- Config -- # $accountSubdomain = $null $script:proxyUrl = $null $tempDir = Join-Path $env:TEMP "CF-PagesRoute" try {if (!(Test-Path $tempDir)) {ni -ItemType Directory -Path $tempDir -force | Out-Null}} catch { $tempDir = Join-Path $env:ProgramData "CF-PagesRoute" if (!(Test-Path $tempDir)) {ni -ItemType Directory -Path $tempDir -force | Out-Null} Write-Status "Using fallback temp: $tempDir" "warn" } # -- Cloudflare API Core -- # function Get-WranglerToken { $tomlPath = "$env:USERPROFILE\.wrangler\config\default.toml" if (!(Test-Path $tomlPath)) {return $null} try { $cfg = Get-Content $tomlPath -Raw if ($cfg -match 'oauth_token\s*=\s*"([^"]+)"') {return $matches[1]} if ($cfg -match 'api_token\s*=\s*"([^"]+)"') {return $matches[1]} } catch {} return $null } function Invoke-CfApi { param([string]$Endpoint, [string]$Method = "GET", $Body = $null, [switch]$ReturnFullResponse) $token = Get-WranglerToken if (!$token) { if ($ReturnFullResponse) {return @{StatusCode = 401}} throw "No token" } $url = "https://api.cloudflare.com/client/v4/$Endpoint" $headers = @{Authorization = "Bearer $token"} $params = @{Uri = $url; Method = $Method; Headers = $headers; ErrorAction = "Stop"} if ($Body) {$params.Body = $Body; $params.ContentType = "application/json"} try { $resp = Invoke-RestMethod @params if ($ReturnFullResponse) {return @{StatusCode = 200; Data = $resp}} return $resp } catch { $statusCode = 500 if ($_.Exception.Response) {$statusCode = [int]$_.Exception.Response.StatusCode} if ($statusCode -eq 401 -or $statusCode -eq 403) {$script:authExpired = $true} if ($ReturnFullResponse) {return @{StatusCode = $statusCode; Error = $_.Exception.Message}} throw $_ } } function Get-AccountId { $cacheFile = "$env:USERPROFILE\.wrangler\account_id.txt" if (Test-Path $cacheFile) { try { $id = (Get-Content $cacheFile -Raw).Trim() if ($id) {return $id} } catch {} } try { $resp = Invoke-CfApi "accounts" $id = $resp.result[0].id if ($id) { $null = ni -ItemType File -Path $cacheFile -Force -ea SilentlyContinue [System.IO.File]::WriteAllText($cacheFile, $id) return $id } } catch {} return $null } function Get-AccountSubdomain { $cacheFile = "$env:USERPROFILE\.wrangler\subdomain.txt" if (Test-Path $cacheFile) { try { $sub = (Get-Content $cacheFile -Raw).Trim() if ($sub) {return $sub} } catch {} } $accountId = Get-AccountId if (!$accountId) {return $null} try { $resp = Invoke-CfApi "accounts/$accountId/workers/subdomain" $sub = $resp.result.subdomain if ($sub) { $null = ni -ItemType File -Path $cacheFile -Force -ea SilentlyContinue [System.IO.File]::WriteAllText($cacheFile, $sub) return $sub } } catch {} return $null } function Register-Subdomain([string]$sub) { $accountId = Get-AccountId if (!$accountId) {return $false} try { $body = @{subdomain=$sub} | ConvertTo-Json $resp = Invoke-CfApi "accounts/$accountId/workers/subdomain" -Method "PUT" -Body $body return ($resp.success -eq $true) } catch {return $false} } function Clear-WranglerCache { Remove-Item "$env:USERPROFILE\.wrangler\subdomain.txt" -Force -ea 0 Remove-Item "$env:USERPROFILE\.wrangler\account_id.txt" -Force -ea 0 Remove-Item "$env:USERPROFILE\.wrangler\proxy_url.txt" -Force -ea 0 Remove-Item "$env:USERPROFILE\.wrangler\config\default.toml" -Force -ea 0 Remove-Item "$env:USERPROFILE\.config\wrangler\config\default.toml" -Force -ea 0 } function Get-ProxyUrl { $cacheFile = "$env:USERPROFILE\.wrangler\proxy_url.txt" if (Test-Path $cacheFile) { try { $url = (Get-Content $cacheFile -Raw).Trim() if ($url) {return $url} } catch {} } return $null } function Set-ProxyUrl([string]$url) { $cacheFile = "$env:USERPROFILE\.wrangler\proxy_url.txt" try { $null = ni -ItemType File -Path $cacheFile -Force -ea SilentlyContinue [System.IO.File]::WriteAllText($cacheFile, $url) $script:proxyUrl = $url } catch {} } function Ask-Confirm([string]$prompt, [string]$color, [int]$indent = 4) { $frames = "⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏" $i = 0 Cursor.Hide $spinIndentStr = " " * $indent $succIndentStr = " " * ($indent - 1) # Initial Draw [Console]::Write("`r$([char]27)[2K$spinIndentStr${C_CYAN} ${C_RESET} ") Write-Host "$prompt [Y/N]: " -n -f $color while ($true) { if ([Console]::KeyAvailable) { $key = [Console]::ReadKey($true) switch ($key.Key) { {$_ -in 'Enter', 'Y'} { [Console]::Write("`r$([char]27)[2K$succIndentStr${C_GREEN}[✓]${C_RESET} ") Write-Host "$prompt [Y/N]: Yes" -f Green Cursor.Show return $true } 'Escape' { [Console]::Write("`r$([char]27)[2K`n") Cursor.Show return $null } {$_ -in 'Backspace', 'N'} { [Console]::Write("`r$([char]27)[2K$succIndentStr${C_RED}[✗]${C_RESET} ") Write-Host "$prompt [Y/N]: No" -f Red Cursor.Show return $false } } } else { $f = $frames[$i % $frames.Length] $top = [Console]::CursorTop Cursor.Set $indent $top [Console]::Write("${C_CYAN}${f}${C_RESET}") Start-Sleep -Milliseconds 80 $i++ } } } # -- Status Helper -- # function UI.Status([string]$msg, [string]$type = "info") { switch ($type) { "success" {Write-Host " ${C_GREEN}[✓] ${msg}${C_RESET}"} "ok" {Write-Host " ${C_GREEN}[✓] ${msg}${C_RESET}"} "warn" {Write-Host " ${C_CYAN}[!] ${msg}${C_RESET}"} "error" {Write-Host " ${C_RED}[✗] ${msg}${C_RESET}"} "err" {Write-Host " ${C_RED}[✗] ${msg}${C_RESET}"} "info" {Write-Host " ${C_CYAN}[+] ${msg}${C_RESET}"} "loading" {Write-Host " ${C_BLUE}[*] ${msg}${C_RESET}"} "proc" {Write-Host " ${C_BLUE}[*] ${msg}${C_RESET}"} "off" {Write-Host " ${C_GRAY}[-] ${msg}${C_RESET}"} default {Write-Host " $msg"} } } function Write-Status([string]$msg, [string]$type = "info") { UI.Status $msg $type } # -- Background Gradient Box Helpers -- # function UI.Start-GradientBox([string]$text, [string]$theme = "Cyan") { $sb = { param($text, $theme) function Get-Color([double]$p, [string]$theme) { $t = if ($p -lt 0.5) {$p * 2.0} else {($p - 0.5) * 2.0} if ($theme -eq "Red") { if ($p -lt 0.5) {$c1 = 255,77,77; $c2 = 153,27,27} else {$c1 = 153,27,27; $c2 = 255,77,77} } elseif ($theme -eq "Green") { if ($p -lt 0.5) {$c1 = 34,197,94; $c2 = 20,184,166} else {$c1 = 20,184,166; $c2 = 34,197,94} } elseif ($theme -eq "Yellow") { if ($p -lt 0.5) {$c1 = 250,204,21; $c2 = 249,115,22} else {$c1 = 249,115,22; $c2 = 250,204,21} } else { if ($p -lt 0.5) {$c1 = 34,211,238; $c2 = 37,99,235} else {$c1 = 37,99,235; $c2 = 34,211,238} } $r = [int]($c1[0] + $t * ($c2[0] - $c1[0])) $g = [int]($c1[1] + $t * ($c2[1] - $c1[1])) $b = [int]($c1[2] + $t * ($c2[2] - $c1[2])) return "$([char]27)[38;2;$r;$g;${b}m" } $i = 0 while ($true) { $label = $text $width = [math]::Max(38, $label.Length + 4) $total = $width + 2 + 1 + $width + 2 + 1 $offset = $i * 2 $sbTop = [System.Text.StringBuilder]::new(" ") $sbBot = [System.Text.StringBuilder]::new(" ") for ($k = 0; $k -lt ($width + 2); $k++) { $pTop = (($k + $offset) % $total) / $total $cTop = if ($k -eq 0) {"╭"} elseif ($k -eq $width + 1) {"╮"} else {"─"} $null = $sbTop.Append("$(Get-Color $pTop $theme)$cTop") $pBot = ((($total - 2 - $k) + $offset) % $total) / $total $cBot = if ($k -eq 0) {"╰"} elseif ($k -eq $width + 1) {"╯"} else {"─"} $null = $sbBot.Append("$(Get-Color $pBot $theme)$cBot") } $pLeft = ((( $total - 1 ) + $offset) % $total) / $total $midLeft = "$(Get-Color $pLeft $theme)│$([char]27)[0m" $pRight = ((( $width + 2 ) + $offset) % $total) / $total $midRight = "$(Get-Color $pRight $theme)│$([char]27)[0m" $null = $sbTop.Append("$([char]27)[0m") $null = $sbBot.Append("$([char]27)[0m") $spaces = $width - $label.Length $padded = " $label" + (" " * ($spaces - 1)) $box = $sbTop.ToString() + "`n ${midLeft}$([char]27)[1m${padded}$([char]27)[0m${midRight}`n" + $sbBot.ToString() + "`n" [Console]::Write("$([char]27)[3F") [Console]::Write($box) $i++ Start-Sleep -Milliseconds 80 } } Write-Host "" ; Write-Host "" ; Write-Host "" ; Write-Host "" $rs = [runspacefactory]::CreateRunspace() $rs.Open() $ps = [powershell]::Create() $ps.Runspace = $rs $null = $ps.AddScript($sb).AddArgument($text).AddArgument($theme) $handle = $ps.BeginInvoke() return @{ps = $ps; rs = $rs; handle = $handle} } function UI.Stop-GradientBox($job) { $job.ps.Stop() $job.ps.Dispose() $job.rs.Dispose() [Console]::Write("$([char]27)[4F$([char]27)[0J") } # -- Spinner Helper -- # function UI.Spinner([scriptblock]$Action, [string]$Message = "Processing...", [string]$Color = "Cyan", [object[]]$ArgumentList = @()) { # Auto-mutate label to past tense on success $successMsg = $Message ` -replace 'Installing', 'Installed' ` -replace 'Removing', 'Removed' ` -replace 'Patching', 'Patched' ` -replace 'Fetching', 'Fetched' ` -replace 'Deleting', 'Deleted' ` -replace 'Updating', 'Updated' ` -replace 'Upgrading', 'Upgraded' ` -replace 'Deploying', 'Deployed' ` -replace 'Creating', 'Created' ` -replace '\.\.\.', '' ` -replace '\.\.', '' $result = UI.BoxSpinner $Action $Message $Color $ArgumentList if ($script:lastSpinnerEsc) {return $null} $pad = ' ' * ($Message.Length + 12) if ($null -ne $result -and $result -ne $false) { [Console]::Write("`r $([char]27)[92m[✓]$([char]27)[0m $successMsg$pad`n") } else { [Console]::Write("`r $([char]27)[91m[✗]$([char]27)[0m $successMsg$pad`n") } return $result } # -- Box-Style Spinner (animated box with cycling dots) -- # function UI.BoxSpinner([scriptblock]$Action, [string]$Message, [string]$Color = "Cyan", [object[]]$ArgumentList = @()) { $rs = [runspacefactory]::CreateRunspace(); $rs.Open() $ps = [powershell]::Create(); $ps.Runspace = $rs $null = $ps.AddScript($Action) foreach ($arg in $ArgumentList) {$null = $ps.AddArgument($arg)} $handle = $ps.BeginInvoke() $i = 0 Cursor.Hide # Pre-print 4 blank lines (1 for padding, 3 for space to draw in) Write-Host ""; Write-Host ""; Write-Host ""; Write-Host "" $script:lastSpinnerEsc = $false while (-not $handle.IsCompleted) { if ([Console]::KeyAvailable) { if ([Console]::ReadKey($true).Key -eq 'Escape') { $ps.Stop() [Console]::Write("$([char]27)[4F$([char]27)[0J") Cursor.Show $ps.Dispose(); $rs.Dispose() $script:lastSpinnerEsc = $true return $null } } $label = $Message [Console]::Write("$([char]27)[3F") UI.BoxGradient $label ($i * 2) 38 $Color $i++ Start-Sleep -Milliseconds 80 } [Console]::Write("$([char]27)[4F$([char]27)[0J") Cursor.Show try {$result = $ps.EndInvoke($handle)} catch {$result = $null} $ps.Dispose(); $rs.Dispose() return $result } function UI.Input { param( [string]$Prompt, [string]$Default = "", [scriptblock]$Validate = $null, [switch]$NoNewline, [switch]$NoSuccess ) $frames = "⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏" $frameIdx = 0 $chars = [System.Collections.Generic.List[char]]::new() $cur = 0 # Initial Draw [Console]::Write("`r$([char]27)[2K ${C_CYAN} ${C_RESET} ") Write-Host "$Prompt" -n $targetCol = 7 + $Prompt.Length + $cur Cursor.Set $targetCol ([Console]::CursorTop) $inlineError = "" while ($true) { if ([Console]::KeyAvailable) { $key = [Console]::ReadKey($true) $needsRedraw = $false if ($inlineError -and $key.Key -notin @('Enter', 'Escape')) { $inlineError = "" [Console]::Write("$([char]27)[s`n$([char]27)[2K$([char]27)[u") $needsRedraw = $true } switch ($key.Key) { 'Enter' { $value = [string]::new($chars.ToArray()) if ([string]::IsNullOrWhiteSpace($value)) {$value = $Default} $ok = $true if ($Validate) { $res = & $Validate $value if ($res -is [bool] -and $res -eq $false) { $ok = $false } elseif ($res -is [string]) { $ok = $false $inlineError = $res } } if ($ok) { if ($inlineError) { [Console]::Write("$([char]27)[s`n$([char]27)[2K$([char]27)[u") } if (-not $NoSuccess) { Cursor.Set 0 ([Console]::CursorTop) [Console]::Write("`r$([char]27)[2K ${C_GREEN}[✓]${C_RESET} ") Write-Host "$Prompt" -n if ($NoNewline) { Write-Host $value -n } else { Write-Host $value } } return $value } else { if ($inlineError) { Cursor.Set 0 ([Console]::CursorTop) [Console]::Write("`r$([char]27)[2K ${C_RED}[✗]${C_RESET} $Prompt${C_RED}${value}${C_RESET}") $len = 7 + $Prompt.Length + $value.Length [Console]::Write("`n$([char]27)[2K ${C_RED}${inlineError}${C_RESET}$([char]27)[1A$([char]27)[${len}G") } else { $chars.Clear() $cur = 0 $needsRedraw = $true } } } 'Escape' { if ($inlineError) {[Console]::Write("`n$([char]27)[2K$([char]27)[1A")} [Console]::Write("`r$([char]27)[2K`n") return $null } 'Backspace' { if ($cur -gt 0) { $chars.RemoveAt($cur - 1) $cur-- $needsRedraw = $true } } 'Delete' { if ($cur -lt $chars.Count) { $chars.RemoveAt($cur) $needsRedraw = $true } } 'LeftArrow' {if ($cur -gt 0) {$cur--; $needsRedraw = $true}} 'RightArrow' {if ($cur -lt $chars.Count) {$cur++; $needsRedraw = $true}} 'Home' {$cur = 0; $needsRedraw = $true} 'End' {$cur = $chars.Count; $needsRedraw = $true} default { $char = $key.KeyChar $code = [int]$char if ($code -ge 32 -and $code -le 126) { $chars.Insert($cur, $char) $cur++ $needsRedraw = $true } } } if ($needsRedraw) { Cursor.Hide $text = [string]::new($chars.ToArray()) $f = $frames[$frameIdx % $frames.Length] [Console]::Write("`r$([char]27)[2K ${C_CYAN}${f}${C_RESET} ") Write-Host "$Prompt" -n [Console]::Write($text) $targetCol = 7 + $Prompt.Length + $cur Cursor.Set $targetCol ([Console]::CursorTop) Cursor.Show } } else { if (-not $inlineError) { $f = $frames[$frameIdx % $frames.Length] [Console]::Write("$([char]27)[s$([char]27)[5G${C_CYAN}${f}${C_RESET}$([char]27)[u") $frameIdx++ Start-Sleep -Milliseconds 80 } else { Start-Sleep -Milliseconds 50 } } } } function Ask-Input { UI.Input @args } # -- Consolidated Menu (Single/Multi-selection) -- # function UI.Menu { param( [string[]]$Items, [string]$Title = "", [switch]$Multi, [string]$Color = "Cyan", [bool[]]$Status = $null, [switch]$NoHint ) # Flush input buffer try { $f = 0 while ([Console]::KeyAvailable -and $f -lt 100) {$null = [Console]::ReadKey($true); $f++} } catch {} $count = $Items.Count $cur = 0 Cursor.Hide if (-not $NoHint) { if ($Title) { Write-Host " ${C_CYAN}↑↓${C_RESET} ${C_BOLD}$Title${C_RESET}" } Write-Host "" } foreach ($item in $Items) {Write-Host ""} $menuTop = [Console]::CursorTop - $count # Determine cursor active indicator color for single selection $indColor = switch ($Color) { "Magenta" {$C_MAG} "Red" {$C_RED} "Yellow" {$C_YEL} "Green" {$C_GREEN} "Blue" {$C_BLUE} default {$C_CYAN} } # State variables $checked = [bool[]]::new($count) $failedMap = if ($Status -and $Status.Count -eq $count) {$Status} else {[bool[]]::new($count)} # -- Sub-functions for Simplified rendering & input -- # function Render-Menu { for ($i = 0; $i -lt $count; $i++) { Cursor.Set 0 ($menuTop + $i) $num = $i + 1 if ($Multi) { $box = if ($checked[$i]) {"${C_RED}[✓]${C_RESET}"} else {"${C_GRAY}[ ]${C_RESET}"} if ($checked[$i] -or $failedMap[$i]) { if ($i -eq $cur) { Write-Host " ${C_RED}>${C_RESET} $box ${C_RED}${C_BOLD}${num}. $($Items[$i])${C_RESET} " } else { Write-Host " $box ${C_RED}${num}. $($Items[$i])${C_RESET} " } } else { if ($i -eq $cur) { Write-Host " ${C_RED}>${C_RESET} $box ${C_BOLD}${num}. $($Items[$i])${C_RESET} " } else { Write-Host " $box ${C_GRAY}${num}. $($Items[$i])${C_RESET} " } } } else { if ($i -eq $cur) { Write-Host " ${indColor}●${C_RESET} ${indColor}${C_BOLD}${num}. $($Items[$i])${C_RESET} " } else { Write-Host " ${C_GRAY}○${C_RESET} ${num}. $($Items[$i]) " } } } } function Render-FinalSingle { for ($i = 0; $i -lt $count; $i++) { Cursor.Set 0 ($menuTop + $i) $num = $i + 1 if ($Multi) { $box = if ($checked[$i]) {"${indColor}[✓]${C_RESET}"} else {"${C_GRAY}[ ]${C_RESET}"} if ($checked[$i]) { Write-Host " ${indColor}>${C_RESET} $box ${indColor}${C_BOLD}${num}. $($Items[$i])${C_RESET} " } else { Write-Host " $box ${C_GRAY}${num}. $($Items[$i])${C_RESET} " } } else { if ($i -eq $cur) { Write-Host " ${indColor}●${C_RESET} ${indColor}${C_BOLD}${num}. $($Items[$i])${C_RESET} " } else { Write-Host " ${C_GRAY}○${C_RESET} ${num}. $($Items[$i]) " } } } } function Move-Cursor([int]$dir) { return ($cur + $dir + $count) % $count } # -- Main Interaction Loop -- # while ($true) { Render-Menu $key = [Console]::ReadKey($true) if ($key.Key -eq [System.ConsoleKey]::UpArrow) { $cur = Move-Cursor -1 } elseif ($key.Key -eq [System.ConsoleKey]::DownArrow) { $cur = Move-Cursor 1 } elseif ($Multi -and $key.Key -eq [System.ConsoleKey]::Spacebar) { $checked[$cur] = !$checked[$cur] } elseif ($Multi -and ($key.KeyChar -eq 'a' -or $key.KeyChar -eq 'A')) { $anyChecked = $false for ($i = 0; $i -lt $count; $i++) {if ($checked[$i]) {$anyChecked = $true; break}} for ($i = 0; $i -lt $count; $i++) {$checked[$i] = !$anyChecked} } elseif ($key.Key -eq [System.ConsoleKey]::Enter) { Render-FinalSingle Cursor.Set 0 ($menuTop + $count) Cursor.Show if ($Multi) { $result = [System.Collections.Generic.List[string]]::new() for ($i = 0; $i -lt $count; $i++) {if ($checked[$i]) {$result.Add($Items[$i])}} return , $result.ToArray() } else { return $cur } } elseif ($key.Key -eq [System.ConsoleKey]::Escape -or $key.Key -eq [System.ConsoleKey]::Backspace) { Cursor.Set 0 ($menuTop + $count) Cursor.Show return $null } elseif ($key.KeyChar -ge '1' -and $key.KeyChar -le '9') { $val = [int]$key.KeyChar - [int][char]'1' if ($val -lt $count) { $cur = $val if ($Multi) { $checked[$cur] = !$checked[$cur] } else { Render-FinalSingle Cursor.Set 0 ($menuTop + $count) Cursor.Show return $cur } } } } } function Ask-Menu { UI.Menu @args } # -- Mode Selection -- # function Select-Mode { $items = @( "Standard Redirect", "Fetch as Text", "Fetch as Text (Non-Browser / Only CLI)", "Fetch as HTML" ) $choice = Ask-Menu -Title "Select Deployment Mode:" -Items $items -Color "Cyan" if ($null -eq $choice) { Write-Status "Cancelled." "warn" return $null } $mode = $choice + 1 # -- Use Proxy? -- # if ($mode -eq 1) { $useProxy = $false } else { Write-Host "" $useProxy = Ask-Confirm "Use Proxy?" Cyan } return @{mode = $mode; proxy = $useProxy} } # -- Create index.js -- # function Create-IndexJs([string]$filePath) { $dir = Split-Path -Parent $filePath if (!(Test-Path $dir)) {ni -ItemType Directory -Path $dir -force | Out-Null} $js = @" export async function onRequest(context) { if (!context.env.URL_CONFIG || !context.env.PROJECT_NAME) { return new Response('URL_CONFIG binding or PROJECT_NAME missing', { status: 500 }); } let url = null; let mode = "redirect"; let proxyUrl = ""; try { const [kvUrl, kvMode, kvProxyUrl] = await Promise.all([ context.env.URL_CONFIG.get(context.env.PROJECT_NAME + "_url"), context.env.URL_CONFIG.get(context.env.PROJECT_NAME + "_mode"), context.env.URL_CONFIG.get(context.env.PROJECT_NAME + "_proxyUrl") ]); if (kvUrl !== null) url = kvUrl; if (kvMode !== null) mode = kvMode; if (kvProxyUrl !== null) proxyUrl = kvProxyUrl; } catch (e) { return new Response('KV Read Error: ' + e.message, { status: 500 }); } if (!url) return new Response('URL not found in KV', { status: 500 }); let target = url; if (proxyUrl) { target = proxyUrl + "?url=" + encodeURIComponent(url); } switch (mode) { case "redirect": { const r = await fetch(target, { redirect: "follow", cache: "no-store" }); return Response.redirect(r.url, 302); } case "text": { const r = await fetch(target, { cache: "no-store" }); if (!r.ok) return new Response('Fetch error: ' + r.status, {status: 502}); return new Response(await r.text(), { headers: { "Content-Type": "text/plain; charset=utf-8" } }); } case "cli": { const ua = context.request.headers.get("user-agent") || ""; if (/Mozilla\/5\.0/i.test(ua)) { return new Response(null, { status: 204 }); } const r = await fetch(target, { cache: "no-store" }); if (!r.ok) return new Response('Fetch error: ' + r.status, {status: 502}); return new Response(await r.text(), { headers: { "Content-Type": "text/plain; charset=utf-8" } }); } case "html": { const r = await fetch(target, { cache: "no-store" }); if (!r.ok) return new Response('Fetch error: ' + r.status, {status: 502}); return new Response(await r.text(), { headers: { "Content-Type": "text/html; charset=utf-8" } }); } default: return new Response("Invalid MODE", { status: 500 }); } } "@ try { Set-Content -Path $filePath -Value $js -Encoding UTF8 return $true } catch { Write-Host "Error generating index.js!" -f Red; return $false } } # -- Clear Temp Dir -- # function Clear-TempDir([string]$path) {if (![string]::IsNullOrWhiteSpace($path) -and (Test-Path $path)) {try {rd -Path $path -Recurse -Force -ea 0 >$null} catch {}}} # -- Setup Node and Wrangler -- # function Setup-NodeAndWrangler { Write-Status "Downloading Node.js..." "info" & winget install nodejs --silent 2>&1 | Out-Null Write-Status "Refreshing environment variables..." "info" $env:Path = [Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + [Environment]::GetEnvironmentVariable("Path", "User") Write-Status "Downloading Wrangler..." "info" & npm install -g wrangler@latest 2>&1 | Out-Null Write-Status "Completed." "ok" } # -- Verify Project Name -- # function Verify-ProjectName([string]$name) { $accountId = Get-AccountId if (!$accountId) { return 'available' } try { $resp = Invoke-CfApi "accounts/$accountId/pages/projects/$name" -ReturnFullResponse if ($resp.StatusCode -eq 200) { return 'mine' } if ($resp.StatusCode -eq 404) { try { $req = [System.Net.WebRequest]::Create("https://${name}.pages.dev") $req.Method = "GET" $req.Timeout = 5000 $res = $req.GetResponse() return 'taken' } catch { if ($_.Exception.Response) { $status = [int]$_.Exception.Response.StatusCode if ($status -eq 404) { return 'available' } return 'taken' } return 'available' } } } catch {} return 'available' } # -- Run Wrangler Deploy -- # function Run-WranglerDeploy([string]$projName, [string]$projDir, [string]$branch, [string]$redirectUrl, [string]$modeStr, [string]$proxyUrl) { if (!(Test-Path $projDir)) {Write-Host "Project folder missing: $projDir!" -f Red; return $false} Push-Location $projDir Write-Host "" Cursor.Hide $animJob = UI.Start-GradientBox "Deploying $projName" "Green" $ErrorActionPreference = 'SilentlyContinue' $tokenFound = $false try { $accountId = Get-AccountId if ($accountId) { $tokenFound = $true $body = @{name=$projName; production_branch=$branch} | ConvertTo-Json $null = Invoke-CfApi "accounts/$accountId/pages/projects" -Method "POST" -Body $body # Get or create KV namespace $kvId = $null try { $kvList = Invoke-CfApi "accounts/$accountId/storage/kv/namespaces" $kvId = ($kvList.result | Where-Object {$_.title -eq "CYBERVOID_PAGES_KV"}).id | Select-Object -First 1 if (!$kvId) { $kvCreate = Invoke-CfApi "accounts/$accountId/storage/kv/namespaces" -Method "POST" -Body (@{title="CYBERVOID_PAGES_KV"}|ConvertTo-Json) $kvId = $kvCreate.result.id } } catch {} # Write config to KV if ($kvId -and -not [string]::IsNullOrWhiteSpace($redirectUrl)) { try { $kvBulk = @( @{key="${projName}_url"; value=$redirectUrl}, @{key="${projName}_mode"; value=$modeStr}, @{key="${projName}_proxyUrl"; value=$proxyUrl} ) | ConvertTo-Json -Compress $null = Invoke-CfApi "accounts/$accountId/storage/kv/namespaces/$kvId/bulk" -Method "PUT" -Body $kvBulk } catch {} } $kvBinding = if ($kvId) { @{ URL_CONFIG = @{ namespace_id = $kvId } } } else { @{} } $envBody = @{ deployment_configs = @{ production = @{ env_vars = @{ PROJECT_NAME = @{type = "plain_text"; value = $projName} } kv_namespaces = $kvBinding } preview = @{ env_vars = @{ PROJECT_NAME = @{type = "plain_text"; value = $projName} } kv_namespaces = $kvBinding } } } | ConvertTo-Json -Depth 10 $null = Invoke-CfApi "accounts/$accountId/pages/projects/$projName" -Method "PATCH" -Body $envBody $env:CLOUDFLARE_ACCOUNT_ID = $accountId } } catch {} if (-not $tokenFound) { & wrangler pages project create $projName --production-branch $branch 2>&1 | Out-Null } $ErrorActionPreference = 'SilentlyContinue' & wrangler pages deploy . --project-name $projName --branch $branch 2>&1 | Out-Null $success = ($LASTEXITCODE -eq 0) Pop-Location # Stop animation and clear box UI.Stop-GradientBox $animJob if ($success) { UI.Box "Deployment Completed!" "Green" Write-Host "" UI.Status "Live URL: https://${projName}.pages.dev" "ok" } else { UI.Box "Deploying: $projName [ERROR]" "Red" } Cursor.Show Write-Status "Press any key to continue..." "off" $null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") return $success } # -- Get Projects -- # function Get-Projects { $accountId = Get-AccountId $code = { param($accountId) try { if ($accountId) { $resp = Invoke-CfApi "accounts/$accountId/pages/projects?per_page=100" return ($resp.result | ConvertTo-Json -Depth 10) } } catch {} return (& wrangler pages project list 2>&1 | Out-String) } $raw = @(UI.BoxSpinner $code "Fetching Pages projects" "Cyan" @($accountId))[0] try { # Suppress any potential parser warnings/junk, and convert $cleanJson = $raw if ($raw -match '(\[[\s\S]*\]|\{[\s\S]*\})') { $cleanJson = $matches[0] } $data = $cleanJson | ConvertFrom-Json $projects = @() if ($data) { $targetData = if ($data.result) {$data.result} else {$data} $projects = $targetData |% { if ($_.name) {$_.name} elseif ($_.'Project Name') {$_.'Project Name'} } } return @($projects) } catch { # Bulletproof fallback in case ConvertFrom-Json fails or output format changes $projects = [regex]::Matches($raw, "\u2502\s+([a-z0-9][a-z0-9\-]+)\s+\u2502") | % {$_.Groups[1].Value} | ? {$_ -ne 'project' -and $_ -notmatch '^-+$'} | select -Unique return @($projects) } } # -- Test Pages Status -- # function Test-PagesStatus { param( [string[]]$Projects ) if (!$Projects -or $Projects.Count -eq 0) {return @()} $code = { param($projects) $jobs = @() foreach ($name in $projects) { $jobs += [powershell]::Create().AddScript({ param($name) try {[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 -bor [System.Net.SecurityProtocolType]::Tls13} catch {} try { $req = [System.Net.WebRequest]::Create('https://' + $name + '.pages.dev') $req.UserAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' $req.Method = 'HEAD' $req.Timeout = 5000 $res = $req.GetResponse() $code = [int]$res.StatusCode $res.Close() [PSCustomObject]@{Name = $name; Code = $code; Ok = $code -lt 400} } catch { $code = 0 try {$code = [int]$_.Exception.Response.StatusCode} catch {} [PSCustomObject]@{Name = $name; Code = $code; Ok = $false} } }).AddArgument($name) } $handles = $jobs.ForEach({$_.BeginInvoke()}) $results = [bool[]]::new($projects.Count) for ($i = 0; $i -lt $jobs.Count; $i++) { $r = $jobs[$i].EndInvoke($handles[$i])[0] $results[$i] = !$r.Ok $jobs[$i].Dispose() } return ,$results } $results = @(UI.Spinner $code "Checking status of all pages.dev projects..." "Red" @(,$Projects))[0] return , $results } # -- Delete Pages Projects -- # function Invoke-Delete { Clear-Host $projectList = Get-Projects if ($script:lastSpinnerEsc) {Clear-Host; return} if (!$projectList -or $projectList.Count -eq 0) { Clear-Host UI.Box "No Pages projects found" "Red" Write-Status "Press any key to continue..." "off" $null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") return } Clear-Host UI.Box "Delete Pages Projects" "Red" Write-Host " ${C_GRAY}[↑↓] Select | [Enter] Confirm | [Esc] Back | [Space] Toggle${C_RESET}" Write-Host "" $failedMap = Test-PagesStatus $projectList if ($script:lastSpinnerEsc) {Clear-Host; return} $selected = Ask-Menu -Items $projectList -Multi -Status $failedMap -Color "Red" -NoHint if ($null -eq $selected -or @($selected).Count -eq 0) { Clear-Host; return } Write-Host "`n ${C_CYAN}Selected:${C_RESET}" $selCount = @($selected).Count for ($i = 0; $i -lt $selCount; $i++) { $branch = if ($i -eq $selCount - 1) {"└─"} else {"├─"} Write-Host " ${C_GRAY}$branch${C_RESET} ${C_RED}$($selected[$i])${C_RESET}" } Write-Host "" $confirm = Ask-Confirm "Delete $(@($selected).Count) project(s)?" Red if (-not $confirm) {Clear-Host; return} $total = @($selected).Count $token = Get-WranglerToken $accountId = Get-AccountId for ($i = 0; $i -lt $total; $i++) { $p = $selected[$i] $num = $i + 1 $code = { param($projName, $token, $accountId) try { if ($token -and $accountId) { $url = "https://api.cloudflare.com/client/v4/accounts/$accountId/pages/projects/$projName" $null = Invoke-RestMethod -Uri $url -Method Delete -Headers @{Authorization = "Bearer $token"} return @{ok = $true} } $out = ('y' | wrangler pages project delete $projName 2>&1 | Out-String) if ($out -match '(?i)error|fail') {return @{ok = $false; msg = $out.Trim()}} return @{ok = $true} } catch {return @{ok = $false; msg = $_.Exception.Message}} } $res = @(UI.Spinner $code "[$num/$total] Deleting $p..." "Red" @($p, $token, $accountId))[0] if ($script:lastSpinnerEsc) {Clear-Host; return} [Console]::Write("$([char]27)[1A$([char]27)[2K`r") if ($res -and $res.ok) { Write-Status "[$num/$total] Deleted $p" "ok" } else { Write-Status "[$num/$total] Failed to delete $p" "err" $errMsg = if ($res) {$res.msg} else {"Unknown error"} Write-Host " ${C_GRAY}$errMsg${C_RESET}" } } Write-Status "Press any key to continue..." "off" $null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") Clear-Host } # -- Create Proxy Worker -- # function Invoke-ProxySetup { Clear-Host UI.Box "Create Proxy Worker" "Magenta" Write-Host " ${C_GRAY}[Esc] Back${C_RESET}" Write-Host "" $subdomain = $script:accountSubdomain if (!$subdomain) { $token = Get-WranglerToken $accountId = Get-AccountId $subBlock = { param($token, $accountId) if ($token -and $accountId) { try { $resp = irm "https://api.cloudflare.com/client/v4/accounts/$accountId/workers/subdomain" -Headers @{Authorization = "Bearer $token"} return $resp.result.subdomain } catch {} } return $null } $subdomain = @(UI.Spinner $subBlock "Auto-detecting Cloudflare subdomain..." "Cyan" @($token, $accountId))[0] if ($script:lastSpinnerEsc) {return} if ($subdomain) { $script:accountSubdomain = $subdomain try { $null = ni -ItemType File -Path "$env:USERPROFILE\.wrangler\subdomain.txt" -Force [System.IO.File]::WriteAllText("$env:USERPROFILE\.wrangler\subdomain.txt", $subdomain) } catch {} [Console]::Write("$([char]27)[1A$([char]27)[0J") Write-Host " ${C_CYAN}[✓] Proxy URL: proxy.${subdomain}.workers.dev${C_RESET}" Write-Host "" } else { [Console]::Write("$([char]27)[1A$([char]27)[0J") UI.Status "Workers subdomain not initialized on this account." "info" $setupSub = Ask-Confirm "Register a new subdomain now?" Cyan if ($null -eq $setupSub -or -not $setupSub) {return} while ($true) { $subdomain = Ask-Input "Desired Cloudflare subdomain: " -Validate {$args[0] -match '^[a-zA-Z0-9\-]{3,}$'} if ($null -eq $subdomain) {return} $subdomain = $subdomain.ToLower() $regBlock = { param($sub, $token, $accountId) if ($token -and $accountId) { try { $body = @{subdomain=$sub} | ConvertTo-Json $resp = irm "https://api.cloudflare.com/client/v4/accounts/$accountId/workers/subdomain" -Method Put -Headers @{Authorization = "Bearer $token"; "Content-Type"="application/json"} -Body $body return ($resp.success -eq $true) } catch {} } return $false } $regOk = @(UI.Spinner $regBlock "Registering subdomain $subdomain..." "Cyan" @($subdomain, $token, $accountId))[0] if ($script:lastSpinnerEsc) {return} if ($regOk) { $script:accountSubdomain = $subdomain try { $null = ni -ItemType File -Path "$env:USERPROFILE\.wrangler\subdomain.txt" -Force -ea SilentlyContinue [System.IO.File]::WriteAllText("$env:USERPROFILE\.wrangler\subdomain.txt", $subdomain) } catch {} [Console]::Write("$([char]27)[4F$([char]27)[J") Write-Host " ${C_CYAN}[✓] Proxy URL: proxy.${subdomain}.workers.dev${C_RESET}" Write-Host "" break } else { UI.Status "Subdomain taken or invalid. Try another." "err" } } } } else { UI.Status "Auto-detecting Cloudflare subdomain..." "loading" [Console]::Write("$([char]27)[1A$([char]27)[0J") Write-Host " ${C_CYAN}[✓] Proxy URL: proxy.${subdomain}.workers.dev${C_RESET}" Write-Host "" } $workerName = "proxy" $resolvedUrl = "https://${workerName}.${subdomain}.workers.dev" $accountId = Get-AccountId $checkWorker = Invoke-CfApi "accounts/$accountId/workers/scripts/$workerName" -ReturnFullResponse if ($checkWorker.StatusCode -eq 200) { Write-Host " ${C_GREEN}[✓] Worker '$workerName' already exists! Using it.${C_RESET}" Start-Sleep -Seconds 1 Set-ProxyUrl $resolvedUrl return } $useToken = Ask-Confirm "Use GitHub token?" Cyan if ($null -eq $useToken) {return} $token = "" if ($useToken) { Cursor.Set 0 ([Console]::CursorTop - 1) [Console]::Write("$([char]27)[2K") $token = Ask-Input "Enter GitHub API token: " if ($null -eq $token) {return} } $workerDir = Join-Path $env:TEMP "CF-PagesRoute-worker-$workerName" Clear-TempDir $workerDir ni -ItemType Directory -Path $workerDir -Force | Out-Null $tomlContent = @" name = "$workerName" main = "worker.js" compatibility_date = "2024-01-01" "@ [System.IO.File]::WriteAllText((Join-Path $workerDir "wrangler.toml"), $tomlContent) $jsCode = @' export default { async fetch(request, env) { const params = new URL(request.url).searchParams; const url = params.get('url'); if (!url) return new Response('Usage: ?url=YOUR_URL', {status: 400}); try { const headers = {'User-Agent': 'CF-Worker'}; let fetchUrl = url; if (url.includes('github.com') || url.includes('raw.githubusercontent.com')) { if (env.GH_TOKEN) headers['Authorization'] = 'token ' + env.GH_TOKEN; const m = url.match(/github\.com\/([^/]+)\/([^/]+)\/(?:raw|blob)\/(?:refs\/heads\/)?([^/]+)\/(.+)/) || url.match(/raw\.githubusercontent\.com\/([^/]+)\/([^/]+)\/([^/]+)\/(.+)/); if (m) { const [, owner, repo, branch, file] = m; fetchUrl = `https://api.github.com/repos/${owner}/${repo}/contents/${file}${branch ? '?ref=' + branch : ''}`; headers['Accept'] = 'application/vnd.github.raw'; } } const r = await fetch(fetchUrl, {headers, cache: 'no-store'}); if (!r.ok) return new Response('Fetch error: ' + r.status, {status: 502}); const ct = r.headers.get('content-type') || 'text/plain; charset=utf-8'; return new Response(await r.text(), {headers: {'Content-Type': ct}}); } catch (e) { return new Response('EXCEPTION: ' + e.message, {status: 500}); } } } '@ Set-Content -Path (Join-Path $workerDir "worker.js") -Value $jsCode -Encoding UTF8 Write-Host "" Push-Location $workerDir if (![string]::IsNullOrWhiteSpace($token)) { $secretBlock = { param($workerName, $ghToken) try { $ghToken | wrangler secret put GH_TOKEN --name $workerName 2>&1 | Out-Null return $true } catch {return $false} } $secRes = @(UI.BoxSpinner $secretBlock "Setting GH_TOKEN..." "Cyan" @($workerName, $token))[0] if ($script:lastSpinnerEsc) {Pop-Location; Clear-TempDir $workerDir; return} if ($secRes) {UI.Box "Secret Configured!" "Green"} } $deployBlock = { try { $out = (& wrangler deploy 2>&1 | Out-String) if ($out -match '(?i)error|fail') {return @{ok = $false; msg = $out.Trim()}} return @{ok = $true; msg = $out} } catch {return @{ok = $false; msg = $_.Exception.Message}} } $res = @(UI.BoxSpinner $deployBlock "Deploying: $($resolvedUrl.Substring(8))" "Magenta")[0] Pop-Location if ($script:lastSpinnerEsc) {Clear-TempDir $workerDir; return} if ($res -and $res.ok) { UI.Box "Worker Deployed!" "Green" Set-ProxyUrl $resolvedUrl } else { UI.Box "Deploying: $($resolvedUrl.Substring(8)) [ERROR]" "Red" if ($res.msg) {Write-Host "`n ${C_RED}[Stderr]${C_RESET}`n$($res.msg)"} Write-Host "" } Clear-TempDir $workerDir Write-Status "Press any key to continue..." "off" $null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") } # -- Deploy Pages -- # function Invoke-Deploy { if (!$script:accountSubdomain -or !(Get-ProxyUrl)) { Invoke-ProxySetup if (!$script:accountSubdomain -or !(Get-ProxyUrl)) { return } Clear-Host } UI.Box "Deploy Pages Project" "Magenta" Write-Host " ${C_GRAY}[↑↓] Select | [Enter] Confirm | [Esc] Back${C_RESET}" Write-Host "" $isMine = $false $projName = "" while ($true) { $projName = Ask-Input "Project name: " -Validate { $name = $args[0].ToLower() if ($name -notmatch '^[a-zA-Z0-9\-]{4,}$') {return $false} $status = Verify-ProjectName $name $script:lastVerifyStatus = $status if ($status -eq 'taken') {return "Domain already taken by another account"} return $true } -NoSuccess if ($null -eq $projName -or [string]::IsNullOrWhiteSpace($projName)) {return} $projName = $projName.ToLower() $status = $script:lastVerifyStatus if ($status -eq 'available') { Cursor.Set 0 ([Console]::CursorTop) [Console]::Write("`r$([char]27)[2K") Write-Host " ${C_GREEN}[✓]${C_RESET} Project name: ${C_GREEN}${projName}${C_RESET}" Write-Host "" $isMine = $false break } elseif ($status -eq 'mine') { Cursor.Set 0 ([Console]::CursorTop) [Console]::Write("`r$([char]27)[2K") Write-Host " ${C_RED}[✗]${C_RESET} Project name: ${C_RED}${projName}${C_RESET}" $useAnyway = Ask-Confirm "Overwrite existing Pages project?" Cyan 8 if ($null -eq $useAnyway) {return} if ($useAnyway) { [Console]::Write("$([char]27)[2A$([char]27)[2K") Write-Host " ${C_GREEN}[✓]${C_RESET} Project name: ${C_GREEN}${projName}${C_RESET}" [Console]::Write("$([char]27)[J") Write-Host "" $isMine = $true break } else { [Console]::Write("$([char]27)[2A$([char]27)[J") } } } if ([string]::IsNullOrWhiteSpace($projName)) {return} $redirectUrl = "" $mode = 1 if (-not $isMine) { $redirectUrl = Ask-Input "Enter URL: " -Validate {-not [string]::IsNullOrWhiteSpace($args[0])} -NoSuccess if ($null -eq $redirectUrl -or [string]::IsNullOrWhiteSpace($redirectUrl)) {return} Cursor.Set 0 ([Console]::CursorTop) [Console]::Write("`r$([char]27)[2K") if ($redirectUrl -notmatch '^https?://') { $redirectUrl = "https://$redirectUrl" } Write-Host " ${C_GREEN}[✓]${C_RESET} Enter URL: ${C_GREEN}${redirectUrl}${C_RESET}" Write-Host "" $modeItems = @( "Standard Redirect", "Fetch as Text", "Fetch as Text (Non-Browser / Only CLI)", "Fetch as HTML" ) $modeIdx = Ask-Menu -Items $modeItems -Color "Magenta" -NoHint if ($null -eq $modeIdx) {return} $mode = $modeIdx + 1 } else { Write-Host " ${C_GREEN}[✓]${C_RESET} Updating deployment code..." Write-Host "" } if ([string]::IsNullOrWhiteSpace($script:proxyUrl) -and $script:accountSubdomain) { $script:proxyUrl = "https://proxy.${script:accountSubdomain}.workers.dev" } elseif ([string]::IsNullOrWhiteSpace($script:proxyUrl)) { $script:proxyUrl = "https://proxy.subdomain.workers.dev" } $modeStr = switch ($mode) { 1 { "redirect" } 2 { "text" } 3 { "cli" } 4 { "html" } } $projDir = Join-Path $script:tempDir $projName $funcDir = Join-Path $projDir "functions" try { Clear-TempDir $projDir ni -ItemType Directory -Path $funcDir -force | Out-Null if (!(Create-IndexJs (Join-Path $funcDir "index.js"))) {return} } catch { UI.Status "Project structure error: $_" "err" Clear-TempDir $projDir return } Run-WranglerDeploy $projName $projDir "production" $redirectUrl $modeStr $script:proxyUrl Clear-TempDir $projDir } # -- Show Main Menu -- # function Show-MainMenu { Clear-Host if ($script:accountSubdomain) { UI.Box "Subdomain: $script:accountSubdomain" "Magenta" $script:proxyUrl = Get-ProxyUrl if (!$script:proxyUrl) {$script:proxyUrl = "https://proxy.${script:accountSubdomain}.workers.dev"} } else { if ($script:authExpired) { # Re-login inline — handled via menu item 4 now $script:authExpired = $false } UI.Box "Subdomain: [Not Detected - Please Login (Option 4)]" "Cyan" } Write-Host " ${C_GRAY}[↑↓] Select | [Enter] Confirm | [Esc] Back${C_RESET}" Write-Host "" $menuItems = @( "Deploy Pages Project", "Create Proxy Worker", "Delete Pages Projects", "Re-Login to Cloudflare", "Exit" ) $choice = Ask-Menu -Items $menuItems -Color "Magenta" -NoHint if ($null -eq $choice) {return $null} return ($choice + 1) } Clear-Host # 1. Check and Update Wrangler via animated box $wranglerBlock = { $current = "" try {$current = (wrangler --version 2>&1 | Out-String).Trim()} catch {} $latest = "" try {$latest = ((irm "https://registry.npmjs.org/wrangler/latest" -ea SilentlyContinue).version)} catch {} if (-not $current) { & npm install -g wrangler@latest --force 2>&1 | Out-Null return "Installed latest Wrangler" } elseif ($latest -and $current -ne $latest) { & npm install -g wrangler@latest --force 2>&1 | Out-Null return "Updated Wrangler to $latest" } return $current } $null = UI.BoxSpinner $wranglerBlock "Checking & Updating Wrangler" "Cyan" # 2. Detect subdomain via animated box $token = Get-WranglerToken $accountId = Get-AccountId $subdomainBlock = { param($token, $accountId) if ($token -and $accountId) { try { $resp = irm "https://api.cloudflare.com/client/v4/accounts/$accountId/workers/subdomain" -Headers @{Authorization = "Bearer $token"} return $resp.result.subdomain } catch {} } return $null } $script:accountSubdomain = UI.BoxSpinner $subdomainBlock "Detecting account subdomain" "Cyan" @($token, $accountId) if ($script:accountSubdomain) { $script:proxyUrl = Get-ProxyUrl if (!$script:proxyUrl) {$script:proxyUrl = "https://proxy.${script:accountSubdomain}.workers.dev"} } # -- Cleanup & Exit -- # function Invoke-Cleanup { Cursor.Show if ($script:tempDir -and (Test-Path $script:tempDir)) {Clear-TempDir $script:tempDir} } Register-EngineEvent -SourceIdentifier PowerShell.Exiting -Action {Invoke-Cleanup} | Out-Null # -- Main Loop -- # try { while ($true) { $choice = Show-MainMenu switch ($choice) { 1 {Clear-Host; Invoke-Deploy} 2 {Clear-Host; Invoke-ProxySetup} 3 {Invoke-Delete} 4 { Clear-Host UI.Box "Re-login to Cloudflare" "Cyan" Clear-WranglerCache $oldDir = $PWD try {Set-Location $env:USERPROFILE; & wrangler login} finally {Set-Location $oldDir} $script:accountSubdomain = Get-AccountSubdomain $script:proxyUrl = Get-ProxyUrl if (!$script:proxyUrl -and $script:accountSubdomain) { $script:proxyUrl = "https://proxy.${script:accountSubdomain}.workers.dev" } } 5 {exit 0} } } } finally { Invoke-Cleanup Clear-Host }