# --- 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 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 = "Purple", [int]$width = 38) { Write-Host "" UI.BoxGradient $text 0 $width $color } function UI.BoxGradient([string]$text, [int]$offset, [int]$width = 38, [string]$theme = "Cyan") { $cleanText = $text.Trim() $width = [math]::Max($width, $cleanText.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 = 239, 68, 68; $c2 = 244, 63, 94} else {$c1 = 244, 63, 94; $c2 = 239, 68, 68} } elseif ($theme -eq "Green") { if ($p -lt 0.5) {$c1 = 34, 197, 94; $c2 = 16, 185, 129} else {$c1 = 16, 185, 129; $c2 = 34, 197, 94} } elseif ($theme -eq "Yellow" -or $theme -eq "Orange") { if ($p -lt 0.5) {$c1 = 249, 115, 22; $c2 = 245, 158, 11} else {$c1 = 245, 158, 11; $c2 = 249, 115, 22} } elseif ($theme -eq "Purple" -or $theme -eq "Purple") { if ($p -lt 0.5) {$c1 = 168, 85, 247; $c2 = 236, 72, 153} else {$c1 = 236, 72, 153; $c2 = 168, 85, 247} } 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") $padded = " $cleanText".PadRight($width) $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 = "Purple", [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 = cat $tomlPath -Raw $token = $null $isApiToken = $false if ($cfg -match 'oauth_token\s*=\s*"([^"]+)"') { $token = $matches[1] } elseif ($cfg -match 'api_token\s*=\s*"([^"]+)"') { $token = $matches[1] $isApiToken = $true } if ($token) { if ($isApiToken) { $env:CLOUDFLARE_API_TOKEN = $token } else { $env:CLOUDFLARE_API_TOKEN = $null } return $token } } 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 = (cat $cacheFile -Raw).Trim() if ($id) { $env:CLOUDFLARE_ACCOUNT_ID = $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) $env:CLOUDFLARE_ACCOUNT_ID = $id return $id } } catch {} return $null } function Get-AccountSubdomain { $cacheFile = "$env:USERPROFILE\.wrangler\subdomain.txt" if (Test-Path $cacheFile) { try { $sub = (cat $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 { rm "$env:USERPROFILE\.wrangler\subdomain.txt" -Force -ea 0 rm "$env:USERPROFILE\.wrangler\account_id.txt" -Force -ea 0 rm "$env:USERPROFILE\.wrangler\proxy_url.txt" -Force -ea 0 rm "$env:USERPROFILE\.wrangler\config\default.toml" -Force -ea 0 rm "$env:USERPROFILE\.config\wrangler\config\default.toml" -Force -ea 0 $env:CLOUDFLARE_API_TOKEN = $null $env:CLOUDFLARE_ACCOUNT_ID = $null } function Invoke-ReLogin { cls UI.Box "Re-login to Cloudflare" "Orange" Clear-WranglerCache $oldDir = $PWD try { Set-Location $env:USERPROFILE & wrangler login } finally {Set-Location $oldDir} PS.UI "CF-PagesRoute [github.com/ztd38f]" $script:accountSubdomain = Get-AccountSubdomain $script:proxyUrl = Get-ProxyUrl if (!$script:proxyUrl -and $script:accountSubdomain) { $script:proxyUrl = "https://proxy.${script:accountSubdomain}.workers.dev" } } function Get-ProxyUrl { $cacheFile = "$env:USERPROFILE\.wrangler\proxy_url.txt" if (Test-Path $cacheFile) { try { $url = (cat $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 Test-ProxyWorkerExists { $accountId = Get-AccountId if (!$accountId) {return $false} try { $checkWorker = Invoke-CfApi "accounts/$accountId/workers/scripts/proxy" -ReturnFullResponse return ($checkWorker.StatusCode -eq 200) } catch { return $false } } function Ask-Confirm([string]$prompt, [string]$color, [int]$indent = 3) { $frames = "⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏" $i = 0 Cursor.Hide $spinIndentStr = " " $succIndentStr = " " # 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] Cursor.Set 3 ([Console]::CursorTop) [Console]::Write("${C_CYAN}${f}${C_RESET}") 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++ 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} if ($null -ne $result -and $result -ne $false) { [Console]::Write("`r$([char]27)[2K $([char]27)[92m[✓]$([char]27)[0m $successMsg`n") } else { [Console]::Write("`r$([char]27)[2K $([char]27)[91m[✗]$([char]27)[0m $successMsg`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 (!$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++ 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 = 6 + $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 (!$NoSuccess) { Cursor.Set 0 ([Console]::CursorTop) [Console]::Write("`r$([char]27)[2K ${C_GREEN}[✓]${C_RESET} ${C_GREEN}$($Prompt.TrimEnd())${C_RESET} ${C_GREEN}${value}${C_RESET}`n") } elseif ($NoNewline) { Write-Host $value -n } 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 = 6 + $Prompt.Length + $cur Cursor.Set $targetCol ([Console]::CursorTop) Cursor.Show } } else { if (!$inlineError) { $f = $frames[$frameIdx % $frames.Length] [Console]::Write("$([char]27)[s$([char]27)[4G${C_CYAN}${f}${C_RESET}$([char]27)[u") $frameIdx++ sleep -Milliseconds 80 } else { 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 (!$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) { "Purple" {$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.PROJECT_NAME) { return new Response('PROJECT_NAME missing', { status: 500 }); } const proj = context.env.PROJECT_NAME; let url = context.env.URL_CONFIG ? await context.env.URL_CONFIG.get("URL_" + proj) : null; let mode = (context.env.URL_CONFIG ? await context.env.URL_CONFIG.get("MODE_" + proj) : null) || "redirect"; let proxyUrl = context.env.PROXY_URL || ""; if (!url) return new Response('URL not found in env variables', { status: 500 }); let target = url; if (proxyUrl) { target = proxyUrl + "?url=" + encodeURIComponent(url); } const noCache = { "Cache-Control": "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" }; const fetchOpts = { cf: { cacheTtl: 0 } }; switch (mode) { case "redirect": { try { const r = await fetch(target, { redirect: "follow", ...fetchOpts }); return new Response(null, { status: 302, headers: { ...noCache, "Location": r.url } }); } catch (e) { return new Response('Fetch Exception: ' + e.message + ' | Target: ' + target, { status: 500 }); } } case "text": { try { const r = await fetch(target, fetchOpts); if (!r.ok) return new Response('Fetch error: ' + r.status, {status: 502, headers: noCache}); return new Response(await r.text(), { headers: { ...noCache, "Content-Type": "text/plain; charset=utf-8" } }); } catch (e) { return new Response('Fetch Exception: ' + e.message + ' | Target: ' + target, { status: 500 }); } } case "cli": { const ua = context.request.headers.get("user-agent") || ""; if (/Mozilla\/5\.0/i.test(ua)) { return new Response(null, { status: 204, headers: noCache }); } try { const r = await fetch(target, fetchOpts); if (!r.ok) return new Response('Fetch error: ' + r.status, {status: 502, headers: noCache}); return new Response(await r.text(), { headers: { ...noCache, "Content-Type": "text/plain; charset=utf-8" } }); } catch (e) { return new Response('Fetch Exception: ' + e.message + ' | Target: ' + target, { status: 500 }); } } case "html": { try { const r = await fetch(target, fetchOpts); if (!r.ok) return new Response('Fetch error: ' + r.status, {status: 502, headers: noCache}); return new Response(await r.text(), { headers: { ...noCache, "Content-Type": "text/html; charset=utf-8" } }); } catch (e) { return new Response('Fetch Exception: ' + e.message + ' | Target: ' + target, { status: 500 }); } } default: return new Response("Invalid MODE", { status: 500, headers: noCache }); } } "@ try { sc -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) { $subResp = Invoke-CfApi "accounts/$accountId/pages/get_subdomain?project_name=$name" -ReturnFullResponse if ($subResp.StatusCode -eq 200 -and $subResp.Data.result.subdomain) { if ($subResp.Data.result.subdomain -eq "$name.pages.dev") { return 'available' } else { return 'taken' } } } } 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 Cursor.Hide $animJob = UI.Start-GradientBox "Deploying $projName" "Green" $ErrorActionPreference = 'SilentlyContinue' $tokenFound = $false try { $token = Get-WranglerToken $accountId = Get-AccountId if ($accountId) { $tokenFound = $true $body = @{name = $projName; production_branch = $branch} | ConvertTo-Json try { $null = Invoke-CfApi "accounts/$accountId/pages/projects" -Method "POST" -Body $body } catch {} $kvId = $null if (![string]::IsNullOrWhiteSpace($redirectUrl)) { $kvResp = Invoke-CfApi "accounts/$accountId/storage/kv/namespaces" $kv = $kvResp.result | ? title -eq "CF-PagesRouter_Database" if (!$kv) { $kvBody = @{title = "CF-PagesRouter_Database"} | ConvertTo-Json $kvCreate = Invoke-CfApi "accounts/$accountId/storage/kv/namespaces" -Method "POST" -Body $kvBody $kvId = $kvCreate.result.id } else { $kvId = $kv.id } if ($kvId) { $putBody = @( @{key = "URL_${projName}"; value = $redirectUrl}, @{key = "MODE_${projName}"; value = $modeStr} ) | ConvertTo-Json -Depth 10 $null = Invoke-CfApi "accounts/$accountId/storage/kv/namespaces/$kvId/bulk" -Method "PUT" -Body $putBody } } $envBody = @{ deployment_configs = @{ production = @{ env_vars = @{ PROJECT_NAME = @{type = "plain_text"; value = $projName} } } preview = @{ env_vars = @{ PROJECT_NAME = @{type = "plain_text"; value = $projName} } } } } if ($kvId) { $envBody.deployment_configs.production["kv_namespaces"] = @{ URL_CONFIG = @{namespace_id = $kvId} } $envBody.deployment_configs.preview["kv_namespaces"] = @{ URL_CONFIG = @{namespace_id = $kvId} } } if (![string]::IsNullOrWhiteSpace($proxyUrl)) { $envBody.deployment_configs.production.env_vars["PROXY_URL"] = @{type = "plain_text"; value = $proxyUrl} $envBody.deployment_configs.preview.env_vars["PROXY_URL"] = @{type = "plain_text"; value = $proxyUrl} } $envBodyJson = $envBody | ConvertTo-Json -Depth 10 $null = Invoke-CfApi "accounts/$accountId/pages/projects/$projName" -Method "PATCH" -Body $envBodyJson $env:CLOUDFLARE_ACCOUNT_ID = $accountId } } catch {} if (!$tokenFound) { & wrangler pages project create $projName --production-branch $branch 2>&1 | Out-Null } $out = & wrangler pages deploy . --project-name $projName --branch $branch 2>&1 | Out-String $success = ($LASTEXITCODE -eq 0) Pop-Location # Stop animation and clear box UI.Stop-GradientBox $animJob if ($success) { UI.Box "Deployment Completed: https://${projName}.pages.dev" "Green" } else { UI.Box "Deploying: $projName [ERROR]" "Red" Write-Host "`n ${C_RED}[Stderr]${C_RESET}`n$out" Write-Host "" } 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 { if ($_.Exception.Response) { $code = [int]$_.Exception.Response.StatusCode } elseif ($_.Exception.InnerException.Response) { $code = [int]$_.Exception.InnerException.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.Code -eq 502) $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 { cls $projectList = Get-Projects if ($script:lastSpinnerEsc) {cls; return} if (!$projectList -or $projectList.Count -eq 0) { cls UI.Box "No Pages projects found" "Red" Write-Status "Press any key to continue..." "off" $null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") return } cls 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) {cls; return} $selected = Ask-Menu -Items $projectList -Multi -Status $failedMap -Color "Red" -NoHint if ($null -eq $selected -or @($selected).Count -eq 0) { cls; 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 (!$confirm) {cls; 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) {cls; 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") cls } # --- Create Proxy Worker --- # function Invoke-ProxySetup { cls UI.Box "Create Proxy Worker" "Purple" 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) [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 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.BoxSpinner $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 !$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) [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 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.BoxSpinner $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" $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.GITHUB_TOKEN) headers['Authorization'] = 'token ' + env.GITHUB_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, cf: { cacheTtl: 0 }}); const noCache = { "Cache-Control": "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" }; if (!r.ok) return new Response('Fetch error: ' + r.status, {status: 502, headers: noCache}); const ct = r.headers.get('content-type') || 'text/plain; charset=utf-8'; return new Response(await r.text(), {headers: {...noCache, 'Content-Type': ct}}); } catch (e) { return new Response('EXCEPTION: ' + e.message, {status: 500, headers: { "Cache-Control": "no-store" }}); } } } '@ $accountId = Get-AccountId $checkWorker = Invoke-CfApi "accounts/$accountId/workers/scripts/$workerName" -ReturnFullResponse if ($checkWorker.StatusCode -eq 200) { $remoteJs = $checkWorker.Data $needRedeploy = $true if ($remoteJs -is [string]) { $normLocal = ($jsCode -replace '\s+', '').Trim() $normRemote = ($remoteJs -replace '\s+', '').Trim() if ($normLocal -eq $normRemote) { $needRedeploy = $false } } if ($needRedeploy) { Write-Host " ${C_GREEN}[✓] Worker '$workerName' already exists but code differs!${C_RESET}" $redeploy = Ask-Confirm "Redeploy to ensure code is up to date?" Cyan 8 if ($null -eq $redeploy) {return} if (!$redeploy) { Set-ProxyUrl $resolvedUrl return } [Console]::Write("$([char]27)[2A$([char]27)[J") } else { Set-ProxyUrl $resolvedUrl return } } $prevToken = $null if ($checkWorker.StatusCode -eq 200) { try { $settings = Invoke-CfApi "accounts/$accountId/workers/scripts/$workerName/settings" if ($settings -and $settings.result -and $settings.result.bindings) { $bind = $settings.result.bindings | ? name -eq "GITHUB_TOKEN" if ($bind -and $bind.text) { $prevToken = $bind.text } } } catch {} } $token = "" if ($prevToken) { $usePrev = Ask-Confirm "Use previous GitHub token?" Cyan if ($null -eq $usePrev) {return} if ($usePrev) { $token = $prevToken } else { $useToken = Ask-Confirm "Use a new GitHub token?" Cyan if ($null -eq $useToken) {return} 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} } } } else { $useToken = Ask-Confirm "Use GitHub token?" Cyan if ($null -eq $useToken) {return} 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 [System.IO.File]::WriteAllText((Join-Path $workerDir "package.json"), "{}") $tomlContent = @" name = "$workerName" main = "worker.js" compatibility_date = "2024-01-01" "@ if (![string]::IsNullOrWhiteSpace($token)) { $tomlContent += "`n[vars]`nGITHUB_TOKEN = `"$token`"" } [System.IO.File]::WriteAllText((Join-Path $workerDir "wrangler.toml"), $tomlContent) sc -Path (Join-Path $workerDir "worker.js") -Value $jsCode -Encoding UTF8 Push-Location $workerDir # Set GITHUB_TOKEN plaintext in wrangler.toml directly (handled above) $deployBlock = { param($wDir) try { Set-Location $wDir $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))" "Purple" @($workerDir))[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 "" if ($res.msg -match '(?i)authentication|token|login|code: 10000|code: 9109|non-interactive') { $relogin = Ask-Confirm "Authentication failed. Re-login now?" Cyan if ($relogin) { Invoke-ReLogin Write-Host "`n [i] Re-login complete. Please run 'Create Proxy Worker' again.`n" -f Yellow } } } 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) -or !(Test-ProxyWorkerExists)) { Invoke-ProxySetup if (!$script:accountSubdomain -or !(Get-ProxyUrl) -or !(Test-ProxyWorkerExists)) { return } cls } UI.Box "Deploy Pages Project" "Purple" 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 (!$isMine) { $redirectUrl = Ask-Input "Enter URL: " -Validate {![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 "Purple" -NoHint if ($null -eq $modeIdx) {return} $mode = $modeIdx + 1 [Console]::Write("$([char]27)[$($modeItems.Count)A$([char]27)[J") Write-Host " ${C_GREEN}[✓]${C_RESET} Mode: ${C_GREEN}$($modeItems[$modeIdx])${C_RESET}" } else { $overwriteUrl = Ask-Confirm "Overwrite URL?" Cyan if ($null -eq $overwriteUrl) {return} if ($overwriteUrl) { [Console]::Write("$([char]27)[1A$([char]27)[2K") $redirectUrl = Ask-Input "Enter URL: " -Validate {![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 "Purple" -NoHint if ($null -eq $modeIdx) {return} $mode = $modeIdx + 1 [Console]::Write("$([char]27)[$($modeItems.Count)A$([char]27)[J") Write-Host " ${C_GREEN}[✓]${C_RESET} Mode: ${C_GREEN}$($modeItems[$modeIdx])${C_RESET}" } else { Write-Host "`n ${C_CYAN}[i]${C_RESET} Deployment skipped.`n" sleep -Seconds 1 return } } 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 [System.IO.File]::WriteAllText((Join-Path $projDir "package.json"), "{}") 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 { cls if ($script:accountSubdomain) { UI.Box "Subdomain: $script:accountSubdomain" "Purple" $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)]" "Orange" } 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 "Purple" -NoHint if ($null -eq $choice) {return $null} return ($choice + 1) } cls # 1. Check Node.js & npm $hasNode = Get-Command node -ErrorAction SilentlyContinue $hasNpm = Get-Command npm -ErrorAction SilentlyContinue if (!$hasNode -or !$hasNpm) { $hasWinget = Get-Command winget -ErrorAction SilentlyContinue if ($hasWinget) { UI.Box "Installing Node.js & npm" "Orange" Write-Status "Please wait, launching winget installer..." "loading" & winget install OpenJS.NodeJS -h --accept-source-agreements --accept-package-agreements 2>&1 | Out-Null $env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path", "User") $hasNode = Get-Command node -ErrorAction SilentlyContinue $hasNpm = Get-Command npm -ErrorAction SilentlyContinue if (!$hasNode -or !$hasNpm) { UI.Box "Node.js Installation Failed" "Red" Write-Host " Node.js & npm are required. Please install Node.js: https://nodejs.org/" -f Red Write-Status "Press any key to continue..." "off" $null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") exit 1 } Write-Status "Node.js & npm installed successfully" "ok" } else { UI.Box "Node.js Required" "Red" Write-Host " Node.js & npm are required. Please install Node.js: https://nodejs.org/" -f Red Write-Status "Press any key to continue..." "off" $null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") exit 1 } } # 2. Check and Update Wrangler $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 {} return [PSCustomObject]@{current = $current; latest = $latest} } $wResult = @(UI.BoxSpinner $wranglerBlock "Checking Wrangler version" "Cyan")[0] if ($wResult) { $current = $wResult.current $latest = $wResult.latest if (!$current) { UI.Box "Installing Cloudflare Wrangler" "Cyan" Write-Status "Installing wrangler globally..." "loading" & npm install -g wrangler@latest --force 2>&1 | Out-Null Write-Status "Wrangler installed successfully" "ok" } elseif ($latest -and $current -ne $latest) { UI.Box "Updating Cloudflare Wrangler" "Cyan" Write-Status "Updating wrangler from v$current to v$latest..." "loading" & npm install -g wrangler@latest --force 2>&1 | Out-Null Write-Status "Wrangler updated successfully" "ok" } } # 3. Detect subdomain $script:accountSubdomain = Get-AccountSubdomain 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 {cls; Invoke-Deploy} 2 {cls; Invoke-ProxySetup} 3 {Invoke-Delete} 4 { Invoke-ReLogin } 5 {exit 0} } } } finally { Invoke-Cleanup cls }